Deploying Fail2ban in Docker

Fail2ban monitors log files for repeated authentication failures and blocks malicious IP addresses at the firewall layer. Running Fail2ban in a Docker container allows it to protect both host-level SSH access and containerized web applications like Nginx Proxy Manager without installing additional packages directly on the host system.

Prerequisites

The following items are required:

  • Ubuntu or Debian host with Docker Engine and Docker Compose installed
  • Nginx Proxy Manager container already deployed
  • Root or sudo access to create directories and manage iptables rules
  • Basic familiarity with iptables firewall concepts

How Containerized Fail2ban Accesses Host Resources

A Fail2ban container requires access to two host-level resources to enforce bans across the entire server:

  • Host log files mounted read-only into the container so Fail2ban filters can parse authentication attempts in real time
  • Host iptables firewall accessed through network_mode: host and Linux capabilities NET_ADMIN and NET_RAW, allowing the container to insert DROP or REJECT rules directly into active iptables chains

Create Directory Structure

Create a dedicated directory structure for Fail2ban configuration files, custom filters, and persistent state data:

mkdir -p /opt/containers/fail2ban/config/jail.d
mkdir -p /opt/containers/fail2ban/config/filter.d
mkdir -p /opt/containers/fail2ban/data

Configure Main Jail Settings

Create /opt/containers/fail2ban/config/jail.local with global defaults and jail definitions:

[DEFAULT]
# Ignore local subnets and internal Docker bridge networks
ignoreip = 127.0.0.1/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16

# Ban duration: 1 hour, evaluation window: 10 mins, max retries: 5
bantime  = 1h
findtime = 10m
maxretry = 5

# Firewall backend action using host iptables
banaction = iptables-multiport
banaction_allports = iptables-allports

# Host SSH Protection
[sshd]
enabled  = true
port     = ssh
logpath  = /var/log/host_logs/auth.log
backend  = polling

# Nginx Proxy Manager Bad Requests and Auth Failures
[npm-auth]
enabled  = true
port     = http,https
filter   = npm-auth
logpath  = /var/log/npm_logs/*access.log
maxretry = 3
findtime = 5m
bantime  = 2h
backend  = polling

The sshd jail monitors the host auth.log file for SSH authentication failures. The npm-auth jail watches Nginx Proxy Manager access logs for HTTP 401, 403, and 400 status codes that indicate unauthorized access attempts or malicious probes.

Create Custom Nginx Proxy Manager Filter

Create /opt/containers/fail2ban/config/filter.d/npm-auth.conf to match HTTP error codes in Nginx Proxy Manager access logs:

[Definition]
# Match 401/403 responses and unauthorized API access attempts in NPM access logs
failregex = ^<HOST> - .* "(GET|POST|HEAD|PUT|DELETE) .* HTTP/.*" (401|403) .*$
            ^<HOST> - .* ".*" 400 .*$

ignoreregex =

The failregex pattern captures the client IP address and matches lines containing HTTP status codes 401 (Unauthorized), 403 (Forbidden), or 400 (Bad Request).

Create Docker Compose Configuration

Create /opt/containers/fail2ban/docker-compose.yml:

version: "3.8"

services:
  fail2ban:
    image: crazymax/fail2ban:latest
    container_name: fail2ban
    network_mode: host
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - NET_RAW
    volumes:
      - /opt/containers/fail2ban/config:/data/config
      - /opt/containers/fail2ban/data:/data/db
      # Mount Host SSH logs (Read-Only)
      - /var/log/auth.log:/var/log/host_logs/auth.log:ro
      # Mount Nginx Proxy Manager logs (Read-Only)
      - /opt/containers/npm/data/logs:/var/log/npm_logs:ro
      # Host time sync
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    environment:
      - TZ=Europe/Berlin

Adjust the TZ environment variable to match your server timezone. If your Nginx Proxy Manager container stores logs in a different location, update the npm logs volume mount path accordingly.

Deploy Container Stack

Start the Fail2ban container:

docker compose -f /opt/containers/fail2ban/docker-compose.yml up -d

Verify the container is running:

docker ps | grep fail2ban

Check container logs for startup errors:

docker logs fail2ban

Verify Active Jails

Check which jails are currently enabled and running:

docker exec -it fail2ban fail2ban-client status

Expected output:

Status
|- Number of jail:      2
`- Jail list:           npm-auth, sshd

Test Filter Patterns

Verify that your custom filter regex matches actual log entries without triggering real bans. Run fail2ban-regex against a live Nginx Proxy Manager access log:

docker exec -it fail2ban fail2ban-regex /var/log/npm_logs/default-host_access.log /data/config/filter.d/npm-auth.conf

The output displays matched lines and statistics showing how many log entries would trigger a ban under current filter rules.

Check Banned IP Addresses

Inspect the current ban list for a specific jail:

docker exec -it fail2ban fail2ban-client status npm-auth

The output shows total failures detected, currently banned IP addresses, and ban statistics.

Unban an IP Address

If you accidentally trigger a ban during testing or need to remove a legitimate client from the ban list, use the unbanip command:

docker exec -it fail2ban fail2ban-client set npm-auth unbanip YOUR_IP_ADDRESS

Replace YOUR_IP_ADDRESS with the actual IP address to unban. To unban an IP from all active jails, specify the sshd jail name or use the global unban command.

Adding Protection for Additional Services

Protecting other containerized applications follows a three-step pattern:

Step 1: Expose Application Logs

Ensure the application writes access or authentication logs to a persistent host directory. Add a volume mount in the Fail2ban docker-compose.yml file to make those logs available inside the Fail2ban container at a path like /var/log/service_logs.

Step 2: Create a Custom Filter

Write a new filter definition in /opt/containers/fail2ban/config/filter.d/service-name.conf. The filter must include a failregex pattern that matches failed authentication attempts in the application log format. Use fail2ban-regex to test the pattern against actual log files before deployment.

Step 3: Enable the Jail

Add a new jail block in /opt/containers/fail2ban/config/jail.local specifying the service name, ports to block, logpath pointing to the mounted log directory, and the name of the filter created in step 2. Set maxretry, findtime, and bantime values appropriate for the service threat model.

After making configuration changes, restart the Fail2ban container:

docker restart fail2ban

Verify the new jail appears in the active jail list using fail2ban-client status.

Monitoring Fail2ban Activity

Monitor real-time Fail2ban activity by tailing container logs:

docker logs -f fail2ban

Fail2ban logs show when IPs are banned, which jail triggered the ban, and when bans expire. This output is useful for identifying attack patterns and verifying that filters are working correctly.

Adjusting Ban Parameters

The default configuration sets conservative ban thresholds. Adjust these values in jail.local based on observed attack patterns:

  • maxretry controls how many failures trigger a ban (lower values are more aggressive)
  • findtime sets the window in which failures are counted (shorter windows catch rapid attacks)
  • bantime determines how long an IP remains blocked (longer bans reduce repeat offenders)

Changes to jail.local require a container restart to take effect.

Troubleshooting Common Issues

Fail2ban Cannot Modify iptables

If logs show permission errors when attempting to add firewall rules, verify that the container includes network_mode: host and both NET_ADMIN and NET_RAW capabilities in the docker-compose.yml file. Rootless Docker installations may require additional configuration to access host iptables.

Jail Shows Zero Failures Despite Visible Attacks

Verify that log file paths inside the container match the paths specified in jail.local. Check that log files are actively being written by the source application and that volume mounts are not stale. Use docker exec to inspect the mounted log directory inside the Fail2ban container.

Filter Regex Does Not Match Log Format

Application log formats change between versions. If a filter stops working after an application update, extract a sample of recent failed authentication log lines and test them against your filter using fail2ban-regex. Adjust the failregex pattern to match the new format.

Persistent Ban Database

The /opt/containers/fail2ban/data directory contains the Fail2ban SQLite database that tracks ban history and current ban states. This database persists across container restarts. To reset all bans and statistics, stop the container, delete the database file, and restart the container.

AI assistance is used on this site for language, formatting, and turning research into a consistent template. It is not used to perform the underlying research or verify technical claims. Every command, configuration, and step in this post is tested by hand before publication.

Leave a Reply

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

20 − one =