Part 4 – Fortifying the Gates with Nginx & Fail2ban

So far on this journey, we’ve operated in the shadows. We acquired our digital real estate and forged unbreakable keys in Part 1: So You Want a Digital Kingdom?. We then went dark, hardening the rig’s core systems and establishing a stealth operational alias in Part 2: Hardening the Rig & Going Dark. Finally, we constructed a completely off-grid communications tunnel and deployed our private command console in Part 3: Building the Operations Wing & Going Off-Grid.

Your server is a digital black site—secure, silent, and invisible. But a fortress is useless without a gate. It’s time to establish a public-facing presence, but we’ll do it on our terms.

In this guide, we’re installing the master gatekeeper: Nginx. It will act as a reverse proxy, the only part of our system that speaks to the public internet. Then, we’ll deploy an automated sentry, Fail2ban, to watch over it with lethal efficiency. This is how you turn a hardened shell into a functional and secure command center.


Phase 1: Conscripting the Gatekeeper (Nginx) 🛡️

Our internal services, running securely in Docker containers, should never be exposed directly to the internet. We need a trusted intermediary. Nginx is a high-performance web server that excels as a reverse proxy. It will catch all incoming web traffic, inspect it, and route it to the correct internal service, all while masking the true architecture of our command center.

First, we’ll install Nginx from the official repositories.

# Update package lists and install nginx
sudo apt update
sudo apt install -y nginx

# Ensure the service starts on boot and is running now
sudo systemctl enable --now nginx

With our gatekeeper installed, we must open the main blast doors just enough to let web traffic through. The Uncomplicated Firewall (UFW) has a pre-built profile for this.

# Allow HTTP (80) and HTTPS (443) traffic through the firewall
sudo ufw allow 'Nginx Full'
sudo ufw reload

Nginx is now online, but it’s running a generic, welcoming configuration. We don’t do “welcoming.” We do “silent and intimidating.”


Phase 2: The Silent Wall – Hardening the Default Configuration

By default, anyone who navigates to your server’s public IP address will see a default Nginx page. This is sloppy operational security. It confirms a web server is active and invites automated scanners to start probing for weaknesses.

Our command center will do the opposite. Any request made directly to our IP, or to a subdomain we haven’t explicitly authorized, will be met with absolute silence. For this, we use the return 444; directive—a special Nginx command that closes the connection without sending a single byte in response. To a scanner, it looks like the server simply doesn’t exist. It’s a ghost.

First, dismantle the default Nginx site configuration.

# Remove the default symlink to avoid conflicts
sudo rm /etc/nginx/sites-enabled/default

Now, we build our own silent wall. Create a new default configuration file.

sudo nano /etc/nginx/sites-available/00-default.conf

Paste in the following blueprint. This block is set as the default_server for all web traffic, catching anything that doesn’t match a specific site we’ll configure later.

# This is the catch-all server block.
# It catches all requests that do not match any other server_name.
# It immediately closes the connection, preventing host header attacks and direct IP access.

server {
    # Listen on port 80 and 443 as the default server for all IPs.
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    # An underscore is a special value for a catch-all server name.
    server_name _;

    # Log these attempts so Fail2ban can see them.
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log warn;

    # Use a generic self-signed "snakeoil" certificate for the SSL handshake.
    # The connection will be dropped immediately anyway.
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    # Immediately and silently close the connection.
    return 444;
}

This configuration requires a dummy SSL certificate to function. Generate one now.

# Generate a self-signed key and certificate valid for 10 years
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
    -keyout /etc/ssl/private/ssl-cert-snakeoil.key \
    -out /etc/ssl/certs/ssl-cert-snakeoil.pem \
    -subj "/C=US/ST=State/L=City/O=Default/CN=localhost"

Finally, activate the new configuration, test it for syntax errors, and reload Nginx.

# Enable the site and test the configuration
sudo ln -s /etc/nginx/sites-available/00-default.conf /etc/nginx/sites-enabled/
sudo nginx -t

# If the test is successful, reload Nginx to apply the changes
sudo systemctl reload nginx

The trap is set. Any uninvited guest will now be met with a digital void.


Phase 3: Activating the Automated Sentry 🤖

In Part 2, we configured Fail2ban to guard our SSH port. Now, we’ll expand its duties to monitor our new gatekeeper. It will watch the Nginx logs for suspicious patterns and automatically ban offending IPs at the firewall.

The Rules of Engagement

First, we create custom “filters” to teach Fail2ban how to spot trouble. We’ll create three: one for generic 444 errors, one for high-confidence exploit scans, and another for malformed or unusual requests.

Create the filter files one by one.

sudo nano /etc/fail2ban/filter.d/nginx-444-errors.conf
[Definition]
failregex = ^<HOST> - .* ".*" 444 .*$
ignoreregex =
sudo nano /etc/fail2ban/filter.d/nginx-exploit-probes.conf
[Definition]
failregex = (?i)^<HOST> -.*"(?:GET|POST|HEAD|PUT) /(?:\.env|\.git/config|phpinfo\.php|_ignition/execute-solution|vendor/phpunit/phpunit/src/Util/PHP/eval-stdin\.php|cgi-bin/.*sh|solr/admin) .* (?:400|401|403|404|444) .*$
ignoreregex =
sudo nano /etc/fail2ban/filter.d/nginx-general-probes.conf
[Definition]
failregex = ^<HOST> -.*"([A-Z_]+) .*" (400|401|403|404|418|444) .*$
            ^<HOST> -.*"\\x16.*" (400) .*$
ignoreregex =

Critical Safety Briefing: Whitelist Your Command Post

Before we arm these new rules, we must ensure our sentry doesn’t mistake the commander for the enemy. A single mistake while testing could get your own IP address banned. To prevent this, we’ll add your home or office IP address to Fail2ban’s ignoreip list.

From your local computer, find your public IP address. You can do this easily by visiting a site like ipchicken.com in your browser, or by running a command in your terminal:

# This command will return only your public IP address
curl icanhazip.com

Once you have your IP, copy it. We will now add it to the main Fail2ban configuration file, which also houses the “jails” for our new filters and the powerful recidive jail—a final punishment for repeat offenders.

sudo nano /etc/fail2ban/jail.local

Find the ignoreip line near the top, under [DEFAULT], and add your IP address to the end of the line (with a space separating it from the previous entry). Then, add the new jails to the bottom of the file.

[DEFAULT]
# Add your IP to this line to prevent locking yourself out!
ignoreip = 127.0.0.1/8 ::1 YOUR_HOME_IP_HERE
bantime = 24h
findtime = 60m
maxretry = 5

# ... other default settings ...

#
# JAILS
#

# ... your existing [sshd] jail ...

[sshd]
enabled = true
# ...

# Jail for any client that generates a high volume of 444 errors.
[nginx-444-errors]
enabled  = true
port     = http,https
filter   = nginx-444-errors
logpath  = /var/log/nginx/access.log
maxretry = 15
findtime = 60m
bantime  = 24h

# Jail for high-confidence vulnerability scans. Ban immediately.
[nginx-exploit-probes]
enabled  = true
port     = http,https
filter   = nginx-exploit-probes
logpath  = /var/log/nginx/access.log
maxretry = 1
findtime = 60m
bantime  = 1d

# Jail for malformed requests or those using weird methods.
[nginx-general-probes]
enabled  = true
port     = http,https
filter   = nginx-general-probes
logpath  = /var/log/nginx/access.log
maxretry = 5
findtime = 1440m
bantime  = 1h

# Recidive jail (Super-ban for repeat offenders).
[recidive]
enabled  = true
logpath  = /var/log/fail2ban.log
findtime = 3d
bantime  = 32d
maxretry = 3

With your IP safely whitelisted, restart Fail2ban to arm the new rules.

sudo systemctl restart fail2ban

The automated sentry is now fully operational, with an escalating response system and—most importantly—orders to never fire on the commander.


The Stage is Set for Operations

Your command center’s defenses are layered and intelligent. Nginx stands as a silent, formidable gatekeeper, and Fail2ban watches its back, ready to neutralize any threat that lingers too long. The perimeter is secure.

In our next installment, Part 5, we will finally bring our operations online. We’ll deploy our first Dockerized applications, configure Nginx to proxy traffic to them, create proactive blocking rules, and use Certbot to secure their domains with valid SSL certificates. The real work begins now.


Comments

2 responses to “Part 4 – Fortifying the Gates with Nginx & Fail2ban”

  1. […] The kingdom is built. The walls are high. The gates are secret. Your work is done. For now. Lets move on to part 4 Fortifying the Gates with Nginx & Fail2ban. […]

  2. […] Part 4: Fortifying the Gates with Nginx & Fail2ban We installed the master gatekeeper and its automated guard. […]

Leave a Reply

Your email address will not be published. Required fields are marked *