Mr. Nice Try Notes from the edge

· 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.

A train station platform with a bank of luggage lockers, one forced open with keys spilling across the floor. A little robot sprints away with an overstuffed key ring while Mr. Nice Try stands beside the broken locker with a coffee, one eyebrow raised.

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.

1,784requests for Railway's files
290sites asked
97%from one client
0from a real crawler

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

Six identical little robots queue up to a front door, each in a different flimsy disguise (a fake moustache, a delivery cap, a top hat, nose-glasses, a chef's hat, a beret), each dragging the same long scroll. Mr. Nice Try waits in the doorway with a clipboard; the footprints they leave on the path are all identical.

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

Mr. Nice Try holds up a very long checklist scroll that unrolls across the floor, ticking the first boxes with a coral pen.

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 .env file: .env, .env.local, .env.production, .env.production.local, .env.staging.local, .env.preprod, .env.keys, .env.secret, .env.credentials and 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/graphql and /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

Mr. Nice Try gives a smug thumbs up to a front door covered in padlocks and deadbolts, while a little robot lifts the doormat to reveal a key underneath.

  1. 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.
  2. Don't just add a new key. If the old one is still active, it still works for whoever has it.
  3. 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.
  4. Rotate database passwords, and check old projects too. One developer reported that variables they had deleted long ago came back from Railway's API.
  5. 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's express.static('public'), never express.static('.'). Static hosts should publish dist/ or build/, 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.