Skip to content

Secret Encryption Key Rotation — Single Fernet Key With No Rotation Path #674

@Harigithub11

Description

@Harigithub11

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 — raises InvalidToken with 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:

  1. 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.

  2. 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.py and the env config — no service-layer changes needed.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions