Skip to content

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:

git log --all --full-history -- .env
git show <sha>:.env

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 into DATABASE_URL for postgres.tcthzakppndkykatoefx@aws-0-us-west-2.pooler.supabase.com).
  • Reset where: Supabase Dashboard → Project Last 1 App + Radio Check (ref tcthzakppndkykatoefx) → Project Settings → Database → Reset database password.
  • Update where after rotation:
  • Local .env for both this repo AND last1.id (they share the project — the new password is also last1.id's SUPABASE_DB_PASSWORD).
  • Any CI / deploy environment that runs supabase db push against this project.
  • Verify: npm run supabase:db:lint:linked exits 0 with the new password loaded.

2. TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN

  • Was: Account SID AC64f12664…, auth token 71837cd8….
  • 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_Ic3lulk for the radiocheck app_client on last1.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_log with grant_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 — see server/routes/oauth.ts:getSystemAppSecret).
  • This repo's .env.
  • Supabase Edge Function secrets if any function uses it server- side.
  • Vault entry radiocheck_app_secret (see rc-vault-secret — one-off SQL).
  • Verify: Trigger a RadioCheck check-in; confirm the check-in-proof-events Edge Function still posts successfully to last1.id /api/trust-events (look for a new row with event_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 anon and service_role keys in one step.
  • Coupling warning: This will also invalidate every service_role key in flight, which is much higher blast radius than just the anon keys. Coordinate with the last1.id team 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 via openssl rand -hex 32 and 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-up rc-fix-sms-cron, it's been failing daily.