--- - 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"