Anatomy of an SSH Botnet: 121,222 Attacks on One Server in 7 Days
We build and then operate the software we ship. That second half is the part most studios hand back with the source code, and it is where this article comes from: Appluex runs the production infrastructure behind the applications we build, which means we also read the logs, answer the pages, and audit the machines. This is one of those audits, published in full.
It started with a client asking a simple question about one of our production servers: is the origin IP address exposed anywhere? It is a good question, because a server behind a CDN loses most of its protection the moment its real address leaks. We went looking, confirmed the address was properly hidden, and then found something else entirely.
In seven days, that one machine absorbed 121,222 failed SSH login attempts from 925 distinct IP addresses. Not a targeted attack. The ordinary background radiation of the internet, which is far heavier than most people running a server imagine.
What follows is the complete measurement rather than a summary of it: where the traffic came from, how we traced the largest cluster to a named company through public registry data, why two unrelated servers were guessed at with two different wordlists, why none of it worked, and the one thing that genuinely was wrong. Every number is from our own logs. Where a figure could be read two ways, we say so.
We are publishing it because the numbers surprised us, and because “use SSH keys” is advice everyone has heard and few people have seen the evidence for. Here is the evidence.
The numbers
| Measurement | Value |
|---|---|
| Window measured | 7 days |
| Failed authentication attempts | 121,222 |
| Failed attempts, most recent 24h | 21,272 |
| Distinct source IP addresses | 925 |
| Distinct usernames attempted | 4,201 |
| Attempts against root alone | 26,419 |
| Successful password logins, all time | 0 |
The last row is the one that matters, and we will come back to it. Not zero for the week. Zero for the entire life of the machine.
A second server, measured the same day
Everything above comes from one machine. We then measured a second, unrelated server on the same day: different provider region, different purpose, different software, no shared DNS name, nothing publicly connecting the two. If this traffic were aimed at anyone, the two should look nothing alike.
| Measure | First server | Second server |
|---|---|---|
| Window measured | 7 days | ~28 hours |
| Failed attempts | 121,222 | 22,479 |
| Implied daily rate | ~17,300 | ~19,300 |
| Distinct source IPs | 925 | 1,830 |
| Distinct usernames tried | 4,201 | 3,178 |
| Successful password logins | 0 | 0 |
The daily rates land within about 10% of each other. two machines that share nothing are being hit at the same rate, which is the clearest evidence available that nobody chose either of them. The second server actually saw twice as many distinct addresses in a quarter of the time, so its attacker pool was broader and shallower.
And the same individual machines turned up at both. Two addresses in 2.57.121.0/24 attacked the first server and the second. That is not two campaigns that resemble each other, it is one campaign that found both.
But they were guessing different things
Here the two diverge, and it is the most interesting result in the whole exercise. The first server was probed with a generic infrastructure list: deploy, ansible, puppet, chef, oracle, minecraft. The second was probed with a cryptocurrency list:
| Username | Attempts | Username | Attempts |
|---|---|---|---|
admin | 1,445 | bitcoin | 157 |
ubuntu | 417 | solana | 138 |
wallet | 292 | ethereum | 125 |
crypto | 216 | blockchain | 120 |
user | 203 | ubnt | 70 |
Those six crypto names are 1,048 attempts, and they barely register on the first server. No Linux distribution creates any of them. They are accounts a person makes when they run a node, a trading bot or an exchange integration, and somebody is betting that a machine with such an account has a weak password on it. ubnt is a different bet again: the Ubiquiti router default, aimed at consumer network gear still on factory credentials.
So the botnet is not running one script everywhere. It is running variants, and a given server gets whichever variant reaches it.
The single setting that removed a server from most of the campaign
On the first server, root was targeted 26,419 times. On the second, once.
The difference is one line: the second server has PermitRootLogin no. When sshd refuses root outright, the attempt never becomes a password guess, and the host stops being worth that part of the campaign at all. It is a useful reminder that the cheapest settings are not merely defensive, they remove you from whole categories of traffic.
It is not one attacker. It is rented infrastructure
Of the traffic we could attribute, 84.6% came from datacenter IP addresses rather than residential connections. This is the single most useful fact in the whole dataset. These are not infected home computers. They are virtual machines somebody rented, by the dozen, specifically to run this.
You can see it in how the addresses cluster. Attacks did not arrive from 925 scattered places. They arrived in blocks:
| Network | Hosts | Attempts | Announced by | Registered |
|---|---|---|---|---|
109.160.32.0/24 | 25 | 51,785 | AS197170 TechTies Inc. | US address, Bulgarian LIR |
77.239.124.0/24 | 5 | 12,187 | AS198364 Banatsync SRL | FR, UK org |
81.17.159.0/24 | 1 | 8,622 | AS215540 Global Connectivity Solutions | DK, UK org |
144.225.6.0/24 | 1 | 4,398 | AS7488 CNServer LLC | US |
47.251.115.0/24 | 1 | 3,994 | AS45102 Alibaba Cloud US | US |
139.99.125.0/24 | 1 | 1,866 | AS16276 OVH SAS | SG |
2.57.121.0/24 | 2 | 1,854 | AS47890 Unmanaged Ltd | GB |
One netblock, 109.160.32.0/24, produced 51,785 attempts from 25 separate hosts inside the same /24. That is 43% of everything, from what is effectively one shelf in one datacenter. Someone rented 25 addresses in a single block and pointed all of them at the internet.
Following the biggest cluster to a real company
Registry data is public, and it goes further than most people expect. Tracing that /24 through RIPE:
- The block is named GBTCloud and announced by AS197170, TechTies Inc.
- Its registered address is in Cornelius, North Carolina.
- Its maintainer objects resolve to ORG-GCN3-RIPE, Global Communication Net Plc, a RIPE Local Internet Registry in Sofia, Bulgaria.
- That organisation does publish a working abuse contact:
[email protected]. - The same block additionally carries a maintainer belonging to QuickPacket LLC.
So the single largest source of attacks against this server is a US-addressed netblock, announced by a US autonomous system, sponsored by a Bulgarian registry, with a third US hosting company on the maintainer list. None of that is illegal. All of it makes a complaint harder to land, which is rather the point of arranging things that way.
The second cluster tells a similar story with a twist worth noting: 77.239.124.0/24 (netname RocketCloud, organisation Rocket & Marinica Ltd, registered in the UK, geolocated to France) is maintained by an object belonging to Ace Data Center. That is the same sponsor behind 144.225.6.0/24, the fourth largest cluster. Two apparently unrelated attack sources, one upstream.
What they were guessing
4,201 distinct usernames were tried. The distribution tells you exactly what this is: a commodity wordlist, unchanged in years, sprayed at everything.
| Username | Attempts | Username | Attempts |
|---|---|---|---|
admin | 1,572 | ubuntu | 1,468 |
user | 949 | oracle | 443 |
test | 430 | debian | 417 |
deploy | 391 | dev | 233 |
developer | 214 | bitrix | 195 |
claude | 150 | ansible | 145 |
git | 139 | guest | 134 |
minecraft | 123 | steam | 121 |
Look at what is in there. ubuntu and debian, the default accounts cloud images ship with. deploy, ansible, puppet, chef and git, the service accounts that automation tends to create and nobody tends to audit. oracle and bitrix, from enterprise software with a long history of default credentials. minecraft and steam, because game servers are famously run by people who are not sysadmins.
And claude, attempted 150 times. The wordlists have started including the names of AI tools, on the reasonable assumption that somebody, somewhere, created a service account called that and gave it a weak password.
The usernames tell you the intent
That list is not random, and the categories in it map cleanly onto what the operator wants:
- Default cloud accounts (
ubuntu,debian,pi). A bet that the image's default user still has a password on it. - Automation service accounts (
deploy,deployer,ansible,puppet,chef,git). This is the valuable category, and it is worth understanding why. Those accounts usually exist, frequently hold sudo or deployment keys, and are often created once with a shared password that nobody ever rotates. A compromiseddeployaccount is not one server. It is the pipeline, and everything that pipeline pushes to. - Abandoned development leftovers (
test,dev,student,guest,aptest,aa,a). Accounts created for one afternoon and never removed. - Names belonging to no Linux distribution (
binux,wkh,panda). These are not defaults anywhere. The reasonable reading is that they are persistence accounts left behind by earlier malware, which means part of this campaign is not looking for secure servers to break into. It is looking for servers somebody else has already broken into.
A second campaign, running in parallel, after credentials
SSH was only half of it. The web logs on the same host showed a separate automated effort with a much more specific goal: 474 requests hunting for secrets files, across 104 distinct path variants of a single filename.
/.env /v1/.env /v2/.env
/staging/.env /production/.env /portal/.env
/secrets.env /ses.env /sendgrid.env
/uploads../.env /userfiles/x?path=../../.env
/.git/HEAD /phpinfo.php /wp-admin/install.phpRead the filenames rather than the paths. ses.env and sendgrid.env are named after specific email delivery providers. They are not fishing generically for configuration. They want email-sending credentials, because a stolen SES or SendGrid key is monetisable the same day as a spam relay operating on somebody else's sender reputation.
The rest fills in the picture. The ../ variants are directory traversal against known framework bugs. The .git/HEAD requests check whether the site was deployed by copying a repository onto the server, which would expose its entire source history including whatever was committed by accident. phpinfo.php and the WordPress paths are checking what the stack is.
The through line across both campaigns is the same: the goal is credentials, not the server. The server is just where credentials are kept.
Why: what a compromised server is actually worth
It is tempting to assume anyone attacking a small server has a specific grudge. Almost nobody does. There is a commodity market for compromised hosts, and a machine is worth taking for reasons that have nothing to do with who owns it:
- Cryptomining. The lowest-effort option. Your CPU, their coins, your electricity and your hosting bill. Usually throttled to avoid the obvious tell of a server pinned at 100%.
- Expanding the botnet. A compromised host is put to work scanning for more hosts. This is almost certainly why 84.6% of the addresses attacking us were datacenter machines rather than home computers: a large share of the attackers are themselves previous victims, conscripted into looking for the next one. The campaign is self-funding in machines.
- Residential and datacenter proxy resale. Bandwidth is sold on to services that need traffic to come from somewhere unremarkable: ad fraud, scraping, credential stuffing against other people. You become the return address for somebody else's abuse.
- Spam and phishing relay. The reason those
.envprobes name email providers. A server with a clean IP reputation, or better, a stolen key to an established email service, is worth real money to a phishing operation. - Pivoting into the cloud account, which is the expensive one. Modern application servers keep provider credentials on disk so the application can use them. An attacker who reads those does not have a server any more, they have an account: the ability to spin up instances on your billing, reach storage buckets, and in the worst arrangement, reach the backups of the very data they just encrypted. This is where a nuisance becomes a company-ending event, and it is the reason the credential hunting above is more dangerous than the brute force.
None of this requires anyone to have heard of you. That is the part worth internalising: you are not being targeted, you are being enumerated, and the economics work at a scale where a tiny success rate across millions of hosts is a profitable business.
The volume is increasing
Daily totals across the window: 15,448, then 14,986, 16,024, 16,735, 18,339, 17,285, and 21,764 on the final full day. That is a 41% increase from the first day to the last. Whatever this is, it is not winding down.
Why none of it worked
Zero successful password logins, ever, against 121,222 attempts in a week. Not because the password was strong. Because every login on that machine had always used an SSH key, and a key cannot be guessed by a wordlist. A brute-force campaign against key-based authentication is not a race the attacker can win slowly; it is a race they cannot enter.
But here is the part worth being honest about, because it is the actual lesson. The server still had password authentication enabled, and it still permitted root login. Nobody used passwords. They simply remained possible. Every one of those 21,272 daily attempts was knocking on a door that was unlocked in principle and merely unused in practice.
That is a very common state, and it is worth naming precisely: enormous care had gone into hiding the server behind a CDN, locking its web ports to the CDN's address ranges, and keeping the origin address out of DNS. All of that protects port 443. Meanwhile port 22 sat open to the entire internet, accepting passwords, for root.
The fix, and the two details that are easy to get wrong
The remedy is unglamorous: disable password authentication, restrict root to key-only, install fail2ban. It took about ten minutes. Two details are worth stating because both fail quietly.
First: drop-in files are read in order, and the first value wins. On Ubuntu, cloud-init writes /etc/ssh/sshd_config.d/50-cloud-init.conf containing PasswordAuthentication yes. SSH takes the first value it obtains for a keyword, so a hardening file named 90-hardening.conf is read afterwards and silently ignored. The file has to sort before the cloud-init one. Ours is numbered 10. Config that looks right, validates cleanly, and does nothing is worse than no config at all.
Second: PermitRootLogin prohibit-password, not no. The naming is genuinely confusing. prohibit-password still allows root to log in by key, which is how most deployment pipelines connect. no blocks root entirely, and if your automation logs in as root, you have just locked yourself out of your own server from a terminal that will not reconnect.
Which leads to the procedure, because editing SSH config over SSH is how people lose access to production:
- Back up the existing config first.
- Validate with
sshd -tbefore reloading anything. - Arm an automatic rollback: schedule a job to remove your change and reload in five minutes.
- Use
reload, notrestart, so existing sessions survive. - Open a brand new connection and prove it works.
- Only then cancel the rollback.
Step three is the one people skip. It converts “I am locked out of production” into “I wait five minutes.”
A false alarm worth publishing
During the intrusion scan, the SUID audit flagged /usr/lib/cargo/bin/su, /usr/lib/cargo/bin/sudo, /usr/bin/sudo.ws, and a set of binaries named gnuuname, gnuid, gnumv and similar. Privileged binaries in an unexpected directory, plus renamed copies of core utilities, is close to a textbook description of a rootkit.
They are completely legitimate. Ubuntu 26.04 ships sudo-rs and the uutils Rust rewrite of coreutils, which take the default command names through the alternatives system and rename the GNU originals with a gnu prefix. One command settles it:
dpkg -S /usr/lib/cargo/bin/sudo sudo-rs: /usr/lib/cargo/bin/sudoIf a file belongs to a package, it arrived through the package manager. We are publishing this because the pattern will trip a lot of people over the next year, and because checking ownership before raising an alarm is the difference between a security audit and a fire drill.
Reporting it
Abuse reports to hosting providers are worth sending, with realistic expectations. Contacts we were able to resolve from registry data:
| Network / operator | Abuse contact |
|---|---|
| Global Communication Net Plc (sponsor of the largest cluster) | [email protected] |
| Ace Data Center (RocketCloud, CNServer blocks) | [email protected] |
| Global Connectivity Solutions | [email protected] |
| Alibaba Cloud | [email protected] |
| OVH | [email protected] |
| TechTies | [email protected] |
Include the source IP, timestamps in UTC, and a handful of raw log lines. Keep it factual. A provider that wants to act can act on that; a provider that does not will not be persuaded by adjectives.
One line we did not cross, and would advise against crossing: we did not scan, probe or connect back to any of these hosts. That is unauthorised access to third-party systems regardless of what they did first, and a meaningful share of attacking machines are themselves victims. Everything in this article came from our own logs and public registry data.
What to actually do about it
If you run a server with a public SSH port, this traffic is already hitting you. The checklist is short:
- Turn off password authentication. This single change makes the entire campaign irrelevant.
- Confirm your hardening config is actually being read, given the drop-in ordering trap above.
sshd -Tprints the values in effect. Trust that, not the file you edited. - Root by key only, using
prohibit-password. - Install fail2ban and put your own address on the ignore list. Ours banned two hosts within twenty seconds of starting.
- Read your auth logs once. Most operators have never looked, and the volume is genuinely surprising the first time.
- If you use a CDN, verify the origin is unreachable directly, and that your allowlist of CDN ranges is current rather than copied once and left to rot.
The reassuring conclusion is that the defence here is old, boring and completely effective. 121,222 attempts, 925 hosts, four thousand usernames, an escalating campaign across ten countries, and it was all defeated in advance by using SSH keys. The attackers are not sophisticated. They are patient and numerous, and they are counting on the small number of servers that still accept a password.
Why we publish this sort of thing
An audit is only worth what it changes. This one produced a short list of fixes on our own infrastructure and we made every one of them the same day, which is the standard we hold client infrastructure to as well. Shipping an application is the first half of the job. Knowing what is happening to it at three in the morning, six months later, is the half that decides whether it stays up.
If you are running production software and have never read your own authentication logs, that is the single cheapest hour you can spend this month. If you would rather someone did it properly, that is work we do — for applications we built and for applications we did not. You might also want phishing-resistant authentication, which is the same argument applied to your users instead of your servers.
Methodology: figures were measured from the authentication logs of two production Linux hosts we operate, over a seven day window and a twenty-eight hour window respectively, both ending 14 September 2026, and enriched with public IP geolocation and RIPE/ARIN registry records. Attribution stops at public registry data: we did not scan, probe or connect back to any attacking host, which would be unauthorised access to third-party systems regardless of what they did first, and a meaningful share of those machines are themselves victims. Details identifying our hosts, their networks or their administrators are deliberately omitted.
Frequently asked questions
How many SSH attacks does a normal server get?
More than most operators expect. A single production host with no public profile absorbed 121,222 failed SSH login attempts from 925 distinct IP addresses in seven days, around 200 per minute sustained, and the daily volume rose 41% across the window. This is untargeted background scanning, not a directed attack.
Are SSH brute-force attacks coming from hacked home computers?
Mostly no. In this dataset 84.6% of attributable traffic came from datacenter IP addresses rather than residential connections, and the addresses arrived in tight blocks: 25 hosts inside a single /24 produced 43% of all traffic. These are rented virtual machines, not an infected consumer botnet.
Does disabling SSH password authentication actually stop them?
Completely. Across 121,222 attempts there were zero successful password logins, because every account used key-based authentication and a key cannot be guessed from a wordlist. Turning password authentication off makes the entire campaign irrelevant rather than merely slower.
Why is my sshd hardening config being ignored?
Almost always drop-in ordering. SSH takes the first value it obtains for a keyword, and on Ubuntu cloud-init writes /etc/ssh/sshd_config.d/50-cloud-init.conf setting PasswordAuthentication yes. A hardening file numbered above 50 is read afterwards and silently ignored. Name it below 50 and verify with sshd -T, which prints the values actually in effect.
What are SSH brute-force attackers actually looking for?
Credentials, not the server itself. The username list they guess is weighted toward automation service accounts like deploy, ansible, puppet and git, because those often hold sudo rights or deployment keys, which turns one server into a whole pipeline. A parallel campaign against the same host made 474 HTTP requests across 104 path variants hunting for .env secrets files, including ones named after specific email providers such as ses.env and sendgrid.env.
What do attackers do with a compromised server?
There is a commodity market for hosts. The common uses are cryptomining, conscripting the machine into scanning for more victims (which is why 84.6% of the attacking addresses in this dataset were datacenter machines, many of them previous victims), reselling its bandwidth as proxy traffic for ad fraud and scraping, running spam and phishing relays, and reading cloud provider credentials off disk to pivot into the billing account. That last one is the expensive outcome, because it can reach storage and backups.
Why would anyone attack a small server nobody has heard of?
Nobody chose it. This is untargeted enumeration of the whole address space, and the economics work because a tiny success rate across millions of hosts is profitable. A meaningful share of the attacking machines are themselves compromised servers that were recruited to scan for the next one, which makes the campaign self-funding in hardware.
Is PermitRootLogin no safer than prohibit-password?
Safer, but it will lock you out if your deployment automation connects as root. prohibit-password still permits root to authenticate by SSH key while refusing passwords, which is the setting most infrastructure actually needs.
Thinking about building this?
Appluex designs and ships production mobile & web apps. Including AI features. Let's talk.