diff --git a/configuration-files/roles/base/handlers/main.yml b/configuration-files/roles/base/handlers/main.yml index 13f97da..937df16 100644 --- a/configuration-files/roles/base/handlers/main.yml +++ b/configuration-files/roles/base/handlers/main.yml @@ -11,14 +11,10 @@ name: ssh state: restarted -- name: persist rules - community.general.iptables_state: - ip_version: "ip{{ item.ip_version }}" - state: saved - path: "/etc/iptables/rules.{{ item.ip_version }}" - loop: - - { ip_version: "v4" } - - { ip_version: "v6" } +- name: firewall | apply ruleset + systemd_service: + name: nftables + state: reloaded - name: apply netplan command: sudo netplan apply diff --git a/configuration-files/roles/base/tasks/firewall.yml b/configuration-files/roles/base/tasks/firewall.yml index a4a3dea..652c0b8 100644 --- a/configuration-files/roles/base/tasks/firewall.yml +++ b/configuration-files/roles/base/tasks/firewall.yml @@ -4,12 +4,14 @@ state: latest name: - iptables - - iptables-persistent + - nftables - name: firewall | uninstall software package: state: absent name: + - iptables-persistent + - netfilter-persistent - ufw # Load firewall connection tracking kernel modules and reconnect. @@ -21,322 +23,18 @@ - name: firewall | reset connection meta: reset_connection -# Accessing the DNS resolver for administration purpose - -- name: firewall | SSH | allow new incoming SYN packets on TCP port "{{ network_port_ssh }}" on v4 and v6 - tags: init - iptables: - ip_version: "{{ item.version }}" - chain: INPUT - protocol: tcp - destination: "{{ item.ip }}" - destination_port: "{{ network_port_ssh }}" - ctstate: - - ESTABLISHED - - NEW - - RELATED - jump: ACCEPT - loop: - - { version: "ipv4", ip: "{{ ip4 }}" } - - { version: "ipv6", ip: "{{ ip6 }}" } - notify: - - persist rules - -- name: firewall | ICMP | allow ping - iptables: - ip_version: "{{ item.version }}" - chain: INPUT - protocol: "{{ item.protocol }}" - jump: ACCEPT - loop: - - { version: "ipv4", protocol: "icmp" } - - { version: "ipv6", protocol: "icmpv6" } - notify: - - persist rules - -# Communication between the DNS resolver and remote (DNS) servers - -- name: firewall | DNS | allow remote server response from port 53/udp/tcp - iptables: - ip_version: "{{ item.version }}" - chain: INPUT - protocol: "{{ item.protocol }}" - destination: "{{ item.ip }}" - source_port: "53" - ctstate: - - ESTABLISHED - jump: ACCEPT - loop: - - { version: "ipv4", ip: "{{ ip4 }}", protocol: "tcp" } - - { version: "ipv4", ip: "{{ ip4 }}", protocol: "udp" } - - { version: "ipv6", ip: "{{ ip6 }}", protocol: "tcp" } - - { version: "ipv6", ip: "{{ ip6 }}", protocol: "udp" } - notify: - - persist rules - -- name: firewall | HTTP | allow remote server response from port 80/tcp and 443/tcp - iptables: - ip_version: "{{ item.version }}" - chain: INPUT - protocol: tcp - destination: "{{ item.ip }}" - source_port: "{{ item.port }}" - ctstate: ESTABLISHED - jump: ACCEPT - loop: - - { version: "ipv4", ip: "{{ ip4 }}", port: "80" } - - { version: "ipv6", ip: "{{ ip6 }}", port: "80" } - - { version: "ipv4", ip: "{{ ip4 }}", port: "443" } - - { version: "ipv6", ip: "{{ ip6 }}", port: "443" } - notify: - - persist rules - -- name: firewall | NTP | allow remote server response from port 123/udp - iptables: - ip_version: "{{ item.version }}" - chain: INPUT - protocol: udp - source_port: "123" - destination: "{{ item.ip }}" - jump: ACCEPT - loop: - - { version: "ipv4", ip: "{{ ip4 }}" } - - { version: "ipv6", ip: "{{ ip6 }}" } - notify: - - persist rules - -- name: firewall | loopback | allow loopback communication - iptables: - chain: INPUT - in_interface: "{{ if_loopback }}" - jump: ACCEPT - notify: - - persist rules - -# Let's Encrypt ACME setup - -- name: webserver | firewall | ensure port 31080/tcp is open on v6 (loopback) - iptables: - ip_version: "ipv6" - chain: "INPUT" - protocol: tcp - source: "{{ ip6 }}" - destination: "{{ ip6 }}" - jump: ACCEPT - notify: - - persist rules - -- name: webserver | firewall | ensure port 31080/tcp is open on v6 (accept SYN) - iptables: - ip_version: "ipv6" - chain: "INPUT" - protocol: tcp - source: "{{ ip6_other }}" - destination_port: "31080" - jump: ACCEPT - notify: - - persist rules - -- name: webserver | firewall | ensure port 31080/tcp is open on v6 (accept SYN/ACK) - iptables: - ip_version: "ipv6" - chain: "INPUT" - protocol: tcp - source: "{{ ip6_other }}" - source_port: "31080" - jump: ACCEPT - notify: - - persist rules - -# VRRP communication between the DNS resolvers - -- name: firewall | VRRP | allow v4 anv v6 vrrp traffic - iptables: - ip_version: "{{ item.version }}" - chain: "{{ item.chain }}" - destination: "{{ item.ip }}" - jump: ACCEPT - loop: - - { version: "ipv4", chain: "INPUT", ip: "224.0.0.18" } - - { version: "ipv4", chain: "OUTPUT", ip: "224.0.0.18" } - - { version: "ipv6", chain: "INPUT", ip: "ff02::12/128" } - - { version: "ipv6", chain: "OUTPUT", ip: "ff02::12/128" } - notify: - - persist rules - -# DoT / DoH - communication between clients and the DNS resolver - -- name: firewall | DoH/DoT | explicitly allow DNS server to receive queries from clients on port 80/tcp, 443/tcp and 853/tcp - iptables: - ip_version: "{{ item.version }}" - chain: INPUT - protocol: tcp - destination: "{{ item.ip }}" - destination_ports: - - "80" - - "443" - - "853" - jump: ACCEPT - loop: - - { version: "ipv4", ip: "{{ ip4_vip_dns1 }}" } - - { version: "ipv4", ip: "{{ ip4_vip_dns2 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns1 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns2 }}" } - notify: - - persist rules - -- name: firewall | DoH/DoT NOTRACK | disable connection tracking on queries from clients - iptables: - ip_version: "{{ item.version }}" - table: raw - chain: PREROUTING - protocol: tcp - destination: "{{ item.ip }}" - jump: NOTRACK - loop: - - { version: "ipv4", ip: "{{ ip4_vip_dns1 }}" } - - { version: "ipv4", ip: "{{ ip4_vip_dns2 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns1 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns2 }}" } - notify: - - persist rules - -- name: firewall | DoH/DoT | explicitly allow DNS server to answer clients on port 80/tcp, 443/tcp and 853/tcp - iptables: - ip_version: "{{ item.version }}" - chain: OUTPUT - protocol: tcp - source: "{{ item.ip }}" - source_port: "{{ item.port }}" - jump: ACCEPT - loop: - - { version: "ipv4", ip: "{{ ip4_vip_dns1 }}", port: "80" } - - { version: "ipv4", ip: "{{ ip4_vip_dns2 }}", port: "80" } - - { version: "ipv6", ip: "{{ ip6_vip_dns1 }}", port: "80" } - - { version: "ipv6", ip: "{{ ip6_vip_dns2 }}", port: "80" } - - { version: "ipv4", ip: "{{ ip4_vip_dns1 }}", port: "443" } - - { version: "ipv4", ip: "{{ ip4_vip_dns2 }}", port: "443" } - - { version: "ipv6", ip: "{{ ip6_vip_dns1 }}", port: "443" } - - { version: "ipv6", ip: "{{ ip6_vip_dns2 }}", port: "443" } - - { version: "ipv4", ip: "{{ ip4_vip_dns1 }}", port: "853" } - - { version: "ipv4", ip: "{{ ip4_vip_dns2 }}", port: "853" } - - { version: "ipv6", ip: "{{ ip6_vip_dns1 }}", port: "853" } - - { version: "ipv6", ip: "{{ ip6_vip_dns2 }}", port: "853" } - notify: - - persist rules - -- name: firewall | DoH/DoT NOTRACK | disable connection tracking on responses to clients - iptables: - ip_version: "{{ item.version }}" - table: raw - chain: OUTPUT - protocol: tcp - source: "{{ item.ip }}" - jump: NOTRACK - loop: - - { version: "ipv4", ip: "{{ ip4_vip_dns1 }}" } - - { version: "ipv4", ip: "{{ ip4_vip_dns2 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns1 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns2 }}" } - - { version: "ipv4", ip: "{{ ip4_vip_dns1 }}" } - - { version: "ipv4", ip: "{{ ip4_vip_dns2 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns1 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns2 }}" } - - { version: "ipv4", ip: "{{ ip4_vip_dns1 }}" } - - { version: "ipv4", ip: "{{ ip4_vip_dns2 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns1 }}" } - - { version: "ipv6", ip: "{{ ip6_vip_dns2 }}" } - notify: - - persist rules - -- name: firewall | DNS | reject unencrypted DNS - iptables: - ip_version: "{{ item.version }}" - chain: INPUT - protocol: "{{ item.protocol }}" - destination: "{{ item.ip }}" - destination_port: "53" - jump: REJECT - loop: - - { version: "ipv4", protocol: "tcp", ip: "{{ ip4_vip_dns1 }}" } - - { version: "ipv4", protocol: "udp", ip: "{{ ip4_vip_dns2 }}" } - - { version: "ipv6", protocol: "tcp", ip: "{{ ip6_vip_dns1 }}" } - - { version: "ipv6", protocol: "udp", ip: "{{ ip6_vip_dns2 }}" } - notify: - - persist rules - -# Statistics - -- name: firewall | HTTP | allow server to answer requests on port 80/tcp and 443/tcp - iptables: - ip_version: "{{ item.version }}" - chain: INPUT - protocol: tcp - destination: "{{ item.ip }}" - destination_ports: - - "80" - - "443" - ctstate: - - NEW - - ESTABLISHED - jump: ACCEPT - loop: - - { version: "ipv4", ip: "{{ ip4 }}" } - - { version: "ipv6", ip: "{{ ip6 }}" } - notify: - - persist rules - -- name: firewall | statistics | allow munin to measure incomming traffic - iptables: - ip_version: "{{ item.version }}" - action: insert - chain: INPUT - destination: "{{ item.ip }}" - loop: - - { version: "ipv4", ip: "{{ ip4 }}" } - - { version: "ipv6", ip: "{{ ip6 }}" } - - { version: "ipv4", ip: "{{ ip4_vrrp1 }}" } - - { version: "ipv6", ip: "{{ ip6_vrrp1 }}" } - - { version: "ipv4", ip: "{{ ip4_vrrp2 }}" } - - { version: "ipv6", ip: "{{ ip6_vrrp2 }}" } - - { version: "ipv4", ip: "{{ ip4_dns1 }}" } - - { version: "ipv6", ip: "{{ ip6_dns1 }}" } - - { version: "ipv4", ip: "{{ ip4_dns2 }}" } - - { version: "ipv6", ip: "{{ ip6_dns2 }}" } - notify: - - persist rules - -- name: firewall | statistics | allow munin to measure outgoing traffic - iptables: - ip_version: "{{ item.version }}" - action: insert - chain: OUTPUT - source: "{{ item.ip }}" - loop: - - { version: "ipv4", ip: "{{ ip4 }}" } - - { version: "ipv6", ip: "{{ ip6 }}" } - - { version: "ipv4", ip: "{{ ip4_vrrp1 }}" } - - { version: "ipv6", ip: "{{ ip6_vrrp1 }}" } - - { version: "ipv4", ip: "{{ ip4_vrrp2 }}" } - - { version: "ipv6", ip: "{{ ip6_vrrp2 }}" } - - { version: "ipv4", ip: "{{ ip4_dns1 }}" } - - { version: "ipv6", ip: "{{ ip6_dns1 }}" } - - { version: "ipv4", ip: "{{ ip4_dns2 }}" } - - { version: "ipv6", ip: "{{ ip6_dns2 }}" } - notify: - - persist rules - -# Drop policy - -- name: firewall | drop on chain INPUT and FORWARD - iptables: - ip_version: "{{ item.version }}" - chain: "{{ item.chain }}" - policy: DROP - loop: - - { version: "ipv4", chain: "INPUT" } - - { version: "ipv4", chain: "FORWARD" } - - { version: "ipv6", chain: "INPUT" } - - { version: "ipv6", chain: "FORWARD" } - notify: - - persist rules +- name: firewall | cleanup + file: + state: absent + path: /etc/iptables + +- name: firewall | rules + notify: + - firewall | apply ruleset + template: + src: nftables.conf.j2 + dest: /etc/nftables.conf + validate: /usr/sbin/nft --check --file %s + owner: root + group: root + mode: 0644 diff --git a/configuration-files/roles/base/templates/nftables.conf.j2 b/configuration-files/roles/base/templates/nftables.conf.j2 new file mode 100644 index 0000000..59f50dc --- /dev/null +++ b/configuration-files/roles/base/templates/nftables.conf.j2 @@ -0,0 +1,151 @@ +#!/usr/sbin/nft -f + +# Useful docs: +# * https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes +# * https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks + +flush ruleset + +# +# IPv4 counters +# +# This table is seen by the iptables binary (used by the munin ip_ plugin). +# +table ip filter { + chain INPUT { + type filter hook input priority filter; policy accept; + ip daddr {{ ip4_dns1 }} counter + ip daddr {{ ip4_dns2 }} counter + ip daddr {{ ip4_vip_dns1 }} counter + ip daddr {{ ip4_vip_dns2 }} counter + ip daddr {{ ip4_vrrp1 }} counter + ip daddr {{ ip4_vrrp2 }} counter + } + + chain OUTPUT { + type filter hook output priority filter; policy accept; + ip saddr {{ ip4_dns1 }} counter + ip saddr {{ ip4_dns2 }} counter + ip saddr {{ ip4_vip_dns1 }} counter + ip saddr {{ ip4_vip_dns2 }} counter + ip saddr {{ ip4_vrrp1 }} counter + ip saddr {{ ip4_vrrp2 }} counter + } +} + +# +# IPv6 counters +# +# This table is seen by the iptables binary (used by the munin ip_ plugin). +# +table ip6 filter { + chain INPUT { + type filter hook input priority filter; policy accept; + ip6 daddr {{ ip6_dns1 }} counter + ip6 daddr {{ ip6_dns2 }} counter + ip6 daddr {{ ip6_vip_dns1 }} counter + ip6 daddr {{ ip6_vip_dns2 }} counter + ip6 daddr {{ ip6_vrrp1 }} counter + ip6 daddr {{ ip6_vrrp2 }} counter + } + + chain OUTPUT { + type filter hook output priority filter; policy accept; + ip6 saddr {{ ip6_dns1 }} counter + ip6 saddr {{ ip6_dns2 }} counter + ip6 saddr {{ ip6_vip_dns1 }} counter + ip6 saddr {{ ip6_vip_dns2 }} counter + ip6 saddr {{ ip6_vrrp1 }} counter + ip6 saddr {{ ip6_vrrp2 }} counter + } +} + +# +# Filter rules +# +table inet nf_filter { + + chain filter_ingress { + type filter hook input priority filter; policy drop; + + # Allow traffic on loopback interface. + iif "lo" counter accept + + # Allow VRRP globally + ip daddr 224.0.0.18 counter accept + ip6 daddr ff02::12 counter accept + + # Allow ICMP globally + ip protocol icmp counter accept + meta l4proto ipv6-icmp counter accept + + # Divert to service ip rules. + ip daddr { {{ ip4_vip_dns1 }}, {{ ip4_vip_dns2 }} } goto filter_ingress_service_ip + ip6 daddr { {{ ip6_vip_dns1 }}, {{ ip6_vip_dns2 }} } goto filter_ingress_service_ip + + # Divert to node ip rules. + ip daddr {{ ip4 }} goto filter_ingress_node_ip + ip6 daddr {{ ip6 }} goto filter_ingress_node_ip + } + + chain filter_ingress_service_ip { + # Allow DoH / DoT queries to service address. + tcp dport { 80, 443, 853 } counter accept + + # Reject unencrypted DNS to service address. + tcp dport 53 counter reject + udp dport 53 counter reject + } + + chain filter_ingress_node_ip { + # Allow traffic from established and related packets, drop invalid + ct state vmap { established : accept, related : accept, invalid : drop } + + # Allow SSH to node address + tcp dport {{ network_port_ssh }} accept + + # Allow HTTP / HTTPS to node address (for Let's Encrypt challenge). + tcp dport { 80, 443 } accept + + # Allow inter-link (for Let's Encrypt challenge). + ip6 saddr { {{ ip6_dns1 }}, {{ ip6_dns2 }} } tcp dport 31080 accept + } + + chain filter_egress { + type filter hook output priority filter; policy accept; + } + + chain filter_forward { + type filter hook forward priority filter; policy drop; + } + +} + +# +# Raw rules +# +# These rules are evaluated before connection tracking kicks in. +# +table inet nf_raw { + + chain raw_ingress { + type filter hook prerouting priority raw; policy accept; + + # Disable connection tracking on queries from clients + ip daddr {{ ip4_vip_dns1 }} notrack + ip daddr {{ ip4_vip_dns2 }} notrack + ip6 daddr {{ ip6_vip_dns1 }} notrack + ip6 daddr {{ ip6_vip_dns2 }} notrack + } + + chain raw_egress { + type filter hook output priority raw; policy accept; + + # Disable connection tracking on responses to clients + ip saddr {{ ip4_vip_dns1 }} notrack + ip saddr {{ ip4_vip_dns2 }} notrack + ip6 saddr {{ ip6_vip_dns1 }} notrack + ip6 saddr {{ ip6_vip_dns2 }} notrack + } + +}