Secret Rotation Runbook¶
Use this when you have committed (or might have committed) a real secret. Time-to-rotate target: under 30 minutes from discovery.
The last1.id repo previously contained live values for several secrets
in .env.example. Even though the repo is private, anyone with repo
read access (collaborators, GitHub Apps, CI runners) had effectively
unlimited control of the Supabase project until rotation. Treat all
values that were ever committed as burned.
This runbook covers each secret type, in the order you should rotate.
0. Pre-flight (2 min)¶
# Confirm the current scrubbed .env.example doesn't contain anything
# that looks like a real JWT (starts with eyJ) or live Turnstile key.
rg -n 'eyJ[A-Za-z0-9_-]{20,}|0x[0-9A-Fa-f]{20,}' .env.example
# Expected: no matches.
If anything matches, scrub it before continuing.
1. Supabase service_role + anon JWTs (10 min) — HIGHEST PRIORITY¶
The service_role key bypasses RLS entirely — whoever holds it owns every row in every table. The anon key is lower risk but still scope for client-side denial-of-service / quota abuse if leaked.
Steps¶
- Rotate keys on last1id-db (R940), not a hosted dashboard.
HISTORICAL: hosted project
nztgswcpnwggymncrnyxis deleted. Do not opensupabase.com/dashboard/project/nztgsw.... - Mint new service_role + anon JWTs from the sovereign JWT secret.
- Write the new values to BWS project
last1id(LAST1ID_DB_PROD_*/ the live GoTrue key names). Never Railway. - Redeploy last1.id from the sovereign unit so
auth.last1.idpicks up the new keys. - last1-admin operator-auth is a separate plane (
api.last1.app). Do not point it at last1id-db. - Smoke test:
Blast radius if delayed¶
Anyone with the old service_role key keeps full DB control until rotated.
Even after rotation, audit auth_audit_logs and the admin_audit_logs
tables for anything suspicious from before rotation.
2. Cloudflare Turnstile (3 min)¶
Turnstile keys gate the human_verified credential. A leaked secret lets an attacker forge "human verified" claims, which then propagate as Verifiable Credentials.
Steps¶
- Go to https://dash.cloudflare.com/?to=/:account/turnstile
- Find the
last1.idsite, rotate the secret key. - Update Railway:
railway variable set TURNSTILE_SECRET_KEY="<new>". - The site key (
VITE_TURNSTILE_SITE_KEY) is public by design — no rotation needed unless you also rotate the secret half. - Smoke test: submit a turnstile-gated flow (signup, phone verify).
3. Twilio Verify (3 min)¶
Twilio Auth Token signs SMS verification requests. Leaked → attacker can impersonate your Verify service and issue codes / drain quota.
Steps¶
- Go to https://console.twilio.com/.
- Account → API keys & tokens → rotate the auth token.
- Update Railway:
railway variable set TWILIO_AUTH_TOKEN="<new>". - Verify Service SID is not a secret (lives in the URL); no rotation.
4. Partner app secrets (5 min each — repeat per partner)¶
LASTVET_APP_SECRET, RADIOCHECK_APP_SECRET, MATR_IOS_APP_SECRET,
MATR_BIZ_APP_SECRET — these gate the cross-app bearer auth path in
server/lib/appClientAuth.ts. A leaked secret lets the leaker post
arbitrary trust events on behalf of that partner.
Steps (using RadioCheck as example)¶
- Generate a new secret:
- Update Railway for
last1.id: - Update the partner's own secret store (BWS / sovereign env).
HISTORICAL: do not
supabase secrets set --project-ref tcthzakppndkykatoefx(hosted RadioCheck/last1.app project is deleted). - If the partner reads from Supabase Vault (the RadioCheck cron does), also rotate the Vault entry:
- Trigger a redeploy on the consumer side so it picks up the new env.
- Smoke test by sending one event through the partner's outbox.
5. CREDENTIAL_SIGNING_KEY (DEFER — non-trivial)¶
This is the KEK that AES-256-GCM-encrypts issuer private keys at rest
in the issuer_keys table. Rotating it requires re-wrapping every row
in issuer_keys with the new KEK. Do not rotate this without a
dedicated maintenance window and the runbook in
docs/SECURITY_COMPLIANCE_PACK.md.
If you believe the KEK was actually exposed (e.g. committed publicly), you must rotate. Until then, treat as a planned-maintenance task and add it to the key-rotation handoff doc for the dev being hired.
6. OAUTH_JWT_PRIVATE_KEY (DEFER — coordinate with JWKS rotation)¶
The id_token signing key. Rotation requires either:
- A maintenance window where all OIDC tokens are invalidated, or
- A multi-key JWKS publication so old tokens verify against the old key while new tokens use the new key during the overlap.
Tracked separately as Tier 3 / dev-handoff work.
7. ADMIN_GATEWAY_SECRET / IDENTITY_EXPORT_SIGNING_SECRET / COMPLIANCE_BUNDLE_SIGNING_SECRET¶
Lower-risk than the Supabase keys but still rotate quarterly. Same
pattern: generate via openssl rand -hex 32, update Railway, redeploy.
8. After rotation — verification & cleanup¶
- Audit logs — scan for activity from before rotation that looks suspicious:
- Git history scrub (optional but recommended) — if you want the
old values out of
git logentirely: If a history rewrite is too disruptive, document the rotated keys as "burned forever" in your runbook and move on. - Update this file with the rotation date and the operator who performed it (below).
Rotation log¶
| Date | Operator | Secrets rotated | Reason |
|---|---|---|---|
| 2026-05-21 | pending | Supabase service_role + anon JWTs, Turnstile, Twilio | Discovered live values in committed .env.example; repo is private but anyone with repo read had effective DB control |