Post

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:

  1. Implement dynamic blocking using firewallD (not just basic iptables)
  2. Create a custom 404 handler that logs attack patterns to a database
  3. Build a real-time Wall of Shame dashboard (like the example at shaji.in)
  4. Analyze attack patterns from logged data
  5. 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?

SolutionDynamic UpdatesState TrackingIPv6 SupportEase of Use
iptablesManualLimitedPartialComplex
UFWVia commandsBasicYesSimple
firewallDAPI-drivenFullNativeMedium
nftablesManual/APIFullNativeComplex

Key advantages for our use case:

  1. Runtime Updates: Add blocking rules without flushing entire rule sets
  2. Integrated Logging: --log-prefix "BlockedShameWall" in rich rules
  3. 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:

  1. IoT Recruitment: Compromised servers become C2 nodes for Mirai-like botnets
  2. Cryptojacking: Silent Monero miners using your resources
  3. Data Exfiltration: Your family photos aren’t safe if Nextcloud gets pwned

The Wall of Shame approach provides three-layer defense:

  1. Preventative: Blocks future requests from malicious IPs
  2. Detective: Logs attack patterns for analysis
  3. 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

  1. OS: Fedora 38+ / RHEL 9+ / Ubuntu 22.04 LTS
  2. Web Server: nginx 1.18+ or Apache 2.4.48+
  3. Database: PostgreSQL 14+ or MariaDB 10.6+
  4. 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

  1. 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;
  1. 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

  1. 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
    
  2. 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'
    
  3. 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

  1. Monthly Report Generation:
    1
    
    psql attack_logs -c "\COPY (SELECT * FROM attacks WHERE timestamp > NOW() - INTERVAL '30 DAYS') TO 'monthly_attacks.csv' CSV HEADER"
    
  2. 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={}
    
  3. 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
This post is licensed under CC BY 4.0 by the author.