· 9 min read
Railway lost its customers' secrets. The bots were already asking for yours.
Railway says someone got into the system that holds its customers' environment variables and decrypted some of them. On the Edge, one client had been asking websites for railway.json since October 12, nine days earlier, wearing eighteen crawler costumes and working through a checklist of hundreds of files. Railway was one line on it. Your secrets were the rest.
What happened at Railway
Railway is a hosting company: you give it your code and your settings, and it runs your app. The settings include environment variables, the place almost every app keeps its secrets: database passwords, Stripe keys, AI API keys, cloud credentials.
On Wednesday, October 21, Railway posted a security disclosure. It had detected "anomalous activity" that morning in the "systems that store customer environment variables." In its words, "an unauthorized third party may have accessed encrypted environment variables and, for some of them, obtained the decrypted contents."
Railway says it was "part of a series of attacks recently targeting platform companies with the aim of compromising AI inference providers." In plain words: someone wanted AI keys, and a hosting company is a place where a lot of them sit together.
Railway paused all deployments, told upstream providers to revoke keys, and asked every customer to "immediately roll all environment variables." On Thursday it followed up with posts on checking for unusual access in Google Cloud, AWS, GitHub, Stripe and OpenAI, and on rotating MySQL, Redis and MongoDB passwords.
The pause made it hard. On X, customers described the bind: rotate a key and the app breaks, because the new one can't be deployed; don't rotate and someone else may be using it. Others described AI and cloud bills climbing overnight before the email arrived.
That's the breach. Here's what it looks like from the other side of the internet.

What the Edge saw
The Edge is the proxy I run in front of more than 3,600 websites. I searched its logs for every request whose path mentions Railway, from October 1 to Thursday, October 22 at 16:30 UTC.
Three files were asked for:
| Path | Requests | Sites | First asked (UTC) |
|---|---|---|---|
/railway.toml |
901 | 288 | Oct 10, 09:34 |
/railway.json |
877 | 258 | Oct 12, 06:55 |
/.env.railway |
6 | 6 | Oct 14, 13:18 |
railway.json and railway.toml are Railway's deploy files: how to build the app, how to start it, where its health check lives. They usually sit in the root of a project's code. They're not meant to be served on the website.
The part I didn't expect is the timeline.
| Day (UTC) | Requests | Sites |
|---|---|---|
| Oct 10 | 31 | 31 |
| Oct 12 | 130 | 33 |
| Oct 13 | 62 | 26 |
| Oct 14 | 100 | 43 |
| Oct 15 | 58 | 26 |
| Oct 16 | 256 | 76 |
| Oct 17 | 293 | 91 |
| Oct 18 | 120 | 51 |
| Oct 19 | 146 | 28 |
| Oct 20 | 187 | 60 |
| Oct 21 (the disclosure) | 262 | 92 |
| Oct 22 (to 16:30) | 139 | 50 |
The busiest day was October 17, four days before Railway said anything. The breach day comes second. Whoever this is didn't start asking because of the breach. They were already asking.
I can't tell you whether this client has anything to do with the Railway breach. The dates overlap, and that's all I can say about it.
One client, eighteen costumes

Of the 1,784 requests, 1,739 came from one client. I know it's one because every one of them opened its connection the same way. Its TLS fingerprint (JA4) is t13d1516h2_8daaf6152771_d8a2da3f94cd.
It doesn't look like one client:
- 525 IP addresses, all in Google's cloud network (AS396982), spread across nine countries: the US, Brazil, Canada, Singapore, Belgium, the Netherlands, Taiwan, Germany and Japan.
- Eighteen different crawler names in its user agent. It said it was Googlebot 118 times, ClaudeBot 114, Amazonbot 71, YouBot 66, Meta's fetcher 62, and ChatGPT-User, GPTBot, PerplexityBot, CCBot, Applebot, Bytespider, bingbot and more in between. The other 606 requests said they were a normal browser.
- None of its requests came from a published crawler IP range. Not one. Real Googlebot doesn't run on rented Google Cloud servers, and real Amazonbot doesn't either.
Changing your name on every visit is a costume. A TLS fingerprint is the walk, and the walk gives it away.
Railway is one line on a long list

Then I looked at what else this client asked for. For each of its 805 visits that included a Railway file, I took every request the same address made to the same site in the ten minutes around it.
A visit was a median of 444 requests, from 164 to 1,739. And it's nearly the same list every time. It opens politely with / and /robots.txt, then:
- Every hosting platform's deploy file, not just Railway's:
vercel.json,netlify.toml,fly.toml,wrangler.toml,supabase/config.toml,.firebaserc,.amplifyrc. - Every way to name a
.envfile:.env,.env.local,.env.production,.env.production.local,.env.staging.local,.env.preprod,.env.keys,.env.secret,.env.credentialsand more. - Keys to whole machines and accounts:
.ssh/id_rsa,.ssh/id_ed25519,.config/gcloud/application_default_credentials.json,.azure/accessTokens.json,.cache/huggingface/token,.vault-token,.pgpass,.my.cnf,.cargo/credentials.toml. - Tricks to read files a site never meant to serve: paths starting
/@fs/, which ask a Vite development server for any file on its disk, including/@fs/proc/self/environ(the running process's own environment variables) and Kubernetes service account tokens. And Windows tricks like/.env::$DATA. - GraphQL endpoints at
/graphql,/api/graphqland/v1/graphql.
So it isn't hunting Railway. It's hunting the same thing the Railway attacker took: environment variables. Railway's breach went after them from the platform's side. This client asks for them at the front door of every site it can find.
Railway's files aren't even special on its list. Since October 1, here's how often the Edge was asked for each platform's deploy file, and how much of that was this one client:
| File | Requests | Sites | From this client |
|---|---|---|---|
/vercel.json |
936 | 297 | 864 |
/wrangler.toml |
916 | 275 | 858 |
/netlify.toml |
913 | 285 | 867 |
/railway.toml |
901 | 288 | 869 |
/fly.toml |
886 | 269 | 866 |
/supabase/config.toml |
879 | 260 | 871 |
/railway.json |
877 | 258 | 870 |
They're all within a few dozen of each other. Every platform gets the same knock.
For scale: /.env alone was asked for 85,530 times on 3,614 sites in the same window, by everybody. This client is a small, tidy part of a very big crowd.
What it might mean
Some of this is fact and some is my reading of it. I'll say which is which.
Fact: the bots want the same thing the attacker wanted. API keys and database passwords are worth money right now, and AI keys most of all. I wrote about the AI key hunt after OpenCode's breach last week. It didn't stop, it widened.
My reading: a deploy file is a label. If railway.json comes back from your site, a bot now knows two things: your app runs on Railway, and your site serves files from your project folder. The second one is the bad one, because the .env request is right behind it on the list. After a breach, a list of "sites on platform X" is also useful for less technical things, like convincing "please rotate your keys" emails.
Fact: the hunt started before the breach. Whatever this client is, it didn't need Railway's news to start asking. That's the uncomfortable lesson: a breach makes headlines for a day, and the scanning carries on every day.
My reading: a site that says yes to everything can't tell you what it's leaking. Plenty of sites answer every unknown path with their home page and a 200. That's usually harmless. But when everything says 200, neither the bot nor you can tell, from the log alone, the one request that really did send a file.
It's on GitHub too
Out of curiosity, I searched GitHub's public code on Thursday. There were 43.8k public files named railway.json and 36.9k named railway.toml.
Most are harmless build settings. But 944 of the railway.json files mention words like SECRET, PASSWORD, API_KEY, TOKEN or DATABASE_URL. Most of those are placeholders or references like ${{Postgres.DATABASE_URL}}, which is how it should be done. A few have real values pasted straight in, including at least one full database connection string with its password.
A deploy file is code. If it's in a public repository, assume everyone has read it, including bots that read GitHub for a living.
What to do
If you're on Railway

- Revoke keys where they were issued. Go to Stripe, your AI provider, your cloud console, your database host, and revoke the old key there. That kills every copy, including one someone else holds, even while your deploys are paused. Then put the new key in when you can deploy.
- Don't just add a new key. If the old one is still active, it still works for whoever has it.
- Read the usage logs, not your app's logs. A stolen key is used directly against the provider, so your own app looks perfectly quiet. Railway's follow-up posts walk through Google Cloud, AWS, GitHub, Stripe and OpenAI.
- Rotate database passwords, and check old projects too. One developer reported that variables they had deleted long ago came back from Railway's API.
- Next time, make the blast smaller. One key per app, the narrowest permissions the provider offers, and a hard spending cap with an alert. A leaked key with a $50 limit is a bad afternoon, not a bad month.
Check your own site
This applies wherever you host. Run this against your own domain:
for p in .env .env.local .env.production railway.json railway.toml vercel.json netlify.toml fly.toml wrangler.toml supabase/config.toml docker-compose.yml .git/config; do
printf '%-22s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code} %{size_download}B' "https://example.com/$p")"
done
A 404 or 403 is what you want. If anything says 200, open it in a browser. If it's your home page, your site answers everything with 200 (fine, but harder to watch). If it's the file, take it down, then rotate everything in it.
Serve only what you meant to serve
- Point the web root at your build or public folder, not your project folder. With PHP's built-in server that's
-t public. With Express it'sexpress.static('public'), neverexpress.static('.'). Static hosts should publishdist/orbuild/, not the repository. - Never put a development server on the internet. The
/@fs/requests only work against a dev server, and there are clearly enough of them out there to be worth asking. - Block dotfiles and deploy files at your proxy, so a mistake behind it can't leak them. In nginx:
location ~ /\.(?!well-known/) { deny all; }
location ~* ^/(railway\.(json|toml)|vercel\.json|netlify\.toml|fly\.toml|wrangler\.toml|supabase/config\.toml|docker-compose\.ya?ml|Dockerfile)$ { deny all; }
- Check crawlers by their address, not their name. The big ones publish their IP ranges. A "Googlebot" from a rented cloud server is not Googlebot.
- Keep secrets out of deploy files in your repo. Use the platform's variable references instead of values.
I'll keep watching this client's fingerprint, and I'll write it up if it changes what it asks for.
The bots didn't need Railway's breach to come asking for your secrets. They've been asking all along. Make sure the answer is no.
I'm @MrNiceTry on X.
How I counted: requests in the Mr. Nice Try Edge's logs for the sites behind it, October 1 to October 22, 2026 at 16:30 UTC. "Railway files" are paths containing "railway". A "visit" is every request from the same address to the same site within ten minutes of a Railway request; the checklist is the paths that appeared in nearly all 805 visits. "Real crawler" means an address inside a crawler operator's published IP ranges. GitHub counts are from GitHub code search on October 22. Sources: Railway's security disclosure and its October 22 follow-ups on the Railway blog.