Introduction

Fail2Ban monitors log files for authentication failures and automatically bans offending IP addresses using firewall rules. Ansible automates the full setup — install Fail2Ban, configure jails for SSH, web servers, mail, and custom applications, manage whitelists, set up email alerts, and deploy custom filters across your fleet.

Install and Configure

---
- name: Deploy Fail2Ban
  hosts: all
  become: true
  vars:
    fail2ban_bantime: 3600
    fail2ban_findtime: 600
    fail2ban_maxretry: 5
    fail2ban_ignoreip:
      - 127.0.0.1/8
      - 10.0.0.0/8
    fail2ban_destemail: security@example.com
    fail2ban_action: action_mwl
  tasks:
    - name: Install Fail2Ban
      ansible.builtin.package:
        name: fail2ban
        state: present

    - name: Deploy jail.local
      ansible.builtin.template:
        src: jail.local.j2
        dest: /etc/fail2ban/jail.local
        mode: '0644'
      notify: restart fail2ban

    - name: Start Fail2Ban
      ansible.builtin.service:
        name: fail2ban
        state: started
        enabled: true

  handlers:
    - name: restart fail2ban
      ansible.builtin.service:
        name: fail2ban
        state: restarted

jail.local Template

# templates/jail.local.j2
# Managed by Ansible
[DEFAULT]
bantime = {{ fail2ban_bantime }}
findtime = {{ fail2ban_findtime }}
maxretry = {{ fail2ban_maxretry }}
ignoreip = {{ fail2ban_ignoreip | join(' ') }}
banaction = iptables-multiport
destemail = {{ fail2ban_destemail }}
sender = fail2ban@{{ ansible_fqdn }}
mta = sendmail
action = %({{ fail2ban_action }})s

# SSH — always enabled
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 7200

{% if 'webservers' in group_names %}
# Nginx
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5

[nginx-botsearch]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 86400

[nginx-limit-req]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
{% endif %}

{% if 'apache' in group_names %}
# Apache
[apache-auth]
enabled = true
port = http,https
logpath = %(apache_error_log)s
maxretry = 5

[apache-badbots]
enabled = true
port = http,https
logpath = %(apache_access_log)s
maxretry = 2
bantime = 86400
{% endif %}

{% if 'mail_servers' in group_names %}
# Postfix
[postfix]
enabled = true
port = smtp,465,submission
logpath = /var/log/mail.log
maxretry = 5

[dovecot]
enabled = true
port = pop3,pop3s,imap,imaps
logpath = /var/log/mail.log
maxretry = 5
{% endif %}

Custom Filters

- name: Deploy custom filter for web app
  ansible.builtin.copy:
    dest: /etc/fail2ban/filter.d/myapp-auth.conf
    content: |
      [Definition]
      failregex = ^.*Authentication failed for user .* from <HOST>.*$
                  ^.*Invalid login attempt from <HOST>.*$
                  ^.*Brute force detected from <HOST>.*$
      ignoreregex =
    mode: '0644'
  notify: restart fail2ban

- name: Add custom jail
  ansible.builtin.blockinfile:
    path: /etc/fail2ban/jail.local
    block: |
      [myapp-auth]
      enabled = true
      port = 8080
      filter = myapp-auth
      logpath = /var/log/myapp/auth.log
      maxretry = 5
      bantime = 3600
  notify: restart fail2ban

Progressive Banning

# Ban longer for repeat offenders
- name: Deploy recidive jail
  ansible.builtin.blockinfile:
    path: /etc/fail2ban/jail.local
    marker: "# {mark} RECIDIVE JAIL"
    block: |
      [recidive]
      enabled = true
      logpath = /var/log/fail2ban.log
      banaction = iptables-allports
      bantime = 604800
      findtime = 86400
      maxretry = 3
  notify: restart fail2ban

Monitoring

- name: Check Fail2Ban status
  ansible.builtin.command: fail2ban-client status
  register: f2b_status
  changed_when: false

- name: Check specific jail
  ansible.builtin.command: fail2ban-client status sshd
  register: sshd_jail
  changed_when: false

- name: Count banned IPs
  ansible.builtin.shell: fail2ban-client status sshd | grep 'Currently banned' | awk '{print $NF}'
  register: banned_count
  changed_when: false

- name: Alert on high ban count
  ansible.builtin.debug:
    msg: "WARNING: {{ banned_count.stdout }} IPs banned on {{ inventory_hostname }}"
  when: banned_count.stdout | int > 50

Unban IPs

- name: Unban specific IP
  ansible.builtin.command: "fail2ban-client set {{ jail_name }} unbanip {{ ip_address }}"
  when: ip_address is defined

- name: Unban all IPs in jail
  ansible.builtin.command: "fail2ban-client unban --all"
  when: unban_all | default(false)

Troubleshooting

Check Filter Regex

- name: Test filter against log
  ansible.builtin.command: >
    fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
  register: regex_test
  changed_when: false

Fail2Ban Not Starting

- name: Validate config
  ansible.builtin.command: fail2ban-client -t
  register: config_test
  changed_when: false

Conclusion

Ansible deploys Fail2Ban across your fleet with group-specific jails — SSH everywhere, Nginx jails on web servers, Postfix jails on mail servers. Use custom filters for application-specific log patterns, the recidive jail for repeat offenders, and whitelists for trusted networks. Template everything from variables so security policy is code.