Files
Ansible-roles/host_firewall/tasks/main.yml
T

364 lines
12 KiB
YAML

---
- name: Install nftables and curl
ansible.builtin.apt:
name: [nftables, curl]
state: present
- name: Check that this node is part of its own cluster ring
ansible.builtin.assert:
that: ansible_wt0.ipv4.address in (firewall.cluster.v4 | dict2items | map(attribute='value') | list)
fail_msg: "wt0 address {{ ansible_wt0.ipv4.address }} is missing from firewall.cluster.v4"
when: firewall.cluster.v4 | default({}, true) | length > 0 and ansible_wt0.ipv4.address is defined
- name: Remove the include block of the old nftables role
ansible.builtin.blockinfile:
path: /etc/nftables.conf
marker: "# {mark} ANSIBLE MANAGED BLOCK - nftables"
state: absent
- name: Remove the rule files of the old nftables role
ansible.builtin.file:
path: /etc/nftables/ansible-managed
state: absent
- name: Create /etc/nftables.d
ansible.builtin.file:
path: /etc/nftables.d
state: directory
mode: "0755"
# The kernel logs nft log statements of other network namespaces (LXC guests)
# only with this sysctl; the lines land in the host kernel log.
- name: Log the firewalls of container network namespaces
ansible.posix.sysctl:
name: net.netfilter.nf_log_all_netns
value: "1"
sysctl_file: /etc/sysctl.d/90-nf-log-all-netns.conf
sysctl_set: true
reload: false
when: firewall.log.all_netns
# The handshake meters match `ct original packets 2`, which counts only with conntrack accounting (off by default).
# The module is loaded at boot so that systemd-sysctl finds the key.
- name: Load nf_conntrack at boot
ansible.builtin.copy:
dest: /etc/modules-load.d/nf_conntrack.conf
content: "nf_conntrack\n"
mode: "0644"
- name: Count packets per connection in conntrack
ansible.posix.sysctl:
name: net.netfilter.nf_conntrack_acct
value: "1"
sysctl_file: /etc/sysctl.d/90-nf-conntrack-acct.conf
sysctl_set: true
reload: false
- name: Install the systemd units
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
mode: "0644"
loop: [nft-host.service, nft-host-geo.service, nft-host-geo.timer, nft-host-spamhaus.service, nft-host-spamhaus.timer, nft-host-firehol.service, nft-host-firehol.timer, nft-host-scanners.service, nft-host-scanners.timer]
register: firewall_units
- name: Install the rollback script
ansible.builtin.copy:
src: nft-host-rollback
dest: /usr/local/sbin/nft-host-rollback
mode: "0755"
- name: Geo sets
when: firewall.geo_countries | length > 0
block:
- name: Install the geo set updater
ansible.builtin.template:
src: nft-host-geo.j2
dest: /usr/local/sbin/nft-host-geo
mode: "0755"
- name: Build the geo sets on first run
ansible.builtin.command: /usr/local/sbin/nft-host-geo
args:
creates: /etc/nftables.d/host-geo.nft
- name: Read the geo sets file
ansible.builtin.stat:
path: /etc/nftables.d/host-geo.nft
register: firewall_geo
# Without geo sets the public SSH port would only answer NetBird and the admin set.
- name: Require non-empty geo sets
ansible.builtin.assert:
that: firewall_geo.stat.exists and firewall_geo.stat.size > 0
fail_msg: /etc/nftables.d/host-geo.nft is missing or empty, refusing to apply the firewall
# Check mode neither installs the updater nor builds the sets.
when: not ansible_check_mode
- name: Enable the geo set timer
ansible.builtin.systemd:
name: nft-host-geo.timer
enabled: true
state: started
daemon_reload: "{{ firewall_units is changed }}"
# Check mode does not install the unit.
ignore_errors: "{{ ansible_check_mode }}"
- name: Stop the geo set refresh
when: firewall.geo_countries | length == 0
block:
- name: Disable the geo set timer
ansible.builtin.systemd:
name: nft-host-geo.timer
enabled: false
state: stopped
daemon_reload: "{{ firewall_units is changed }}"
# Check mode does not install the unit.
ignore_errors: "{{ ansible_check_mode }}"
- name: Remove the geo sets file
ansible.builtin.file:
path: /etc/nftables.d/host-geo.nft
state: absent
- name: Spamhaus sets
when: firewall.spamhaus
block:
- name: Install the Spamhaus set updater
ansible.builtin.copy:
src: nft-host-spamhaus
dest: /usr/local/sbin/nft-host-spamhaus
mode: "0755"
# The rule set includes this file, so a missing one would fail the load at boot.
- name: Build the Spamhaus sets on first run
ansible.builtin.command: /usr/local/sbin/nft-host-spamhaus
args:
creates: /etc/nftables.d/host-spamhaus.nft
- name: Read the Spamhaus sets file
ansible.builtin.stat:
path: /etc/nftables.d/host-spamhaus.nft
register: firewall_spamhaus
- name: Require non-empty Spamhaus sets
ansible.builtin.assert:
that: firewall_spamhaus.stat.exists and firewall_spamhaus.stat.size > 0
fail_msg: /etc/nftables.d/host-spamhaus.nft is missing or empty, refusing to apply the firewall
# Check mode neither installs the updater nor builds the sets.
when: not ansible_check_mode
- name: Enable the Spamhaus set timer
ansible.builtin.systemd:
name: nft-host-spamhaus.timer
enabled: true
state: started
daemon_reload: "{{ firewall_units is changed }}"
# Check mode does not install the unit.
ignore_errors: "{{ ansible_check_mode }}"
- name: Stop the Spamhaus set refresh
when: not firewall.spamhaus
block:
- name: Disable the Spamhaus set timer
ansible.builtin.systemd:
name: nft-host-spamhaus.timer
enabled: false
state: stopped
daemon_reload: "{{ firewall_units is changed }}"
# Check mode does not install the unit.
ignore_errors: "{{ ansible_check_mode }}"
- name: Remove the Spamhaus sets file
ansible.builtin.file:
path: /etc/nftables.d/host-spamhaus.nft
state: absent
- name: FireHOL set
when: firewall.firehol
block:
- name: Install the FireHOL set updater
ansible.builtin.copy:
src: nft-host-firehol
dest: /usr/local/sbin/nft-host-firehol
mode: "0755"
# The rule set includes this file, so a missing one would fail the load at boot.
- name: Build the FireHOL set on first run
ansible.builtin.command: /usr/local/sbin/nft-host-firehol
args:
creates: /etc/nftables.d/host-firehol.nft
- name: Read the FireHOL set file
ansible.builtin.stat:
path: /etc/nftables.d/host-firehol.nft
register: firewall_firehol
- name: Require non-empty FireHOL set
ansible.builtin.assert:
that: firewall_firehol.stat.exists and firewall_firehol.stat.size > 0
fail_msg: /etc/nftables.d/host-firehol.nft is missing or empty, refusing to apply the firewall
# Check mode neither installs the updater nor builds the sets.
when: not ansible_check_mode
- name: Enable the FireHOL set timer
ansible.builtin.systemd:
name: nft-host-firehol.timer
enabled: true
state: started
daemon_reload: "{{ firewall_units is changed }}"
# Check mode does not install the unit.
ignore_errors: "{{ ansible_check_mode }}"
- name: Stop the FireHOL set refresh
when: not firewall.firehol
block:
- name: Disable the FireHOL set timer
ansible.builtin.systemd:
name: nft-host-firehol.timer
enabled: false
state: stopped
daemon_reload: "{{ firewall_units is changed }}"
# Check mode does not install the unit.
ignore_errors: "{{ ansible_check_mode }}"
- name: Remove the FireHOL set file
ansible.builtin.file:
path: /etc/nftables.d/host-firehol.nft
state: absent
- name: Internet scanner sets
when: firewall.scanners
block:
- name: Install the internet scanner set updater
ansible.builtin.copy:
src: nft-host-scanners
dest: /usr/local/sbin/nft-host-scanners
mode: "0755"
# The updater leaves out entries overlapping these addresses.
- name: Install the internet scanner allowlist
ansible.builtin.template:
src: nft-host-scanners.allowlist.j2
dest: /etc/nft-host-scanners.allowlist
mode: "0644"
# The rule set includes this file, so a missing one would fail the load at boot.
- name: Build the internet scanner sets on first run
ansible.builtin.command: /usr/local/sbin/nft-host-scanners
args:
creates: /etc/nftables.d/host-scanners.nft
- name: Read the internet scanner sets file
ansible.builtin.stat:
path: /etc/nftables.d/host-scanners.nft
register: firewall_scanners
- name: Require non-empty internet scanner sets
ansible.builtin.assert:
that: firewall_scanners.stat.exists and firewall_scanners.stat.size > 0
fail_msg: /etc/nftables.d/host-scanners.nft is missing or empty, refusing to apply the firewall
# Check mode neither installs the updater nor builds the sets.
when: not ansible_check_mode
- name: Enable the internet scanner set timer
ansible.builtin.systemd:
name: nft-host-scanners.timer
enabled: true
state: started
daemon_reload: "{{ firewall_units is changed }}"
# Check mode does not install the unit.
ignore_errors: "{{ ansible_check_mode }}"
- name: Stop the internet scanner set refresh
when: not firewall.scanners
block:
- name: Disable the internet scanner set timer
ansible.builtin.systemd:
name: nft-host-scanners.timer
enabled: false
state: stopped
daemon_reload: "{{ firewall_units is changed }}"
# Check mode does not install the unit.
ignore_errors: "{{ ansible_check_mode }}"
- name: Remove the internet scanner sets file
ansible.builtin.file:
path: /etc/nftables.d/host-scanners.nft
state: absent
- name: Render the rule set
ansible.builtin.template:
src: host.nft.j2
dest: /etc/nftables.d/host.nft
mode: "0644"
validate: nft -c -f %s
register: firewall_rules
- name: Read the firewall unit state
ansible.builtin.command: systemctl is-active nft-host.service
register: firewall_active
changed_when: false
failed_when: false
check_mode: false
- name: Apply the rule set with a rollback timer
when:
- firewall_rules is changed or firewall_units is changed or firewall_active.stdout != 'active'
- not ansible_check_mode
block:
# A failed run of the transient unit keeps its name taken for systemd-run.
- name: Clear a leftover rollback timer
ansible.builtin.shell: >-
systemctl stop nft-host-rollback.timer;
systemctl reset-failed nft-host-rollback.timer nft-host-rollback.service;
true
changed_when: false
# Unless disarmed below, this moves the rejected rule file aside so a reboot
# does not load it again, and reloads the last confirmed one; without one
# it drops the table.
- name: Arm the rollback
ansible.builtin.command: systemd-run --on-active={{ firewall.rollback_seconds }} --unit=nft-host-rollback /usr/local/sbin/nft-host-rollback
changed_when: false
- name: Load the rule set
ansible.builtin.systemd:
name: nft-host.service
enabled: true
state: reloaded
daemon_reload: true
# A reused SSH control socket would hide a lockout, so test a fresh connection.
- name: Drop the SSH connection
ansible.builtin.meta: reset_connection
- name: Reconnect through the new rule set
ansible.builtin.wait_for_connection:
timeout: "{{ (firewall.rollback_seconds / 2) | int }}"
- name: Check the services behind the new rule set
ansible.builtin.shell: "{{ item.value }}"
loop: "{{ firewall.confirm | default({}, true) | dict2items | rejectattr('value', 'none') }}"
loop_control:
label: "{{ item.key }}"
register: firewall_confirm
until: firewall_confirm.rc == 0
retries: "{{ (firewall.rollback_seconds / 20) | int }}"
delay: 5
changed_when: false
# Fails when the timer already fired, so a slow check cannot pass for a rolled-back host.
- name: Disarm the rollback
ansible.builtin.shell: >-
test "$(systemctl show --property=SubState --value nft-host-rollback.timer)" = waiting &&
systemctl stop nft-host-rollback.timer
changed_when: true
- name: Keep the rule set as the last confirmed one
ansible.builtin.copy:
src: /etc/nftables.d/host.nft
dest: /etc/nftables.d/host.nft.good
remote_src: true
mode: "0644"