Most people set up fail2ban and forget about it. It runs and bans IPs, but you only know what it does if you SSH in and read the logs.
This post adds fail2ban metrics to a Prometheus stack and shows them in Grafana, on the same board as CPU, memory and disk.
What you will see
- Current bans and failed attempts per jail
- Total bans over time
- Which jail fires most: SSH, Nginx or recidive
- Whether the exporter can still reach fail2ban at all
The exporter gives counts and jail names. It does not put banned IP addresses into metrics, so you see how much is happening, not who.
Step 1: check the socket
The exporter reads the fail2ban control socket, not the log files.
test -S /var/run/fail2ban/fail2ban.sock && ls -l /var/run/fail2ban/fail2ban.sock
Stop here if that path is missing or is a directory. Expected mode is owner-only, for example srw------- root root.
Step 2: add the exporter
Add this block to the services: section of your docker-compose.yml:
fail2ban-exporter:
image: registry.gitlab.com/hctrdev/fail2ban-prometheus-exporter:0.10.3
container_name: fail2ban-exporter
restart: unless-stopped
ports:
- "127.0.0.1:9191:9191"
volumes:
- /var/run/fail2ban:/var/run/fail2ban:ro
read_only: true
security_opt:
- no-new-privileges:true
networks:
- monitoring-net
deploy:
resources:
limits:
memory: 32M
Two things I learned the hard way.
Mount the directory, not the socket file. If the file is missing when the container is created, Docker creates a directory at that path and fail2ban can no longer bind its socket.
This image runs as uid 0, so the socket stays owner-only and I limit the container instead: read-only mount, read_only, no-new-privileges, port on localhost.
Step 3: add a scrape job to Prometheus
In prometheus.yml:
- job_name: 'fail2ban'
static_configs:
- targets: ['fail2ban-exporter:9191']
labels:
instance: 'your-server'
scrape_interval: 30s
scrape_timeout: 10s
Prometheus and the exporter must be on the same Compose network. Reload Prometheus, then check:
docker compose up -d fail2ban-exporter
curl -s http://127.0.0.1:9191/metrics | grep '^f2b_up'
f2b_up must be 1. The Prometheus target can show UP while f2b_up is 0: the process is serving metrics but cannot use the socket.
Step 4: add the alerts
- alert: Fail2banHighAttackRate
expr: increase(f2b_jail_banned_total[5m]) > 10
labels:
severity: warning
annotations:
summary: "High fail2ban ban rate on {{ $labels.instance }}"
- alert: Fail2banSocketDown
expr: f2b_up == 0
for: 5m
labels:
severity: warning
annotations:
summary: "fail2ban exporter cannot read the socket on {{ $labels.instance }}"
The first one fires when more than 10 IPs are banned in 5 minutes. The second one covers the case above: the exporter is up but blind.
Step 5: import the dashboard
Download fail2ban.json from grafana-dashboards/json/ in the repo and import it in Grafana via Dashboards, Import. Select the fail2ban job in the $job dropdown.
Why bother
The first time I saw a ban spike in Grafana, 40 IPs banned in under 2 minutes at 3am, I realized I had had no view of this before. It was happening, I just could not see it.
One gotcha: by default systemd removes /var/run/fail2ban when fail2ban stops and creates a new directory on start. A container that mounted the old one keeps a deleted directory, and f2b_up stays 0. A systemd drop-in with RuntimeDirectoryPreserve=yes fixes it. I checked it on Ubuntu 24.04 (arm64) with fail2ban 1.0.2.
What I did not check: other distributions and other fail2ban versions.
Do you export fail2ban metrics, or do you still read the logs?
Repo: github.com/Airat71/monitoring-stack (guide: docs/FAIL2BAN_ENHANCED.md)
Disclosure: I used an AI assistant to run the repetitive checks and to help draft this text. The findings, the decisions and the fixes are mine.

Top comments (0)