FreeBSD.software
Home/Guides/How to Set Up Fail2ban on FreeBSD
tutorial·2026-03-29·22 min read

How to Set Up Fail2ban on FreeBSD

Step-by-step guide to setting up Fail2ban on FreeBSD with PF integration. Covers installation, SSH jail, custom filters, action configuration, whitelisting, and monitoring.

How to Set Up Fail2ban on FreeBSD

Fail2ban watches your log files for repeated authentication failures and other suspicious patterns, then dynamically updates firewall rules to ban the offending IP addresses. It is one of the most effective tools for stopping brute-force attacks on SSH, web servers, and mail services. On FreeBSD, Fail2ban integrates cleanly with PF, giving you automated, real-time protection without writing custom scripts.

This guide covers a complete Fail2ban deployment on FreeBSD 14.x: installation, PF action configuration, SSH jail setup, NGINX and mail server jails, custom filter creation, whitelisting, monitoring, and persistent bans. Every configuration block has been tested on a production FreeBSD system running PF.

If you have not set up PF yet, start with our PF firewall guide first. Fail2ban needs a working firewall to enforce bans. For broader server security, see our FreeBSD hardening checklist.

What Fail2ban Does

Fail2ban is a log-parsing daemon written in Python. It monitors one or more log files for patterns you define -- failed SSH logins, HTTP authentication errors, SMTP rejections -- and when a single IP address triggers too many matches within a time window, Fail2ban executes a ban action. On FreeBSD with PF, that action adds the IP to a PF table, which your firewall rules then use to block all traffic from that address.

The core concepts are straightforward:

  • Jails define what to monitor. Each jail specifies a log file, a filter (regex patterns), a ban action, and thresholds (how many failures before banning, how long to ban).
  • Filters contain the regular expressions that match log lines. Fail2ban ships with filters for dozens of common services. You can write your own.
  • Actions define what happens when an IP is banned or unbanned. On FreeBSD, the PF action adds and removes IPs from a PF table.
  • Ban thresholds are configurable per jail. You might tolerate 5 failed SSH attempts in 10 minutes but only 2 failed WordPress login attempts in 5 minutes.

Fail2ban runs as a background service, reading logs in near real-time. When a ban fires, the effect is instant. When the ban expires, Fail2ban removes the IP from the firewall table automatically.

Installing Fail2ban on FreeBSD

Fail2ban is available in the FreeBSD package repository. Install it with pkg:

sh
pkg install py311-fail2ban

The package name includes the Python version. On FreeBSD 14.x, py311-fail2ban is the current package. If your system uses a different Python version, adjust accordingly -- pkg search fail2ban will show available options.

Enable Fail2ban to start at boot by adding it to /etc/rc.conf:

sh
sysrc fail2ban_enable="YES"

Do not start the service yet. You need to configure jails and the PF action first. Starting Fail2ban with no valid jail configuration will produce errors.

Dependencies

Fail2ban depends on Python and a few Python libraries, all handled automatically by pkg. It has no kernel module requirements. It does require that the log files it monitors actually exist and are readable by the Fail2ban process, which runs as root by default.

Configuration Overview: jail.conf vs jail.local

Fail2ban configuration lives in /usr/local/etc/fail2ban/. The key files and directories:

shell
/usr/local/etc/fail2ban/ fail2ban.conf # Daemon settings (log level, socket path) jail.conf # Default jail definitions -- DO NOT EDIT jail.local # Your overrides -- CREATE THIS FILE filter.d/ # Filter definitions (regex patterns) action.d/ # Action definitions (ban/unban commands)

The critical rule: never edit jail.conf directly. Package upgrades overwrite it. Instead, create jail.local with your customizations. Fail2ban reads jail.conf first, then jail.local, and values in jail.local override anything in jail.conf.

The same pattern applies to filters and actions. To customize filter.d/sshd.conf, create filter.d/sshd.local. To customize an action, create the corresponding .local file. This keeps your changes upgrade-safe.

Global Defaults in jail.local

Start your jail.local with sensible global defaults that apply to all jails unless overridden:

ini
[DEFAULT] # Ban duration: 1 hour bantime = 3600 # Time window for counting failures findtime = 600 # Number of failures before ban maxretry = 5 # Action to take -- use PF on FreeBSD banaction = pf # Whitelist your own IPs ignoreip = 127.0.0.1/8 ::1 # Backend for log monitoring backend = auto # Email notifications (optional) # destemail = admin@example.com # sender = fail2ban@example.com # action = %(action_mwl)s

These defaults mean: any IP that fails 5 times within 10 minutes gets banned for 1 hour, using PF as the firewall backend. Localhost is always whitelisted.

PF Action Setup

Fail2ban ships with a PF action file, but you need to verify it works with your PF configuration. The action adds banned IPs to a PF table, and your PF rules must reference that table.

The PF Action File

Check the existing action at /usr/local/etc/fail2ban/action.d/pf.conf:

ini
[Definition] actionstart = actionstop = actioncheck = actionban = /sbin/pfctl -t <tablename> -T add <ip> actionunban = /sbin/pfctl -t <tablename> -T delete <ip> [Init] tablename = fail2ban

This is the core mechanism. When Fail2ban bans an IP, it runs pfctl -t fail2ban -T add . When the ban expires, it runs pfctl -t fail2ban -T delete . The table name defaults to fail2ban, but you can set a per-jail table name if you want separate tables for SSH, HTTP, and mail bans.

If you need to customize this action, create /usr/local/etc/fail2ban/action.d/pf.local with your overrides rather than editing pf.conf directly.

PF Rules for the Fail2ban Table

Your /etc/pf.conf must reference the fail2ban table and block traffic from it. Add these lines to your PF configuration:

shell
# Fail2ban table -- populated dynamically table <fail2ban> persist # Block all traffic from banned IPs -- place this early in your ruleset block drop in quick on egress from <fail2ban> to any block drop in quick on egress from any to <fail2ban>

The persist keyword keeps the table in memory even when it is empty. Without it, PF drops the table when the last entry is removed, and subsequent pfctl -t fail2ban -T add commands fail.

The quick keyword ensures banned IPs are dropped immediately without evaluating further rules. Place these block rules near the top of your ruleset, after your scrub and NAT rules but before your pass rules.

Reload PF to apply the changes:

sh
pfctl -f /etc/pf.conf

Verify the table exists:

sh
pfctl -t fail2ban -T show

This should return an empty list if no IPs are banned yet. For a complete PF configuration reference, see our PF firewall guide.

SSH Jail Configuration

SSH protection is the most common Fail2ban use case. Automated scanners hit port 22 on every public-facing server, generating thousands of failed login attempts per day. Even if you have already hardened SSH with key-only authentication, banning scanners reduces log noise and frees resources.

Add this SSH jail to your /usr/local/etc/fail2ban/jail.local:

ini
[sshd] enabled = true filter = sshd port = ssh logpath = /var/log/auth.log maxretry = 3 findtime = 300 bantime = 3600 banaction = pf

What Each Directive Does

  • enabled = true -- Activates this jail. Jails default to disabled.
  • filter = sshd -- Uses the filter at filter.d/sshd.conf, which contains regex patterns matching OpenSSH authentication failure messages.
  • port = ssh -- The port the service runs on. Used by some actions to restrict bans to specific ports. With the PF action as configured above, the ban blocks all ports, so this is mainly informational.
  • logpath = /var/log/auth.log -- The log file Fail2ban monitors. On FreeBSD, SSH authentication messages go to /var/log/auth.log by default.
  • maxretry = 3 -- Ban after 3 failures (overrides the global default of 5).
  • findtime = 300 -- Count failures within a 5-minute window.
  • bantime = 3600 -- Ban lasts 1 hour.
  • banaction = pf -- Use the PF action to enforce the ban.

Aggressive SSH Protection

For servers that should only accept key-based authentication, even a single password attempt is suspicious. You can tighten the thresholds:

ini
[sshd-aggressive] enabled = true filter = sshd[mode=aggressive] port = ssh logpath = /var/log/auth.log maxretry = 1 findtime = 86400 bantime = 86400 banaction = pf

The mode=aggressive parameter activates additional regex patterns in the sshd filter that catch connection attempts that never complete authentication, port scans, and other pre-authentication probes. Combined with maxretry = 1, a single suspicious connection triggers a 24-hour ban.

Incremental Ban Times

Fail2ban supports increasing ban durations for repeat offenders. Add these to your [DEFAULT] section:

ini
bantime.increment = true bantime.factor = 2 bantime.maxtime = 604800

With this configuration, a first-time offender gets a 1-hour ban. If they appear again after the ban expires, the second ban is 2 hours. The third is 4 hours. This doubles until hitting the maximum of 604800 seconds (1 week). Persistent attackers end up banned for increasingly long periods without manual intervention.

NGINX Jail: Blocking Bad Bots and Auth Failures

If you run NGINX on FreeBSD, Fail2ban can protect against HTTP authentication brute force, aggressive scrapers, and vulnerability scanners. For full NGINX setup details, see our NGINX production guide.

HTTP Authentication Failures

ini
[nginx-http-auth] enabled = true filter = nginx-http-auth port = http,https logpath = /var/log/nginx/error.log maxretry = 5 findtime = 300 bantime = 3600 banaction = pf

This jail catches failed HTTP Basic Auth attempts logged by NGINX. The nginx-http-auth filter ships with Fail2ban.

Blocking Bad Bots and Scanners

Create a custom filter for common vulnerability scanners and bad bots. First, the jail definition in jail.local:

ini
[nginx-badbots] enabled = true filter = nginx-badbots port = http,https logpath = /var/log/nginx/access.log maxretry = 3 findtime = 60 bantime = 86400 banaction = pf

Then create the filter at /usr/local/etc/fail2ban/filter.d/nginx-badbots.conf:

ini
[Definition] failregex = ^<HOST> -.*"(GET|POST|HEAD).*(/wp-login\.php|/xmlrpc\.php|/\.env|/\.git|/config\.php|/admin\.php|/setup\.php).*".*$ ^<HOST> -.*"(GET|POST).*/phpmyadmin.*".*$ ^<HOST> -.*"(GET|POST).*/wp-admin.*".*$ ^<HOST> -.*"(GET|POST).*/wp-content/uploads/.*\.php.*".*$ ignoreregex =

This filter bans IPs that repeatedly probe for WordPress, phpMyAdmin, environment files, git repositories, or common PHP exploits. On a non-WordPress FreeBSD server, any request for /wp-login.php is inherently hostile.

Rate Limiting Aggressive Crawlers

ini
[nginx-limit-req] enabled = true filter = nginx-limit-req port = http,https logpath = /var/log/nginx/error.log maxretry = 10 findtime = 60 bantime = 7200 banaction = pf

This works with NGINX's limit_req module. When NGINX logs rate-limit violations, Fail2ban bans the offending IP. You need the limit_req_zone and limit_req directives configured in your NGINX configuration for this to work.

Postfix and Dovecot Jails: Mail Server Protection

Mail servers are frequent brute-force targets. If you run Postfix and Dovecot on FreeBSD, these jails provide automated protection.

Postfix SMTP Authentication

ini
[postfix] enabled = true filter = postfix[mode=auth] port = smtp,465,587 logpath = /var/log/maillog maxretry = 5 findtime = 300 bantime = 3600 banaction = pf

This catches failed SMTP authentication attempts. The postfix filter ships with Fail2ban and matches common Postfix authentication failure messages in /var/log/maillog.

Dovecot IMAP/POP3 Authentication

ini
[dovecot] enabled = true filter = dovecot port = pop3,pop3s,imap,imaps logpath = /var/log/maillog maxretry = 5 findtime = 300 bantime = 3600 banaction = pf

Dovecot logs authentication failures to /var/log/maillog on FreeBSD. The built-in dovecot filter matches these entries. Combined with the Postfix jail, this protects both sending and receiving mail services.

Postfix Relay Rejection

ini
[postfix-rbl] enabled = true filter = postfix-rbl port = smtp,465,587 logpath = /var/log/maillog maxretry = 3 findtime = 300 bantime = 86400 banaction = pf

This jail catches IPs rejected by Postfix's built-in RBL checks and bans them at the firewall level, preventing them from even establishing a TCP connection on subsequent attempts.

Custom Filter Creation

Fail2ban's real power is writing filters for any log format. A filter is a set of regular expressions with one special token: , which Fail2ban replaces with a regex matching an IP address.

Filter File Structure

Create custom filters in /usr/local/etc/fail2ban/filter.d/. Every filter file follows this structure:

ini
[Definition] # Required: one or more regex patterns, one per line # <HOST> matches the attacker's IP failregex = ^.*authentication failure.*from <HOST>.*$ ^.*invalid user.*from <HOST>.*$ # Optional: patterns to exclude (overrides failregex) ignoreregex = ^.*authentication failure.*from 10\.0\.0\..*$ # Optional: specify date detection datepattern = %%Y-%%m-%%d %%H:%%M:%%S

Writing Effective Regex

The key to good filters is matching exactly what you need:

  1. Anchor with . Every failregex line must contain so Fail2ban can extract the IP address.
  2. Be specific. Overly broad patterns cause false positives. Match the exact log message format your service produces.
  3. Use fail2ban-regex to test. Before deploying a filter, test it against your actual log file:
sh
fail2ban-regex /var/log/auth.log /usr/local/etc/fail2ban/filter.d/sshd.conf

This shows how many lines the filter matches and what IPs it extracts. Run this every time you write or modify a filter.

Example: Custom Application Filter

Suppose you have a FreeBSD application that logs authentication failures like this:

shell
2026-03-29 10:15:33 AUTH_FAIL user=admin ip=203.0.113.50 reason=bad_password

Create /usr/local/etc/fail2ban/filter.d/myapp.conf:

ini
[Definition] failregex = ^.*AUTH_FAIL.*ip=<HOST>.*$ ignoreregex = datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S

Then add the jail to jail.local:

ini
[myapp] enabled = true filter = myapp port = 8080 logpath = /var/log/myapp/auth.log maxretry = 5 findtime = 300 bantime = 3600 banaction = pf

Test the filter before enabling it:

sh
fail2ban-regex /var/log/myapp/auth.log /usr/local/etc/fail2ban/filter.d/myapp.conf

Multi-Line Log Matching

Some applications produce multi-line log entries. Fail2ban supports this with the maxlines setting in the filter:

ini
[Definition] failregex = ^.*Connection from <HOST>.*$ ^.*Authentication failed.*$ maxlines = 3

Use multi-line matching sparingly. It is slower and harder to debug than single-line patterns.

Whitelisting: ignoreip and ignorecommand

Whitelisting prevents Fail2ban from banning IPs you trust. This is critical -- locking yourself out of your own server by banning your management IP is a common and painful mistake.

ignoreip

The simplest whitelist is the ignoreip directive. Set it in the [DEFAULT] section of jail.local to apply to all jails, or per-jail for granular control:

ini
[DEFAULT] ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.1.0/24 203.0.113.50

This whitelists:

  • Localhost (IPv4 and IPv6)
  • Private network ranges
  • Your management IP (replace 203.0.113.50 with your actual IP)

You can specify individual IPs, CIDR ranges, or hostnames. Separate entries with spaces.

Per-Jail Whitelisting

Override the global whitelist for specific jails:

ini
[sshd] enabled = true filter = sshd logpath = /var/log/auth.log ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8

ignorecommand

For dynamic whitelisting, use ignorecommand. This runs an external command for each potential ban. If the command returns exit code 0, the IP is whitelisted.

ini
[DEFAULT] ignorecommand = /usr/local/bin/fail2ban-whitelist-check <ip>

A practical use case is checking whether an IP belongs to a known CDN or monitoring service. Create the script:

sh
#!/bin/sh # /usr/local/bin/fail2ban-whitelist-check # Returns 0 (whitelist) if IP is in a trusted range IP="$1" # Cloudflare ranges (example -- maintain your own list) for range in 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22; do if echo "$IP" | /usr/local/bin/ipcalc -c "$range" > /dev/null 2>&1; then exit 0 fi done exit 1

Make it executable:

sh
chmod +x /usr/local/bin/fail2ban-whitelist-check

Use ignorecommand carefully. It runs for every potential ban event, so expensive checks slow down Fail2ban's response time.

Starting Fail2ban

With your jails, actions, and PF rules configured, start the service:

sh
service fail2ban start

Check for errors in the log:

sh
tail -50 /var/log/fail2ban.log

A clean startup looks like this:

shell
2026-03-29 10:00:01,234 fail2ban.server [12345]: INFO Starting Fail2ban v1.1.0 2026-03-29 10:00:01,567 fail2ban.jail [12345]: INFO Jail 'sshd' started 2026-03-29 10:00:01,890 fail2ban.jail [12345]: INFO Jail 'nginx-http-auth' started

If a jail fails to start, the log will explain why -- usually a missing log file, a regex syntax error in a filter, or an invalid action reference.

Monitoring Fail2ban

Overall Status

sh
fail2ban-client status

Output shows all active jails:

shell
Status |- Number of jail: 3 `- Jail list: dovecot, nginx-http-auth, sshd

Per-Jail Status

sh
fail2ban-client status sshd

Detailed output for the SSH jail:

shell
Status for the jail: sshd |- Filter | |- Currently failed: 2 | |- Total failed: 847 | `- File list: /var/log/auth.log `- Actions |- Currently banned: 5 |- Total banned: 203 `- Banned IP list: 45.33.32.10 185.220.101.42 ...

Checking the PF Table

See all currently banned IPs directly in PF:

sh
pfctl -t fail2ban -T show

Count banned entries:

sh
pfctl -t fail2ban -T show | wc -l

Log Analysis

Fail2ban's own log at /var/log/fail2ban.log records every ban and unban event with timestamps. For a summary of recent bans:

sh
grep "Ban " /var/log/fail2ban.log | tail -20

For a count of bans per jail:

sh
grep "Ban " /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rn

To see which IPs have been banned most frequently:

sh
grep "Ban " /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20

Unbanning IPs

If you accidentally ban yourself or a legitimate user, Fail2ban provides immediate unban commands.

Unban from a Specific Jail

sh
fail2ban-client set sshd unbanip 203.0.113.50

This removes the IP from both Fail2ban's internal database and the PF table.

Unban from All Jails

sh
fail2ban-client unban 203.0.113.50

Unban All IPs

sh
fail2ban-client unban --all

Use this cautiously. It removes every ban across all jails.

Manual PF Table Removal

If Fail2ban is not running or not responding, you can remove an IP directly from the PF table:

sh
pfctl -t fail2ban -T delete 203.0.113.50

This lifts the firewall block immediately. However, Fail2ban still has the ban in its database. When Fail2ban restarts, it may re-add the IP. To prevent this, also flush Fail2ban's database:

sh
fail2ban-client set sshd unbanip 203.0.113.50

Or restart Fail2ban, which reloads its state:

sh
service fail2ban restart

Integration with PF Tables: Persistent Bans Across Reboots

By default, PF tables are stored in memory. When the server reboots, the fail2ban table is empty. Fail2ban restores its active bans on startup from its internal database (/var/lib/fail2ban/fail2ban.sqlite3 or /usr/local/var/lib/fail2ban/fail2ban.sqlite3 on FreeBSD), but there is a window between PF starting and Fail2ban starting where previously banned IPs can connect.

Option 1: PF Table Persistence File

Save banned IPs to a file and load them on PF startup. Add a cron job or use the action's actionstart hook:

Create a custom PF action at /usr/local/etc/fail2ban/action.d/pf-persistent.local:

ini
[Definition] actionstart = /sbin/pfctl -t <tablename> -T add -f /usr/local/etc/fail2ban/persistent-bans.txt 2>/dev/null; true actionstop = /sbin/pfctl -t <tablename> -T show > /usr/local/etc/fail2ban/persistent-bans.txt actioncheck = actionban = /sbin/pfctl -t <tablename> -T add <ip> echo "<ip>" >> /usr/local/etc/fail2ban/persistent-bans.txt actionunban = /sbin/pfctl -t <tablename> -T delete <ip> sed -i '' '/<ip>/d' /usr/local/etc/fail2ban/persistent-bans.txt [Init] tablename = fail2ban

This saves banned IPs to a text file. When Fail2ban starts, it loads the file into the PF table. When it stops, it dumps the current table to the file. The 2>/dev/null; true on actionstart handles the case where the file does not exist yet.

To use this action, set banaction = pf-persistent in your jails.

Option 2: Load Table from File in pf.conf

Reference the persistence file directly in /etc/pf.conf:

shell
table <fail2ban> persist file "/usr/local/etc/fail2ban/persistent-bans.txt"

PF loads the file contents when it starts. Combined with the custom action above, this ensures bans survive reboots with no gap in coverage.

Option 3: Extended bantime Database

Fail2ban's incremental ban feature stores ban history in its SQLite database. With bantime.increment = true configured, Fail2ban restores unexpired bans from the database when it starts. This is the simplest approach and requires no custom actions, but there is still a brief window between PF startup and Fail2ban startup where the table is empty.

For most servers, Option 2 combined with the incremental bantime feature provides the most robust protection.

Complete jail.local Reference

Here is a complete, production-ready jail.local for a FreeBSD server running SSH, NGINX, and mail services:

ini
[DEFAULT] # Ban for 1 hour initially bantime = 3600 # Look back 10 minutes for failures findtime = 600 # Ban after 5 failures maxretry = 5 # Incremental bans for repeat offenders bantime.increment = true bantime.factor = 2 bantime.maxtime = 604800 # Use PF firewall banaction = pf # Whitelist trusted IPs ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 # Log backend backend = auto # ------------------------------------------- # SSH Protection # ------------------------------------------- [sshd] enabled = true filter = sshd port = ssh logpath = /var/log/auth.log maxretry = 3 findtime = 300 bantime = 3600 [sshd-aggressive] enabled = true filter = sshd[mode=aggressive] port = ssh logpath = /var/log/auth.log maxretry = 1 findtime = 86400 bantime = 86400 # ------------------------------------------- # NGINX Protection # ------------------------------------------- [nginx-http-auth] enabled = true filter = nginx-http-auth port = http,https logpath = /var/log/nginx/error.log maxretry = 5 findtime = 300 bantime = 3600 [nginx-badbots] enabled = true filter = nginx-badbots port = http,https logpath = /var/log/nginx/access.log maxretry = 3 findtime = 60 bantime = 86400 [nginx-limit-req] enabled = true filter = nginx-limit-req port = http,https logpath = /var/log/nginx/error.log maxretry = 10 findtime = 60 bantime = 7200 # ------------------------------------------- # Mail Server Protection # ------------------------------------------- [postfix] enabled = true filter = postfix[mode=auth] port = smtp,465,587 logpath = /var/log/maillog maxretry = 5 findtime = 300 bantime = 3600 [dovecot] enabled = true filter = dovecot port = pop3,pop3s,imap,imaps logpath = /var/log/maillog maxretry = 5 findtime = 300 bantime = 3600

Remove any jail sections for services you do not run. Enabling a jail for a service that is not installed or does not produce log output wastes resources and generates warnings.

Troubleshooting

Fail2ban Does Not Start

Check the log:

sh
tail -100 /var/log/fail2ban.log

Common causes:

  • Missing log file. The logpath in a jail points to a file that does not exist. Disable the jail or create the log file.
  • Syntax error in jail.local. Fail2ban's config parser is strict. Watch for tabs vs spaces, missing = signs, or unclosed brackets.
  • PF not running. If PF is not enabled and started, the pfctl ban command fails. Ensure PF is running with pfctl -si.

IPs Not Getting Banned

  1. Verify the jail is active: fail2ban-client status
  2. Check the filter matches your logs: fail2ban-regex /var/log/auth.log /usr/local/etc/fail2ban/filter.d/sshd.conf
  3. Confirm the log file is being written to: tail -f /var/log/auth.log
  4. Check that the IP is not in ignoreip

Banned IPs Can Still Connect

  1. Verify the IP is in the PF table: pfctl -t fail2ban -T test 203.0.113.50
  2. Check your PF rules are ordered correctly. The block rule for the fail2ban table must come before any pass rule that would match the traffic.
  3. Reload PF rules: pfctl -f /etc/pf.conf

Frequently Asked Questions

Does Fail2ban work with IPFW instead of PF?

Yes. Fail2ban ships with an IPFW action at action.d/ipfw.conf. Set banaction = ipfw in your jail.local. However, PF is the recommended firewall for most FreeBSD deployments because of its readable syntax and powerful table support. If you are deciding between firewalls, our PF firewall guide covers the advantages.

How do I ban an IP permanently?

Set bantime = -1 in the jail configuration. A negative bantime means the ban never expires. Combine this with the persistent bans setup (PF table file loading) to survive reboots. Be cautious with permanent bans -- IP addresses get reassigned, and you may eventually block legitimate users who inherit a previously abusive address.

Can I use Fail2ban with jails (FreeBSD jails)?

Yes, but with caveats. Fail2ban running in the host system can monitor log files inside jails if those files are accessible (via nullfs mounts or shared paths). Running Fail2ban inside a jail does not work for PF actions because jails do not have access to pfctl by default. The recommended setup is running Fail2ban on the host and pointing logpath directives to the appropriate log files.

How much memory and CPU does Fail2ban use?

Fail2ban is lightweight. On a typical FreeBSD server monitoring 3-5 jails, it uses under 50 MB of RAM and negligible CPU. The main resource cost is the Python process reading log files. If you monitor very large, high-volume log files (multi-gigabyte access logs), consider using maxlines in your filters and rotating logs frequently.

What happens when Fail2ban restarts? Are active bans lost?

Fail2ban stores ban information in a SQLite database. When it restarts, it restores bans that have not yet expired. With bantime.increment = true, it also remembers ban history for calculating escalating ban times. The brief restart window is typically a few seconds. For zero-gap protection, use the persistent PF table file approach described in the persistent bans section.

Can Fail2ban send email notifications when it bans an IP?

Yes. Set the action to %(action_mwl)s in your jail configuration and configure destemail and sender in the [DEFAULT] section. This sends an email with the ban details, whois information, and relevant log lines. On FreeBSD, ensure you have a working MTA (Postfix, Sendmail, or DMA) configured for outbound mail.

How do I see the regex Fail2ban uses for a specific filter?

Run fail2ban-client get failregex to see the active regex patterns for any jail. For example:

sh
fail2ban-client get sshd failregex

This outputs every regex pattern the SSH jail is using to match log lines, which is useful for debugging filter issues.

Summary

Fail2ban on FreeBSD with PF gives you automated, real-time protection against brute-force attacks with minimal configuration overhead. The setup process is straightforward: install the package, create jail.local with your jail definitions, configure PF to block the fail2ban table, and start the service.

The key points to remember:

  1. Never edit jail.conf -- always use jail.local.
  2. Test custom filters with fail2ban-regex before deploying them.
  3. Always whitelist your management IPs in ignoreip.
  4. Use incremental ban times to deal with persistent attackers.
  5. Set up persistent bans to survive reboots.
  6. Monitor regularly with fail2ban-client status and PF table queries.

Combined with SSH hardening, a properly configured PF firewall, and a solid FreeBSD hardening baseline, Fail2ban completes the automated defense layer of your security stack.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.