Supabase security · 5 minute read
Leaked Supabase service_role key: the 30-minute rotation runbook
A service_role key in a public repository is not an information leak — it is a complete bypass of every RLS policy you have. Anyone holding it reads, writes and deletes across your whole database with the PostgREST API and no login. The fix has one correct order, and most people do it backwards: they purge the git history first and rotate last. Rotate first.
Why the order is: rotate → verify dead → purge → prevent → prove
- Rotate first. Until the key is rotated in the Supabase dashboard, the leaked copy works — no amount of history rewriting changes that. This is the only step that actually ends the exposure.
- Verify the old key is dead.Rotation that "looked done" is the failure that repeats. One curl proves it.
- Then purge history. The old key is in every clone, fork and archive; removing it protects the next reader, and takes the string out of scanner indexes over time.
- Prevent the next one. A pre-commit hook and a CI secret scan catch the class, not the instance.
- Then prove the policies held. The exposure window existed; what you need for your incident notes is evidence of what the key could reach.
Step 1 — rotate in the dashboard (2 minutes)
Project Settings → API → service_role secret → roll it. Do this before touching git. Note the timestamp: that is the start of your exposure window for the incident writeup, and every deployment that used the old key needs the new one from your secret store.
Step 2 — verify the old key is dead (1 minute)
curl -s -o /dev/null -w '%{http_code}' \
"$SUPABASE_URL/rest/v1/?apikey=$OLD_SERVICE_ROLE_KEY" \
-H "Authorization: Bearer $OLD_SERVICE_ROLE_KEY"
# healthy rotation: 401
# still live: 200 - keep the old one revoked and re-check after propagationA 200 immediately after rotation can be propagation delay — re-check after a minute. A persistent 200 means the roll did not take; do not continue to the history steps until this reads 401.
Step 3 — purge it from git history (10 minutes)
The key string is in history even after you delete the file. Two tools, both destructive — they rewrite every commit SHA, so coordinate with your team:
# git-filter-repo (recommended; install: pip install git-filter-repo)
echo "$OLD_SERVICE_ROLE_KEY" > /tmp/secret.txt
git filter-repo --replace-text /tmp/secret.txt
git push --force --all && git push --force --tags
# or BFG: bfg --replace-text /tmp/secret.txtThen delete and re-clone: filter-repo warns that the old objects stay in your local clone until you re-clone. Forks and PR refs keep their own copies — if the repository was popular, treat the key as permanently public even after purging, which is exactly why step 1 came first.
Step 4 — make it not happen again (5 minutes)
- Pre-commit hook: gitleaks catches the service_role pattern before the commit exists:
gitleaks protect --stagedvia pre-commit, orgh secret set+ a CIgitleaks detectstep. - Move it out of files.
.envin a repo is the leak; the secret store (CI secrets, Vault, 1Password) is the fix. Add.env*to.gitignorein the same commit as the cleanup. - Check the other copies: CI logs, build caches, Docker image layers, and any
NEXT_PUBLIC_variable that accidentally carries a service key — a browser bundle is a public place.
Step 5 — evidence for the incident (10 minutes)
Two answers protect you later: what the key could reach, and whether it did. Reconstruct the first from the schema:
-- with the NEW key: what could the old one touch?
select table_name from information_schema.tables
where table_schema in ('public','storage');
select count(*) from auth.users; -- reachable via /auth/v1/admin with the keyFor the second, the honest answer is usually "we cannot know" from the database alone — REST access through the Data API is not logged per key unless you have log retention configured. State that plainly, and check your project logs for the window. If any user-scoped table was reachable, the incident template in the audit kit covers what to tell affected users.
The check that matters after the rotation
Rotating the key restores the posture your policies already promised — so the final proof is that the policies actually hold without it. Run the anon-key probe from the first-15-minutes audit guide: if the public anon key can read user-scoped tables, the leak was never the worst of it — your RLS was already open, and the service_role exposure just removed the need for it. That combination — a live key plus open policies — is what a fixed-price audit from $99 is for.
The runbook as files, plus the regression SQL
Supabase RLS Audit Kit · $29 one-time
The rotation checklist, history-purge commands, gitleaks pre-commit setup and RLS regression SQL from this article, plus the seven commented audits and the role-simulation harness that prove isolation after the rotation.
Get the kit - $29 ↗Review the files, scope and license →Related reading: 7 ways your Supabase app still leaks data with RLS enabled, what your AI security reviewer structurally cannot see and why your RLS tests pass and your data still leaks.