Replace the nftables role with host_firewall, one atomic inet host input table with zones, geo sets and lockout rollback
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
# Port and address maps are name -> value so group_vars and host_vars levels
|
||||||
|
# merge into one map (hash_behaviour = merge); a null value drops an entry.
|
||||||
|
# Ports are numbers or "first-last" ranges.
|
||||||
|
firewall:
|
||||||
|
public:
|
||||||
|
tcp: {}
|
||||||
|
udp: {}
|
||||||
|
# Accepted only from the geo sets or the admin set.
|
||||||
|
geo:
|
||||||
|
tcp: {}
|
||||||
|
# Accepted on wt0 before the public zone is evaluated.
|
||||||
|
vpn:
|
||||||
|
tcp: {}
|
||||||
|
udp: {}
|
||||||
|
admin:
|
||||||
|
v4: {}
|
||||||
|
v6: {}
|
||||||
|
# Interface names (nft wildcards allowed) whose traffic is trusted.
|
||||||
|
internal_ifaces: {}
|
||||||
|
# Guest bridges (Proxmox vmbr*): DHCP, router solicitations and the public
|
||||||
|
# zone only, never the geo ports.
|
||||||
|
guest_ifaces: {}
|
||||||
|
# Kubernetes ring: pod interfaces, and node NetBird IPs / pod CIDRs on wt0.
|
||||||
|
cluster:
|
||||||
|
ifaces: {}
|
||||||
|
v4: {}
|
||||||
|
v6: {}
|
||||||
|
ssh_rate_limit: "10/minute burst 5 packets"
|
||||||
|
# EU-27 plus CH, NO, GB, IS, LI (ipdeny country codes). Empty leaves the geo
|
||||||
|
# ports to the admin set; a load peaks at about 60 MB of kernel memory, too much for a 1 GB host.
|
||||||
|
geo_countries: [at, be, bg, hr, cy, cz, dk, ee, fi, fr, de, gr, hu, ie, it, lv, lt, lu, mt, nl, pl, pt, ro, sk, si, es, se, ch, "no", gb, is, li]
|
||||||
|
rollback_seconds: 120
|
||||||
|
# name -> shell command that has to exit 0 through the new rule set before the
|
||||||
|
# rollback is disarmed; retried for a quarter of the rollback window.
|
||||||
|
confirm: {}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Refresh the geo sets of the host firewall
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/nft-host-geo
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daily refresh of the host firewall geo sets
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
RandomizedDelaySec=6h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Started by a transient timer before a rule set load; Ansible stops the timer
|
||||||
|
# once the new rule set is confirmed.
|
||||||
|
cd /etc/nftables.d || exit 1
|
||||||
|
mv -f host.nft host.nft.rolled-back
|
||||||
|
if [ -s host.nft.good ]; then
|
||||||
|
cp host.nft.good host.nft && nft -f host.nft && exit 0
|
||||||
|
fi
|
||||||
|
nft delete table inet host
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Host input firewall (nftables table inet host)
|
||||||
|
# Same early-boot placement as nftables.service. Its "flush ruleset" on start,
|
||||||
|
# reload and stop would wipe this table, so load after it and follow all three.
|
||||||
|
DefaultDependencies=no
|
||||||
|
Wants=network-pre.target
|
||||||
|
Before=network-pre.target shutdown.target
|
||||||
|
After=nftables.service
|
||||||
|
PartOf=nftables.service
|
||||||
|
ReloadPropagatedFrom=nftables.service
|
||||||
|
Conflicts=shutdown.target
|
||||||
|
ConditionPathExists=/etc/nftables.d/host.nft
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=/usr/sbin/nft -f /etc/nftables.d/host.nft
|
||||||
|
ExecReload=/usr/sbin/nft -f /etc/nftables.d/host.nft
|
||||||
|
ExecStop=-/usr/sbin/nft delete table inet host
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sysinit.target
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
- 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"
|
||||||
|
|
||||||
|
- 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]
|
||||||
|
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 }}"
|
||||||
|
|
||||||
|
- name: Remove the geo sets file
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /etc/nftables.d/host-geo.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"
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/sbin/nft -f
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
{% macro list(map) -%}
|
||||||
|
{{ map | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | map('string') | unique | join(', ') }}
|
||||||
|
{%- endmacro %}
|
||||||
|
{% macro addr_set(name, type, map) -%}
|
||||||
|
set {{ name }} {
|
||||||
|
type {{ type }}; flags interval; auto-merge;
|
||||||
|
{% if list(map) %}
|
||||||
|
elements = { {{ list(map) }} }
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
{%- endmacro %}
|
||||||
|
{% set cluster = firewall.cluster | default({}, true) %}
|
||||||
|
{% set cluster_ifaces = cluster.ifaces | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | unique | list %}
|
||||||
|
{% set internal_ifaces = firewall.internal_ifaces | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | unique | list %}
|
||||||
|
{% set guest_ifaces = firewall.guest_ifaces | default({}, true) | dict2items | rejectattr('value', 'none') | map(attribute='value') | unique | list %}
|
||||||
|
{% set guest_match = 'iifname { "' ~ (guest_ifaces | join('", "')) ~ '" }' %}
|
||||||
|
|
||||||
|
# Declare-then-delete makes every load one atomic transaction that replaces
|
||||||
|
# only this table; Cilium, Docker, NetBird and CrowdSec tables stay untouched.
|
||||||
|
table inet host {}
|
||||||
|
delete table inet host
|
||||||
|
|
||||||
|
{% if firewall.geo_countries %}
|
||||||
|
include "/etc/nftables.d/host-geo.nft"
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
table inet host {
|
||||||
|
{{ addr_set('admin_v4', 'ipv4_addr', firewall.admin.v4) }}
|
||||||
|
{{ addr_set('admin_v6', 'ipv6_addr', firewall.admin.v6) }}
|
||||||
|
{{ addr_set('cluster_v4', 'ipv4_addr', cluster.v4) }}
|
||||||
|
{{ addr_set('cluster_v6', 'ipv6_addr', cluster.v6) }}
|
||||||
|
set ssh_meter_v4 {
|
||||||
|
type ipv4_addr; flags dynamic; timeout 10m; size 65535;
|
||||||
|
}
|
||||||
|
set ssh_meter_v6 {
|
||||||
|
type ipv6_addr; flags dynamic; timeout 10m; size 65535;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority filter; policy drop;
|
||||||
|
|
||||||
|
ct state established,related accept
|
||||||
|
ct state invalid drop
|
||||||
|
iif lo accept
|
||||||
|
|
||||||
|
icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept
|
||||||
|
icmpv6 type { echo-request, destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, mld-listener-query } accept
|
||||||
|
udp sport 67 udp dport 68 accept
|
||||||
|
meta nfproto ipv6 udp sport 547 udp dport 546 accept
|
||||||
|
{% if cluster_ifaces or list(cluster.v4) or list(cluster.v6) %}
|
||||||
|
|
||||||
|
# ponytail: the cluster ring trusts every port between nodes and pods;
|
||||||
|
# per-port lists (6443, 10250, 2379-2380, 4240, 8472, ...) are the upgrade path.
|
||||||
|
{% for iface in cluster_ifaces %}
|
||||||
|
iifname "{{ iface }}" accept
|
||||||
|
{% endfor %}
|
||||||
|
iifname "wt0" ip saddr @cluster_v4 accept
|
||||||
|
iifname "wt0" ip6 saddr @cluster_v6 accept
|
||||||
|
{% endif %}
|
||||||
|
{% if internal_ifaces %}
|
||||||
|
|
||||||
|
# ponytail: internal bridges are trusted on every port; per-port lists are the upgrade path.
|
||||||
|
{% for iface in internal_ifaces %}
|
||||||
|
iifname "{{ iface }}" accept
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% if guest_ifaces %}
|
||||||
|
|
||||||
|
# Pods on k3s guests egress through the node IP, so guest bridges get
|
||||||
|
# only DHCP and router solicitations beyond the public zone.
|
||||||
|
{{ guest_match }} udp dport 67 accept
|
||||||
|
{{ guest_match }} icmpv6 type nd-router-solicit accept
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# VPN peers get the vpn ports, then everything public.
|
||||||
|
iifname "wt0" jump vpn
|
||||||
|
jump public
|
||||||
|
}
|
||||||
|
|
||||||
|
chain vpn {
|
||||||
|
{% if list(firewall.vpn.tcp) %}
|
||||||
|
tcp dport { {{ list(firewall.vpn.tcp) }} } accept
|
||||||
|
{% endif %}
|
||||||
|
{% if list(firewall.vpn.udp) %}
|
||||||
|
udp dport { {{ list(firewall.vpn.udp) }} } accept
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain public {
|
||||||
|
{% if list(firewall.public.tcp) %}
|
||||||
|
tcp dport { {{ list(firewall.public.tcp) }} } accept
|
||||||
|
{% endif %}
|
||||||
|
{% if list(firewall.public.udp) %}
|
||||||
|
udp dport { {{ list(firewall.public.udp) }} } accept
|
||||||
|
{% endif %}
|
||||||
|
{% if list(firewall.geo.tcp) %}
|
||||||
|
tcp dport { {{ list(firewall.geo.tcp) }} } jump geo
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain geo {
|
||||||
|
{% if guest_ifaces %}
|
||||||
|
# Guest IPv6 addresses sit inside the geo sets.
|
||||||
|
{{ guest_match }} return
|
||||||
|
{% endif %}
|
||||||
|
ip saddr @admin_v4 accept
|
||||||
|
ip6 saddr @admin_v6 accept
|
||||||
|
tcp dport 22 ct state new update @ssh_meter_v4 { ip saddr limit rate over {{ firewall.ssh_rate_limit }} } drop
|
||||||
|
# One IPv6 client usually holds a whole /64.
|
||||||
|
tcp dport 22 ct state new update @ssh_meter_v6 { ip6 saddr & ffff:ffff:ffff:ffff:: limit rate over {{ firewall.ssh_rate_limit }} } drop
|
||||||
|
{% if firewall.geo_countries %}
|
||||||
|
ip saddr @geo_v4 accept
|
||||||
|
ip6 saddr @geo_v6 accept
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
# Rebuilds the geo sets of table inet host from ipdeny. Any failed download or
|
||||||
|
# check exits before the swap, so the previous file stays in place.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
out=/etc/nftables.d/host-geo.nft
|
||||||
|
countries="{{ firewall.geo_countries | join(' ') }}"
|
||||||
|
prefix='^[0-9a-f.:]+/[0-9]+$'
|
||||||
|
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
curl -fsS --retry 3 --max-time 120 -o "$2" "$1"
|
||||||
|
grep -Eq "$prefix" "$2" || { echo "no prefixes in $1" >&2; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
for cc in $countries; do
|
||||||
|
fetch "https://www.ipdeny.com/ipblocks/data/aggregated/$cc-aggregated.zone" "$tmp/$cc.v4"
|
||||||
|
fetch "https://www.ipdeny.com/ipv6/ipaddresses/aggregated/$cc-aggregated.zone" "$tmp/$cc.v6"
|
||||||
|
done
|
||||||
|
|
||||||
|
elements() {
|
||||||
|
cat "$@" | grep -E "$prefix" | sed 's/$/,/'
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Generated by $0, do not edit."
|
||||||
|
echo 'table inet host {'
|
||||||
|
echo ' set geo_v4 { type ipv4_addr; flags interval; auto-merge; elements = {'
|
||||||
|
elements "$tmp"/*.v4
|
||||||
|
echo ' } }'
|
||||||
|
echo ' set geo_v6 { type ipv6_addr; flags interval; auto-merge; elements = {'
|
||||||
|
elements "$tmp"/*.v6
|
||||||
|
echo ' } }'
|
||||||
|
echo '}'
|
||||||
|
} >"$tmp/host-geo.nft"
|
||||||
|
|
||||||
|
nft -c -f "$tmp/host-geo.nft"
|
||||||
|
install -m 0644 "$tmp/host-geo.nft" "$out.new"
|
||||||
|
mv -f "$out.new" "$out"
|
||||||
|
|
||||||
|
if systemctl -q is-active nft-host.service; then
|
||||||
|
systemctl reload nft-host.service
|
||||||
|
fi
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
nftables:
|
|
||||||
# Rules to add
|
|
||||||
# Handled as templates
|
|
||||||
# Creates separate files for each entry.
|
|
||||||
# The identifier is necessary for ansible to be able to merge the keys (when 'hash_behaviour = merge')
|
|
||||||
# rule-ids have to be unique across files and raw
|
|
||||||
rules:
|
|
||||||
# Files with Rules to add
|
|
||||||
files:
|
|
||||||
#'<group_identifier>': '<relative-location>'
|
|
||||||
#'<group_identifier>':
|
|
||||||
# main: <relative-location>
|
|
||||||
# '<identifier>': '<relative-location>'
|
|
||||||
|
|
||||||
# Rules to add
|
|
||||||
raw:
|
|
||||||
#'<group_identifier>': '<content>'
|
|
||||||
#'<group_identifier>':
|
|
||||||
# main: <content>
|
|
||||||
# '<identifier>': '<content>'
|
|
||||||
|
|
||||||
# Decides if /etc/nftables.conf is applied or separate files which have changed
|
|
||||||
# Separate changes require the files to be self-tyding to not end up with duplicate rules
|
|
||||||
# e.g.
|
|
||||||
# table ip mytable
|
|
||||||
# flush table ip mytable
|
|
||||||
# delete table ip mytable
|
|
||||||
# table ip mytable {} ...
|
|
||||||
apply_global: false
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
- name: Load group rules
|
|
||||||
command: "nft -f /etc/nftables/ansible-managed/{{ item }}.nft"
|
|
||||||
loop: "{{ combined_rules | list }}"
|
|
||||||
when: not nftables.apply_global
|
|
||||||
|
|
||||||
- name: Load global rule file
|
|
||||||
command: "nft -f /etc/nftables.nft"
|
|
||||||
when: nftables.apply_global
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
- name: Deploying group files
|
|
||||||
include_tasks: ./per-group-template-file.yml
|
|
||||||
with_items:
|
|
||||||
- "{{ nftables.rules.files | list }}"
|
|
||||||
|
|
||||||
- name: Deploying group raw-files
|
|
||||||
include_tasks: ./per-group-template.yml
|
|
||||||
with_items:
|
|
||||||
- "{{ nftables.rules.raw | list }}"
|
|
||||||
|
|
||||||
- include_tasks: ./remove-files.yml
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
- set_fact:
|
|
||||||
group_identifier: "{{ item }}"
|
|
||||||
value: "{{ nftables.rules.files[item] }}"
|
|
||||||
when: "item is defined"
|
|
||||||
|
|
||||||
#'<group_identifier>': '<relative-location>'
|
|
||||||
- block:
|
|
||||||
- name: Create main rule file
|
|
||||||
template:
|
|
||||||
src: "{{ value }}"
|
|
||||||
dest: "/etc/nftables/ansible-managed/{{ group_identifier }}.nft"
|
|
||||||
when: value is string
|
|
||||||
|
|
||||||
#'<group_identifier>':
|
|
||||||
# main: <relative-location>
|
|
||||||
# '<identifier>': '<relative-location>'
|
|
||||||
- block:
|
|
||||||
- set_fact:
|
|
||||||
items: "{{ nftables.rules.files[item] }}"
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Create main rule file
|
|
||||||
template:
|
|
||||||
src: "{{ items['main'] }}"
|
|
||||||
dest: "/etc/nftables/ansible-managed/{{ group_identifier }}.nft"
|
|
||||||
|
|
||||||
- name: Include rule files
|
|
||||||
lineinfile:
|
|
||||||
path: "/etc/nftables/ansible-managed/{{ group_identifier }}.nft"
|
|
||||||
regexp: "include\\s+(\"|')\\/etc\\/nftables\\/ansible-managed\\/{{ group_identifier }}\\/.*$"
|
|
||||||
line: 'include "/etc/nftables/ansible-managed/{{ group_identifier }}/*.nft"'
|
|
||||||
when: items['main'] is defined
|
|
||||||
|
|
||||||
- name: Create group folder
|
|
||||||
file:
|
|
||||||
path: "/etc/nftables/ansible-managed/{{ group_identifier }}/"
|
|
||||||
state: directory
|
|
||||||
when: items|length > 0
|
|
||||||
|
|
||||||
- set_fact:
|
|
||||||
test: "{{ items | dict2items | selectattr('key', 'ne', 'main') }}"
|
|
||||||
|
|
||||||
- name: Create included rule files
|
|
||||||
template:
|
|
||||||
src: "{{ fileItem.value }}"
|
|
||||||
dest: "/etc/nftables/ansible-managed/{{ group_identifier }}/{{ fileItem.key }}.nft"
|
|
||||||
loop: "{{ items | dict2items | selectattr('key', 'ne', 'main') }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: fileItem
|
|
||||||
|
|
||||||
when: value is mapping
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
- set_fact:
|
|
||||||
group_identifier: "{{ item }}"
|
|
||||||
value: "{{ nftables.rules.raw[item] }}"
|
|
||||||
when: "item is defined"
|
|
||||||
|
|
||||||
#'<group_identifier>': '<content>'
|
|
||||||
- block:
|
|
||||||
- name: Create main rule file
|
|
||||||
copy:
|
|
||||||
content: "{{ value }}"
|
|
||||||
dest: "/etc/nftables/ansible-managed/{{ group_identifier }}.nft"
|
|
||||||
when: value is string
|
|
||||||
|
|
||||||
#'<group_identifier>':
|
|
||||||
# main: <content>
|
|
||||||
# '<identifier>': '<content>'
|
|
||||||
- block:
|
|
||||||
- set_fact:
|
|
||||||
items: "{{ nftables.rules.raw[item] }}"
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: Create main rule file
|
|
||||||
copy:
|
|
||||||
content: "{{ items['main'] }}"
|
|
||||||
dest: "/etc/nftables/ansible-managed/{{ group_identifier }}.nft"
|
|
||||||
|
|
||||||
- name: Include rule files
|
|
||||||
lineinfile:
|
|
||||||
path: "/etc/nftables/ansible-managed/{{ group_identifier }}.nft"
|
|
||||||
regexp: "include\\s+(\"|')\\/etc\\/nftables\\/ansible-managed\\/{{ group_identifier }}\\/.*$"
|
|
||||||
line: 'include "/etc/nftables/ansible-managed/{{ group_identifier }}/*.nft"'
|
|
||||||
when: items['main'] is defined
|
|
||||||
|
|
||||||
- name: Create group folder
|
|
||||||
file:
|
|
||||||
path: "/etc/nftables/ansible-managed/{{ group_identifier }}/"
|
|
||||||
state: directory
|
|
||||||
when: items|length > 0
|
|
||||||
|
|
||||||
- name: Create included rule files
|
|
||||||
copy:
|
|
||||||
content: "{{ included_item.value }}"
|
|
||||||
dest: "/etc/nftables/ansible-managed/{{ group_identifier }}/{{ included_item.key }}.nft"
|
|
||||||
loop: "{{ items | dict2items | selectattr('key', 'ne', 'main') }}"
|
|
||||||
loop_control:
|
|
||||||
loop_var: included_item
|
|
||||||
|
|
||||||
when: value is mapping
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
- name: Install Packages
|
|
||||||
package:
|
|
||||||
name:
|
|
||||||
- nftables
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
- import_tasks: ./prerequisites.yml
|
|
||||||
|
|
||||||
- import_tasks: ./setup-packages.yml
|
|
||||||
|
|
||||||
- import_tasks: ./deploy-rules/main.yml
|
|
||||||
|
|
||||||
- import_tasks: ./apply-files.yml
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Defaults if missing
|
|
||||||
- name: Set defaults if missing
|
|
||||||
set_fact:
|
|
||||||
nftables:
|
|
||||||
rules:
|
|
||||||
files: "{{ nftables.rules.files | default({}) | combine({}) }}"
|
|
||||||
raw: "{{ nftables.rules.raw | default({}) | combine({}) }}"
|
|
||||||
combined_rules: "{{ nftables.rules.raw | combine(nftables.rules.files, recursive=true) }}"
|
|
||||||
|
|
||||||
#- name: Check items for consistency
|
|
||||||
# assert:
|
|
||||||
# that: "{{ nftables.rules.files.values() | length }} + {{ nftables.rules.raw.values() | length }} == {{ combined_rules.values() | length }}"
|
|
||||||
# fail_msg: "files and raw rules share the same identifier"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
- name: Handle removed group files
|
|
||||||
block:
|
|
||||||
- find:
|
|
||||||
paths: /etc/nftables/ansible-managed/
|
|
||||||
file_type: 'any'
|
|
||||||
excludes: '{% for item in combined_rules %}{{ item }},{{ item }}.nft,{% endfor %}'
|
|
||||||
depth: 1
|
|
||||||
register: removeFiles
|
|
||||||
|
|
||||||
- file:
|
|
||||||
path: "{{ fileItem.path }}"
|
|
||||||
state: absent
|
|
||||||
loop: "{{ removeFiles.files }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ fileItem.path }}"
|
|
||||||
loop_var: fileItem
|
|
||||||
|
|
||||||
- name: Handle removed included files per group
|
|
||||||
include_tasks: ./remove-per-group.yml
|
|
||||||
with_items:
|
|
||||||
- "{{ combined_rules | list }}"
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
- set_fact:
|
|
||||||
group_identifier: "{{ item }}"
|
|
||||||
group_items: "{{ combined_rules[item] }}"
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- find:
|
|
||||||
paths: "/etc/nftables/ansible-managed/{{ group_identifier }}/"
|
|
||||||
file_type: 'any'
|
|
||||||
excludes: '{% for item in group_items %}{{ item }}.nft,{% endfor %}'
|
|
||||||
register: removeFiles
|
|
||||||
|
|
||||||
- file:
|
|
||||||
path: "{{ fileItem.path }}"
|
|
||||||
state: absent
|
|
||||||
loop: "{{ removeFiles.files }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ fileItem.path }}"
|
|
||||||
loop_var: fileItem
|
|
||||||
|
|
||||||
when: group_items is mapping
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
- name: Install nftables
|
|
||||||
package:
|
|
||||||
name:
|
|
||||||
- nftables
|
|
||||||
|
|
||||||
- name: Create /etc/nftables/ansible-managed
|
|
||||||
file:
|
|
||||||
path: /etc/nftables/ansible-managed
|
|
||||||
state: directory
|
|
||||||
|
|
||||||
- name: Include files in /etc/nftables/ansible-managed/ from /etc/nftables.conf
|
|
||||||
blockinfile:
|
|
||||||
path: /etc/nftables.conf
|
|
||||||
marker: "# {mark} ANSIBLE MANAGED BLOCK - nftables"
|
|
||||||
content: 'include "/etc/nftables/ansible-managed/*.nft"'
|
|
||||||
Reference in New Issue
Block a user