commit 989693564a011f4d9340b89a169ad01ac8b50248 Author: OldTyT Date: Thu Nov 28 00:01:14 2024 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ae5da4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.log +*.pyc +**/__pycache__/ +.env +.secrets + diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..ad0be76 --- /dev/null +++ b/.yamllint @@ -0,0 +1,11 @@ +extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + line-length: disable + truthy: disable diff --git a/README.md b/README.md new file mode 100644 index 0000000..718ea4f --- /dev/null +++ b/README.md @@ -0,0 +1,193 @@ +# Fail2ban + +Role which installs and configures Fail2ban. + +## Role usage + +The role should be used after other roles installing software which needs protection. + +## Deploy example (do not copy blindly!) + +```yaml +roles: + - role: fail2ban + fail2ban_ignores_ips: ['10.0.0.0/8'] + fail2ban_enable_ignorecommand: true + fail2ban_custom_ipset_lists: [whitelist, whitelist6] + fail2ban_recidive_ignore_jails: [some-jail, another-jail] + fail2ban_filters: [ + { name: nginx-req-limits, + failregex: [ '^\s*\[error\] \d+#\d+: \*\d+ limiting requests, excess: [\d\.]+ by zone "[^"]+", client: ' ], + ignoreregex: [] + }, + { name: nginx-con-limits, + failregex: [ '^\s*\[error\] \d+#\d+: \*\d+ limiting connections by zone "[^"]+", client: ' ], + ignoreregex: [] + }, + { name: multiple-regexps-example, + failregex: [ 'some-fail-regexp1', 'some-fail-regexp2' ], + ignoreregex: [ 'some-ignore-regexp1', 'some-ignore-regexp2' ] + } + ] + fail2ban_sshd: + maxretry: 5 + bantime: 3600 + findtime: 600 + fail2ban_services: [ + { name: nginx-req-limits, + filter: nginx-req-limits, + port: 'http,https', + logpath: '/var/log/nginx/*error.log', + bantime: 600, + findtime: 300, + maxretry: 5 + }, + { name: nginx-con-limits, + filter: nginx-con-limits, + port: 'http,https', + logpath: '/var/log/nginx/*error.log', + bantime: 600, + findtime: 300, + maxretry: 5 + } + ] + fail2ban_containers: [ + { name: web-prod, + logpath: /tmp/web-prod.log + }, + { name: backend-prod, + logpath: /tmp/backend-prod.log, + bantime: 9000 + } + ] +``` + +## About available parameters + +### Main params + +| Param | Default | Description | +| -------- | -------- | -------- | +| `fail2ban_setup` | `full` | - | +| `fail2ban_defaults` | see defaults/main.yml | controls default bantime, findtime and maxretry params | +| `fail2ban_ignores_ips` | - | controls list of IP's to ignore (see ignoreip fail2ban param) | +| `fail2ban_alerts` | see defaults/main.yml | control on alert-sending, just in case if we'll need it anywhere | +| `fail2ban_filters` | - | describes the filters to create, more details below | +| `fail2ban_services` | - | describes the custom jails to create, more details below | +| `fail2ban_containers` | - | params for sshd-jails for LXC containers, more details below | +| `fail2ban_blocktype` | `DROP` | we want to drop packets instead of rejecting them. Do not change this without a reason | +| `fail2ban_role_test_mode` | - | could be used for test purposes only, do not use it in production playbooks | +| `fail2ban_enable_ignorecommand` | `false` | enables ignorecommand options in jail.local and checking script generation | +| `fail2ban_default_ipset_lists` | `[whitelist, whitelist6]` | describes the default ipset lists for checking script | +| `fail2ban_custom_ipset_lists` | `[]` | describes the custom ipset lists for checking script | +| `fail2ban_dummy_logs` | `false` | Whether to create dummy logs to avoid service failing to start due to absence of any jail logs. | +| `fail2ban_dummy_log_path` | `/var/log/fail2ban-dummy.log` | Path to dummy log (will be automatically created). | +| `fail2ban_recidive_ignore_jails` | `[]` | List of jails that need to be ignored by recidive jail. | + +### Jail generation + +#### SSH jails + +Our common sshd jail is generated automatically if openssh-server package was found during role setup. By default it will look like this: + +```plaintext +[sshd] +enabled = true +ports = 22 +maxretry = 3 +bantime = 7200 +findtime = 1800 +``` + +If needed, default params for this jail could be overwritten from playbook with **fail2ban_sshd** variable, for example: + +#### SSH jails for LXC containers + +```yaml + fail2ban_sshd: + maxretry: 5 + bantime: 3600 + findtime: 600 +``` + +Jails for LXC containers are being created only if **fail2ban_containers** list is not empty. You can control any param of this jail from playbook, for example: + +```yaml + fail2ban_containers: [ + { name: web-prod, + logpath: /mnt/data/containers/web-prod/var/log/auth.log.log + }, + { name: backend-prod, + logpath: /mnt/data/containers/backend-prod/var/log/auth.log, + bantime: 9000, + findtime: 600 + } + ] +``` + +Make sure to set "logpath" variable, role can't guess the correct path by itself! + +#### Custom jails + +Could be created with **services** list, usage example: + +```yaml + fail2ban_services: [ + { name: nginx-req-limits, + filter: nginx-req-limits, + port: 'http,https', + logpath: '/var/log/nginx/*error.log', + bantime: 600, + findtime: 300, + maxretry: 5 + }, + { name: nginx-con-limits, + filter: nginx-con-limits, + port: 'http,https', + logpath: '/var/log/nginx/*error.log', + bantime: 600, + findtime: 300, + maxretry: 5 + } + ] +``` + +You can describe any key-value pairs here. **name** is mandatory for jail creation. + +### Custom filters creation + +Can be done with **filters** variable. For example: + +```yaml + fail2ban_filters: [ + { name: nginx-req-limits, + failregex: [ '^\s*\[error\] \d+#\d+: \*\d+ limiting requests, excess: [\d\.]+ by zone "[^"]+", client: ' ], + ignoreregex: [] + }, + { name: nginx-con-limits, + failregex: [ '^\s*\[error\] \d+#\d+: \*\d+ limiting connections by zone "[^"]+", client: ' ], + ignoreregex: [] + }, + { name: multiple-regexps-example, + failregex: [ 'some-fail-regexp1', 'some-fail-regexp2' ], + ignoreregex: [ 'some-ignore-regexp1', 'some-ignore-regexp2' ] + } + ] +``` + +You can describe any key-value pairs here. **name** is mandatory for filter creation. + +### Checking ip in ipset lists + +Fail2ban can use external command to dynamically check if IP should be ingored. Option fail2ban_enable_ignorecommand enables it. External script will check the IP’s presence in every ipset list from fail2ban_default_ipset_lists and fail2ban_custom_ipset_lists. If IP will be found in any list given - fail2ban will ignore it, if not - IP will be placed in appropriate jail. + +## Useful links + +- [Official wiki](https://www.fail2ban.org/wiki/index.php/Main_Page) +- [Documentation on ubuntu.ru](https://help.ubuntu.ru/wiki/fail2ban) + +## TODO + +- update readme +- remove syslog user creation (leave in test mode only maybe) +- move names and paths from check_ip_script template to vars diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..c5a7f88 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,15 @@ +--- + +fail2ban_setup: full +fail2ban_defaults: { bantime: 7200, findtime: 600, maxretry: 3 } +fail2ban_ignores_ips: [] +fail2ban_services: [] +fail2ban_alerts: { email: '', enabled: false, from: '' } + +fail2ban_blocktype: DROP +fail2ban_default_ipset_lists: [whitelist, whitelist6] +fail2ban_custom_ipset_lists: [] +fail2ban_enable_ignorecommand: false +fail2ban_dummy_logs: false +fail2ban_dummy_log_path: /var/log/fail2ban-dummy.log +fail2ban_recidive_ignore_jails: [] diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..5af20a7 --- /dev/null +++ b/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: reload fail2ban unit + systemd: + daemon_reload: yes + +- name: fail2ban restart + service: + name: fail2ban + state: restarted diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..623d03b --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,14 @@ +--- +galaxy_info: + author: OldTyT + description: Fail2Ban + min_ansible_version: 2.8 + platforms: + - name: Ubuntu + versions: + - xenial + - bionic + - jammy + - noble + galaxy_tags: + - fail2ban diff --git a/tasks/configure.yml b/tasks/configure.yml new file mode 100644 index 0000000..3616cf0 --- /dev/null +++ b/tasks/configure.yml @@ -0,0 +1,81 @@ +--- + +- name: prepare systemd override + block: + - name: create fail2ban.service.d directory + file: + path: /etc/systemd/system/fail2ban.service.d + state: directory + mode: '0755' + + - name: create fail2ban.service override + template: + src: systemd-override.j2 + dest: /etc/systemd/system/fail2ban.service.d/override.conf + notify: + - reload fail2ban unit + - fail2ban restart + when: fail2ban_dummy_logs + +- name: create jail.local + template: + src: jail.j2 + dest: /etc/fail2ban/jail.local + mode: 0644 + notify: fail2ban restart + +- name: create iptables-multiport-fw.conf + template: + src: action_iptables-multiport-fw.j2 + dest: /etc/fail2ban/action.d/iptables-multiport-fw.conf + mode: 0644 + notify: fail2ban restart + +- name: deploy custom filters + template: + src: custom-filter.j2 + dest: "/etc/fail2ban/filter.d/{{ item.name }}.conf" + with_items: "{{ fail2ban_filters }}" + when: fail2ban_filters is defined + notify: fail2ban restart + +- name: adjust blocktype in iptables conf + replace: + path: "{{ '/etc/fail2ban/action.d/iptables-ipset.conf' if ansible_distribution_release == 'noble' else '/etc/fail2ban/action.d/iptables-common.conf' }}" + regexp: '^blocktype =.+' + replace: "blocktype = {{ fail2ban_blocktype }}" + notify: fail2ban restart + when: not ansible_check_mode + +- name: create scripts directory + file: + dest: /usr/local/etc/scripts + state: directory + when: fail2ban_enable_ignorecommand + +- name: create custom.fail2ban-check-ip.conf + template: + src: check-ip_conf.j2 + dest: /usr/local/etc/scripts/custom.fail2ban-check-ip.conf + mode: 0644 + when: fail2ban_enable_ignorecommand and not ansible_check_mode + +- name: create custom.fail2ban-check-ip + template: + src: fail2ban-check-ip.j2 + dest: /usr/local/sbin/custom.fail2ban-check-ip + mode: 0744 + when: fail2ban_enable_ignorecommand + +- name: adjust ignoreregex in recidive filter + lineinfile: + path: /etc/fail2ban/filter.d/recidive.conf + regexp: '^ignoreregex' + line: "ignoreregex = .*\\[({{ fail2ban_recidive_ignore_jails | join('|') }})\\].*" + when: not ansible_check_mode and fail2ban_recidive_ignore_jails | length > 0 + +- name: ensure fail2ban service is enabled and started + service: + name: fail2ban + enabled: true + state: started diff --git a/tasks/install.yml b/tasks/install.yml new file mode 100644 index 0000000..2c7a6cd --- /dev/null +++ b/tasks/install.yml @@ -0,0 +1,9 @@ +--- +- name: install fail2ban + apt: + name: fail2ban + update_cache: true + +- name: gather installed packages + package_facts: + manager: apt diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..43aa51d --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,8 @@ +--- + +- include_tasks: prepare.yml + +- include_tasks: install.yml + when: fail2ban_setup == "full" + +- include_tasks: configure.yml diff --git a/tasks/prepare.yml b/tasks/prepare.yml new file mode 100644 index 0000000..3a96b94 --- /dev/null +++ b/tasks/prepare.yml @@ -0,0 +1,45 @@ +--- + +- name: create test dirs + file: + path: "{{ item }}" + state: directory + when: fail2ban_role_test_mode is defined + with_items: + - /var/log/nginx + +- name: create test log files + template: + src: testfile.j2 + dest: "{{ item }}" + when: fail2ban_role_test_mode is defined + with_items: + - /var/log/nginx/test-error.log + - /tmp/web-prod.log + - /tmp/backend-prod.log + +- name: ensure syslog group is present + group: + name: syslog + system: true + state: present + +- name: ensure syslog user is present + user: + name: syslog + group: syslog + groups: adm + home: "{{ '/nonexistent' if ansible_distribution_release == 'noble' else '/home/syslog' }}" + create_home: no + shell: "{{ '/usr/sbin/nologin' if ansible_distribution_release == 'bionic' else '/bin/false' }}" + system: true + state: present + +- name: prepare auth.log + copy: + content: "" + dest: /var/log/auth.log + force: false + group: adm + owner: syslog + mode: 0640 diff --git a/templates/action_iptables-multiport-fw.j2 b/templates/action_iptables-multiport-fw.j2 new file mode 100644 index 0000000..c6cb18e --- /dev/null +++ b/templates/action_iptables-multiport-fw.j2 @@ -0,0 +1,23 @@ +{% raw %}[INCLUDES] + +before = iptables-blocktype.conf + +[Definition] + +actionstart = iptables -N fail2ban- + iptables -A fail2ban- -j RETURN + iptables -I -p -m multiport --dports -j fail2ban- +actionstop = iptables -D -p -m multiport --dports -j fail2ban- + iptables -F fail2ban- + iptables -X fail2ban- +actioncheck = iptables -n -L | grep -q 'fail2ban-[ \t]' +actionban = iptables -I fail2ban- 1 -s -j +actionunban = iptables -D fail2ban- -s -j + +[Init] + +name = default +port = ssh +protocol = tcp +fwchain = FORWARD +{% endraw %} diff --git a/templates/check-ip_conf.j2 b/templates/check-ip_conf.j2 new file mode 100644 index 0000000..935f4fe --- /dev/null +++ b/templates/check-ip_conf.j2 @@ -0,0 +1,3 @@ +#!/bin/bash + +check_ip_lists=({% for list in fail2ban_default_ipset_lists %} {{ list }} {% endfor %}{% for list in fail2ban_custom_ipset_lists %} {{ list }} {% endfor %}) diff --git a/templates/custom-filter.j2 b/templates/custom-filter.j2 new file mode 100644 index 0000000..9230b26 --- /dev/null +++ b/templates/custom-filter.j2 @@ -0,0 +1,31 @@ +[Definition] + +{% for key, value in item.items() %} +{% if key != 'name' and key == 'failregex' %} +failregex = {% for item in value %} +{% if item == value|first %} +{{ item }} +{% else %} +{{ item|indent(12, True) }} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} + +{% for key, value in item.items() %} +{% if key != 'name' and key == 'ignoreregex' %} +ignoreregex = {% for item in value %} +{% if item == value|first %} +{{ item }} +{% else %} +{{ item|indent(14, True) }} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} + +{% for key, value in item.items() %} +{% if key != 'name' and key != 'failregex' and key != 'ignoreregex' %} +{{ key }} = {{ value }} +{% endif %} +{% endfor %} diff --git a/templates/fail2ban-check-ip.j2 b/templates/fail2ban-check-ip.j2 new file mode 100644 index 0000000..e072474 --- /dev/null +++ b/templates/fail2ban-check-ip.j2 @@ -0,0 +1,14 @@ +#!/bin/bash + +target_ip=$1 +check_ip_config='/usr/local/etc/scripts/custom.fail2ban-check-ip.conf' + +test -s "${check_ip_config}" && . "${check_ip_config}" +function check_ip_in_ipset() { + for list in "${check_ip_lists[@]}"; do + /sbin/ipset save "${list}" | grep -qE "${target_ip}" && return 0 + done + return 1 +} + +check_ip_in_ipset diff --git a/templates/jail.j2 b/templates/jail.j2 new file mode 100644 index 0000000..0fe2b85 --- /dev/null +++ b/templates/jail.j2 @@ -0,0 +1,60 @@ +[DEFAULT] +{{ "%-13s = %s"|format('bantime', fail2ban_defaults.bantime | default('7200', true)) }} +{{ "%-13s = %s"|format('findtime', fail2ban_defaults.findtime | default('600', true)) }} +{{ "%-13s = %s"|format('maxretry', fail2ban_defaults.maxretry | default('3', true)) }} +{% if fail2ban_alerts.enabled %} +{{ "%-13s = %s"|format('sendername', fail2ban_alerts.from) }} +{{ "%-13s = %s"|format('destemail', fail2ban_alerts.email) }} +{{ "%-13s = %s"|format('action', '%(action_mwl)s') }} +{% endif %} +{{ "%-13s = %s %s"|format('ignoreip', '127.0.0.1/8', fail2ban_ignores_ips|join(' ')) }} +{% if fail2ban_enable_ignorecommand %} +{{ "%-13s = %s"|format('ignorecommand', '/usr/local/sbin/custom.fail2ban-check-ip ') }} +{% endif %} +{% if 'openssh-server' in ansible_facts.packages %} + +[sshd] +{{ "%-10s = %s"|format('enabled', fail2ban_sshd.enabled | default('true', true)) }} +{{ "%-10s = %s"|format('ports', fail2ban_sshd.ports | default('22', true)) }} +{{ "%-10s = %s"|format('maxretry', fail2ban_sshd.maxretry | default(fail2ban_defaults.maxretry, true)) }} +{{ "%-10s = %s"|format('bantime', fail2ban_sshd.bantime | default(fail2ban_defaults.bantime, true)) }} +{{ "%-10s = %s"|format('findtime', fail2ban_sshd.findtime | default('1800', true)) }} +{% endif %} +{% if fail2ban_containers is defined and fail2ban_containers|length %} +{% for container in fail2ban_containers %} + +[sshd-lxc-{{ container.name }}] +{{ "%-10s = %s"|format('enabled', container.enabled | default('true', true)) }} +{{ "%-10s = %s"|format('logpath', container.logpath | mandatory) }} +{% if fail2ban_dummy_logs %} +{{ fail2ban_dummy_log_path|indent(13, True) }} +{% endif %} +{{ "%-10s = %s"|format('filter', container.filter | default('sshd', true)) }} +{{ "%-10s = %s"|format('port', container.port | default('22', true)) }} +{{ "%-10s = %s"|format('backend', container.backend | default('polling', true)) }} +{{ "%-10s = %s"|format('chain', container.chain | default('FORWARD', true)) }} +{{ "%-10s = %s"|format('banaction', container.banaction | default('iptables-multiport', true)) }} +{{ "%-10s = %s"|format('bantime', container.bantime | default('10000', true)) }} +{{ "%-10s = %s"|format('maxretry', container.maxretry | default('3', true)) }} +{{ "%-10s = %s"|format('findtime', container.findtime | default('1800', true)) }} +{% endfor %} +{% endif %} +{% if fail2ban_services is defined and fail2ban_services|length %} +{% for service in fail2ban_services %} + +[{{ service.name }}] +{% if not service.enabled is defined %} +{{ "%-10s = %s"|format("enabled", "True") }} +{% endif %} +{% for key, value in service.items()|sort() %} +{% if key != 'name' %} +{% if key == 'logpath' and fail2ban_dummy_logs %} +{{ "%-10s = %s"|format(key, value) }} +{{ fail2ban_dummy_log_path|indent(13, True) }} +{% else %} +{{ "%-10s = %s"|format(key, value) }} +{% endif %} +{% endif %} +{% endfor %} +{% endfor %} +{% endif %} diff --git a/templates/systemd-override.j2 b/templates/systemd-override.j2 new file mode 100644 index 0000000..412644c --- /dev/null +++ b/templates/systemd-override.j2 @@ -0,0 +1,2 @@ +[Service] +ExecStartPre=/usr/bin/touch {{ fail2ban_dummy_log_path }} diff --git a/templates/testfile.j2 b/templates/testfile.j2 new file mode 100644 index 0000000..7cc6db8 --- /dev/null +++ b/templates/testfile.j2 @@ -0,0 +1 @@ +This file is needed for role testing.