
Automate fail2ban Jail Reports to Slack with Bash
Before you run this
This script reads fail2ban's current state — the list of active jails and how many IPs each one has banned — and posts a short text summary to a Slack channel through an incoming webhook. Its purpose is visibility: you get a scheduled report of what fail2ban is blocking without SSHing in to run fail2ban-client status by hand.
It needs root. fail2ban-client talks to the fail2ban server over a socket that is normally only readable by root, so the script must run as root (directly, via sudo, or from root's crontab). It does not need any special network privilege beyond outbound HTTPS to Slack.
What it changes: nothing in fail2ban and nothing in your firewall. The script only reads status and sends a message — it never bans, unbans, or reconfigures anything. The one persistent change you make is adding a cron entry to schedule it, which is trivially reversible (delete the line). So this is low-risk, but two things still deserve care: the webhook URL is a secret (anyone with it can post to your channel), and the report will contain banned IP addresses, which some organizations treat as sensitive. Keep the script file readable only by root.
Test it first. Read the script before you run it. Run it once by hand and watch it post to a test Slack channel before you schedule it or point it at a channel your whole team watches. Confirm your webhook works with a one-line curl (shown at the end) before blaming the script.
Assumed environment: Debian 12 or Ubuntu 22.04, fail2ban installed from the distro's apt package, systemd, and bash. I use jq to build the JSON payload safely; install it with apt install jq. On RHEL/Alma the only differences are the package manager (dnf) and, if you installed fail2ban from EPEL, the same fail2ban-client command still applies. curl is assumed present.
Get a Slack incoming webhook
In Slack, create an app (or use an existing one) and enable Incoming Webhooks, then add a webhook for the target channel. Slack gives you a URL of the form https://hooks.slack.com/services/T00000000/B00000000/XXXX.... That URL is the credential — treat it like a password. The exact click-path lives in Slack's own documentation under "Sending messages using incoming webhooks"; I won't reproduce it here because Slack changes its UI more often than this script will change.
Confirm what fail2ban gives you
Before automating, look at the raw output you're going to parse:
sudo fail2ban-client status
sudo fail2ban-client status sshd # replace sshd with one of your jails
The top-level status prints a Jail list: line with comma-separated jail names. Each per-jail status prints a Currently banned: count and a Banned IP list: line. The script keys off exactly those labels, so if your version's wording differs, adjust the patterns to match what you see.
The script
Save this as /usr/local/bin/fail2ban-slack-report.sh:
#!/usr/bin/env bash
set -euo pipefail
# --- Slack incoming webhook URL. Replace with your own. Keep this file root-only. ---
WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX"
# Identify this server in the report.
HOST=$(hostname -f 2>/dev/null || hostname)
# Pull the jail list and turn the comma-separated names into plain words.
jails=$(fail2ban-client status | sed -n 's/^.*Jail list:[[:space:]]*//p' | sed 's/,//g')
# Start the report body.
report="*fail2ban report — ${HOST}*"
for jail in $jails; do
status=$(fail2ban-client status "$jail")
# The last field of the "Currently banned" line is the count.
banned=$(printf '%s\n' "$status" | awk '/Currently banned/ {print $NF}')
# Everything after "Banned IP list:" is the IPs (can be empty).
ips=$(printf '%s\n' "$status" | sed -n 's/^.*Banned IP list:[[:space:]]*//p')
report+=$'\n'"• ${jail}: ${banned} banned"
[ -n "$ips" ] && report+=" (${ips})"
done
# Build valid JSON with jq so quoting and newlines are handled safely.
payload=$(jq -n --arg text "$report" '{text: $text}')
# Post to Slack. -sS stays quiet but still shows errors.
curl -sS -X POST -H 'Content-type: application/json' --data "$payload" "$WEBHOOK_URL"
Then lock down the permissions, because the file holds your webhook secret:
sudo chown root:root /usr/local/bin/fail2ban-slack-report.sh
sudo chmod 750 /usr/local/bin/fail2ban-slack-report.sh
A few notes on the non-obvious parts. set -euo pipefail makes the script stop on an error or an unset variable rather than silently posting a half-empty report. Building the JSON with jq -n --arg is deliberate: hand-assembling JSON in bash breaks the moment a value contains a quote or a newline, and jq escapes everything correctly. The awk '/Currently banned/ {print $NF}' grabs only the count from that specific line, so it won't collide with the "Currently failed" line above it.
Run it once by hand
Point WEBHOOK_URL at a test channel first, then:
sudo /usr/local/bin/fail2ban-slack-report.sh
You should see the message appear in Slack within a second or two. If curl prints something like invalid_payload or no_service, the webhook URL is wrong or revoked — fix that before scheduling anything.
Schedule it with cron
fail2ban-client needs root, so use root's crontab:
sudo crontab -e
Add one line to send the report every morning at 08:00:
# fail2ban -> Slack daily ban report
0 8 * * * /usr/local/bin/fail2ban-slack-report.sh >/dev/null 2>&1
Adjust the schedule to taste — 0 */6 * * * for every six hours, for example. If you prefer systemd timers over cron, the equivalent is a .service unit that runs the script plus a .timer unit; the mechanism is standard but I'll leave the unit-file details to the systemd documentation rather than half-specify them here.
Verify it worked
Three checks, in order of trust:
- Manual run: the command above posts to Slack immediately. If the message shows the right hostname and your real jails, the logic is correct.
- Scheduled run: confirm cron actually fired it. On Debian/Ubuntu, cron activity is logged; check with
journalctl -u cron --since today(orgrep CRON /var/log/syslogif your system logs there). You should see the job execute at the scheduled minute. - Cross-check the numbers: run
sudo fail2ban-client status sshdby hand and confirm the "Currently banned" count matches what landed in Slack.
Undo
There is nothing to roll back inside fail2ban — the script never changed it. To stop the reports, remove the cron line:
sudo crontab -e # delete the fail2ban -> Slack line
To remove it entirely, delete the script file and, in Slack, delete or revoke the incoming webhook so the URL can no longer post. Revoking the webhook is the real "off switch": even if a copy of the script leaks, a revoked webhook is inert.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
