-
Notifications
You must be signed in to change notification settings - Fork 560
Description
All database-stored secrets (API keys, integration tokens) are encrypted with a single global Fernet key loaded at startup. There is no mechanism to rotate this key without immediately breaking every existing encrypted value in the database.
Current Behavior
secret_manager.py loads exactly one encryption key and uses it for all encrypt/decrypt operations:
- Single key load at
secret_manager.py:95–104 - Decrypt path at
secret_manager.py:131–140— raisesInvalidTokenwith no fallback if the key has changed
There is no key versioning, no fallback to a previous key, and no transition window. The result: if SECRET_ENCRYPTION_KEY is rotated (compromised, expired, or changed between environments), every user's stored secret becomes undecryptable immediately.
Expected Behavior
Rotating the active encryption key should not invalidate existing secrets. Old and new keys should coexist during a transition window — secrets encrypted under the old key should still decrypt, and all new writes should use the new key.
Why This Matters
- Security posture: A leaked key currently exposes every user's credentials with no granular recovery path.
- Operability: Key rotation is a standard compliance requirement; right now it's operationally impossible without data loss.
- Blast radius: A single key means a single point of total compromise — there is no per-user or per-secret isolation.
Suggested Fix Direction
Two reasonable approaches — pick one based on operational requirements:
-
Key versioning prefix — prepend a short key ID (e.g.
v1:...) to each encrypted blob. On decrypt, parse the prefix to select the correct key from a small key ring. New writes always use the current key. Old blobs continue to decrypt using the keyed version they were written with. -
HKDF-derived per-user keys — derive a unique encryption key per user from the master key using HKDF. Rotation then becomes scoped: only affected users need re-encryption.
Either approach keeps the SECRET_ENCRYPTION_KEY env var as the root secret; the change is in how secret_manager.py loads, selects, and falls back across keys.
Do not change what is stored or the API surface — this is a refactor of the encrypt/decrypt internals only.
Notes for Contributors
- Where to start:
secret_manager.py— specifically the key loading block and the decrypt method. - Goal: make key rotation non-destructive; observable behavior (secrets are stored and retrieved correctly) must stay the same.
- Scope: self-contained to
secret_manager.pyand the env config — no service-layer changes needed.