From 754c3da31fe9e0c5343eec91144cdf0b71374d38 Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Mon, 23 Feb 2026 09:56:33 +0100 Subject: [PATCH] Import firewall role from network repo Move, actually. --- roles/alpine/tasks/main.yml | 2 +- roles/facts/tasks/main.yml | 10 ++ roles/firewall/files/conntrackd.conf | 2 + roles/firewall/files/sshd_config.friwall | 15 ++ roles/firewall/files/update | 41 +++++ roles/firewall/handlers/main.yml | 45 +++++ roles/firewall/tasks/config.yml | 59 ++++++ roles/firewall/tasks/conntrackd.yml | 36 ++++ roles/firewall/tasks/frr.yml | 48 +++++ roles/firewall/tasks/main.yml | 44 +++++ roles/firewall/tasks/nftables.yml | 26 +++ roles/firewall/tasks/wireguard.yml | 26 +++ roles/firewall/templates/conntrackd.conf.j2 | 50 ++++++ roles/firewall/templates/firewall.intf.j2 | 16 ++ roles/firewall/templates/frr.conf.j2 | 141 +++++++++++++++ roles/firewall/templates/interfaces.nft.j2 | 10 ++ roles/firewall/templates/networks.nft.j2 | 9 + roles/firewall/templates/nftables.nft.j2 | 190 ++++++++++++++++++++ roles/firewall/templates/sysctl.conf.j2 | 16 ++ roles/firewall/templates/wg.intf.j2 | 12 ++ setup.yml | 4 + 21 files changed, 801 insertions(+), 1 deletion(-) create mode 100644 roles/firewall/files/conntrackd.conf create mode 100644 roles/firewall/files/sshd_config.friwall create mode 100644 roles/firewall/files/update create mode 100644 roles/firewall/handlers/main.yml create mode 100644 roles/firewall/tasks/config.yml create mode 100644 roles/firewall/tasks/conntrackd.yml create mode 100644 roles/firewall/tasks/frr.yml create mode 100644 roles/firewall/tasks/main.yml create mode 100644 roles/firewall/tasks/nftables.yml create mode 100644 roles/firewall/tasks/wireguard.yml create mode 100644 roles/firewall/templates/conntrackd.conf.j2 create mode 100644 roles/firewall/templates/firewall.intf.j2 create mode 100644 roles/firewall/templates/frr.conf.j2 create mode 100644 roles/firewall/templates/interfaces.nft.j2 create mode 100644 roles/firewall/templates/networks.nft.j2 create mode 100644 roles/firewall/templates/nftables.nft.j2 create mode 100644 roles/firewall/templates/sysctl.conf.j2 create mode 100644 roles/firewall/templates/wg.intf.j2 diff --git a/roles/alpine/tasks/main.yml b/roles/alpine/tasks/main.yml index 4292d41..5f1a862 100644 --- a/roles/alpine/tasks/main.yml +++ b/roles/alpine/tasks/main.yml @@ -100,7 +100,7 @@ - name: Set authorized SSH keys authorized_key: user: root - exclusive: true + #exclusive: true # is problematic if there are other keys (e.g. clusters) key: "{{ ssh_keys | join('\n') }}" - when: is_virtual diff --git a/roles/facts/tasks/main.yml b/roles/facts/tasks/main.yml index c900b27..96c848e 100644 --- a/roles/facts/tasks/main.yml +++ b/roles/facts/tasks/main.yml @@ -38,6 +38,16 @@ prefixes: '{{ query("netbox.netbox.nb_lookup", "prefixes", raw_data=true) | sort(attribute="prefix") | sort(attribute="family.value") }}' + - name: Select VLAN and BGP prefixes + run_once: true + set_fact: + vrf_prefixes: '{{ prefixes | selectattr("vrf") + | selectattr("vlan") | selectattr("vlan.id", "in", vlans|map(attribute="id")) + | sort(attribute="vlan.vid") }}' + bgp_prefixes: '{{ prefixes | selectattr("tenant") + | selectattr("role") | selectattr("role.slug", "==", "bgp") + | sort(attribute="tenant.slug") }}' + # can only get NetBox ID for device/VM from (any) interface # probably safe to assume at least one interface - name: Get my ID diff --git a/roles/firewall/files/conntrackd.conf b/roles/firewall/files/conntrackd.conf new file mode 100644 index 0000000..0fa08d9 --- /dev/null +++ b/roles/firewall/files/conntrackd.conf @@ -0,0 +1,2 @@ +# The init script for conntrackd wants this, not sure about conntrackd itself. +net.netfilter.nf_conntrack_tcp_be_liberal = 1 diff --git a/roles/firewall/files/sshd_config.friwall b/roles/firewall/files/sshd_config.friwall new file mode 100644 index 0000000..6cdd411 --- /dev/null +++ b/roles/firewall/files/sshd_config.friwall @@ -0,0 +1,15 @@ +# This is used by sshd in default VRF to receive configuration updates. Lock +# down to only allow executing the update script. + +# Only allow pubkey auth. +KbdInteractiveAuthentication no +PasswordAuthentication no +PermitRootLogin prohibit-password + +# Disable what we can. +AllowTcpForwarding no +GatewayPorts no +X11Forwarding no + +# And then disable everything else. +ForceCommand /usr/local/bin/update diff --git a/roles/firewall/files/update b/roles/firewall/files/update new file mode 100644 index 0000000..f2d5413 --- /dev/null +++ b/roles/firewall/files/update @@ -0,0 +1,41 @@ +#!/bin/sh + +set -e + +apply() { + cp -R /opt/config/etc/nftables.d /etc || return 1 + ip vrf exec mgmt nft -I /etc/nftables.d -f /etc/nftables.nft || return 2 + cp -R /opt/config/etc/wireguard /etc || return 3 + wg syncconf wg /etc/wireguard/wg.conf || return 4 +} + +cleanup() { + rm -fr /opt/config +} + +message() { + logger "${@}" + echo "${@}" +} + +# clean now and on exit +cleanup +trap cleanup EXIT + +mkdir -p /opt/config +tar xz -C /opt/config --warning=no-timestamp + +current="$(cat /opt/version 2>/dev/null || echo -1)" +next="$(cat /opt/config/version 2>/dev/null || echo -1)" +message "Updating config from v${current} to v${next}" +if [ "${next:-0}" -ne "${current:-0}" ] ; then + message "Applying config v${next}" + if apply ; then + echo "${next}" > /opt/version + message "Applied config v${next}" + else + error="$?" + message "Could not apply config v${next}, error ${error}" + exit "${error}" + fi +fi diff --git a/roles/firewall/handlers/main.yml b/roles/firewall/handlers/main.yml new file mode 100644 index 0000000..d2beb3b --- /dev/null +++ b/roles/firewall/handlers/main.yml @@ -0,0 +1,45 @@ +- name: enable interfaces + command: ifup --auto + when: "'handler' not in ansible_skip_tags" + +- name: mkinitfs + command: mkinitfs + when: "'handler' not in ansible_skip_tags" + +- name: reboot + reboot: + when: "'handler' not in ansible_skip_tags" + +- name: reload frr + command: /usr/lib/frr/frr-reload.py --reload /etc/frr/frr.conf + when: "'handler' not in ansible_skip_tags" + +- name: reload nftables + service: + name: nftables + state: reloaded + when: "'handler' not in ansible_skip_tags" + +- name: restart conntrackd + service: + name: conntrackd + state: restarted + when: "'handler' not in ansible_skip_tags" + +- name: restart frr + service: + name: frr + state: restarted + when: "'handler' not in ansible_skip_tags" + +- name: reload sshd.friwall + service: + name: sshd.friwall + state: reloaded + when: "'handler' not in ansible_skip_tags" + +- name: restart sshd.friwall + service: + name: sshd.friwall + state: restarted + when: "'handler' not in ansible_skip_tags" diff --git a/roles/firewall/tasks/config.yml b/roles/firewall/tasks/config.yml new file mode 100644 index 0000000..8a0ea1b --- /dev/null +++ b/roles/firewall/tasks/config.yml @@ -0,0 +1,59 @@ +- name: Install packages for config updates + package: + name: tar + +- name: Limit SSH for config updates + copy: + dest: /etc/ssh/ + src: sshd_config.friwall + notify: reload sshd.friwall + +- name: Create SSH service for config updates + file: + path: /etc/init.d/sshd.friwall + src: /etc/init.d/sshd + state: link + +- name: Configure SSH service for config updates + copy: + dest: /etc/conf.d/sshd.friwall + content: | + cfgfile="/etc/ssh/sshd_config.friwall" + vrf="default" + notify: restart sshd.friwall + +- name: Enable SSH service for config updates + service: + name: sshd.friwall + enabled: yes + state: started + +- name: Install config updater + copy: + dest: /usr/local/bin/ + src: update + mode: 0700 + +- name: Get master SSH key + delegate_to: '{{ master }}' + command: "cat ~friwall/.ssh/id_ed25519.pub" + register: master_key + changed_when: false + +- name: Deploy master key on node + authorized_key: "user=root key={{ master_key.stdout }}" + +- name: Get my host SSH key + slurp: + src: /etc/ssh/ssh_host_ed25519_key.pub + register: node_key + +- name: Introduce myself to master + delegate_to: '{{ master }}' + become: yes + become_user: friwall + become_method: su + become_flags: "-s /bin/sh" # no login shell for user + known_hosts: + name: "{{ inventory_hostname }}" + key: "{{ inventory_hostname }},{{ interfaces | selectattr('name', '==', 'lo') | map(attribute='ip_addresses') | first | selectattr('role') | selectattr('role.value', '==', 'loopback') | map(attribute='address') | ipv4 | first | ipaddr('address') }} {{ node_key.content | b64decode }}" # TODO make IP retrieval less terrifying diff --git a/roles/firewall/tasks/conntrackd.yml b/roles/firewall/tasks/conntrackd.yml new file mode 100644 index 0000000..4412574 --- /dev/null +++ b/roles/firewall/tasks/conntrackd.yml @@ -0,0 +1,36 @@ +- name: Install conntrack-tools + package: + name: conntrack-tools + +# Ensure the module is loaded before setting sysctl values. +- name: Autoload nf_conntrack + lineinfile: + dest: /etc/modules-load.d/netfilter.conf + line: nf_conntrack + create: yes + +# Set required sysctl values. +- name: Set sysctl values for conntrackd + copy: + dest: /etc/sysctl.d/ + src: conntrackd.conf + +- name: Set up conntrackd + template: + dest: /etc/conntrackd/conntrackd.conf + src: conntrackd.conf.j2 + mode: 0644 + notify: restart conntrackd + +- name: Run conntrackd in default VRF + lineinfile: + dest: /etc/conf.d/conntrackd + line: 'vrf="default"' + regexp: '^vrf=' + notify: restart conntrackd + +- name: Enable conntrackd + service: + name: conntrackd + enabled: yes + state: started diff --git a/roles/firewall/tasks/frr.yml b/roles/firewall/tasks/frr.yml new file mode 100644 index 0000000..d7450ec --- /dev/null +++ b/roles/firewall/tasks/frr.yml @@ -0,0 +1,48 @@ +- name: Enable sysctl service + service: + name: sysctl + enabled: yes + runlevel: boot + state: started + +- name: Enable community package repo + lineinfile: + path: /etc/apk/repositories + regexp: '^# *(http.*/v[^/]*/community)' + line: '\1' + backrefs: yes + +- name: Install FRR + package: + name: frr,frr-pythontools + state: latest + +- name: Set datacenter defaults + lineinfile: + path: /etc/frr/daemons + regexp: '^frr_profile=' + line: 'frr_profile="datacenter"' + notify: restart frr + +- name: Enable BGP and BFD + lineinfile: + path: /etc/frr/daemons + regexp: "^{{ item }}=" + line: "{{ item }}=yes" + loop: + - bfdd + - bgpd + notify: restart frr + +- name: Enable FRR service + service: + name: frr + enabled: yes + state: started + +- name: Copy FRR config + template: + dest: /etc/frr/frr.conf + src: frr.conf.j2 + mode: 0644 + notify: reload frr diff --git a/roles/firewall/tasks/main.yml b/roles/firewall/tasks/main.yml new file mode 100644 index 0000000..8ecc262 --- /dev/null +++ b/roles/firewall/tasks/main.yml @@ -0,0 +1,44 @@ +- name: Update package cache + package: + update_cache: yes + +- name: Install packages + package: + name: bash,bonding,iproute2 + state: latest + +- name: Set up custom interfaces + template: + dest: /etc/network/interfaces.d/firewall.intf + src: firewall.intf.j2 + mode: 0644 + notify: enable interfaces + +- name: Set up sysctls + template: + dest: /etc/sysctl.d/firewall.conf + src: sysctl.conf.j2 + +- name: Run SSH in management VRF + lineinfile: + path: /etc/conf.d/sshd + regexp: "#* *vrf=" + line: "vrf=\"mgmt\"" + notify: reboot + +- name: Set up FRR + import_tasks: frr.yml + +- name: Set up wireguard + import_tasks: wireguard.yml + +- name: Set up nftables + import_tasks: nftables.yml + +# causes issues in normal operation +# the conntrack tables seem to get synced incorrectly +#- name: Set up conntrackd +# import_tasks: conntrackd.yml + +- name: Set up configuration channel + import_tasks: config.yml diff --git a/roles/firewall/tasks/nftables.yml b/roles/firewall/tasks/nftables.yml new file mode 100644 index 0000000..a80ec6f --- /dev/null +++ b/roles/firewall/tasks/nftables.yml @@ -0,0 +1,26 @@ +- name: Install nftables + package: + name: nftables + +- name: Copy nftables config + template: + dest: /etc/nftables.nft + src: nftables.nft.j2 + mode: 0644 + notify: reload nftables + +- name: Copy static nftables includes + template: + dest: '/etc/nftables.d/{{ item }}' + src: '{{ item }}.j2' + mode: 0644 + loop: + - interfaces.nft + - networks.nft + notify: reload nftables + +- name: Enable nftables service + service: + name: nftables + enabled: yes + state: started diff --git a/roles/firewall/tasks/wireguard.yml b/roles/firewall/tasks/wireguard.yml new file mode 100644 index 0000000..d49a380 --- /dev/null +++ b/roles/firewall/tasks/wireguard.yml @@ -0,0 +1,26 @@ +# All firewall nodes share one external IP for wireguard connections. +# Private key and peer configuration is the same for all nodes. Peers +# connected to each node are installed in the routing table and +# distributed into fabric. + +- name: Install wireguard tools + package: + name: wireguard-tools + +- name: Create wireguard directory + file: + path: /etc/wireguard + state: directory + +- name: Touch wireguard config + file: + path: /etc/wireguard/wg.conf + state: touch + access_time: preserve + modification_time: preserve + +- name: Add wireguard interface + template: + dest: /etc/network/interfaces.d/wg.intf + src: wg.intf.j2 + notify: enable interfaces diff --git a/roles/firewall/templates/conntrackd.conf.j2 b/roles/firewall/templates/conntrackd.conf.j2 new file mode 100644 index 0000000..578f00d --- /dev/null +++ b/roles/firewall/templates/conntrackd.conf.j2 @@ -0,0 +1,50 @@ +{% set fw = inventory_hostname.split('-')[1]|int -%} + +Sync { + Mode FTFW { + # Add received rules immediately so we don’t need a + # signal on failover. + DisableExternalCache On + } + + UDP { + Interface {{ iface_sync }} + IPv6_address fe80::{{ fw }} + IPv6_Destination_Address fe80::{{ 2 if fw == 1 else 1 }} + Port 3780 + + # Recommended by manual. + Checksum on + RcvSocketBuffer 1249280 + SndSocketBuffer 1249280 + } + + #Options { + # TCPWindowTracking Off + #} +} + +General { + UNIX { + Path /var/run/conntrackd.ctl + } + Syslog on + + # Recommended by manual. + HashLimit 524288 + NetlinkBufferSize 2097152 + NetlinkBufferSizeMaxGrowth 8388608 + + Filter From Kernelspace { + # Don’t replicate rules for traffic from/to firewall. + Address Ignore { + IPv4_address 127.0.0.1/8 + IPv6_address ::1/128 + IPv6_address fe80::/64 # link-local addresses + IPv4_address {{ wg_ip }} +{% for address in interfaces | map(attribute='ip_addresses') | flatten | sort(attribute='address') %} + IPv{{ address.family.value }}_address {{ address.address }} +{% endfor %} + } + } +} diff --git a/roles/firewall/templates/firewall.intf.j2 b/roles/firewall/templates/firewall.intf.j2 new file mode 100644 index 0000000..12328b5 --- /dev/null +++ b/roles/firewall/templates/firewall.intf.j2 @@ -0,0 +1,16 @@ +# disable IP forwarding on management interfaces +{% for iface in interfaces | map(attribute="name") | select("match", "^mgmt[0-9]+") %} +iface {{ iface }} + pre-up sysctl -w net.ipv4.conf.$IFACE.forwarding=0 + pre-up sysctl -w net.ipv6.conf.$IFACE.forwarding=0 + +{% endfor -%} + +# create VLANs 2 and 4 on firewal—exit links for inside and outside traffic +{% for iface in interfaces | map(attribute="name") | select("match", "^lan") + | product([2, 4]) + | map("join", ".") %} +auto {{ iface }} +iface {{ iface }} + +{% endfor %} diff --git a/roles/firewall/templates/frr.conf.j2 b/roles/firewall/templates/frr.conf.j2 new file mode 100644 index 0000000..05660a0 --- /dev/null +++ b/roles/firewall/templates/frr.conf.j2 @@ -0,0 +1,141 @@ +{% set addrs = interfaces | selectattr('name', '==', 'lo') | + map(attribute='ip_addresses') | first | selectattr('role') %} +{% set loopback = addrs | selectattr('role.value', '==', 'loopback') | map(attribute='address') -%} + +frr defaults datacenter +service integrated-vtysh-config +log syslog + +# Without this frr and kernel ECMP routes sometimes get desynced when a link is +# lost and found. Maybe related to https://github.com/FRRouting/frr/issues/12239. +zebra nexthop-group keep 1 + +router-id {{ loopback | ipv4 | first | ansible.utils.ipaddr('address') }} + +bfd + profile fast + receive-interval 150 + transmit-interval 150 + +# Default VRF has two connections to each exit, one for inside and one +# for outside networks. The efault route is received from the outside +# peers and distributed back to inside peers. Routes to office +# networks and NAT IPs are distributed to outside peers. +router bgp {{ asn.asn }} + # Allow multipathing through different ASs with equal path length. + bgp bestpath as-path multipath-relax + # NAT IPs are not on any interface so disable checking for it. + no bgp network import-check + +{% for group in ['inside', 'outside'] %} + neighbor {{ group }} peer-group + neighbor {{ group }} remote-as external + neighbor {{ group }} capability extended-nexthop +{% endfor %} + +{% for iface in interfaces | selectattr('name', 'match', '^lan') %} + neighbor {{ iface.name }}.2 interface peer-group inside + neighbor {{ iface.name }}.2 bfd profile fast + neighbor {{ iface.name }}.4 interface peer-group outside + neighbor {{ iface.name }}.4 bfd profile fast +{% endfor %} + +{% for family in ['ipv4', 'ipv6'] %} + address-family {{ family }} unicast +{% if family == 'ipv4' %} +{% for network in nat %} + network {{ network }} +{% endfor %} + +{% endif %} + redistribute connected route-map loopback + maximum-paths 16 + + neighbor outside activate + neighbor outside soft-reconfiguration inbound + neighbor outside route-map outside->default in + neighbor outside route-map default->outside out + + neighbor inside activate + neighbor inside allowas-in origin + neighbor inside default-originate + neighbor inside soft-reconfiguration inbound + neighbor inside route-map inside->default in + neighbor inside route-map default->inside out + exit-address-family + +{% endfor %} + +# Prefix lists. +ip prefix-list default permit 0.0.0.0/0 +ipv6 prefix-list default permit ::/0 + +ip prefix-list fabric permit 10.34.0.0/24 ge 32 + +{% for prefix in vrf_prefixes | rejectattr('vrf.name', '==', 'outside') + | sort(attribute='family.value') %} +{% if prefix.family.value == 4 %} +ip prefix-list office permit {{ prefix.prefix }} +{% elif prefix.family.value == 6 %} +ipv6 prefix-list office permit {{ prefix.prefix }} +{% endif %} +{% endfor %} + +{% if wg_net is defined %} +ip prefix-list vpn permit {{ wg_net | ansible.utils.ipaddr('subnet') }} +{% endif %} +{% if wg_net6 is defined %} +ipv6 prefix-list vpn permit {{ wg_net6 | ansible.utils.ipaddr('subnet') }} +{% endif %} + +{% for network in nat %} +ip prefix-list nat permit {{ network }} +{% endfor %} +{# TODO WG endpoint should probably be in a separate prefix-list. #} +ip prefix-list nat permit {{ wg_ip }} + +route-map loopback permit 1 + match interface lo +route-map loopback permit 2 + match interface wg + +# Get routes to offices from inside peers. +route-map inside->default permit 10 + match ip address prefix-list fabric +route-map inside->default permit 20 + match ip address prefix-list office +route-map inside->default permit 21 + match ipv6 address prefix-list office + +# Send default route and VPN network to inside peers. +route-map default->inside permit 1 + match interface lo +route-map default->inside permit 20 + match ip address prefix-list default +route-map default->inside permit 21 + match ipv6 address prefix-list default +# I don’t think these /need/ to be announced separately since we are sending the default route anyway. +#route-map default->inside permit 30 +# match ip address prefix-list vpn +#route-map default->inside permit 31 +# match ipv6 address prefix-list vpn + +# Get default route from outside peers. +route-map outside->default permit 10 + match ip address prefix-list default +route-map outside->default permit 11 + match ipv6 address prefix-list default + +# Send inside and NAT addresses to outside peers so inbound packets go through the firewall. +route-map default->outside permit 1 + match interface lo +route-map default->outside permit 20 + match ip address prefix-list office +route-map default->outside permit 21 + match ipv6 address prefix-list office +route-map default->outside permit 30 + match ip address prefix-list nat +route-map default->outside permit 40 + match ip address prefix-list vpn +route-map default->outside permit 41 + match ipv6 address prefix-list vpn diff --git a/roles/firewall/templates/interfaces.nft.j2 b/roles/firewall/templates/interfaces.nft.j2 new file mode 100644 index 0000000..a8b7fda --- /dev/null +++ b/roles/firewall/templates/interfaces.nft.j2 @@ -0,0 +1,10 @@ +{% set ifaces_fabric = interfaces | selectattr('name', 'match', '^lan') | map(attribute='name') %} +set inside { + type iface_index + elements = { {{ ifaces_fabric | product(['2']) | map('join', '.') | join(', ') }}, wg } +} + +set outside { + type iface_index + elements = { {{ ifaces_fabric | product(['4']) | map('join', '.') | join(', ') }} } +} diff --git a/roles/firewall/templates/networks.nft.j2 b/roles/firewall/templates/networks.nft.j2 new file mode 100644 index 0000000..cb74b20 --- /dev/null +++ b/roles/firewall/templates/networks.nft.j2 @@ -0,0 +1,9 @@ +{% for family, family_prefixes in vrf_prefixes | groupby('family.value') %} +{% for vlan, vlan_prefixes in family_prefixes | groupby('vlan.vid') %} +set {{ vlan_prefixes[0].vlan.name }}{% if family == 6 %}/6{% endif %} { + type ipv{{ family }}_addr; flags interval + elements = { {{ vlan_prefixes | map(attribute='prefix') | join(',') }} } +} +{% endfor %} + +{% endfor %} diff --git a/roles/firewall/templates/nftables.nft.j2 b/roles/firewall/templates/nftables.nft.j2 new file mode 100644 index 0000000..18f4b76 --- /dev/null +++ b/roles/firewall/templates/nftables.nft.j2 @@ -0,0 +1,190 @@ +#!/usr/sbin/nft -f +{% set ifaces_fabric = interfaces | selectattr('name', 'match', '^lan') | map(attribute='name') %} + +flush ruleset + +table inet filter { + include "/etc/nftables.d/interfaces.nft" + include "/etc/nftables.d/networks.nft" + include "/etc/nftables.d/sets.nft*" + + set link { + type iface_index + elements = { {{ ifaces_fabric | product(['2', '4']) | map('join', '.') | join(', ') }} } + } + + # convenience port set definitions + set ad-ports { # https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/config-firewall-for-ad-domains-and-trusts + type inet_proto . inet_service + flags interval + elements = { + tcp . 53, + tcp . 88, + tcp . 135, + tcp . 139, + tcp . 389, + tcp . 445, + tcp . 464, + tcp . 636, + tcp . 3268-3269, + #tcp . 3389, # RDP + tcp . 5000-5100, + tcp . 5985, + tcp . 5986, + tcp . 9389, + tcp . 22222-22224, + tcp . 49152-65535, + udp . 53, + udp . 88, + udp . 135, + udp . 137, # netbios, maybe can do without + udp . 138, # netbios, maybe can do without + udp . 389, + udp . 464, + udp . 3269 + } + } + + set ldap-ports { + type inet_proto . inet_service + flags interval + elements = { + tcp . 88, + tcp . 389, + tcp . 636, + tcp . 3268, + tcp . 3269, + udp . 88, + udp . 389 + } + } + + chain input { + type filter hook input priority 0; policy drop + + ct state vmap { established : accept, related : accept, invalid : drop } \ + comment "Accept established streams and drop invalid connections" + + iif lo accept \ + comment "Accept any localhost traffic" + + iif mgmt tcp dport ssh accept \ + comment "Accept SSH from management VRF" + + # allow SSH connections from firewall master’s IPs +{% for iface in hostvars[master].interfaces %} +{% for address in iface.ip_addresses %} + tcp dport ssh {{ 'ip' if address.family.value == 4 else 'ip6' }} saddr {{ address.address | ansible.utils.ipaddr('address') }} accept +{% for nat_address in address.nat_outside %} + tcp dport ssh ip saddr {{ nat_address.address | ansible.utils.ipaddr('address') }} accept +{% endfor %} +{% endfor %} +{% endfor %} + + iif @link tcp dport bgp ip6 saddr fe80::/10 accept \ + comment "Accept link-local BGP on fabric links" + + iif @link udp dport 3784 ip6 saddr fe80::/10 accept \ + comment "Accept link-local BFD on fabric links" + + udp dport 51820 accept \ + comment "Accept WireGuard from anywhere" + + iif {{ iface_sync }} ip6 saddr fe80::/10 udp dport 3780 accept \ + comment "Accept connection tracking sync data" + + tcp dport auth reject with icmpx type port-unreachable \ + comment "Reject AUTH to make it fail fast" + + # ICMPv4 + ip protocol icmp icmp type { + echo-request, echo-reply, destination-unreachable, + parameter-problem, time-exceeded, + } accept \ + comment "Accept ICMP" + + # ICMPv6 + ip6 nexthdr icmpv6 icmpv6 type { + echo-request, echo-reply, destination-unreachable, + packet-too-big, parameter-problem, time-exceeded, + } accept \ + comment "Accept basic IPv6 functionality" + + ip6 nexthdr icmpv6 icmpv6 type { + nd-neighbor-solicit, nd-neighbor-advert, + nd-router-solicit, nd-router-advert, + } ip6 hoplimit 255 accept \ + comment "Allow IPv6 neighbor discovery" + } + + chain forward { + type filter hook forward priority filter; policy drop + + ct state { established, related } accept \ + comment "Forward all established and related traffic" + + ct status dnat accept \ + comment "Forward DNAT traffic for servers and suchlike" + + ip protocol icmp icmp type { + echo-request, echo-reply, destination-unreachable, + parameter-problem, time-exceeded, + } accept \ + comment "Accept ICMPv4" + + ip6 nexthdr icmpv6 icmpv6 type { + echo-request, echo-reply, destination-unreachable, + packet-too-big, parameter-problem, time-exceeded, + } accept \ + comment "Accept ICMPv6" + + include "/etc/nftables.d/forward.nft*" + } + + chain output { + type filter hook output priority 0; policy accept + } +} + +table inet wireguard { + chain input { + type filter hook prerouting priority raw; policy accept + ip daddr 193.2.76.190 udp dport 51820 notrack \ + comment "Disable connection tracking for wireguard" + } + chain output { + type route hook output priority raw; policy accept + meta mark 51820 meta nfproto ipv4 ip saddr set 193.2.76.190 notrack \ + comment "Disable connection tracking and set anycast source IP for wireguard" + } +} + +table ip nat { + include "/etc/nftables.d/interfaces.nft" + include "/etc/nftables.d/networks.nft" + include "/etc/nftables.d/sets.nft*" + include "/etc/nftables.d/netmap.nft*" + + # Ensure these maps exist even if empty. + map netmap-in { type ipv4_addr : interval ipv4_addr; flags interval; } + map netmap-out { type ipv4_addr : interval ipv4_addr; flags interval; } + + chain postrouting { + type nat hook postrouting priority srcnat + + iif @inside oif @outside snat ip prefix to ip saddr map @netmap-out \ + comment "Static source NAT for 1:1 mapped addresses" + + include "/etc/nftables.d/nat.nft*" + } + + chain prerouting { + type nat hook prerouting priority dstnat + + dnat ip prefix to ip daddr map @netmap-in \ + comment "Static destination NAT for 1:1 mapped addresses" + } +} + +{# for compatibility with base alpine role #} +include "/etc/nftables.d/services.nft*" diff --git a/roles/firewall/templates/sysctl.conf.j2 b/roles/firewall/templates/sysctl.conf.j2 new file mode 100644 index 0000000..1c40f43 --- /dev/null +++ b/roles/firewall/templates/sysctl.conf.j2 @@ -0,0 +1,16 @@ +# We are router. +net.ipv4.ip_forward = 1 +net.ipv6.conf.all.forwarding = 1 + +# Zebra docs recommend these. +net.ipv6.conf.all.keep_addr_on_down = 1 +net.ipv6.route.skip_notify_on_dev_down = 1 + +# Do not send ICMP redirects. Happens because firewall sees all office +# networks coming from the same routers, and gets confused as to why +# firewall is routing packets between them. +net.ipv4.conf.all.send_redirects = 0 +net.ipv4.conf.default.send_redirects = 0 + +# Increase max connections for netfilter. +net.netfilter.nf_conntrack_max = 1048576 diff --git a/roles/firewall/templates/wg.intf.j2 b/roles/firewall/templates/wg.intf.j2 new file mode 100644 index 0000000..f66ff27 --- /dev/null +++ b/roles/firewall/templates/wg.intf.j2 @@ -0,0 +1,12 @@ +iface lo + address {{ wg_ip }} + +auto wg +iface wg + use wireguard +{% if wg_net is defined %} + address {{ wg_net }} +{% endif %} +{% if wg_net6 is defined %} + address {{ wg_net6 }} +{% endif %} diff --git a/setup.yml b/setup.yml index 273ffcc..ddebebd 100644 --- a/setup.yml +++ b/setup.yml @@ -18,6 +18,10 @@ - frr - ceph +- hosts: fw-* + roles: + - firewall + - hosts: mgmt-gw roles: - radvd # we are router for mgmt networks