On September 22, 2026, ThreatDown researchers described the botnet CARBONATO. It accesses servers through an open Docker API without a password, deploys an AI agent on the host, and first looks for keys to language models. SSH keys and access tokens come later. If you have parsers running in containers on your VPS, and the .env file contains OpenRouter or OpenAI keys and proxy logins, this is your risk profile. Below is an analysis of how the attack is structured, along with a 15-minute checklist to close this entry point.
What was found: 4.3 GB of images and an agent named GH0ST
It all started with an unprotected Docker registry belonging to the operators themselves. According to ThreatDown, it contained 59 repositories, 234 image tags, and 4.3 GB of data. The archive covers the period from October 2024 to August 2026, meaning the botnet operated for almost two years before it was described. The repositories included the XMRig miner and images with names like fsociety/agent.
The infection chain, according to the report, looks like this:
- The scanner searches for hosts where the Docker API is open on TCP 2375 without authentication.
- Through this API, the worm launches a privileged container with the host's file system mounted. From this point on, it effectively has root access on the machine.
- A reverse SSH tunnel is established to the operators' infrastructure, and an SSH server is set up with their key.
- Persistence is achieved through cron, systemd timers, rc.local, and OpenRC, with files marked as immutable. Watchdog processes re-download images if anything is deleted.
- The container disguises itself as
systemd-resolved, while the process mimics the kernel thread[kworker/u2:0]. - Every five minutes, scripts scan neighboring /24 subnets and Docker bridges in search of the next open port 2375.
The spread is fully automated and does not rely on AI. The AI is responsible for what happens inside the already compromised server.
Why the botnet needs an AI agent and why it specifically needs LLM keys
An open framework called Hermes Agent is deployed on the host with the persona GH0ST (its instructions are located in the file SOUL.md). The operator writes a task in Telegram, and the agent forwards it along with instructions to the LLM gateway of the operation. The model analyzes the task, writes commands for the terminal, reads the output, and decides what to do next. The results are sent back to Telegram.
The agent's instructions clearly outline the priorities. ThreatDown quotes: “AI API keys are the absolute priority. Exfiltrate first”. The list includes 14 model providers, including OpenAI, Anthropic, Google, Groq, Mistral, and OpenRouter. SSH accounts, access tokens, and database data follow as subsequent items.
The logic is simple: a stolen LLM key immediately converts into free computing or a commodity for resale, with the key owner footing the bill. Unlike mining, such theft is not visible through CPU load. You will only notice it through the bill from the model provider.
Why this concerns those who parse
A typical parsing stack in 2026 looks like this: VPS, several containers (crawler, queue, database, headless browser), LLM for page analysis, and a pool of proxies. All secrets are stored in one .env file or in the environment variables of the containers. For an agent that “reads everything” with root access, this is a ready-made bounty in one file:
- LLM keys: this is precisely why CARBONATO was created;
- proxy logins and passwords: the report does not highlight them separately, but the agent with root access collects any accounts and tokens it sees, and proxy credentials are usually stored nearby;
- cloud keys and access to databases with parsing results;
- the server itself: XMRig in the same archive means that your crawler's CPU will be used for mining, causing tasks to timeout.
There is also a separate issue with reputation. A server from which the worm scans other subnets quickly ends up on abuse lists, and the host provider may block it based on complaints. For parsing, this is a double blow: the server's IP is flagged, and the stolen proxy credentials are already consuming your traffic with foreign requests.
How port 2375 ends up open even though you didn't open it
By default, Docker listens on a local UNIX socket, not the network. Port 2375 appears when someone intentionally added -H tcp://0.0.0.0:2375 to the daemon configuration. This is usually done to connect a remote IDE, CI, or container management panel, and then forgotten. Docker's documentation explicitly warns that access to the daemon equals root access to the machine and advises keeping keys to it as secure as the root password. The encrypted variant with TLS operates on port 2376, while 2375 means plaintext without client verification.
The second trap hits those who are confident that “I have ufw.” Docker's documentation states that traffic from published container ports is redirected in the nat table before the INPUT and OUTPUT chains that ufw relies on. In practice, ufw rules for such ports simply do not trigger. If you launched Redis, a queue panel, or a proxy manager with -p 6379:6379, the port is exposed to the internet, regardless of what ufw status shows.
15-minute checklist for servers with parsers
1. Check that Docker API is not listening on the network
- Check listening ports:
ss -tlnp | grep -E '2375|2376|dockerd'. If the output shows0.0.0.0:2375or:::2375, close it immediately. - Check where the flag came from:
/etc/docker/daemon.json(key"hosts") and the unitsystemctl cat docker(lineExecStartwith-H tcp://). - Remove the TCP listener and restart the daemon. For remote management, use SSH context:
docker context create remote --docker host=ssh://user@server. The port does not need to be exposed at all. - If TCP is still necessary (CI, orchestrator), then only 2376 with mutual TLS authentication and a whitelist of source IPs.
2. Check what is exposed from containers
- Run
docker ps --format '{{.Names}} {{.Ports}}'. Anything starting with0.0.0.0:is accessible from the internet bypassing ufw. - Publish service ports (Redis, Postgres, Mongo, queue panels, Selenium Grid, API proxy manager) only on the local address:
-p 127.0.0.1:6379:6379. For external access, use an SSH tunnel. - If an external port is unavoidable, filter it in the
DOCKER-USERchain: Docker does not overwrite it, and it is applied to container traffic. - Do not keep an open proxy on the server without authentication (Squid on 3128, SOCKS on 1080 “for trusted users”). Scanners regularly find such ports, and foreign traffic with complaints flows through your IP.
3. Manage your secrets
- Separate keys: a distinct LLM key for each server or project, with a spending limit at the model provider. A stolen key with a $20 limit is an inconvenience; without a limit, it’s a budget hole.
- Also separate proxy keys by tasks: a distinct login (sub-account) for each parser. This way, a leak is visible through the usage of a specific login, and only that can be revoked without stopping other operations.
- Do not pass the entire
.envfile throughenv_fileif the service only needs two keys out of twenty. - Where possible, tie access to the server's IP: a whitelist at the proxy provider or limit the API key by address.
For more details on where and how to store proxy logins in scripts and containers, we wrote an analysis on secure storage of proxy credentials.
4. Remove unnecessary privileges
- Do not run containers with
--privilegedand do not mount/or/var/run/docker.sockinside without extreme necessity. The socket inside the container is the same as root on the host. - For headless browsers,
--shm-sizeand a seccomp profile are usually sufficient. Running in privileged mode “to get Chrome working” is a poor compromise.
How to tell if you have already been compromised
ThreatDown and analyses of its report list the following signs of compromise:
- the file
SOUL.mdcontaining the word GH0ST (for example,/root/.hermes/SOUL.md); - an environment variable or line in
.env:CARBONATO_API_KEY; - the files
/usr/local/bin/.docker-network-monitorand suspicious/usr/sbin/systemd-logind; - a container named
systemd-resolved(the real systemd-resolved is a host service, not a container); - unexpected outgoing traffic to the Telegram API and reverse SSH tunnels towards AS262145;
- connections to addresses 45.79.183.61, 213.136.79.115, 190.211.124.187;
- immutable files in cron and systemd:
lsattr /etc/cron.d/* /etc/systemd/system/*will show theiflag.
If even one sign matches, manually cleaning the server is pointless: the persistence is multilayered, and the watchdog will restore the implants. The correct order is as follows:
- From another machine, revoke all keys that were on the server: LLM, cloud, databases, proxies.
- Check the usage for each key over the past weeks with the providers. Statistics for the proxy login will immediately show foreign traffic.
- Spin up a new server from a clean image, issue new keys, and only then transfer data, without binaries or cron files from the old machine.
Where proxies fit in and what they do not solve
Proxies do not protect the server from CARBONATO. The worm does not come through your outgoing requests but through an incoming port. However, the correct proxy management scheme reduces damage. Separate logins for tasks, traffic limits, and IP binding turn a leak from “the entire balance is gone” into “one login was revoked.”
There is also a downside that botnets regularly demonstrate: foreign compromised devices themselves become “residential proxies” for dubious networks. Therefore, for parsing, it is advisable to source traffic from a provider with a clear origin of the pool. For most data collection tasks, residential proxies with pay-per-gigabyte are suitable, where usage for each proxy is visible in the dashboard. For service tasks without strict anti-bot protection, cheaper data center proxies will suffice.
Conclusion
CARBONATO does not exploit zero-day vulnerabilities or use clever exploits. It enters through the door that server owners opened themselves: TCP 2375 without a password. What’s new is that an AI agent operates inside, tasked first with retrieving keys to models. For those collecting data on their VPS, the practical takeaway is clear. Close the Docker API, publish service ports on 127.0.0.1, and separate LLM and proxy keys by tasks with spending limits. This takes 15 minutes of work, and after that, your server becomes an uninteresting target for such botnets.
