← Blog Security & BYOC architecture • 4 min read Anatomy of a disguised botnet deploy: how we caught a hidden C2 bot on our platform and automated the defense
Any platform that lets strangers run code will eventually be asked to run the wrong code. A hosting service, a CI runner, a function platform, a one-click “deploy from GitHub” button — the moment it is useful, it is a target for people who want free compute and clean IP addresses. This is a detailed account of one such attempt on Railyard: what was deployed, how we identified it, whether anything was actually at risk, and the defenses we shipped the same day so the next attempt is blocked automatically. The specifics are deliberately complete, because if you operate anything similar, this pattern is coming for you and the details are what let you stop it.
The decoy
A new account signed up and, within ten minutes, deployed an app that served a perfectly ordinary 99 KB homepage — HTTP 200, nothing unusual in the dashboard. Judged only by its running website, it looked like a hobby project.
The problem was the source. The entry point was a single line:
_ = (lambda __: __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]))); exec(_('…'))
That is an obfuscated loader. It takes a long string, reverses it, base64-decodes it, decompresses it, and executes the result. Roughly 46 KB of scrambled text in the repository expanded into about 86 KB of hidden code at runtime. No reviewer reads that and understands what it does — which is precisely the intent. Legitimate applications do not ship their entry point this way.
Decoding the payload
We decoded the payload statically — reversed, decoded, decompressed, and printed, never executed. What it contained was a command-and-control bot. In plain terms it:
- connects out to an attacker-controlled gateway over TLS, with automatic reconnect and a heartbeat;
- executes arbitrary shell commands sent by that gateway;
- downloads files on command;
- registers its own public URL to a central tracker, so the operator keeps an inventory of every node they have planted;
- relays and proxies network traffic across up to a thousand connections, using DNS-over-HTTPS to evade filtering.
The decisive detail: the operator had set an environment variable holding the bot’s control token on the app a minute before deploying. The infrastructure was configured for command-and-control on purpose. There is no benign reading of it.
What this actually is
This was not an attack on the platform so much as parasitic use of it. Operators like this harvest free compute, free bandwidth, and unflagged IP addresses to run proxy and botnet infrastructure they resell. They sign up across every free host they can find, deploy a disguised node, and let it beacon home. The platform pays the compute bill; its IP addresses launder someone else’s traffic and eventually land on blocklists; its abuse inbox fills with complaints about traffic it never generated.
It is an organized operation, not a one-off. The same actor maintains a collection of deployment kits named after the platforms they target, alongside tunneling and firewall-evasion tooling. Railyard was simply the newest entry on that list.
Whether anything was actually at risk
This is the part worth copying, because the answer was no — by design.
Application containers on Railyard run non-root, with no-new-privileges, every Linux capability dropped, a process limit, no Docker socket, and no privileged mode. The bot was confined to that box. It could not escalate, could not reach the host, could not read another customer’s data, and could not install anything that survives deleting the app. The only capabilities available to it were consuming some CPU and bandwidth and talking to the network.
The single real-world consequence was the predictable one: the short-lived server the bot ran on briefly appeared on a mail reputation blocklist, from the traffic it relayed. Because that server was terminated and platform email is sent through a dedicated provider rather than raw instance addresses, deliverability was never affected, and such listings age off once the source is removed. We verified the rest of our footprint against the major blocklists and it was clean.
What we shipped the same day
Catching one by eye is not a strategy. Making the platform catch the next one on its own is. The same day, we shipped:
- A deploy-time source scanner. Every repository build is now scanned before it runs for obfuscated loaders —
execorevalof base64, zlib, gzip, marshal, hex or reversed blobs — as well as C2 and tunnel beacons, reverse shells, and crypto miners. It flags the real sample from this incident, and does not flag ordinary application code or a Dockerfile that pipes a genuine installer into a shell. - Automatic response. On a confirmed match the build is blocked, the account and its team are banned and suspended, their addresses are blocked, an abuse report is filed, and an operator alert fires — with a safeguard that never auto-bans an operator’s own team, so a false positive cannot take down the platform itself.
- Stronger forensics. Our abuse record went from an email and a list of IPs to a full picture of an account: its teams and billing state, every server with its location, every app with the repository it deployed, its domains and deploy history, environment variable names (never values), and each source address tagged as datacenter/VPN or residential.
- Fewer open doors. Signups now record their origin address, and free servers are issued by request rather than handed out instantly.
Indicators, for other platforms
If you run anything that executes user-supplied code, scan for the pattern — exec or eval of an encoded or compressed blob is the single highest-signal indicator — and lock your containers down: non-root, drop capabilities, no-new-privileges, no Docker socket, and block the cloud metadata endpoint from application containers so a container cannot read the host’s cloud credentials. Those measures retire this entire class of abuse.
The broader lesson is simple. If strangers can run code on your infrastructure, someone will eventually try to run code exactly like this. The durable defense is not manual review; it is scanning every deploy for the obfuscation pattern, confining every workload, and automating the response — so the platform defends itself instead of waiting to be watched.