Secrets Rotation — RadioCheck¶
Status¶
.env was tracked in git from 2026-01-27 until 2026-05-22. The
file was finally git rm --cached'd in commit
<this commit>. The repository is private, so the exposure window
was bounded to GitHub collaborators with read access, not the public
internet — but every secret that ever lived in that file should be
treated as burned and rotated.
The full historical contents remain readable via:
We are not rewriting history. The pragmatic posture is: rotate everything, then the old values in the log are worthless.
Rotation runbook¶
Work the table top-to-bottom. Each row is independently safe to do — they are ordered by blast radius (highest first).
1. SUPABASE_DB_PASSWORD + DATABASE_URL¶
- Was:
MSIOInjZJI2NILHZ(and bundled intoDATABASE_URLforpostgres.tcthzakppndkykatoefx@aws-0-us-west-2.pooler.supabase.com). - Reset where: Supabase Dashboard → Project
Last 1 App + Radio Check(reftcthzakppndkykatoefx) → Project Settings → Database → Reset database password. - Update where after rotation:
- Local
.envfor both this repo ANDlast1.id(they share the project — the new password is alsolast1.id'sSUPABASE_DB_PASSWORD). - Any CI / deploy environment that runs
supabase db pushagainst this project. - Verify:
npm run supabase:db:lint:linkedexits 0 with the new password loaded.
2. TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN¶
- Was: Account SID
AC64f12664…, auth token71837cd8…. - Reset where: Twilio Console → Account → API keys & tokens → Auth Token → "Promote a new secondary token to primary". Keep the old token live during the swap so SMS check-ins don't gap.
- Update where after rotation:
- Local
.env - Every Supabase Edge Function that calls Twilio
(
sms-checkin-cron, the SMS webhook receiver, etc.) — these pull from project secrets, not the repo.env. Update in Supabase Dashboard → Edge Functions → Secrets. - Verify: Send a test check-in SMS, confirm the cron job's next
run still authenticates (
supabase functions logs sms-checkin-cron).
3. LAST1_CLIENT_SECRET (RadioCheck's OAuth client secret on last1.id)¶
- Was:
l1c_Np2Yj4w8JwSeHJHMdkEZ5zXx40nJ2JBkWmz_Ic3lulkfor theradiocheckapp_client onlast1.id. - Reset where: last1.id admin → App Clients →
radiocheck→ "Rotate client secret". Capture the new value before closing the dialog (it's only shown once). - Update where after rotation:
- Local
.env - Supabase Edge Function secrets for any function that exchanges auth codes / refreshes tokens against last1.id.
- Verify: Run the OAuth code-exchange end-to-end from a
TestFlight build; confirm a non-zero count in
oauth_token_audit_logwithgrant_type='authorization_code'.
4. RADIOCHECK_APP_SECRET (trust-event ingest)¶
- Was:
c57b39496654d551b316a496a575285c9cda6d8194cb4824406a3a35b4d89a35. - Reset where: Generate a fresh value:
openssl rand -hex 32. - Update where after rotation:
- last1.id
.env(matching system-app secret env var:RADIOCHECK_APP_SECRET— seeserver/routes/oauth.ts:getSystemAppSecret). - This repo's
.env. - Supabase Edge Function secrets if any function uses it server- side.
- Vault entry
radiocheck_app_secret(seerc-vault-secret— one-off SQL). - Verify: Trigger a RadioCheck check-in; confirm the
check-in-proof-eventsEdge Function still posts successfully to last1.id/api/trust-events(look for a new row withevent_type='radiocheck.checkin_*'for the test identity).
5. EXPO_PUBLIC_SUPABASE_ANON_KEY + EXPO_PUBLIC_LAST1_SUPABASE_ANON_KEY¶
- Was: Long-lived anon JWTs (
iat: 1769456322,exp: 2085032322— usable for ~10 years). - Risk profile: Anon keys are designed to be public-by-design (shipped in the iOS bundle) but having a 10-year-validity key in git history is needlessly long. Rotating gives us a fresh expiry and revokes any privilege creep that may have been added to the anon role.
- Reset where: Supabase Dashboard → Project Settings → API → JWT
Settings → Generate new JWT secret. This rotates BOTH
anonandservice_rolekeys in one step. - Coupling warning: This will also invalidate every
service_rolekey in flight, which is much higher blast radius than just the anon keys. Coordinate with thelast1.idteam before pulling the trigger — there is no per-role rotation. - Recommended: Do this LAST, after items 1-4 are confirmed good.
6. CHECK_IN_PROOF_SECRET, UNIT_REQUEST_ACTION_SECRET,¶
LAST1_WEBHOOK_SECRET
- These are not in the leaked
.env(only set in production env) but if they ever passed through this repo locally, rotate them too viaopenssl rand -hex 32and update both ends of the webhook handshake.
After every rotation is complete¶
- [ ] Confirm
git ls-files | grep "^\.env$"returns nothing (it will not after this commit; this is a sanity check for future commits). - [ ] Delete the secrets that were ever copy-pasted into chat / Slack / Linear comments. They are burned and should be treated as identifying information, not as still-active credentials.
- [ ] Open a follow-up issue to fix
sms-checkin-cron— per the earlier follow-uprc-fix-sms-cron, it's been failing daily.