ADR-028: Credential rotation cadence — quarterly minimum, immediate on breach
Status: Accepted Date: 2026-04-21
Context
Credentials — HMAC session secrets, API keys, DB passwords, SSH keys, OAuth client secrets — do not stop being valuable to an attacker on any schedule. The longer a credential is valid, the longer an attacker who has exfiltrated it can use it. Rotation is how we bound the exposure window.
security-audit-2026-04.md §secret-storage records the starting point: secrets in .env on the deploy VPS, no rotation history in git, no calendar cadence, no automated rotation. This is the "rotate when you remember" mode, which is indistinguishable from "never rotate".
Industry norms vary wildly:
- Quarterly (NIST SP 800-63 historical baseline; common among SaaS pre-2020) — still the default for credentials without auto-rotation tooling.
- Never, unless breached (NIST's updated guidance, post-2017) — applies to user passwords, where rotation fatigue causes weaker passwords. Not applicable to machine credentials.
- Automated, short-lived (ideal state — e.g. Vault-issued 1-hour DB creds) — requires a secret manager and code to handle rotation.
Our position: we don't have a secret manager today (security-audit §secret-storage gap), so automated rotation is not yet achievable. We have to commit to a manual cadence that actually gets done.
Decision
- Quarterly rotation is the minimum for every long-lived credential. Every credential in the master list (security-runbook §credential-rotation-master-list) is rotated at least once per quarter.
- Immediate rotation on any breach signal. A breach signal is: a tripwire firing with evidence of credential compromise, a provider's notification (AWS telling us an access key leaked), a developer reporting a credential was committed, any
gitleakshit in the git history. - Annual rotation for the "god keys" that have blast radius (SSH deploy key, Let's Encrypt account key). The blast-radius tradeoff: a bad rotation of these keys takes services offline; the quarterly cost outweighs the quarterly benefit at current breach frequency (zero known).
- Calendar-driven, not event-driven. A repeating calendar entry on the first Monday of each quarter, owned by the operator on rotation. The operator works the master list top to bottom; the rotation PR merges before the end of the day.
- Every rotation leaves a record. The commit that updates
.env.example(or the SSOT for credential identity) carries arotated:trailer:rotated: session-secret-distill, 2026-07-01.git log --grep=rotated:becomes the rotation history. - A missed rotation is a finding. The Monday after the first Monday of the quarter, if any credential from the master list is not rotated, the operator files a debt entry and the alert-review cadence picks it up as an action item.
The master list is authoritative (security-runbook §credential-rotation-master-list). Adding a new credential to the platform requires an entry in the master list in the same PR; without the entry, the credential has no rotation path and is rejected at review.
Consequences
- Exposure window is bounded by 90 days for everything except god keys. An attacker with a leaked session secret has at most 90 days to use it before it's invalidated. For god keys: 365 days. Within the breach-detection window of our tripwires (most detect within hours), this means the attacker's practical window is 90 days × (1 - detection probability).
- Operational cost is predictable. One morning per quarter. Rotation of 10 credentials, each 5-15 minutes, totals 2-3 hours. Scheduled and visible.
- Users experience session invalidation quarterly. When the HMAC session secret rotates, all existing sessions are invalidated. Users re-login. This is the single most user-visible cost and is by design — a rotation that preserves existing sessions also preserves any session a compromised secret was already authorising. Rotation with invalidation is a feature, not a bug.
- Provider-side rotations are blocking. A Stripe secret rotation requires their UI. A GitHub PAT rotation requires their UI. Each provider has a different UI; the runbook codifies the sequence per provider so it's not a per-rotation learning curve.
- "Annual" god-key rotation is aspirational. The SSH deploy key in particular has not been rotated in its existence. The first annual rotation will discover which hosts we've lost track of. Scheduled for 2026-12 — enough lead time that the discovery doesn't block this tranche.
- Automated rotation is the escape from this cadence. Once a secret manager ships (Tier-2 item, ADR-026), short-lived credentials replace the long-lived ones. Rotation cadence moves from "quarterly" to "continuous" as the secret manager's lease duration becomes the effective window. This ADR's cadence is the interim posture.
- Breach rotation is a separate procedure. The cadence captures the baseline; breaches follow security-runbook §phase-4-recover, which is "rotate everything the attacker could plausibly have seen" — broader, not waiting for the next Monday.
Alternatives considered
- Annual rotation for everything. Simpler schedule, less operational cost. Rejected: 12-month windows are too long for credentials that are known to be exfiltrated by novel classes of attack (the agent threat model, for instance). Quarterly is the balance between operational cost and exposure.
- Never-rotate-unless-breached (NIST's user-password guidance, applied to machine credentials). Rejected for machine credentials: the NIST guidance explicitly scopes to user passwords, where rotation fatigue weakens passwords. Machine credentials don't have that failure mode. Bounded exposure is worth the operational cost.
- Automated rotation immediately (Vault / similar). Right target, wrong starting point. Standing up a secret manager is a multi-week project. The quarterly cadence gets us to "exposure bounded" inside days; the secret manager migration is the Tier-2 follow-up.
- Rotation on every deploy. Attractive (every deploy issues fresh secrets) but breaks the HMAC-signed-session UX for any user mid-session. Rejected. Sessions could be rotated-to-new-secret + grace-period-on-old, but the engineering cost is high relative to quarterly's operational cost.
- Per-credential cadence tuned to threat. High-value credentials (admin, SSH) rotated monthly; low-value credentials (a webhook signing key) rotated annually. Rejected for first pass: the mental overhead of "which credential is in which tier?" leads to drift. Uniform quarterly is enforceable; per-credential cadence is ignorable. Revisit once the list exceeds 20 items.
Supersedes: none Superseded by: