Hash remoteStorage access tokens at rest #259

Open
opened 2026-11-02 13:24:39 +00:00 by raucao · 0 comments
Owner

Follow-up to #254. remoteStorage access tokens are currently stored in plaintext in two places:

  • the remote_storage_authorizations.token DB column, and
  • the Redis key authorizations:<cn>:<token>.

A leak of either (DB dump, Redis access, backup) hands over usable tokens for users' storage. Store only SHA256(token) instead, keeping the plaintext in memory just long enough to return it to the client at issuance. The token has 128 bits of entropy (SecureRandom.hex(16)), so a plain SHA-256 (no salt/stretching) is sufficient.

Scope

akkounts

  • Replace the plaintext token column with token_digest; the Redis key becomes authorizations:<cn>:<digest>, and deletion works off the digest.
  • Backfill token_digest = SHA256(token) for existing rows and re-key Redis (add the digest key, drop the plaintext key). Because the plaintext still exists in the DB today, this needs no re-authorization — that window closes once the plaintext column is dropped, so the backfill must ship with the migration.
  • Nothing in akkounts looks an authorization up by token, so only issuance, Redis storage and deletion are affected.

Liquor Cabinet

  • It reads the bearer token from the Authorization header and looks it up with SMEMBERS authorizations:<user>:<token> (lib/remote_storage/rest_provider.rb). It must hash the presented token before the lookup.
  • For a seamless rollout, look up the plaintext form first and fall back to the digest (or vice versa) during the transition, so existing client tokens keep working while akkounts re-keys Redis.

Notes

  • Cross-repo change (akkounts + Liquor Cabinet), so two PRs and a coordinated deploy.
  • Per-device revocation stays immediate (delete the key), unlike self-contained tokens.

Part of #254.

Follow-up to #254. remoteStorage access tokens are currently stored in plaintext in two places: - the `remote_storage_authorizations.token` DB column, and - the Redis key `authorizations:<cn>:<token>`. A leak of either (DB dump, Redis access, backup) hands over usable tokens for users' storage. Store only `SHA256(token)` instead, keeping the plaintext in memory just long enough to return it to the client at issuance. The token has 128 bits of entropy (`SecureRandom.hex(16)`), so a plain SHA-256 (no salt/stretching) is sufficient. ## Scope **akkounts** - Replace the plaintext `token` column with `token_digest`; the Redis key becomes `authorizations:<cn>:<digest>`, and deletion works off the digest. - Backfill `token_digest = SHA256(token)` for existing rows and re-key Redis (add the digest key, drop the plaintext key). Because the plaintext still exists in the DB today, this needs no re-authorization — that window closes once the plaintext column is dropped, so the backfill must ship with the migration. - Nothing in akkounts looks an authorization up by token, so only issuance, Redis storage and deletion are affected. **Liquor Cabinet** - It reads the bearer token from the `Authorization` header and looks it up with `SMEMBERS authorizations:<user>:<token>` (`lib/remote_storage/rest_provider.rb`). It must hash the presented token before the lookup. - For a seamless rollout, look up the plaintext form first and fall back to the digest (or vice versa) during the transition, so existing client tokens keep working while akkounts re-keys Redis. ## Notes - Cross-repo change (akkounts + Liquor Cabinet), so two PRs and a coordinated deploy. - Per-device revocation stays immediate (delete the key), unlike self-contained tokens. Part of #254.
raucao added the security
integration
remotestorage
labels 2026-11-02 13:24:39 +00:00
raucao added the enhancement label 2026-11-02 13:26:03 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kosmos/akkounts#259