
Rotating Secrets After Railway's Env-Var Breach: A PaaS Blast-Radius Audit
Introduction: The Secret Was Never Yours Alone
On 23 October 2026, a Rescana report described a security breach at Railway in which customer environment variables were exposed, and developers were told to rotate their secrets. The obvious response is to open the dashboard, hit rotate, and move on. That response is mostly wrong, and it is wrong in a way that leaves you with the same exposure plus a broken deploy at 2 a.m. What follows is a PaaS blast-radius audit in the order that actually closes the exposure: inventory every copy of the secret, rotate in dependency order behind an overlap window, then prove the old values are dead.
The credential in your dashboard is a copy living in someone else's failure domain
When you set DATABASE_URL in a PaaS dashboard, you did not create a secret. You created a copy of a secret that already exists in Postgres, in your CI runner, in a teammate's .env, and possibly in a build log. The PaaS is one custodian among several, and it just had a bad week. Your security posture is bounded by the weakest custodian, not the one with the nicest UI.
The position: rotate in dependency order and prove the old values are dead
"Rotate everything tonight" is not a security plan; it is a change-management incident. The order matters because some secrets are consumed by upstream stores, some by running services, and some invalidate state the moment they change. The correct sequence is: inventory, rotate with overlap, then prove the old credential no longer works. Panic skips step three, and step three is the only step that actually closes the exposure.
Scope note
The only source available for this post is the Rescana report summary dated 2026-10-23. Everything asserted as fact about the incident traces to that summary. Gaps are labelled as gaps. No CVE, version range, affected-customer count, or official Railway statement appears here, because none was in hand.
Three artifacts you should leave with
A secret inventory (names, consumers, owners), an ordered rotation ledger (mechanism, overlap window, revocation path), and a dated verification record showing what you tested and what the result was. The third one is what you will reach for during the next incident.
What the Rescana Report Says, and What It Does Not
Confirmed by the source
The Rescana report, published 2026-10-23, states that Railway suffered a security breach that exposed customer environment variables and that developers were urged to rotate secrets. That is the full extent of what this post treats as established.
Unknown from this source alone
| Question | Status |
|---|---|
| Which variable classes were exposed (app env vars vs. platform tokens) | Unknown |
| Whether values were read, exfiltrated wholesale, or both | Unknown |
| Whether Railway revoked platform API tokens or sessions | Unknown |
| Exact disclosure timeline and detection window | Unknown |
| Number of affected projects or customers | Unknown |
Deliberately not invented
No CVE identifier, no affected-version range, no customer count, no quoted Railway spokesperson. If a source does not state it, a blog post should not manufacture it to sound authoritative. The gaps are themselves operationally meaningful, which is the next point.
Why the gap changes the rotation order
If the exposure was limited to application environment variables, your blast radius is whatever those variables unlock: databases, third-party APIs, signing keys. If platform API tokens or build artifacts were also in scope, the attacker could read variables and re-read them after your rotation, plus push a deploy. The first case is a credential rotation problem. The second is an account-takeover problem: resecure the platform account before touching application secrets, or you are rotating into a channel the attacker still controls.
Where a PaaS-Held Secret Actually Lives, and Who Can Read It
Enumerate the copies, not the one you set
- The runtime process environment of every service and every replica
- Build and deploy logs that echo env at startup (yes, this happens)
- Preview and PR-branch deployments that inherit variables from the parent environment
- The platform's own API and database, readable by anyone with project access
- A CLI session that pulled
.envdown to a laptop - Support or debug dumps, including stack traces that stringify config
The corollary: this audit is also an access review
Anyone with read access to your project's variables has precisely the access an attacker just obtained. If you would not hand that person the production database password in chat, they should not have the role that reveals it. The rotation exercise is a good excuse to list who holds that role and why.
Secret classes, ranked by how much a leak hurts
| Secret class | Realistic impact if leaked | Rotation difficulty | Short-lived credentials feasible? |
|---|---|---|---|
| App database password | Full read/write on app data | Low–medium | Partly — IAM/STS auth to the DB |
| Service-to-service API key | Lateral movement inside your own estate | Medium | Yes — mTLS or OIDC |
| JWT signing key | Forge a token for any user | High — invalidates state | No, but kid overlap mitigates |
| Webhook HMAC secret | Forge inbound events | Medium | Partly — accept two secrets during overlap |
| Cloud static access key | Whatever the IAM policy allows | Medium | Yes — OIDC federation |
| PaaS platform API token | Read every variable you just rotated | Medium | Prefer scoped, short session tokens |
Step 1: Inventory Every Copy of the Secret Before You Rotate
Why blind rotation fails
Rotate a credential you have not traced, and you will miss a consumer: a cron worker, a Lambda outside the platform, an analytics job someone set up in 2023. It breaks at 2 a.m., the on-call engineer rolls back to the old value to restore service, and now the leaked credential is live again. The inventory exists to prevent that rollback.
Non-destructive enumeration: names only
The public GraphQL endpoint returns a name→value map. Filter to keys before anything reaches your scrollback or CI log.
## Names only. Values never leave the pipe.
curl -s https://backboard.railway.app/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"query($p:String!,$e:String!,$s:String!){variables(projectId:$p,environmentId:$e,serviceId:$s)}","variables":{"p":"PROJECT_ID","e":"ENV_ID","s":"SERVICE_ID"}}' \
| jq -r '.data.variables | keys[]'
Expected output shape (names illustrative):
DATABASE_URL
REDIS_URL
STRIPE_SECRET_KEY
JWT_SIGNING_KEY_A
WEBHOOK_HMAC_SECRET
Two honesty notes: I did not run this against a live project while writing, so verify the field names against your CLI and API version; and the keys[] filter is the point — whatever the API returns, only names survive.
The ledger
| Secret name | Issuer | Consumers | Rotation mechanism | Revocation mechanism | Owner | Class |
|---|---|---|---|---|---|---|
DATABASE_URL | Managed Postgres | api, worker, migration job | ALTER ROLE ... PASSWORD | ALTER ROLE ... NOLOGIN on old role | data | rotatable-with-overlap |
STRIPE_SECRET_KEY | Stripe | api, billing-cron | Provider roll, two active keys | Revoke old key in dashboard | payments | rotatable-with-overlap |
JWT_SIGNING_KEY_A | You | api (sign + verify) | Publish new kid | Remove old key from verify set | platform | static |
Classify honestly
Mark each row static, rotatable-with-overlap, or cannot-rotate-cheaply. The last bucket — partner-issued credentials that need a support ticket, keys baked into shipped firmware, keys protecting already-encrypted data — is where real blast-radius work belongs. Everything in the middle bucket is just a Tuesday.
Step 2: Rotate in Dependency Order, With an Overlap Window
The two-phase pattern
Issue the new credential while the old one is still valid → deploy every consumer → confirm the new credential is in use → revoke the old one → confirm you get 401s only where you expect them.
Ordering heuristics
Upstream data stores first (a dead database password breaks everything downstream, so fix it while you still have a working fallback). Service-to-service credentials second. Signing keys last, because they invalidate state rather than access.
Worked example
Postgres password. Create a second role with identical grants, deploy with the new connection string, watch connection counts move from the old role to the new one, then ALTER ROLE old NOLOGIN. Overlap window: one deploy plus a safety margin. Failure symptom if you skip the overlap: FATAL: password authentication failed across all replicas simultaneously.
Service-to-service API key. Issue key B, deploy consumers with both keys configured, confirm logs show B in use, revoke A. Overlap: one deploy. Failure symptom: 401s between your own services that look like a network partition.
JWT signing key. Do not swap the secret. Publish a second key with a new kid, sign with the newest, verify against the set, and let old tokens expire naturally.
// Verify against a set keyed by kid; sign with the newest.
const keys = new Map([
["2026-10-a", process.env.JWT_KEY_2026_10_A],
["2026-10-b", process.env.JWT_KEY_2026_10_B], // newest
]);
export function verifyToken(token) {
const { header } = jwt.decode(token, { complete: true });
const secret = keys.get(header.kid);
if (!secret) throw new Error(`unknown kid: ${header.kid}`);
return jwt.verify(token, secret, { algorithms: ["HS256"] });
}
Overlap window: your maximum token lifetime, plus clock skew. Remove the old key only after that window closes, then redeploy.
Revoking a signing key or a webhook HMAC secret without an overlap logs every user out or silently drops inbound webhooks. The second one is worse: webhooks that fail HMAC verification are often dropped with a 2xx, so you get an outage that looks like a quiet week.
Step 3: Prove the Old Credentials Are Dead
Direct checks
Take the old value and use it. Expect rejection, not ambiguous failure.
| Check | Expected result | If it succeeds |
|---|---|---|
| Old DB password against the database | password authentication failed | Rotation never applied |
| Old API key against a harmless authenticated endpoint | 401 | Key not revoked at the issuer |
| Old JWT signing key minting a token | Rejected as unknown kid | Old key still in the verify set |
| Old webhook secret signing a test event | 403 from your endpoint | Dual-secret window still open |
An 401/403 is a pass. A 200 means you have a live copy of the leaked credential in production and should treat the incident as ongoing. A connection timeout is not a pass — it is an unverified result, and unverified is where leaks hide.
Indirect checks
Watch auth-failure rate, webhook delivery failures, and cron job alerts for 24–48 hours after revocation. A small, stable bump in 401s from a decommissioned client is expected; a rising line is a consumer you missed.
Hunt the stragglers
- Search deployed environments for the revoked variable name in every service, including ones you forgot existed.
- Grep CI configs and infrastructure-as-code for the old value's prefix (
sk_live_,postgres://, and so on) — never for the full value. - List preview and fork environments; they inherit variables and outlive the branch that created them.
- Ask the issuer for last-used telemetry. Cloud providers and payment APIs expose "last used" timestamps on keys, which is the strongest signal available that nothing is still holding the old value.
Write it down
A dated record of what changed, by whom, which verification ran, and what it returned. During the next incident, this is the artifact that tells you whether the credential in front of you is pre- or post-rotation.
Shrinking the Blast Radius So This Is Boring Next Time
Prefer short-lived credentials
OIDC workload identity federation replaces a static cloud key with a token minted per workload and expiring in minutes. A dump of your environment variables then yields credentials with a shelf life measured in minutes, not quarters.
Split secrets by blast radius
A preview deployment should never be able to read the production superuser connection string. Separate variable sets by environment and by privilege, so a read-only analytics key and a production write key are never in the same bundle.
Separate the platform trust boundary
Someone who can read production variables can exfiltrate them. Require SSO, assign the minimum role that lets people do their job, and make sure the platform account itself has an audit trail you can read.
Keep the ledger offline
If the platform is the thing that was compromised, a recovery plan that lives only inside the platform is not a recovery plan.
What Would Change This Advice
If platform tokens or build artifacts were exposed
Move platform-account credential rotation to the front: revoke sessions and API tokens, re-enable SSO enforcement, then audit deploy history for changes you did not make. Application secret rotation comes after, because otherwise the attacker can simply read the new values.
If exposure was limited to environment variables
The ordering above holds. The priority is verification, not panic: rotate in dependency order, prove the old values fail, and keep watching for 48 hours.
This branch is unresolved in the available source
The Rescana summary does not state the scope. What would confirm it: an official Railway statement or postmortem, a status-page incident entry, or a support communication to affected projects. Until one of those exists, treat the platform-token scenario as untested and plan for it, because the cost of planning is an hour and the cost of being wrong is a second breach.
Conclusion
Platform-held secrets are a shared failure domain. You cannot audit your way out of that, and faster incident response is not the fix — shorter-lived credentials, tighter scoping, and a written rotation ledger are. Those three things make the next PaaS breach a checklist instead of an all-nighter.
Today: build the ledger, then rotate your three highest-impact secrets in dependency order and verify each one with a direct check. If the old value still returns a 200, you are not done.
Further Reading
Rescana report on the Railway environment variable exposure
Primary source for this post: the Rescana report item, published 2026-10-23 (aggregator redirect).
Railway documentation on variables and environments
How values are scoped to services and environments, which determines what a preview deployment can read: docs.railway.com variables guide.
OIDC workload identity documentation
The mechanism for replacing static cloud keys with short-lived federated tokens: AWS IAM OIDC identity providers and Google Cloud workload identity federation.
NIST SP 800-57 and the OWASP Secrets Management Cheat Sheet
Key-lifecycle and rotation guidance beyond the emergency case: NIST SP 800-57 Part 1 Rev. 5 and the OWASP Secrets Management Cheat Sheet.


