From 2268044797f98b7727f2d7456f320511ac8941f7 Mon Sep 17 00:00:00 2001 From: hiperman Date: Tue, 2 Dec 2025 22:35:27 -0500 Subject: [PATCH] Initial role commit --- .gitignore | 3 + README.md | 405 +++++++++++++++++++++++++++++++++- defaults/main.yaml | 42 ++++ files/actions/ufw-docker.conf | 48 ++++ handlers/main.yaml | 6 + meta/main.yaml | 0 tasks/main.yaml | 106 +++++++++ templates/jail.local.j2 | 33 +++ vars/main.yaml | 27 +++ 9 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 defaults/main.yaml create mode 100644 files/actions/ufw-docker.conf create mode 100644 handlers/main.yaml create mode 100644 meta/main.yaml create mode 100644 tasks/main.yaml create mode 100644 templates/jail.local.j2 create mode 100644 vars/main.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c199eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# ---> Ansible +*.retry + diff --git a/README.md b/README.md index f1cfb03..7fe5160 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,404 @@ -# ansible-role-fail2ban +# Ansible Role: fail2ban -An Ansible role for installing and configuring fail2ban. \ No newline at end of file +An Ansible role for installing and configuring fail2ban on Linux systems. This role provides flexible configuration options for fail2ban's main settings, jail configurations, and custom filters/actions. + +## Requirements + +- Ansible 2.9 or higher +- Target systems: Debian/Ubuntu or RHEL-based distributions +- Python 3 on target hosts + +## Role Variables + +### Default Variables + +Available variables are listed below, along with default values (see `defaults/main.yaml`): + +```yaml +# Packages to install +fail2ban_dependencies: + - fail2ban + - python3-systemd + +# Service configuration +fail2ban_service: fail2ban +fail2ban_loglevel: INFO +fail2ban_logtarget: /var/log/fail2ban.log + +# Default jail settings +fail2ban_ignoreself: "true" +fail2ban_ignoreips: + - 127.0.0.1/8 + - ::1 +fail2ban_bantime: 10m +fail2ban_findtime: 10m +fail2ban_maxretry: 5 +fail2ban_backend: auto +``` + +### Important Default Settings to Configure + +Before deploying to production, consider adjusting these critical settings: + +1. **IP Whitelist** (`fail2ban_ignoreips`): Add your management IPs to prevent lockouts +2. **Ban Duration** (`fail2ban_bantime`): Default is 10 minutes; consider longer for production (1h, 24h, 1w) +3. **Max Retry** (`fail2ban_maxretry`): Default is 5 attempts; lower for stricter security +4. **Find Time Window** (`fail2ban_findtime`): Default is 10 minutes; adjust based on your threat model +5. **Backend** (`fail2ban_backend`): Set to `systemd` for systemd-based distros for better performance + +### Custom Configuration Variables + +#### Using the INI Module Format + +You can add custom configuration to `fail2ban.local` and `jail.local` by defining options in the INI module format: + +```yaml +# Additional fail2ban.local configuration +fail2ban_configuration: [] + - option: loglevel # The INI option name + value: "DEBUG" # The value to set + section: Definition # The INI section + +# Additional jail.local configuration +fail2ban_jail_configuration: + - option: destemail + value: "admin@example.com" + section: DEFAULT + - option: sender + value: "fail2ban@example.com" + section: DEFAULT + - option: action + value: "%(action_mwl)s" + section: DEFAULT +``` + +**INI Format Explanation:** +- `section`: The configuration section in the INI file (e.g., `DEFAULT`, `Definition`, `sshd`) +- `option`: The configuration parameter name +- `value`: The value to assign (must be a string, number, or boolean) + +This format allows you to set any fail2ban configuration option without modifying the role itself. + +### Debian-Specific Configuration + +#### SSH Service Name Issue + +On Debian systems, there's a common issue where fail2ban's default SSH jail doesn't work out of the box. The problem: + +- fail2ban's default configuration expects the service to be named `sshd` +- On many Debian systems, the actual systemd service is named `ssh.service` (not `sshd.service`) +- When using the `systemd` backend, fail2ban looks for logs from the `sshd` service and finds nothing +- Result: **SSH protection silently fails** - no errors, but no banning occurs + +#### The Solution + +This role provides the `fail2ban_default_debian_jail_configuration` variable to override Debian's default jail settings: +```yaml +fail2ban_default_debian_jail_configuration: + - option: backend + value: systemd + section: sshd + - option: journalmatch + value: "_SYSTEMD_UNIT=ssh.service" # Use ssh.service instead of sshd.service + section: sshd +``` + +This configuration: +- Explicitly sets the backend to `systemd` for better performance +- Corrects the journal match to look for `ssh.service` instead of the default `sshd.service` +- Is applied only on Debian systems (via `when: ansible_facts['distribution'] == 'Debian'`) + +#### Usage Example +```yaml +- hosts: debian_servers + become: yes + roles: + - role: fail2ban + vars: + fail2ban_backend: systemd + + # Fix SSH jail on Debian + fail2ban_default_debian_jail_configuration: + - option: backend + value: systemd + section: sshd + - option: journalmatch + value: "_SYSTEMD_UNIT=ssh.service" + section: sshd + + fail2ban_jails: + - name: sshd + enabled: true + maxretry: 3 + bantime: 1h +``` + +> [!NOTE] This only affects the default SSH jail in `/etc/fail2ban/jail.d/defaults-debian.conf`. If you're defining custom SSH jails using `fail2ban_jails`, make sure to specify the correct `journalmatch` or `logpath` for your system. + +#### Verifying SSH Protection is Working + +After applying the role, verify fail2ban is monitoring SSH: +```bash +# Check if the sshd jail is active +sudo fail2ban-client status sshd + +# Check what logs fail2ban is monitoring +sudo fail2ban-client get sshd logpath + +# Test with a failed login and check +sudo fail2ban-client status sshd +``` + +If you see "Currently banned: 0" and "Total banned: 0" after several failed login attempts, the jail may not be configured correctly for your system's SSH service name. + +### Creating Custom Jails + +#### Method 1: Define Jails as Dictionaries (Recommended for Simple Jails) + +Create custom jails by defining them as dictionaries. This method uses a Jinja2 template to generate jail files: + +```yaml +fail2ban_jails: + - name: sshd # Jail name (required) + enabled: true # Enable/disable jail + port: ssh # Service port(s) + logpath: /var/log/auth.log # Log file to monitor + maxretry: 3 # Override default maxretry + bantime: 1h # Override default bantime + findtime: 10m # Override default findtime + banaction: iptables-multiport # Ban action to use + filter: sshd # Filter to use +``` + +**Common jail parameters:** +- `name`: Jail identifier (required) +- `enabled`: true/false +- `port`: Service port (ssh, http, https, etc.) +- `filter`: Filter name (must exist in filter.d/) +- `logpath`: Path to log file(s) to monitor +- `maxretry`: Number of failures before ban +- `findtime`: Time window for counting failures +- `bantime`: Duration of ban +- `banaction`: Action to execute on ban +- `action`: Full action with parameters + +#### Method 2: Copy Complete Jail Files (For Complex Configurations) + +For more complex jail configurations that require specific options not supported by the template, copy complete jail files: + +```yaml +# Path to directory containing custom jail files +fail2ban_jails_path: "{{ playbook_dir }}/files/fail2ban/jails" +``` + +Place your `.conf` or `.local` files in the specified directory. They will be copied to `/etc/fail2ban/jail.d/`. + +### Custom Filters and Actions + +#### Copying Custom Filters + +Define custom filters for application-specific log patterns: + +```yaml +# Path to directory containing custom filter files +fail2ban_filters_path: "{{ playbook_dir }}/files/fail2ban/filters" +``` + +Place your filter `.conf` files in the specified directory. They will be copied to `/etc/fail2ban/filter.d/`. + +**Example custom filter** (`files/fail2ban/filters/myapp.conf`): +```ini +[Definition] +failregex = ^ .* "POST /login HTTP/.*" 401 + ^Authentication failure for .* from +ignoreregex = +``` + +#### Copying Custom Actions + +Define custom actions for specific ban/unban behaviors: + +```yaml +# Path to directory containing custom action files +fail2ban_actions_path: "files/fail2ban/actions" +``` + +Place your action `.conf` files in the specified directory. They will be copied to `/etc/fail2ban/action.d/`. + +To see an example of a custom action, I've included a [ufw-docker action](files/actions/ufw-docker.conf) with the role as I use it frequently. It is an enhanced UFW action that provides additional protection for Docker environments. To use it, just set `fail2ban_actions_path` to `actions`. + +> [!WARNING] +> For this action to work properly, you'll need to configure UFW to work properly with Docker, either manually or using the [ufw-docker](https://github.com/chaifeng/ufw-docker) script. + +## Dependencies + +None. + +## Example Playbook + +### Basic Usage with Recommended Defaults + +```yaml +- hosts: servers + become: yes + roles: + - role: fail2ban + vars: + # Add your management IPs to prevent lockouts + fail2ban_ignoreips: + - 127.0.0.1/8 + - ::1 + - 10.0.0.0/8 # Your private network + + + # Stricter default settings + fail2ban_bantime: 1h + fail2ban_maxretry: 3 + fail2ban_backend: systemd +``` + +### Custom Configuration with INI Format + +The role uses the `ini_file` module to edit the default configuration files. You may override or add to this configuration: + +```yaml +- hosts: servers + become: yes + roles: + - role: fail2ban + vars: + # Override fail2ban.conf base settings using INI format + fail2ban_configuration: + - option: loglevel + value: "NOTICE" + section: Definition + - option: logtarget + value: "SYSLOG" + section: Definition + + # Configure jail.conf email notifications + fail2ban_jail_configuration: + - option: destemail + value: "security@example.com" + section: DEFAULT + - option: sender + value: "fail2ban@example.com" + section: DEFAULT + - option: action + value: "%(action_mwl)s" # Mail with logs + section: DEFAULT +``` + +### Creating Custom Jails with Dictionaries + +```yaml +- hosts: servers + become: yes + roles: + - role: fail2ban + vars: + fail2ban_bantime: 1h + fail2ban_maxretry: 3 + + fail2ban_jails: + # SSH protection + - name: sshd + enabled: true + port: ssh + logpath: /var/log/auth.log + maxretry: 3 + bantime: 24h + + # Nginx authentication failures + - name: nginx-http-auth + enabled: true + port: http,https + logpath: /var/log/nginx/error.log + + # Recidive jail for repeat offenders + - name: recidive + enabled: true + maxretry: 3 + findtime: 1w + bantime: 30d + logpath: /var/log/fail2ban.log +``` + +### Using Complete Jail Files for Complex Configurations + +```yaml +- hosts: servers + become: yes + roles: + - role: fail2ban + vars: + # Use dictionaries for simple jails + fail2ban_jails: + - name: sshd + enabled: true + maxretry: 3 + + # For complex jails, copy complete jail files + fail2ban_jails_path: "{{ playbook_dir }}/files/fail2ban/jails" + +``` + +## Role Tasks + +This role performs the following tasks: + +1. Installs fail2ban and required dependencies +2. Configures `fail2ban.local` with base and custom settings +3. Configures `jail.local` with default jail parameters +4. Configures Debian-specific jail settings (when applicable) +5. Copies custom filter configurations (if provided) +6. Copies custom action configurations (if provided) +7. Copies or generates jail configurations +8. Starts and enables the fail2ban service + +## Handlers + +- `Restart fail2ban`: Restarts the fail2ban service when configuration changes are made + +## File Organization + +Organize your custom files in the following structure: + +``` +playbook_directory/ +├── files/ +│ └── fail2ban/ +│ ├── filters/ +│ │ ├── myapp.conf +│ │ └── custom-filter.conf +│ ├── actions/ +│ │ ├── slack-notify.conf +│ │ └── custom-action.conf +│ └── jails/ +│ └── complex-jail.local +├── roles/ +│ └── fail2ban/ +└── playbook.yml +``` + +## Tips and Best Practices + +1. **Always whitelist management IPs** in `fail2ban_ignoreips` to prevent lockouts +2. **Start with lenient settings** (higher maxretry, shorter bantime) and adjust based on logs +3. **Use systemd backend** when available for better performance: `fail2ban_backend: systemd` +4. **Monitor fail2ban logs** at `/var/log/fail2ban.log` to tune your rules +5. **Test custom filters** before deploying to production using `fail2ban-regex` +6. **Use dictionaries for simple jails**, copy files for complex ones requiring specific options +7. **Keep custom filters and actions in version control** alongside your playbooks + +## Testing Custom Filters + +Test your custom filters before deployment: + +```bash +fail2ban-regex /var/log/myapp.log /etc/fail2ban/filter.d/myapp.conf +``` + +## License + +MIT diff --git a/defaults/main.yaml b/defaults/main.yaml new file mode 100644 index 0000000..6c7c8d3 --- /dev/null +++ b/defaults/main.yaml @@ -0,0 +1,42 @@ +fail2ban_dependencies: + - fail2ban + - python3-systemd + +fail2ban_service: fail2ban + +fail2ban_loglevel: INFO +fail2ban_logtarget: /var/log/fail2ban.log + +fail2ban_ignoreself: "true" +fail2ban_ignoreips: + - 127.0.0.1/8 + - ::1 + +fail2ban_bantime: 10m +fail2ban_findtime: 10m +fail2ban_maxretry: 5 +fail2ban_backend: auto + +fail2ban_configuration: [] +# - option: loglevel +# value: "INFO" +# section: Definition + +fail2ban_jail_configuration: [] +# - option: ignoreself +# value: "true" +# section: DEFAULT + +fail2ban_default_debian_jail_configuration: [] +# - option: backend +# value: systemd +# section: sshd + +fail2ban_jails: +# - name: recidive +# enabled: true +# maxretry: 3 +# findtime: 1w +# bantime: 30d +# banaction: ufw +# logpath = /var/log/fail2ban.log diff --git a/files/actions/ufw-docker.conf b/files/actions/ufw-docker.conf new file mode 100644 index 0000000..9a6f066 --- /dev/null +++ b/files/actions/ufw-docker.conf @@ -0,0 +1,48 @@ +[Definition] + +actionstart = + +actionstop = + +actioncheck = + +actionban = ufw from comment "" + ufw out to comment "" + iptables -I DOCKER-USER 1 -s -j REJECT + + + +actionunban = ufw delete from + ufw delete out to + iptables -D DOCKER-USER -s -j REJECT + + +# Option: kill-mode +# Notes.: can be set to ss or conntrack (may be extended later with other modes) to immediately drop all connections from banned IP, default empty (no kill) +# Example: banaction = ufw[kill-mode=ss] +kill-mode = + +# intern conditional parameter used to provide killing mode after ban: +_kill_ = +_kill_ss = ss -K dst "[]" +_kill_conntrack = conntrack -D -s "" + +# Option: kill +# Notes.: can be used to specify custom killing feature, by default depending on option kill-mode +# Examples: banaction = ufw[kill='ss -K "( sport = :http || sport = :https )" dst "[]"'] +# banaction = ufw[kill='cutter ""'] +kill = <_kill_> + +[Init] +# Option: add +# Notes.: can be set to "insert 1" to insert a rule at certain position (here 1): +add = prepend + +# Option: blocktype +# Notes.: reject or deny +blocktype = deny + +# Option: comment +# Notes.: comment for rule added by fail2ban +comment = by Fail2Ban after attempts against + diff --git a/handlers/main.yaml b/handlers/main.yaml new file mode 100644 index 0000000..c59f08c --- /dev/null +++ b/handlers/main.yaml @@ -0,0 +1,6 @@ +--- +- name: Restart fail2ban + ansible.builtin.service: + name: "{{ fail2ban_service }}" + state: restarted + listen: Restart fail2ban \ No newline at end of file diff --git a/meta/main.yaml b/meta/main.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tasks/main.yaml b/tasks/main.yaml new file mode 100644 index 0000000..9f36f08 --- /dev/null +++ b/tasks/main.yaml @@ -0,0 +1,106 @@ +--- +- name: Install fail2ban and dependencies + ansible.builtin.package: + name: "{{ item }}" + state: latest + with_items: "{{ fail2ban_dependencies }}" + +- name: Configure fail2ban.local + community.general.ini_file: + path: /etc/fail2ban/fail2ban.local + section: "{{ item.section }}" + option: "{{ item.option }}" + value: "{{ item.value }}" + owner: root + group: root + mode: 0644 + loop: "{{ fail2ban_base_configuration + fail2ban_configuration }}" + loop_control: + label: "{{ item.option }}" + notify: Restart fail2ban + +- name: Configure jail.local + community.general.ini_file: + path: /etc/fail2ban/jail.local + section: "{{ item.section }}" + option: "{{ item.option }}" + value: "{{ item.value }}" + owner: root + group: root + mode: 0644 + loop: "{{ fail2ban_base_jail_configuration + fail2ban_jail_configuration }}" + loop_control: + label: "{{ item.option }}" + notify: Restart fail2ban + +- name: Configure Debian default jail config + community.general.ini_file: + path: /etc/fail2ban/jail.d/defaults-debian.conf + section: "{{ item.section }}" + option: "{{ item.option }}" + value: "{{ item.value }}" + owner: root + group: root + mode: 0644 + loop: "{{ fail2ban_default_debian_jail_configuration }}" + loop_control: + label: "{{ item.option }}" + notify: Restart fail2ban + when: ansible_facts['distribution'] == 'Debian' + +- name: Copy filter configs + community.general.ini_file: + src: "{{ item }}" + dest: /etc/fail2ban/filter.d/ + owner: root + group: root + mode: 0644 + with_fileglob: + - "{{ fail2ban_filters_path }}/*" + when: fail2ban_filters_path is defined + notify: Restart fail2ban + +- name: Copy action configs + ansible.builtin.copy: + src: "{{ item }}" + dest: /etc/fail2ban/action.d/ + owner: root + group: root + mode: 0644 + with_fileglob: + - "{{ fail2ban_actions_path }}/*" + when: fail2ban_actions_path is defined + notify: Restart fail2ban + +- name: Copy jail configs + ansible.builtin.copy: + src: "{{ item }}" + dest: /etc/fail2ban/jail.d/ + owner: root + group: root + mode: 0644 + with_fileglob: + - "{{ fail2ban_jails_path }}/*" + when: fail2ban_jails_path is defined + notify: Restart fail2ban + +- name: Create jail configs + ansible.builtin.template: + src: jail.local.j2 + dest: /etc/fail2ban/jail.d/{{ jail.name }}.local + owner: root + group: root + mode: 0644 + loop: "{{ fail2ban_jails }}" + loop_control: + label: "{{ jail.name }}" + loop_var: jail + when: fail2ban_jails + notify: Restart fail2ban + + +- name: Start and enable service + systemd: + name: fail2ban + state: started + enabled: true diff --git a/templates/jail.local.j2 b/templates/jail.local.j2 new file mode 100644 index 0000000..b2da4f6 --- /dev/null +++ b/templates/jail.local.j2 @@ -0,0 +1,33 @@ +# /etc/fail2ban/jail.d/{{ jail.name }}.local +# {{ ansible_managed }} + +[{{ jail.name }}] +enabled = {{ jail.enabled }} +filter = {{ jail.filter }} +{% if jail.port is defined %} +port = {{ jail.port }} +{% endif %} +{% if jail.logpath is defined %} +logpath = {{ jail.logpath }} +{% endif %} +{% if jail.maxretry is defined %} +maxretry = {{ jail.maxretry }} +{% endif %} +{% if jail.findtime is defined %} +findtime = {{ jail.findtime }} +{% endif %} +{% if jail.bantime is defined %} +bantime = {{ jail.bantime }} +{% endif %} +{% if jail.banaction is defined %} +banaction = {{ jail.banaction }} +{% endif %} +{% if jail.ignoreip is defined %} +ignoreip = {{ jail.ignoreip | join(' ') }} +{% endif %} +{% if jail.backend is defined %} +backend = {{ jail.backend }} +{% endif %} +{% if jail.action is defined %} +action = {{ jail.action }} +{% endif %} \ No newline at end of file diff --git a/vars/main.yaml b/vars/main.yaml new file mode 100644 index 0000000..27e988f --- /dev/null +++ b/vars/main.yaml @@ -0,0 +1,27 @@ +fail2ban_base_configuration: + - option: loglevel + value: "{{ fail2ban_loglevel }}" + section: Definition + - option: logtarget + value: "{{ fail2ban_logtarget }}" + section: Definition + +fail2ban_base_jail_configuration: + - option: ignoreself + value: "{{ fail2ban_ignoreself }}" + section: DEFAULT + - option: ignoreip + value: "{{ fail2ban_ignoreips | join(' ') }}" + section: DEFAULT + - option: bantime + value: "{{ fail2ban_bantime }}" + section: DEFAULT + - option: findtime + value: "{{ fail2ban_findtime }}" + section: DEFAULT + - option: maxretry + value: "{{ fail2ban_maxretry }}" + section: DEFAULT + - option: backend + section: DEFAULT + value: "{{ fail2ban_backend }}" \ No newline at end of file