How to rotate API keys after a breach at your host
You should rotate API keys after a breach at your host, because every secret stored there may have been read. List every secret, replace them in order of damage, redeploy, then revoke the old ones. Railway's October 2026 incident shows why: it told customers to roll (replace) all their variables, because it couldn't say which ones the attacker decrypted.
QUICK CHECKLIST
- Confirm the host says the attacker's access ended
- List every secret, including copies in CI and .env files
- Rotate keys that cost money first, then database credentials
- Redeploy every service and preview that reads the secret
- Revoke the old key once the new one works
- Check usage logs and invalidate old sessions
Why you must rotate API keys after a breach
A host stores your secrets so your app can read them at runtime. If the host is breached, those secrets are in play even when your own code and accounts were never touched. Changing your code doesn't help, because the old values keep working until you replace them.
What a stolen secret allows depends on its type:
- A provider key (Stripe, OpenAI, email, SMS) lets someone spend your money or quota and read what the key can read.
- A database URL gives direct access to your data.
- A JWT or session secret (the value your app uses to sign login tokens) lets someone forge logins.
- A webhook secret lets someone send fake events your app trusts. See Stripe webhook signature verification.
- A CI token or git deploy key lets someone push code or read your repos.
If the key leaked from your own code or a public repo, the steps are the same. Start with what to do about a leaked API key.
What the Railway security incident told customers to do
On October 21, 2026, Railway disclosed that an unauthorized third party may have accessed encrypted environment variables and, for some of them, obtained the decrypted contents. Variables can hold API keys plus database and registry credentials (logins for the private store where your container images live). Railway told all users to roll every environment variable stored on Railway and any private registry credentials, and to review logs for those services.
The preliminary report from October 22 put the number at no more than 1.85% of workspaces, about 100,000, with variables potentially compromised. It recommended that all customers who signed up before October 21 rotate the keys stored on Railway.
The attacker appeared to be after AI provider keys, likely to run distillation attacks (using stolen access to train a copycat model). The keys were envelope encrypted (the data is encrypted with a key that is itself encrypted), so the database record alone couldn't show which variables were decrypted. If your host can't say which secret was read, rotate all of them.
Railway's other advice applies to any host:
- Remove public network access to databases.
- Check your GitHub account audit logs and review all app access.
- Check audit logs in registries such as Dockerhub.
Railway also asked providers including GitHub, GitLab, Anthropic, OpenAI, OpenRouter and Stripe to revoke potentially compromised keys. For some vendors it sent every key stored on the platform. A revocation notice from a provider doesn't mean your credentials were affected, but your app fails until you set a new key.
Railway held off telling users to roll credentials until it believed the attacker was out of its infrastructure, so new credentials wouldn't be taken right away. Look for that confirmation from your host, and re-rotate anything you changed before it. Once builds resumed, Railway told users to redeploy the latest commit and reconnect GitHub and CLI and CI credentials.
How to rotate secrets after a host breach, step by step
This rotation checklist works for any host.
Check that the hole is closed
Read the host's incident post and look for a statement that the attacker no longer has access. If the host is still investigating, rotate now and repeat the important ones when its final report lands.
Inventory every secret
List each variable the host stores, then every other copy: local
.envfiles, CI settings, preview environments, other hosts and teammates' machines. Assume every variable in an affected project was read. The grep command in the test section finds most names your code reads, but your host's variable list is the full source. Also check for .env file leaks on Vercel and Netlify.Rotate in order of damage
Go in this order. It's our suggestion, based on what can be abused fastest.
- Provider keys that cost money or move it: Stripe, OpenAI, Anthropic, email, SMS
- Database credentials
- JWT and session secrets (everyone gets signed out)
- Webhook signing secrets
- OAuth client secrets
- CI tokens and registry credentials
- Git deploy keys and personal access tokens
Update the host and redeploy
Set the new values, then redeploy every service that reads them. On Railway, containers read variables only at startup, so a service you forget keeps the old value. On Vercel, changes apply only to new deployments, and a team-level variable means redeploying every linked project.
Revoke the old key
Once the new deployment works, revoke the old credential. A key you rotated but never revoked is still a valid key. If you know it was stolen, revoke first and accept the downtime.
Check usage logs and sessions
Look at each provider's usage page for requests you don't recognize. Check GitHub audit logs, authorized apps, registry audit logs such as Dockerhub, and Stripe's Workbench request logs. If sessions or tokens lived in a host-run Redis, invalidate them and rotate your app's signing secret.
Zero-downtime rotation: overlap, then revoke
Zero-downtime rotation means two valid credentials exist for a short time. Create the new one, ship it, then revoke the old one. Providers support the overlap:
- Vercel: its docs say to update Vercel before you invalidate the old credential.
- Stripe: both keys can work for up to 7 days when you rotate in the Dashboard. For a key that sat on a breached host, set the old key to expire now instead.
- Supabase: new and legacy keys work side by side.
On Railway, a pasted connection string doesn't follow a rotation. A reference variable does, though you still redeploy.
# Railway service variables DATABASE_URL=postgresql://postgres:OLD_PASSWORD@host.example:5432/railway
# Railway service variables
DATABASE_URL=${{ Postgres.DATABASE_URL }}Webhook secrets need overlap in your own code. Accept the old and new secret for a short window, then delete the old one. Do this only when you merely suspect the secret. If it sat on a breached host, roll the endpoint secret with immediate expiry and accept a few failed deliveries, which Stripe retries.
Provider notes: Stripe, Supabase, OpenAI and GitHub
- Stripe: live keys start with
sk_live_orrk_live_. Stripe recommends restricted keys, so rotating is a good time to switch. Webhook signing secrets are separate from API keys, so rotate both if both lived on the host. - Supabase: a secret key (
sb_secret_...) bypasses Row Level Security, the per-row access rules in your database. Create a new one in Settings > API Keys, replace it everywhere, confirm, then retire the old one. Deleting a secret key can't be undone. - Supabase legacy keys:
service_rolekeys stay valid until you deactivate the legacy keys, which also turns off the legacyanonkey. Move clients to a publishable key first. Re-enabling brings the leaked key back. - Supabase publishable keys and database password: publishable keys are public by design (see is the Supabase anon key safe to expose). The database password is separate, so reset it if it was in the host's variables.
- OpenAI: create a new key, deploy it, delete the old one and check the usage page for spikes. Dashboard names change, so check the current docs.
- GitHub: revoke personal access tokens and deploy keys the host used, review authorized apps and read the account audit log.
For a wider pass over your app, use the vibe coding security checklist.
Test it yourself
Run these against your own app and accounts only. They check that old credentials are dead and that nothing unknown is connected.
# 1. List env var names your code reads (repo root). Misses bracket access
# and import.meta.env, so also export the full list from your host.
grep -rhoE "process\.env\.[A-Z0-9_]+" . \
--include=*.ts --include=*.tsx --include=*.js \
--exclude-dir=node_modules --exclude-dir=.next | sort -u
# 2. Is the OLD Stripe key dead? Run after it expired. Expect 401.
# A 200 (or 403 for a restricted key) means it still works.
# Clear your shell history afterwards.
curl -s -o /dev/null -w "%{http_code}\n" \
https://api.stripe.com/v1/balance -u "sk_live_xxx:"
# 3. Is the OLD database password dead? Expect an auth error.
psql "postgresql://USER:OLD_PASSWORD@HOST:PORT/DB" -c "select 1"Reading the results:
- Bad: a 200 from the old Stripe key, or a successful psql connection.
- Good: a 401, and a message like
password authentication failed. - A timeout or refusal proves nothing about the password, only that the host isn't reachable. Run the check from somewhere that can reach the database.
- After you redeploy, watch the deploy logs. A Postgres auth error, or
NOAUTHandWRONGPASSon Redis, means a service still holds the old value.
Then connect with the new credential (on Railway, railway connect Postgres) and look at who's connected:
SELECT pid, usename, client_addr, application_name, backend_start, state FROM pg_stat_activity WHERE datname = current_database(); SELECT rolname FROM pg_roles;
Look for client addresses you don't recognize and roles you didn't create. Postgres doesn't log successful connections by default, so a clean log doesn't prove the old password was never used.
Fix prompt you can paste
Give this to Cursor or Claude Code. It builds the inventory and the order for you, and it is told not to print your secret values or edit anything before you approve.
My host had a security incident and I must rotate every secret this app uses. Never print a full secret value, and do not edit any file until I approve the table from step 2. 1. Search the whole repo, including .env.example, next.config.*, Dockerfile, docker-compose files, GitHub Actions workflows, vercel.json, railway.json and scripts/, for every secret and environment variable: process.env.*, import.meta.env.*, Deno.env.get and hardcoded keys (sk_live_, sk_test_, sb_secret_, eyJ, ghp_, whsec_). 2. Output a table: variable name, file:line, what it is for, server-only or browser-exposed (NEXT_PUBLIC_, VITE_), and what breaks when it changes. 3. Sort the table into this rotation order: provider API keys, database credentials, JWT/session secrets, webhook signing secrets, OAuth client secrets, CI/registry tokens, git deploy keys. 4. List every hardcoded secret as file:line, showing only its first 4 characters. Treat each one as already exposed. After I approve the table, replace each hardcoded value with an environment variable read. 5. List every webhook handler that verifies a signature (for example Stripe constructEvent) as file:line. Change each to also accept an optional *_OLD secret, with a comment to delete it after the switch. Leave the _OLD variable unset for any secret that was exposed on the breached host. 6. Detect my host from the repo (railway.json, vercel.json, Dockerfile) and finish with a runbook for it: create new, update host variables, redeploy every service that reads them, verify, revoke old. Name the exact dashboard page or CLI command for each step. Explain each change you make in one or two sentences.
If you want a second look at your app after the cleanup, run it through Leakcade.
Further reading
- Railway: Preliminary report on the October 21st security incident
- Railway: Security disclosure, Wednesday October 21st
- Railway: Rotate PostgreSQL database credentials
- Vercel docs: Rotating environment variables
- Stripe docs: API keys (rolling keys)
- Supabase docs: API keys
FAQ
Do I need to rotate API keys if my hosting provider was breached?
Yes. Rotate every secret that was stored on the host, even if you see no sign of misuse. Hosts often can't tell which variables were read: Railway's records couldn't show which keys were decrypted, so it told customers who signed up before October 21 to rotate the keys stored there. Start with keys that cost money, then database credentials.
In what order should I rotate secrets after a breach?
Start with provider keys that cost money or move it (Stripe, OpenAI, email, SMS), then database credentials, session and JWT secrets, webhook secrets, OAuth client secrets, CI tokens and deploy keys. This order is our suggestion: it puts the credentials that can be spent or used to read your data first.
How do I rotate secrets without downtime?
Create the new credential and keep the old one active. Update the host, redeploy every service, check it works, then revoke the old one. Stripe can keep a rolled key working for up to 7 days, and Vercel's docs say to update Vercel before you invalidate the old credential. If you know the key was stolen, revoke it first and accept the downtime.
Why did GitHub or Stripe revoke my key after the Railway incident?
Railway sent lists of potentially decrypted keys to providers and asked them to revoke them. For some vendors it sent every key stored on the platform. A revocation notice doesn't mean your credentials were affected. Create a new key and redeploy.
How do I know if someone used my old key?
Open the provider's usage page and look for spikes or requests you don't recognize: OpenAI's usage page, Stripe's Workbench request logs, GitHub's account audit log. For Postgres, run the pg_stat_activity and pg_roles queries from the test section. Postgres doesn't log successful connections by default, so a clean log doesn't prove the old password was never used.
Want a second pair of eyes?
We review your whole repo and write a fix prompt for every finding. $19, report within an hour.
Check my repoLeakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms