Runbook: Secrets-Key Rotation
**Applies to:** the control-plane secrets-encryption key `DAGY_SECRETS_KEY`. User secrets are Fernet-encrypted at rest in `dagy-secrets-<env>` using this single key (a 44-char URL-safe base64 Fernet key, or any passphrase that is SHA-256 derived into one). The key lives **only** on the control-plane API Lambda and is never shipped to user-code compute. Infra/service secrets injected into workers live separately under the Secrets Manager `dagy/*` prefix.
Severity: planned change (Sev-2 if rotating due to suspected key compromise).
When to rotate
- Suspected exposure of
DAGY_SECRETS_KEY(compromise → treat as Sev-1/2). - Routine rotation policy.
Important: Fernet here has no key-version envelope — a single active key encrypts and decrypts everything. Simply swapping the env var to a new key makes all existing ciphertext undecryptable. Rotation therefore requires re-encrypting every stored secret. Plan a maintenance window.
Diagnosis / pre-checks
export ENV=develop REGION=us-east-1
-
Confirm current secrets decrypt (the app must have the current key set):
curl -fsS -H "Authorization: Bearer $TOKEN" \ "$DAGY_PUBLIC_API_URL/secrets" | jq '.[].secret_name' # Reading a secret value exercises decrypt with the current key. -
Inventory scope — how many secrets across how many orgs:
aws dynamodb scan --region "$REGION" --table-name dagy-secrets-$ENV \ --select COUNT -
Generate a new Fernet key (keep it secret):
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Remediation (rotation procedure)
Because there is no dual-key window in code, perform a re-encrypt migration with both keys available to the migration process:
-
Freeze writes to secrets for the window (announce; optionally disable the secrets write endpoints or coordinate with tenants).
-
Snapshot for safety — the table has PITR; note the pre-rotation timestamp so you can restore if the migration fails:
aws dynamodb describe-continuous-backups --region "$REGION" \ --table-name dagy-secrets-$ENV \ --query 'ContinuousBackupsDescription.PointInTimeRecoveryDescription' -
Re-encrypt every row: read with the OLD key → decrypt → encrypt with the NEW key → write back. Run this as a one-off migration with both keys in the environment (OLD as
DAGY_SECRETS_KEYfor the read/decrypt, NEW for the re-encrypt). The DynamoDB migration runner (python -m dagy_api.persistence.migrations) is the sanctioned framework for a versioned, resumable backfill of this shape — add a migration that performs the re-encrypt so it is auditable and idempotent, then:python -m dagy_api.persistence.migrations status --env $ENV python -m dagy_api.persistence.migrations run --env $ENV --dry-run python -m dagy_api.persistence.migrations run --env $ENV -
Flip the live key — update
DAGY_SECRETS_KEYto the NEW key on the API Lambda and redeploy (the value comes from the stack config / secret store, not a hand-edit). -
Verify — read several secrets across multiple orgs; they must decrypt with only the NEW key present.
-
Retire the OLD key — remove it from all locations and rotate any place it was stored.
Escalation
- Page L2 to schedule/own the maintenance window and the migration.
- Page L3 for a suspected key compromise: in addition to rotation, rotate every downstream credential those user secrets protected (they must be assumed leaked), and open a security incident.