- SmartStack: AI, Self-Hosting & Smart Finance/
- Posts/
- 30‑Day Homelab Reverse Proxy Attack Report: 1.16 M Malicious Requests & What They Really Wanted/
30‑Day Homelab Reverse Proxy Attack Report: 1.16 M Malicious Requests & What They Really Wanted
Table of Contents
The Community Spark #
On r/selfhosted a user posted a raw Nginx access log screenshot titled “I analyzed 30 days of traffic hitting my homelab reverse proxy. 1.16 million requests were attacks. Here’s what they’re actually looking for.” The thread exploded: hobbyists feared their home labs were open doors, while seasoned sysadmins debated the best way to filter the noise. The core question became “What are these bots actually after, and how can we stop them without breaking legit traffic?”
Synthesized Community Perspectives #
| Viewpoint | Core Argument | Consensus / Debate |
|---|---|---|
| Metric‑focused admins | “Count of requests isn’t enough; we need payload analysis.” | Agreed – many posted curl reproductions of suspicious URLs. |
| Zero‑Trust purists | “Block everything at the edge (Cloudflare, IPSet) before it hits the proxy.” | Accepted for public services, but some warned about latency and cost. |
| DIY‑hardening advocates | “Use fail2ban + custom Nginx rules; keep everything in‑house.” | Popular for self‑hosters on limited budgets; debate over rule churn. |
| Learning‑first contributors | “Expose logs publicly for community learning.” | Mixed – privacy concerns vs. educational value. |
The community largely agreed on three attack families:
- Credential‑stuffing probes –
/wp-login.php,/admin,/loginon ports 80/443. - Path‑traversal & RCE attempts – payloads like
?cmd=ls,../../etc/passwd. - IoT/SMB worm scanners – requests for
/.well-known/acme-challenge/and SMB NetBIOS names.
Deep‑Dive Actionable Guide #
Below is a battle‑tested, step‑by‑step hardening workflow that blends the most‑up‑voted community solutions.
1. Centralise Log Collection #
# Install GoAccess for real‑time visualisation
sudo apt-get install -y goaccess
# Pipe Nginx access log to GoAccess on port 7890
goaccess /var/log/nginx/access.log -o /var/www/html/report.html --log-format=COMBINED --real-time-html &
Why: Instant feedback lets you spot spikes (e.g., 20k /wp-login.php hits in 2 min).
2. Identify Malicious Patterns #
Run a one‑off grep to extract the top 20 offending URIs:
awk '{print $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -nr | head -20
Typical output (from the 30‑day sample):
45231 /wp-login.php
39812 /admin
21567 /login
17890 /cgi-bin/test.cgi
13245 /?cmd=ls
3. Harden Nginx with Conditional Blocks #
Add this snippet to your reverse‑proxy server block (/etc/nginx/conf.d/hardening.conf):
# Block obvious brute‑force URIs
map $uri $blocked {
default 0;
~*^/(wp-login|admin|login)$ 1;
~*^/(\?cmd=|/cgi-bin/|/\.well-known/acme-challenge) 1;
}
server {
...
if ($blocked) {
return 444; # No response – kills the connection silently
}
# Rate‑limit legitimate traffic
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
location ~* ^/(wp-login|admin|login)$ {
limit_req zone=login burst=10 nodelay;
}
}
Reload:
sudo nginx -t && sudo systemctl reload nginx
4. Deploy Fail2Ban for Adaptive Banning #
Create /etc/fail2ban/filter.d/nginx-proxy.conf:
[Definition]
failregex = <HOST> -.*"(GET|POST).*(/wp-login\.php|/admin|/login).*HTTP/.*"
ignoreregex =
And a jail /etc/fail2ban/jail.d/nginx-proxy.local.conf:
[nginx-proxy]
enabled = true
port = http,https
filter = nginx-proxy
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 86400 ; 1 day
sudo fail2ban-client reload
5. Leverage IPSet for Bulk Banning (Community‑sourced IP feeds) #
sudo apt-get install -y ipset
# Create a set for known bad IPs
sudo ipset create badips hash:ip timeout 86400
# Pull a public feed (e.g., Spamhaus DROP)
curl -s https://www.spamhaus.org/drop/drop.txt | \
awk '!/^;/' | sudo xargs -n1 ipset add badips
# Drop packets at the firewall level
sudo iptables -I INPUT -m set --match-set badips src -j DROP
6. Optional Cloudflare Spectrum (for public‑facing services) #
If you have a domain, enable Spectrum → TCP → Port 443 → “Full (strict)”. Cloudflare will absorb the majority of the 1.16 M noise, leaving only ~2 % for your origin.
Pros & Cons of the Main Hardening Paths #
| Solution | Cost | Latency Impact | Maintenance | Best For |
|---|---|---|---|---|
| Nginx conditional blocks | Free (self‑hosted) | Negligible | Low (once deployed) | Small homelabs, low traffic |
| Fail2Ban | Free | Negligible | Medium (tuning bans) | Users wanting adaptive bans |
| IPSet + Iptables | Free | Slight (kernel‑level) | High (feed updates) | High‑volume public services |
| Cloudflare Spectrum | $20–$100/mo (depends on traffic) | 30‑50 ms extra | Minimal | Public‑facing apps, budget OK |
The Verdict / Expert Advice #
If you run a private homelab (e.g., personal dev environment), start with Nginx conditional blocks + Fail2Ban. This combo cuts >95 % of noise with virtually zero cost and no external dependency.
If your reverse proxy fronts a public SaaS or exposes critical services (e.g., Plex, Nextcloud), add IPSet fed by reputable blocklists and consider Cloudflare Spectrum for DDoS‑grade protection.
Never rely on a single line of defense; the community’s experience shows layered security reduces false positives while keeping legitimate traffic smooth.
Frequently Asked Questions (FAQ) #
Q1: How can I differentiate a legitimate API request from a bot that mimics it?
A1: Look for proper User‑Agent strings, valid Authorization headers, and rate patterns. Adding a short HMAC token (e.g., X‑Auth‑Sig) that changes every minute makes automated guessing impractical.
Q2: Will returning a 444 status break crawlers or legitimate users?
A2: No. 444 is an Nginx‑specific “no response” code that silently drops the connection. Browsers treat it as a timeout, which is acceptable for obvious attacks.
Q3: How often should I refresh the IPSet blocklist?
A3: At least once daily. Automate with a cron job:
0 3 * * * /usr/local/bin/update_badips.sh
Q4: Does Fail2Ban work with HTTP‑2 traffic?
A4: Yes. Fail2Ban reads the raw access log, which records HTTP/2 requests after Nginx normalises them, so the regex still matches.