Bots Kept Hitting My Server So I Built A Wall Of Shame
Bots Kept Hitting My Server So I Built A Wall Of Shame
Introduction
When you expose a server to the internet - even a modest home lab setup - within minutes, automated scanners start knocking on every port and endpoint. These bots probe for vulnerabilities like unpatched CVEs, exposed admin interfaces, or default credentials. The experience described in our opening Reddit post mirrors what countless system administrators face daily: 8,000+ malicious requests in a month targeting everything from WordPress XML-RPC endpoints to Redis unauthorized access attempts.
This isn’t just about nuisance traffic - these probes are reconnaissance for credential stuffing, ransomware deployment, and botnet recruitment. The 2023 Verizon DBIR reports that 74% of all breaches start with web application vulnerabilities, many discovered through automated scanning. While enterprises have WAFs and SIEMs, homelab enthusiasts often lack enterprise-grade tooling.
In this comprehensive guide, you’ll learn how to:
- Implement dynamic blocking using firewallD (not just basic iptables)
- Create a custom 404 handler that logs attack patterns to a database
- Build a real-time Wall of Shame dashboard (like the example at shaji.in)
- Analyze attack patterns from logged data
- Harden your defenses against common CVE probes
This approach transforms passive defense into active intelligence gathering - turning attackers’ noise into your security telemetry. Let’s turn your server from a target into a honeypot-lite that feeds your firewall.
Understanding the Technology Stack
firewallD: Dynamic Defense for Modern Systems
firewallD (daemon) provides a dynamic firewall management solution with D-Bus interface, replacing static iptables rules. Unlike its predecessor system-config-firewall, it introduces key concepts:
- Zones: Predefined trust levels (public, dmz, home, etc.)
- Services: Protocol/port definitions (e.g., “ssh” = tcp/22)
- Rich Rules: Context-aware filtering (source IP, port, rate limiting)
Why firewallD Over Alternatives?
Solution | Dynamic Updates | State Tracking | IPv6 Support | Ease of Use |
---|---|---|---|---|
iptables | Manual | Limited | Partial | Complex |
UFW | Via commands | Basic | Yes | Simple |
firewallD | API-driven | Full | Native | Medium |
nftables | Manual/API | Full | Native | Complex |
Key advantages for our use case:
- Runtime Updates: Add blocking rules without flushing entire rule sets
- Integrated Logging:
--log-prefix "BlockedShameWall"
in rich rules - Time-Based Rules: Automatically remove blocks after X hours
The Attack Logging Pipeline
1
2
3
4
5
6
┌──────────────┐ ┌─────────────┐ ┌─────────────┐
Request → │ Web Server │ → 404 │ Custom 404 │ → Log →│ PostgreSQL │
│ (nginx/httpd)│ │ Handler │ │ / MySQL │
└──────────────┘ └─────────────┘ └─────────────┘
↑ ↓
└─────────── firewallD Block ────────────┘
Why This Matters for Homelabs
Enterprise security teams have budgets for Cloudflare, CrowdStrike, and dedicated SOC analysts. Your Raspberry Pi or decommissioned enterprise server doesn’t. Yet the same attackers target both:
- IoT Recruitment: Compromised servers become C2 nodes for Mirai-like botnets
- Cryptojacking: Silent Monero miners using your resources
- Data Exfiltration: Your family photos aren’t safe if Nextcloud gets pwned
The Wall of Shame approach provides three-layer defense:
- Preventative: Blocks future requests from malicious IPs
- Detective: Logs attack patterns for analysis
- Psychological: Public shaming discourages repeat attacks (optional)
Prerequisites
Hardware Requirements
The Reddit poster’s setup (Fedora Rawhide, 16GB RAM, 12TB storage, i5 4th Gen) is overkill. Minimum viable specs:
- CPU: Any x86_64 or ARMv8+ (Raspberry Pi 4 works)
- RAM: 2GB+ (for DB + web server)
- Storage: 10GB+ (logs grow quickly!)
Software Requirements
- OS: Fedora 38+ / RHEL 9+ / Ubuntu 22.04 LTS
- Web Server: nginx 1.18+ or Apache 2.4.48+
- Database: PostgreSQL 14+ or MariaDB 10.6+
- Firewall: firewallD 1.0+ (pre-installed on Fedora/RHEL)
Security Preparation
Network Isolation:
1
2
3
4
# Create a dedicated VLAN for exposed services
sudo nmcli con add type vlan ifname vlan42 dev eth0 id 42
sudo nmcli con mod vlan42 ipv4.addresses '192.168.42.2/24'
sudo nmcli con up vlan42
User Privileges:
1
2
3
# Non-root user with sudo for firewallD access
sudo useradd -m -G wheel firewall_logger
sudo passwd firewall_logger
Firewall Pre-Check:
1
2
3
4
5
6
# Verify firewallD is active
sudo systemctl status firewalld
# If inactive:
sudo systemctl enable --now firewalld
sudo firewall-cmd --state # Should return 'running'
Installation & Setup
Step 1: Configure firewallD for Dynamic Blocking
First, create a new zone for blocked IPs:
1
2
sudo firewall-cmd --permanent --new-zone=blocked_ips
sudo firewall-cmd --reload
Set default zone policies:
1
2
sudo firewall-cmd --zone=blocked_ips --set-target=DROP
sudo firewall-cmd --permanent --zone=blocked_ips --set-target=DROP
Add logging to blocked requests:
1
2
sudo firewall-cmd --zone=blocked_ips --add-rich-rule='rule family="ipv4" source address="0.0.0.0/0" log prefix="BLOCKED_SHAME_WALL" level="notice"'
sudo firewall-cmd --runtime-to-permanent
Step 2: Custom 404 Handler (PHP Example)
Create /var/www/html/404.php
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
// Log attacker details
$attacker_ip = $_SERVER['REMOTE_ADDR'];
$request_uri = $_SERVER['REQUEST_URI'];
$user_agent = $_SERVER['HTTP_USER_AGENT'];
$timestamp = date('Y-m-d H:i:s');
// Identify common attack patterns
$cve_patterns = [
'/wp-login.php' => 'CVE-2023-1234 (WordPress Brute Force)',
'/solr/admin/cores' => 'CVE-2019-0193 (Apache SolR RCE)',
'/.env' => 'Laravel Config Exposure'
];
$cve = 'Unknown';
foreach ($cve_patterns as $pattern => $description) {
if (strpos($request_uri, $pattern) !== false) {
$cve = $description;
break;
}
}
// Insert into PostgreSQL
$pdo = new PDO('pgsql:host=localhost;dbname=attack_logs', 'logger', 'SecurePassword123!');
$stmt = $pdo->prepare("INSERT INTO attacks (ip, uri, user_agent, cve, timestamp) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$attacker_ip, $request_uri, $user_agent, $cve, $timestamp]);
// Block IP in firewallD
exec("sudo firewall-cmd --zone=blocked_ips --add-source=$attacker_ip --timeout=48h");
// Return 404
http_response_code(404);
echo "Resource not found";
?>
Security Note: Run web server as limited user with sudo access only to firewallD:
1
2
# Create sudoers rule
echo "www-data ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --zone=blocked_ips --add-source=*" | sudo tee /etc/sudoers.d/www-firewall
Step 3: Database Schema (PostgreSQL)
Create attack_logs
database:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE DATABASE attack_logs;
\c attack_logs
CREATE TABLE attacks (
id SERIAL PRIMARY KEY,
ip INET NOT NULL,
uri TEXT,
user_agent TEXT,
cve VARCHAR(255),
timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_attacks_ip ON attacks USING gist (ip inet_ops);
CREATE INDEX idx_attacks_timestamp ON attacks (timestamp);
Step 4: Configure Web Server
nginx (/etc/nginx/sites-available/default
):
1
2
3
4
5
6
7
8
9
10
server {
error_page 404 /404.php;
location = /404.php {
internal;
root /var/www/html;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Apache (/etc/apache2/sites-available/000-default.conf
):
1
ErrorDocument 404 /404.php
Step 5: Verification Tests
- Trigger Test 404: ```bash curl -I http://yourserver.com/nonexistent-page
Should return 404 and block your IP temporarily
Check logs:
sudo journalctl -u firewalld -g BLOCKED_SHAME_WALL
1
2
3
4
2. **Database Check**:
```sql
SELECT * FROM attacks ORDER BY timestamp DESC LIMIT 5;
- Firewall Rule Verification:
1
sudo firewall-cmd --zone=blocked_ips --list-sources
Configuration & Optimization
firewallD Tuning
Rate Limiting to prevent false positives:
1
2
# Allow 5 connections/min before blocking
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="0.0.0.0/0" service name="http" limit value="5/m" accept'
Whitelist Your IP:
1
2
sudo firewall-cmd --zone=trusted --add-source=your.ip.address.here/32
sudo firewall-cmd --zone=trusted --add-service=http
Database Optimization
Partitioning by Time (PostgreSQL):
1
2
3
CREATE TABLE attacks_2023 (
CHECK (timestamp >= DATE '2023-01-01' AND timestamp < DATE '2024-01-01')
) INHERITS (attacks);
Autovacuum Settings (postgresql.conf
):
1
2
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02
Security Hardening
- Web Server Sandboxing:
1 2
# Create systemd scope sudo systemd-run --uid=www-data --gid=www-data --scope -p MemoryMax=500M -p CPUQuota=50% /usr/sbin/nginx
- Database Firewalling:
1 2 3
# Only allow localhost connections sudo firewall-cmd --zone=trusted --remove-service=postgresql sudo firewall-cmd --zone=trusted --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="5432" protocol="tcp" accept'
- Log Sanitization:
1 2 3
// In 404.php before DB insert: $request_uri = substr(filter_var($request_uri, FILTER_SANITIZE_STRING), 0, 2048); $user_agent = substr(filter_var($user_agent, FILTER_SANITIZE_STRING), 0, 512);
Usage & Operations
Daily Monitoring Commands
Top Attackers:
1
2
3
4
5
6
SELECT ip, COUNT(*) AS attempts
FROM attacks
WHERE timestamp > NOW() - INTERVAL '24 HOURS'
GROUP BY ip
ORDER BY attempts DESC
LIMIT 10;
firewallD Block List:
1
sudo firewall-cmd --zone=blocked_ips --list-all
Log Analysis:
1
2
# Count attacks in last hour
sudo journalctl -u firewalld --since "1 hour ago" -g BLOCKED_SHAME_WALL | wc -l
Maintenance Tasks
- Monthly Report Generation:
1
psql attack_logs -c "\COPY (SELECT * FROM attacks WHERE timestamp > NOW() - INTERVAL '30 DAYS') TO 'monthly_attacks.csv' CSV HEADER"
- Block List Rotation:
1 2
# Remove blocks older than 30 days sudo firewall-cmd --zone=blocked_ips --list-sources | awk '{print $1}' | xargs -I {} sudo firewall-cmd --zone=blocked_ips --remove-source={}
- Performance Optimization:
1 2
# Reindex database monthly psql attack_logs -c "REINDEX DATABASE attack_logs;"
Troubleshooting
Common Issues & Solutions
1. firewallD Rules Not Persisting
- Cause: Missing
--permanent
flag or failed reload - Fix:
1 2
sudo firewall-cmd --runtime-to-permanent sudo systemctl restart firewalld
2. Database Connection Refused
- Cause: PostgreSQL not listening on local socket
- Diagnosis:
1
sudo -u postgres psql -c "SHOW listen_addresses;" # Should include 'localhost'
3. False Positives Blocking Legit Users
- Solution: Implement whitelist and rate limiting
1
sudo firewall-cmd --zone=trusted --add-source=legit.ip.here/32
Debugging Commands
Trace firewallD Decisions:
1
2
sudo firewall-cmd --set-log-denied=all
sudo tail -f /var/log/firewalld
Test 404 Handler Manually:
1
curl -X POST http://localhost/404.php -d "test=malicious"
Check Web Server Errors:
1