diff --git a/bundles/dhcpd/metadata.py b/bundles/dhcpd/metadata.py index 6c13ea3..bdf47b7 100644 --- a/bundles/dhcpd/metadata.py +++ b/bundles/dhcpd/metadata.py @@ -37,18 +37,20 @@ def get_static_allocations(metadata): @metadata_reactor.provides( - 'iptables/bundle_rules/dhcpd', + 'nftables/rules/input/dhcpd', ) -def iptables(metadata): +def nftables(metadata): rules = set() - for subnet in node.metadata.get('dhcpd/subnets', {}): - rules.add('iptables -A INPUT -i {} -p udp --dport 67:68 -j ACCEPT'.format(subnet)) + for iface in node.metadata.get('dhcpd/subnets', {}): + rules.add(f'udp dport {{ 67, 68 }} iif {iface} accept') return { - 'iptables': { - 'bundle_rules': { - # can't use port_rules here, because we're generating interface based rules. - 'dhcpd': sorted(list(rules)), + 'nftables': { + 'rules': { + 'input': { + # can't use port_rules here, because we're generating interface based rules. + 'dhcpd': sorted(rules), + }, }, } } diff --git a/bundles/dovecot/metadata.py b/bundles/dovecot/metadata.py index 82adb0a..bd1427e 100644 --- a/bundles/dovecot/metadata.py +++ b/bundles/dovecot/metadata.py @@ -76,13 +76,13 @@ def import_database_settings_from_postfixadmin(metadata): @metadata_reactor.provides( - 'iptables/port_rules/143', - 'iptables/port_rules/993', - 'iptables/port_rules/4190', + 'firewall/port_rules/143', + 'firewall/port_rules/993', + 'firewall/port_rules/4190', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { # imap(s) '143': atomic(metadata.get('dovecot/restrict-to', {'*'})), diff --git a/bundles/icinga2/metadata.py b/bundles/icinga2/metadata.py index 48b441f..9bf7d26 100644 --- a/bundles/icinga2/metadata.py +++ b/bundles/icinga2/metadata.py @@ -103,11 +103,11 @@ def add_users_from_json(metadata): @metadata_reactor.provides( - 'iptables/port_rules/5665', + 'firewall/port_rules/5665', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { '5665': atomic(metadata.get('icinga2/restrict-to', set())), }, diff --git a/bundles/iptables/files/iptables-enforce b/bundles/iptables/files/iptables-enforce deleted file mode 100644 index ea7a206..0000000 --- a/bundles/iptables/files/iptables-enforce +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash - -% if not node.metadata.get('iptables/enabled', True): -exit 0 -% endif - -lock_try=0 -while ! mkdir /run/bw-iptables.lock >/dev/null 2>&1 -do - ((lock_try++)) - if (( lock_try == 10 )) - then - echo 'FATAL: iptables-enforce: Could not get lock!' >&2 - exit 1 - fi - sleep 1 -done -trap 'rmdir /run/bw-iptables.lock' EXIT - -iptables_both() -{ - iptables "$@" - ip6tables "$@" -} - -iptables_both -P INPUT DROP -iptables_both -P OUTPUT ACCEPT -iptables_both -P FORWARD DROP -iptables_both -F -iptables_both -X -iptables_both -t nat -F -iptables_both -t nat -X -iptables_both -t nat -Z -iptables_both -t filter -F -iptables_both -t filter -X -iptables_both -t filter -Z -iptables_both -t mangle -F -iptables_both -t mangle -X -iptables_both -t mangle -Z - -# Workaround for CVE-2019-11477, CVE-2019-11478 and CVE-2019-11479 -# https://www.openwall.com/lists/oss-security/2019/06/17/5 -# https://people.canonical.com/~ubuntu-security/cve/2019/CVE-2019-11477.html -iptables_both -I INPUT -p tcp -m tcpmss --mss 1:500 -j DROP - -# Dummy rules to make sure the conntrack table(s) will be updated. -iptables_both -I INPUT -m state --state NEW,ESTABLISHED,RELATED -iptables_both -I OUTPUT -m state --state NEW,ESTABLISHED,RELATED -iptables_both -I FORWARD -m state --state NEW,ESTABLISHED,RELATED - -# open up local loopback -iptables_both -A INPUT -i lo -j ACCEPT - -# Set Up counting rules -% for ip in sorted(ipv4): -iptables -A INPUT -d ${ip} -iptables -A OUTPUT -s ${ip} -% endfor - -% for ip in sorted(ipv6): -ip6tables -A INPUT -d ${ip} -ip6tables -A OUTPUT -s ${ip} -% endfor - -iptables -A INPUT -p ICMP --icmp-type timestamp-request -j DROP -iptables -A INPUT -p ICMP --icmp-type timestamp-reply -j DROP -# allow ICMP -- answers for IPv4 are covered by conntrack -iptables -A INPUT -p icmp -j ACCEPT - -# ICMP6 is used for so many things, we should under no circumstances -# ignore it and thus should not rely on any conntrack heuristics. -ip6tables -A INPUT -p ipv6-icmp -j ACCEPT - -# Allow incoming answers. Install this first (before the larger ruleset -# from /etc/network/iptables-rules.d/), so that iptables can match/exit -# early. -iptables_both -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT - -shopt -s nullglob -for i in /etc/iptables-rules.d/* -do - . "$i" -done - -cat /etc/sysctl.d/*.conf /etc/sysctl.conf | sysctl -e -p - diff --git a/bundles/iptables/files/iptables-enforce.service b/bundles/iptables/files/iptables-enforce.service deleted file mode 100644 index 6eeac90..0000000 --- a/bundles/iptables/files/iptables-enforce.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Run iptables-enforce after networkd startup -Requires=network-online.target -After=network-online.target - -[Service] -Type=oneshot -ExecStart=/usr/local/sbin/iptables-enforce - -[Install] -WantedBy=multi-user.target diff --git a/bundles/iptables/items.py b/bundles/iptables/items.py deleted file mode 100644 index 7e9a647..0000000 --- a/bundles/iptables/items.py +++ /dev/null @@ -1,66 +0,0 @@ -directories = { - '/etc/iptables-rules.d': { - 'purge': True, - 'triggers': { - 'action:iptables_enforce', - }, - }, -} - -files = { - '/etc/systemd/system/iptables-enforce.service': { - 'triggers': { - 'action:systemd-reload', - }, - }, - '/usr/local/sbin/iptables-enforce': { - 'content_type': 'mako', - 'context': repo.libs.tools.resolve_identifier(repo, node.name), - 'mode': '0700', - 'triggers': { - 'action:iptables_enforce', - }, - }, -} - -enforce_deps = { - 'directory:/etc/iptables-rules.d', - 'file:/usr/local/sbin/iptables-enforce', -} - -for bundle, rules in node.metadata.get('iptables/bundle_rules', {}).items(): - files[f'/etc/iptables-rules.d/20-{bundle}'] = { - # We must never use sorted() here. Bundles might rely on their order. - 'content': '\n'.join(rules) + '\n', - 'triggers': { - 'action:iptables_enforce', - }, - } - enforce_deps.add(f'file:/etc/iptables-rules.d/20-{bundle}') - -if 'custom_rules' in node.metadata.get('iptables', {}): - files['/etc/iptables-rules.d/40-custom'] = { - 'content': '\n'.join(node.metadata['iptables']['custom_rules']) + '\n', - 'triggers': { - 'action:iptables_enforce', - }, - } - enforce_deps.add('file:/etc/iptables-rules.d/40-custom') - - -actions = { - 'iptables_enforce': { - 'command': '/usr/local/sbin/iptables-enforce', - 'triggered': True, - 'needs': enforce_deps, - }, -} - -svc_systemd = { - 'iptables-enforce': { - 'running': None, - 'needs': { - 'file:/etc/systemd/system/iptables-enforce.service', - }, - }, -} diff --git a/bundles/iptables/metadata.py b/bundles/iptables/metadata.py deleted file mode 100644 index e5615a0..0000000 --- a/bundles/iptables/metadata.py +++ /dev/null @@ -1,61 +0,0 @@ -from bundlewrap.exceptions import BundleError - -defaults = { - 'pacman': { - 'packages': { - 'iptables': {}, - }, - }, -} - -@metadata_reactor.provides( - 'iptables/bundle_rules/iptables', -) -def port_rules_to_iptables(metadata): - # Using this, bundles can simply set up port based rules. This - # reactor will then take care of converting those rules to actual - # iptables rules - ruleset = set() - - # Plese note we do not set any defaults for ports. Bundles are - # expected to know themselves which default to use. - for portdef, targets in metadata.get('iptables/port_rules', {}).items(): - if '/' in portdef: - port, proto = portdef.split('/', 2) - - if proto not in {'udp'}: - raise BundleError(f'iptables/port_rules: illegal identifier {portdef} in metadata for {node.name}') - else: - port = portdef - proto = 'tcp' - - for target in targets: - if port == '*' and target == '*': - raise BundleError('iptables/port_rules: setting both port and target to * is unsupported') - - comment = f'-m comment --comment "iptables port_rules {target}"' - - if port != '*': - port_str = f'--dport {port}' - else: - port_str = '' - - if target == '*': - ruleset.add(f'iptables_both -A INPUT -p {proto} {port_str} {comment} -j ACCEPT') - else: - resolved = repo.libs.tools.resolve_identifier(repo, target) - - for address in resolved['ipv4']: - ruleset.add(f'iptables -A INPUT -p {proto} -s {address} {port_str} {comment} -j ACCEPT') - - for address in resolved['ipv6']: - ruleset.add(f'ip6tables -A INPUT -p {proto} -s {address} {port_str} {comment} -j ACCEPT') - - return { - 'iptables': { - 'bundle_rules': { - # order does not matter here. - 'iptables': list(sorted(ruleset)), - }, - }, - } diff --git a/bundles/kodi/metadata.py b/bundles/kodi/metadata.py index 0d888f9..f21afff 100644 --- a/bundles/kodi/metadata.py +++ b/bundles/kodi/metadata.py @@ -44,11 +44,11 @@ defaults = { @metadata_reactor.provides( - 'iptables/port_rules/8080', + 'firewall/port_rules/8080', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { '8080': atomic(metadata.get('kodi/restrict-to', {'*'})), }, diff --git a/bundles/mosquitto/metadata.py b/bundles/mosquitto/metadata.py index 3834b86..6c2b3d1 100644 --- a/bundles/mosquitto/metadata.py +++ b/bundles/mosquitto/metadata.py @@ -26,9 +26,9 @@ defaults = { @metadata_reactor.provides( - 'iptables/port_rules', + 'firewall/port_rules', ) -def iptables(metadata): +def firewall(metadata): sources = metadata.get('mosquitto/restrict-to', {'*'}) result = {} @@ -36,7 +36,7 @@ def iptables(metadata): result[listener] = atomic(sources) return { - 'iptables': { + 'firewall': { 'port_rules': result, }, } diff --git a/bundles/netdata/metadata.py b/bundles/netdata/metadata.py index 9dea2b4..feb00ff 100644 --- a/bundles/netdata/metadata.py +++ b/bundles/netdata/metadata.py @@ -19,11 +19,11 @@ defaults = { @metadata_reactor.provides( - 'iptables/port_rules/19999', + 'firewall/port_rules/19999', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { '19999': atomic(metadata.get('netdata/restrict-to', set())), }, diff --git a/bundles/nfs-server/metadata.py b/bundles/nfs-server/metadata.py index b0bbd32..d203560 100644 --- a/bundles/nfs-server/metadata.py +++ b/bundles/nfs-server/metadata.py @@ -10,9 +10,9 @@ defaults = { @metadata_reactor.provides( - 'iptables/port_rules', + 'firewall/port_rules', ) -def iptables(metadata): +def firewall(metadata): ips = set() for share_items in metadata.get('nfs-server/shares', {}).values(): @@ -20,7 +20,7 @@ def iptables(metadata): ips.add(share_target) return { - 'iptables': { + 'firewall': { 'port_rules': { '111': atomic(ips), '111/udp': atomic(ips), diff --git a/bundles/nftables/files/nftables.conf b/bundles/nftables/files/nftables.conf new file mode 100644 index 0000000..f417856 --- /dev/null +++ b/bundles/nftables/files/nftables.conf @@ -0,0 +1,72 @@ +#!/usb/sbin/nft -f + +flush ruleset + +table inet filter { + chain input { + type filter hook input priority 0 + policy drop + + tcp flags syn tcp option maxseg size 1-500 drop + + ct state { established, related } accept + ct state invalid drop + + iif lo accept + + icmp type timestamp-request drop + icmp type timestamp-reply drop + ip protocol icmp accept + + ip6 nexthdr ipv6-icmp accept +% for ruleset, rules in sorted(node.metadata.get('nftables/rules/input', {}).items()): + + # ${ruleset} +% for rule in rules: + ${rule} +% endfor + # / ${ruleset} +% endfor + } + + chain output { + type filter hook output priority 0 + policy accept + } + + chain forward { + type filter hook forward priority 0 + policy drop + + icmp type timestamp-request drop + icmp type timestamp-reply drop + +% for ruleset, rules in sorted(node.metadata.get('nftables/rules/forward', {}).items()): + + # ${ruleset} +% for rule in rules: + ${rule} +% endfor + # / ${ruleset} +% endfor + } +} + +table nat { + chain prerouting { + type nat hook prerouting priority -100 + +% for rule in node.metadata.get('nftables/rules/nat_prerouting', []): + ${rule} +% endfor + } + chain postrouting { + type nat hook postrouting priority 100 + +% for rule in node.metadata.get('nftables/rules/nat_postrouting', []): + ${rule} +% endfor + } +} + +include "/etc/nftables-rules.d/*-*" diff --git a/bundles/nftables/items.py b/bundles/nftables/items.py new file mode 100644 index 0000000..f755916 --- /dev/null +++ b/bundles/nftables/items.py @@ -0,0 +1,35 @@ +if node.has_bundle('pacman'): + package = 'pkg_pacman:nftables' +else: + package = 'pkg_apt:nftables' + +directories = { + # used by other bundles + '/etc/nftables-rules.d': { + 'purge': True, + 'triggers': { + 'svc_systemd:nftables:reload', + }, + }, +} + +files = { + '/etc/nftables.conf': { + 'content_type': 'mako', + 'needs': { + 'directory:/etc/nftables-rules.d', + }, + 'triggers': { + 'svc_systemd:nftables:reload', + }, + }, +} + +svc_systemd = { + 'nftables': { + 'needs': { + 'file:/etc/nftables.conf', + package, + }, + }, +} diff --git a/bundles/nftables/metadata.py b/bundles/nftables/metadata.py new file mode 100644 index 0000000..a960c32 --- /dev/null +++ b/bundles/nftables/metadata.py @@ -0,0 +1,91 @@ +from bundlewrap.exceptions import BundleError + +defaults = { + 'apt': { + 'packages': { + 'nftables': {}, + + # XXX remove after all systems have been migrated + }, + }, + 'pacman': { + 'packages': { + 'nftables': {}, + 'iptables-nft': { + # uninstalls iptables automatically + 'needed_by': { + 'pkg_pacman:nftables', + }, + }, + }, + }, +} + +if not node.has_bundle('vmhost'): + # see comment in bundles/vmhost/items.py + defaults['apt']['packages']['iptables'] = { + 'installed': False, + 'needed_by': { + 'pkg_apt:nftables', + }, + } + +@metadata_reactor.provides( + 'nftables/rules/input/port_rules', +) +def port_rules_to_nftables(metadata): + # Using this, bundles can simply set up port based rules. This + # reactor will then take care of converting those rules to actual + # nftables rules + ruleset = set() + + # Plese note we do not set any defaults for ports. Bundles are + # expected to know themselves which default to use. + for portdef, targets in metadata.get('firewall/port_rules', {}).items(): + if '/' in portdef: + port, proto = portdef.split('/', 2) + + if proto not in {'udp'}: + raise BundleError(f'firewall/port_rules: illegal identifier {portdef} in metadata for {node.name}') + else: + port = portdef + proto = 'tcp' + + for target in targets: + if port == '*' and target == '*': + raise BundleError('firewall/port_rules: setting both port and target to * is unsupported') + + comment = f'# port_rules {target}' + + if port != '*': + if ':' in port: + parts = port.split(':') + port_str = f'dport {{ {parts[0]}-{parts[1]} }} ' + else: + port_str = f'dport {port} ' + prefix = '' + else: + port_str = '' + prefix = 'meta l4proto ' + + if target == '*': + ruleset.add(f'{prefix}{proto} {port_str}accept {comment}') + else: + resolved = repo.libs.tools.resolve_identifier(repo, target) + + for address in resolved['ipv4']: + ruleset.add(f'{prefix}{proto} {port_str}ip saddr {address} accept {comment}') + + for address in resolved['ipv6']: + ruleset.add(f'{prefix}{proto} {port_str}ip6 saddr {address} accept {comment}') + + return { + 'nftables': { + 'rules': { + # order does not matter here. + 'input': { + 'port_rules': sorted(ruleset), + }, + }, + }, + } diff --git a/bundles/nginx/metadata.py b/bundles/nginx/metadata.py index 2432f42..bfd19ff 100644 --- a/bundles/nginx/metadata.py +++ b/bundles/nginx/metadata.py @@ -169,12 +169,12 @@ def monitoring(metadata): @metadata_reactor.provides( - 'iptables/port_rules/80', - 'iptables/port_rules/443', + 'firewall/port_rules/80', + 'firewall/port_rules/443', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { '80': atomic(metadata.get('nginx/restrict-to', {'*'})), '443': atomic(metadata.get('nginx/restrict-to', {'*'})), diff --git a/bundles/oidentd/metadata.py b/bundles/oidentd/metadata.py index 5d997b4..f9d4390 100644 --- a/bundles/oidentd/metadata.py +++ b/bundles/oidentd/metadata.py @@ -10,11 +10,11 @@ defaults = { @metadata_reactor.provides( - 'iptables/port_rules/113', + 'firewall/port_rules/113', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { '113': atomic(metadata.get('oidentd/restrict-to', {'*'})), }, diff --git a/bundles/openssh/metadata.py b/bundles/openssh/metadata.py index a7a8424..3cad1b9 100644 --- a/bundles/openssh/metadata.py +++ b/bundles/openssh/metadata.py @@ -16,11 +16,11 @@ defaults = { } @metadata_reactor.provides( - 'iptables/port_rules/22', + 'firewall/port_rules/22', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { '22': atomic(metadata.get('openssh/restrict-to', {'*'})), }, diff --git a/bundles/postfix/files/main.cf b/bundles/postfix/files/main.cf index 24eaa80..890428d 100644 --- a/bundles/postfix/files/main.cf +++ b/bundles/postfix/files/main.cf @@ -18,7 +18,7 @@ alias_maps = hash:/etc/aliases relayhost = ${node.metadata['postfix']['relayhost']} % endif -% if node.has_bundle('postfixadmin') or node.has_bundle('iptables'): +% if node.has_bundle('postfixadmin') or node.has_bundle('nftables'): inet_interfaces = all % else: inet_interfaces = 127.0.0.1 diff --git a/bundles/postfix/metadata.py b/bundles/postfix/metadata.py index 7d2b821..cea5cf1 100644 --- a/bundles/postfix/metadata.py +++ b/bundles/postfix/metadata.py @@ -100,11 +100,11 @@ def letsencrypt(metadata): @metadata_reactor.provides( - 'iptables/port_rules/25', - 'iptables/port_rules/587', - 'iptables/port_rules/2525', + 'firewall/port_rules/25', + 'firewall/port_rules/587', + 'firewall/port_rules/2525', ) -def iptables(metadata): +def firewall(metadata): if node.has_bundle('postfixadmin'): default = {'*'} else: @@ -119,7 +119,7 @@ def iptables(metadata): rules['2525'] = atomic(metadata.get('postfix/restrict-to', default)) return { - 'iptables': { + 'firewall': { 'port_rules': rules, }, } diff --git a/bundles/powerdns/metadata.py b/bundles/powerdns/metadata.py index ff087a1..2cd0e53 100644 --- a/bundles/powerdns/metadata.py +++ b/bundles/powerdns/metadata.py @@ -182,11 +182,11 @@ def hosts_entries_for_all_dns_servers(metadata): @metadata_reactor.provides( - 'iptables/port_rules', + 'firewall/port_rules', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { '53': atomic(metadata.get('powerdns/restrict-to', {'*'})), '53/udp': atomic(metadata.get('powerdns/restrict-to', {'*'})), diff --git a/bundles/pppd/files/ip-down b/bundles/pppd/files/ip-down index 6bdf74c..7cdcd09 100644 --- a/bundles/pppd/files/ip-down +++ b/bundles/pppd/files/ip-down @@ -1,6 +1,6 @@ #!/bin/bash -rm /etc/iptables-rules.d/90-pppd +rm /etc/nftables-rules.d/90-pppd rm /etc/sysctl.d/90-pppd.conf -/usr/local/sbin/iptables-enforce +systemctl reload nftables diff --git a/bundles/pppd/files/ip-up b/bundles/pppd/files/ip-up index beca42a..fa00fe6 100644 --- a/bundles/pppd/files/ip-up +++ b/bundles/pppd/files/ip-up @@ -2,9 +2,9 @@ INTERFACE=$1 -echo "iptables -t nat -A POSTROUTING -o $INTERFACE -j MASQUERADE" > /etc/iptables-rules.d/90-pppd +echo "add rule nat postrouting oif $INTERFACE masquerade" > /etc/nftables-rules.d/90-pppd echo "net.ipv6.conf.$INTERFACE.accept_ra=2" > /etc/sysctl.d/90-pppd.conf -/usr/local/sbin/iptables-enforce +systemctl reload nftables rdisc6 $INTERFACE diff --git a/bundles/pppd/items.py b/bundles/pppd/items.py index 7e2cf6e..f358b5c 100644 --- a/bundles/pppd/items.py +++ b/bundles/pppd/items.py @@ -32,7 +32,7 @@ directories = { } files = { - '/etc/iptables-rules.d/90-pppd': { + '/etc/nftables-rules.d/90-pppd': { 'content_type': 'any', }, '/etc/ppp/chap-secrets': { @@ -53,11 +53,11 @@ files = { 'svc_systemd:pppoe:restart', }, }, - '/etc/ppp/ip-down.d/iptables': { + '/etc/ppp/ip-down.d/nftables': { 'source': 'ip-down', 'mode': '0755', }, - '/etc/ppp/ip-up.d/iptables': { + '/etc/ppp/ip-up.d/nftables': { 'source': 'ip-up', 'mode': '0755', }, diff --git a/bundles/transmission/metadata.py b/bundles/transmission/metadata.py index 3ee0ad9..5f5c682 100644 --- a/bundles/transmission/metadata.py +++ b/bundles/transmission/metadata.py @@ -50,11 +50,11 @@ if node.has_bundle('telegraf'): @metadata_reactor.provides( - 'iptables/port_rules', + 'firewall/port_rules', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { str(metadata.get('transmission/config/peer-port')): atomic({'*'}), str(metadata.get('transmission/config/peer-port')) + '/udp': atomic({'*'}), diff --git a/bundles/unbound/files/unbound.conf b/bundles/unbound/files/unbound.conf index 8d520fe..ef0cbf8 100644 --- a/bundles/unbound/files/unbound.conf +++ b/bundles/unbound/files/unbound.conf @@ -10,8 +10,8 @@ server: num-threads: ${threads} -% if node.has_bundle('iptables') and not node.has_bundle('vmhost'): - # Use iptables to manage access to this service +% if node.has_bundle('nftables') and not node.has_bundle('vmhost'): + # Use nftables to manage access to this service interface: 0.0.0.0 interface: ::0 access-control: 0.0.0.0/0 allow diff --git a/bundles/unbound/metadata.py b/bundles/unbound/metadata.py index efa54d2..caf3d04 100644 --- a/bundles/unbound/metadata.py +++ b/bundles/unbound/metadata.py @@ -56,11 +56,11 @@ def cpu_cores_to_config_values(metadata): @metadata_reactor.provides( - 'iptables/port_rules', + 'firewall/port_rules', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { '53': atomic(metadata.get('unbound/restrict-to', set())), '53/udp': atomic(metadata.get('unbound/restrict-to', set())), diff --git a/bundles/vmhost/items.py b/bundles/vmhost/items.py index 52ad0ec..c8ff9e1 100644 --- a/bundles/vmhost/items.py +++ b/bundles/vmhost/items.py @@ -3,3 +3,24 @@ files = { 'mode': '0755', }, } + +if node.has_bundle('nftables'): + # libvirt on debian depends on either iptables or firewalld. Since + # we're managing firewall rules using bundlewrap, we don't want either + # of thos to interfere. So we install firewalld, then ensure it is + # never running. After that, we ensure the bundlewrap managed rules + # are active. + svc_systemd['firewalld'] = { + 'running': False, + 'enabled': False, + 'masked': True, + 'needs': { + 'pkg_apt:firewalld', + }, + 'needed_by': { + 'svc_systemd:nftables', + }, + 'triggers': { + 'svc_systemd:nftables:reload', + }, + } diff --git a/bundles/vmhost/metadata.py b/bundles/vmhost/metadata.py index c5e7a59..a01e0dc 100644 --- a/bundles/vmhost/metadata.py +++ b/bundles/vmhost/metadata.py @@ -28,3 +28,10 @@ if node.os == 'debian' and node.os_version[0] < 11: if node.has_bundle('zfs'): defaults['apt']['packages']['libvirt-daemon-driver-storage-zfs'] = {} + +if node.has_bundle('nftables'): + defaults['apt']['packages']['firewalld'] = { + 'needed_by': { + 'pkg_apt:libvirt-daemon-system', + }, + } diff --git a/bundles/webfs/metadata.py b/bundles/webfs/metadata.py index dc2bb72..ca6356f 100644 --- a/bundles/webfs/metadata.py +++ b/bundles/webfs/metadata.py @@ -16,11 +16,11 @@ defaults = { @metadata_reactor.provides( - 'iptables/port_rules', + 'firewall/port_rules', ) -def iptables(metadata): +def firewall(metadata): return { - 'iptables': { + 'firewall': { 'port_rules': { str(metadata.get('webfs/port')): atomic(metadata.get('webfs/restrict-to', {'*'})), }, diff --git a/bundles/wide-dhcp6c/metadata.py b/bundles/wide-dhcp6c/metadata.py index 04495db..f1a6957 100644 --- a/bundles/wide-dhcp6c/metadata.py +++ b/bundles/wide-dhcp6c/metadata.py @@ -4,12 +4,14 @@ defaults = { 'wide-dhcpv6-client': {}, }, }, - 'iptables': { - 'bundle_rules': { - 'wide-dhcp6c': [ - 'ip6tables -A INPUT -p udp -s ff00::/12 -j ACCEPT', - 'ip6tables -A INPUT -p udp -s fe80::/10 -j ACCEPT', - ], + 'nftables': { + 'rules': { + 'input': { + 'wide-dhcp6c': [ + 'udp dport { 546, 547 } ip6 saddr ff00::/12 accept', + 'udp dport { 546, 547 } ip6 saddr fe80::/10 accept', + ], + }, }, }, 'icinga2_api': { diff --git a/bundles/wireguard/metadata.py b/bundles/wireguard/metadata.py index 14e68d6..de360e5 100644 --- a/bundles/wireguard/metadata.py +++ b/bundles/wireguard/metadata.py @@ -18,12 +18,14 @@ defaults = { }, }, }, - 'iptables': { - 'bundle_rules': { - 'wireguard': [ - 'iptables_both -A FORWARD -i wg0 -j ACCEPT', - 'iptables_both -A FORWARD -o wg0 -j ACCEPT', - ], + 'nftables': { + 'rules': { + 'forward': { + 'wireguard': [ + 'iif wg0 accept', + 'oif wg0 accept', + ], + }, }, }, 'wireguard': { @@ -149,9 +151,9 @@ def icinga2(metadata): @metadata_reactor.provides( - 'iptables/port_rules', + 'firewall/port_rules', ) -def iptables(metadata): +def firewall(metadata): sources = set(metadata.get('wireguard/restrict-to', set())) for peer_name in metadata.get('wireguard/peers'): try: @@ -162,7 +164,7 @@ def iptables(metadata): sources.add(peer_name) return { - 'iptables': { + 'firewall': { 'port_rules': { '51820/udp': atomic(sources), }, diff --git a/groups/os.py b/groups/os.py index b320906..36b237c 100644 --- a/groups/os.py +++ b/groups/os.py @@ -20,7 +20,7 @@ groups['linux'] = { 'bundles': { 'basic', 'cron', - 'iptables', + 'nftables', 'openssh', 'postfix', 'sshmon', @@ -39,7 +39,7 @@ groups['linux'] = { 'backup-client': { 'server': 'franzi-home.kunbox.net:2022', }, - 'iptables': { + 'firewall': { 'port_rules': { '*': { 'ovh.icinga2', diff --git a/nodes/home/nas.py b/nodes/home/nas.py index 7c48bbd..e4ef65d 100644 --- a/nodes/home/nas.py +++ b/nodes/home/nas.py @@ -55,7 +55,7 @@ nodes['home.nas'] = { 'groups': { 'nas': {}, }, - 'iptables': { + 'firewall': { 'port_rules': { '4679': { # Dell ULNM '172.19.136.0/25', diff --git a/nodes/home/router.py b/nodes/home/router.py index cf1c9df..c13d5d5 100644 --- a/nodes/home/router.py +++ b/nodes/home/router.py @@ -94,24 +94,27 @@ nodes['home.router'] = { 'vars.notification.sms': True }, - 'iptables': { - 'custom_rules': [ - # This is a router. Allow forwarding traffic for internal networks. - 'iptables_both -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT', - 'iptables_both -A FORWARD -i enp1s0.23 -o ppp0 -j ACCEPT', - 'iptables_both -A FORWARD -i enp1s0.42 -j ACCEPT', + 'nftables': { + 'rules': { + 'forward': { + 'router': [ + # This is a router. Allow forwarding traffic for internal networks. + 'ct state { related, established } accept', + 'iif enp1s0.23 oif ppp0 accept', + 'iif enp1s0.42 accept', - # External port 2022 should be home.nas - 'iptables -t nat -A PREROUTING -p tcp --dport 2022 -j DNAT --to 172.19.138.20:22', - 'iptables -A FORWARD -p tcp -d 172.19.138.20 --dport 22 -j ACCEPT', - - # use MASQUERADE for tun0 (c3voc) - 'iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE', - - # yaaaaay, IPv6! No NAT! - 'ip6tables -A FORWARD -p ipv6-icmp -j ACCEPT', - 'ip6tables -A FORWARD -p tcp --dport 22 -j ACCEPT', - ], + # yaaaaay, IPv6! No NAT! + 'ip6 nexthdr ipv6-icmp accept', + 'tcp dport 22 accept', + ], + }, + 'nat_prerouting': [ + 'tcp dport 2022 dnat 172.19.138.20:22', + ], + 'nat_postrouting': [ + 'oif tun0 masquerade', + ], + }, }, 'netdata': { 'restrict-to': { diff --git a/nodes/htz-cloud/miniserver.py b/nodes/htz-cloud/miniserver.py index e1f5933..d893fd8 100644 --- a/nodes/htz-cloud/miniserver.py +++ b/nodes/htz-cloud/miniserver.py @@ -83,13 +83,6 @@ nodes['htz-cloud.miniserver'] = { 'icinga_options': { 'vars.notification.sms': False, }, - 'iptables': { - 'custom_rules': [ - 'iptables_both -A INPUT -p udp --dport 60000:61000 -j ACCEPT', # mosh - 'iptables_both -A INPUT -p tcp --dport 9001 -j ACCEPT', # weechat - - ], - }, 'letsencrypt': { 'concat_and_deploy': { 'sophie-weechat': { @@ -151,6 +144,16 @@ nodes['htz-cloud.miniserver'] = { 'bot_token': '""', }, }, + 'nftables': { + 'rules': { + 'input': { + 'sophie-weechat': [ + 'udp dport { 60000-61000 } accept', + 'tcp dport 9001 accept', + ], + }, + }, + }, 'nginx': { 'vhosts': { #'dimension.sophies-kitchen.eu': { diff --git a/nodes/htz/ex42-1048908.py b/nodes/htz/ex42-1048908.py index 95fd9b2..4602d9b 100644 --- a/nodes/htz/ex42-1048908.py +++ b/nodes/htz/ex42-1048908.py @@ -137,24 +137,6 @@ nodes['htz.ex42-1048908'] = { 'icinga_options': { 'pretty_name': 'kunsmann.eu', }, - 'iptables': { - # TODO move to bundles - 'custom_rules': [ - 'iptables_both -A INPUT -p udp --dport 60000:61000 -j ACCEPT', # mosh - 'iptables_both -A INPUT -p tcp --dport 9001 -j ACCEPT', # weechat - - # libvirt rules. These are also added by libvirt itself, - # but they would be overridden by our own iptables - # management. - 'iptables -A INPUT -i virbr0 -p udp --dport 53 -j ACCEPT', - 'iptables -A INPUT -i virbr0 -p tcp --dport 53 -j ACCEPT', - 'iptables -A INPUT -i virbr0 -p udp --dport 67:68 -j ACCEPT', - 'iptables -A INPUT -i virbr0 -p tcp --dport 67:68 -j ACCEPT', - 'iptables -A FORWARD -i virbr0 -j ACCEPT', - 'iptables -A FORWARD -o virbr0 -j ACCEPT', - 'iptables -t nat -A POSTROUTING -o enp0s31f6 -j MASQUERADE', - ], - }, 'letsencrypt': { 'concat_and_deploy': { 'kunsi-weechat': { @@ -247,6 +229,30 @@ nodes['htz.ex42-1048908'] = { '@.*:franzi\\\\.business', }, }, + 'nftables': { + 'rules': { + 'input': { + 'kunsi-weechat': [ + 'udp dport { 60000-61000 } accept', + 'tcp dport 9001 accept', + ], + 'libvirt': [ + 'tcp dport 53 iif virbr0 accept', + 'udp dport 53 iif virbr0 accept', + 'udp dport { 67, 68 } iif virbr0 accept', + ], + }, + 'forward': { + 'libvirt': [ + 'iif virbr0 accept', + 'oif virbr0 accept', + ], + }, + 'nat_postrouting': { + 'oif enp0s31f6 masquerade', + }, + }, + }, 'nginx': { 'vhosts': { # TODO maybe some of this can be moved to a bundle? diff --git a/nodes/kunsi-t470.py b/nodes/kunsi-t470.py index 621c28e..9555c3a 100644 --- a/nodes/kunsi-t470.py +++ b/nodes/kunsi-t470.py @@ -31,7 +31,7 @@ nodes['kunsi-t470'] = { }, # there is also wlp4s0, but that's managed by netctl }, - 'iptables': { + 'firewall': { 'port_rules': { # For the occasional file-share using `python -m http.server` '8000': {'*'},