summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2020-01-23 04:29:12 +0100
committerGuilhem Moulin <guilhem@fripost.org>2020-01-23 05:57:01 +0100
commit7641a5d5d152db349082b1d0ec93a40888b2ef8e (patch)
tree3f80c14c0e50b187a6698346cf8cffb9c5200154
parent456e09fa40d01b70ac1788d0338fba00079e4121 (diff)
Convert firewall to nftables.
Debian Buster uses the nftables framework by default.
-rw-r--r--production4
-rwxr-xr-xroles/common/files/etc/network/if-post-down.d/iptables36
-rwxr-xr-xroles/common/files/etc/network/if-pre-up.d/iptables47
-rwxr-xr-xroles/common/files/usr/local/sbin/update-firewall61
-rwxr-xr-xroles/common/files/usr/local/sbin/update-firewall.sh445
-rw-r--r--roles/common/tasks/firewall.yml48
-rw-r--r--roles/common/tasks/main.yml1
-rw-r--r--roles/common/tasks/sysctl.yml2
-rw-r--r--roles/common/templates/etc/ipsec.conf.j22
-rw-r--r--roles/common/templates/etc/iptables/services.j248
-rwxr-xr-xroles/common/templates/etc/network/if-up.d/ipsec.j211
-rwxr-xr-xroles/common/templates/etc/nftables.conf.j2193
12 files changed, 283 insertions, 615 deletions
diff --git a/production b/production
index 0ce2b4e..ec655ab 100644
--- a/production
+++ b/production
@@ -82,3 +82,7 @@ benjamin
# hostnames resolving to a dynamic IP
[DynDNS:children]
benjamin
+
+# need dhcp client
+[dhclient:children]
+benjamin
diff --git a/roles/common/files/etc/network/if-post-down.d/iptables b/roles/common/files/etc/network/if-post-down.d/iptables
deleted file mode 100755
index d27977d..0000000
--- a/roles/common/files/etc/network/if-post-down.d/iptables
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/sh
-
-# A post-down hook to flush ip tables and delete custom chains in the
-# loaded v4 and v6 rulesets.
-# Copyright © 2013 Guilhem Moulin <guilhem@fripost.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-set -ue
-PATH=/usr/sbin:/usr/bin:/sbin:/bin
-
-# Ignore the loopback interface; run the script for ifdown only.
-[ "$IFACE" != lo -a "$MODE" = stop ] || exit 0
-
-case "$ADDRFAM" in
- inet) ipts=/sbin/iptables-save; ipt=/sbin/iptables;;
- inet6) ipts=/sbin/ip6tables-save; ipt=/sbin/ip6tables;;
- *) exit 0
-esac
-
-$ipts | sed -nr 's/^\*//p' | \
-while read table; do
- $ipt -t "$table" -F
- $ipt -t "$table" -X
-done
diff --git a/roles/common/files/etc/network/if-pre-up.d/iptables b/roles/common/files/etc/network/if-pre-up.d/iptables
deleted file mode 100755
index 2b83cdc..0000000
--- a/roles/common/files/etc/network/if-pre-up.d/iptables
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/bin/bash
-
-# A pre-up hook to auto-(re)load the iptables rulesets whenever the
-# network is brought up. If the action fails, an alert message is passed
-# to syslogd.
-# Copyright © 2013 Guilhem Moulin <guilhem@fripost.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-set -uo pipefail
-PATH=/usr/sbin:/usr/bin:/sbin:/bin
-
-# NOTE: syslog starts after networking during the boot process, messages
-# won't be logged at boot time.
-log="/usr/bin/logger -st firewall"
-
-# Ignore the loopback interface; run the script for ifup only.
-[ "$IFACE" != lo -a "$MODE" = start ] || exit 0
-
-# We support only IPv4 and IPv6.
-[ "$ADDRFAM" = inet -o "$ADDRFAM" = inet6 ] || exit 0
-
-$log -p user.info -- "Loading $ADDRFAM firewall on interface $IFACE."
-
-case "$ADDRFAM" in
- inet) iptr=/sbin/iptables-restore; rules=rules.v4;;
- inet6)iptr=/sbin/ip6tables-restore; rules=rules.v6;;
-esac
-rules="/etc/iptables/$rules"
-
-$iptr < $rules 2>&1 | $log -p user.err
-rv=$?
-
-[ $rv -gt 0 ] && $log -p user.alert \
- "WARN: Failed to load iptables rulesets; the machine may be unprotected!"
-exit $rv
diff --git a/roles/common/files/usr/local/sbin/update-firewall b/roles/common/files/usr/local/sbin/update-firewall
new file mode 100755
index 0000000..957bdc1
--- /dev/null
+++ b/roles/common/files/usr/local/sbin/update-firewall
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+set -ue
+PATH=/usr/sbin:/usr/bin:/sbin:/bin
+export PATH
+
+NFTABLES="/etc/nftables.conf"
+
+script="$(mktemp --tmpdir=/dev/shm)"
+oldrules="$(mktemp --tmpdir=/dev/shm)"
+newrules="$(mktemp --tmpdir=/dev/shm)"
+netns=
+cleanup(){
+ rm -f -- "$script" "$oldrules" "$newrules"
+ [ -z "$netns" ] || ip netns del "$netns"
+}
+trap cleanup EXIT INT TERM
+
+echo "flush ruleset" >"$script" # should be included already, but...
+cat <"$NFTABLES" >>"$script"
+
+ip netns add "nft-dryrun"
+netns="nft-dryrun"
+
+# clear sets in the old rules before diff'ing with the new ones
+nft list ruleset -sn >"$oldrules"
+ip netns exec "$netns" nft -f - <"$oldrules"
+ip netns exec "$netns" nft flush set inet filter fail2ban
+ip netns exec "$netns" nft flush set inet filter fail2ban6
+ip netns exec "$netns" nft list ruleset -sn >"$oldrules"
+
+declare -a INTERFACES=()
+for iface in /sys/class/net/*; do
+ idx="$(< "$iface/ifindex")"
+ INTERFACES[idx]="${iface#/sys/class/net/}"
+done
+
+# create dummy interfaces so we can use iif/oif in the nft rules
+# (we preserve indices to preserve canonical set representation)
+for idx in "${!INTERFACES[@]}"; do
+ [ "${INTERFACES[idx]}" != "lo" ] || continue
+ ip netns exec "$netns" ip link add "${INTERFACES[idx]}" index "$idx" type dummy
+done
+
+ip netns exec "$netns" nft -f - <"$script"
+ip netns exec "$netns" nft list ruleset -sn >"$newrules"
+ip netns del "$netns"
+netns=
+
+if [ ! -t 0 ] || [ ! -t 1 ]; then
+ diff -q -- "$oldrules" "$newrules" && exit 0 || exit 1
+elif ! diff -u --color=auto --label=a/ruleset --label=b/ruleset \
+ -- "$oldrules" "$newrules" && nft -f - <"$script"; then
+ read -p "Ruleset applied. Revert? [Y/n] " -r -t10 r || r="y"
+ if [ "${r,,[a-z]}" != "n" ]; then
+ echo "Reverting..."
+ echo "flush ruleset" >"$script"
+ cat <"$oldrules" >>"$script"
+ nft -f - <"$script"
+ fi
+fi
diff --git a/roles/common/files/usr/local/sbin/update-firewall.sh b/roles/common/files/usr/local/sbin/update-firewall.sh
deleted file mode 100755
index 8ef3ab9..0000000
--- a/roles/common/files/usr/local/sbin/update-firewall.sh
+++ /dev/null
@@ -1,445 +0,0 @@
-#!/bin/bash
-
-# Create iptables (v4 and v6) rules. Unless one of [-f] or [-c] is
-# given, or if the ruleset is unchanged, a confirmation is asked after
-# loading the new rulesets; if the user answers No or doesn't answer,
-# the old ruleset is restored. If the user answer Yes (or if the flag
-# [-f] is given), the new ruleset is made persistent (requires a pre-up
-# hook) by moving it to /etc/iptables/rules.v[46].
-#
-# The [-c] flag switch to dry-run (check) mode. The rulesets are not
-# applied, but merely checked against the existing ones. The return
-# value is 0 iff. they do not differ.
-#
-# This firewall is only targeted towards end-servers, not gateways. In
-# particular, there is no NAT'ing at the moment.
-#
-# Dependencies: netmask(1)
-#
-# Copyright © 2013 Guilhem Moulin <guilhem@fripost.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-set -ue
-PATH=/usr/sbin:/usr/bin:/sbin:/bin
-timeout=10
-
-force=0
-check=0
-verbose=0
-addrfam=
-
-secproto=esp # must match /etc/ipsec.conf; ESP is the default (vs AH/IPComp)
-if [ -x /usr/sbin/ipsec ] && /usr/sbin/ipsec status >/dev/null; then
- ipsec=y
-else
- ipsec=n
-fi
-
-fail2ban_re='^(\[[0-9]+:[0-9]+\]\s+)?-A f2b-\S'
-IPsec_re=" -m policy --dir (in|out) --pol ipsec --reqid [0-9]+ --proto $secproto -j ACCEPT$"
-declare -A rss=() tables=()
-
-usage() {
- cat >&2 <<- EOF
- Usage: $0 [OPTIONS]
-
- Options:
- -f force: no confirmation asked
- -c check: check (dry-run) mode
- -v verbose: see the difference between old and new ruleset
- -4 IPv4 only
- -6 IPv6 only
- EOF
- exit 1
-}
-
-log() {
- logger -st firewall -p user.info -- "$@"
-}
-fatal() {
- logger -st firewall -p user.err -- "$@"
- exit 1
-}
-
-iptables() {
- # Fake iptables/ip6tables(8); use the more efficient
- # iptables-restore(8) instead.
- echo "$@" >>"$new";
-}
-commit() {
- # End a table
- echo COMMIT >>"$new"
-}
-inet46() {
- case "$1" in
- 4) echo "$2";;
- 6) echo "$3";;
- esac
-}
-ipt-chains() {
- # Define new (tables and) chains.
- while [ $# -gt 0 ]; do
- case "$1" in
- ?*:*) echo ":${1%:*} ${1##*:} [0:0]";;
- ?*) echo "*$1";;
- esac
- shift
- done >>"$new"
-}
-
-ipt-trim() {
- # Remove dynamic chain/rules from the input stream, as they are
- # automatically included by third-party servers (such as strongSwan
- # or fail2ban). The output is ready to be made persistent.
- grep -Ev -e '^:f2b-\S' \
- -e "$IPsec_re" \
- -e '-j f2b-\S+$' \
- -e "$fail2ban_re"
-}
-
-ipt-diff() {
- # Get the difference between two rulesets.
- if [ $verbose -eq 1 ]; then
- diff -u -I '^#' --color=auto "$@"
- else
- diff -q -I '^#' "$@" >/dev/null
- fi
-}
-
-ipt-persist() {
- # Make the current ruleset persistent. (Requires a pre-up hook
- # script to load the rules before the network is configured.)
-
- log "Making ruleset persistent... "
- [ -d /etc/iptables ] || mkdir /etc/iptables
-
- local f rs table
- for f in "${!tables[@]}"; do
- ipts=$(inet46 $f iptables ip6tables)-save
- rs=/etc/iptables/rules.v$f
-
- for table in ${tables[$f]}; do
- ip netns exec $netns $ipts -t $table
- done | ipt-trim >"$rs"
- chmod 0600 "$rs"
- done
-}
-
-ipt-revert() {
- [ $check -eq 0 ] || return
- log "Reverting to old ruleset... "
-
- local rs
- for f in "${!rss[@]}"; do
- $(inet46 $f iptables ip6tables)-restore -c <"${rss[$f]}"
- rm -f "${rss[$f]}"
- done
- exit 1
-}
-
-run() {
- # Build and apply the firewall for IPv4/6.
- local f="$1"
- local ipt=$(inet46 $f iptables ip6tables)
- tables[$f]=filter
-
- # The default interface associated with this address.
- local if=$( /bin/ip -$f -o route show to default scope global \
- | sed -nr '/^default via \S+ dev (\S+).*/ {s//\1/p;q}' )
-
- # Store the old (current) ruleset
- local old=$(mktemp --tmpdir current-rules.v$f.XXXXXX) \
- new=$(mktemp --tmpdir new-rules.v$f.XXXXXX)
- for table in ${tables[$f]}; do
- $ipt-save -ct $table
- done >"$old"
- rss[$f]="$old"
-
- local fail2ban=0
- # XXX: As of Wheezy, fail2ban is IPv4 only. See
- # https://github.com/fail2ban/fail2ban/issues/39 for the current
- # state of the art.
- if [ "$f" = 4 ] && which fail2ban-server >/dev/null; then
- fail2ban=1
- fi
-
- # The usual chains in filter, along with the desired default policies.
- ipt-chains filter INPUT:DROP FORWARD:DROP OUTPUT:DROP
-
- if [ ! "$if" ]; then
- # If the interface is not configured, we stop here and DROP all
- # packets by default. Thanks to the pre-up hook this tight
- # policy will be activated whenever the interface goes up.
- commit
- mv "$new" /etc/iptables/rules.v$f
- return 0
- fi
-
- # Fail2ban-specific chains and traps
- if [ $fail2ban -eq 1 ]; then
- echo ":fail2ban - [0:0]"
- # Don't remove existing rules & traps in the current rulest
- grep -- '^:f2b-\S' "$old" || true
- grep -E -- ' -j f2b-\S+$' "$old" || true
- grep -E -- "$fail2ban_re" "$old" || true
- fi >>"$new"
-
- if [ "$f" = 4 -o "$f" = 6 ] && [ "$ipsec" = y ]; then
- # IPsec tunnels come first (IPv4 only).
- grep -E -- "$IPsec_re" "$old" >>"$new" || true
-
- # Allow any IPsec $secproto protocol packets to be sent and received.
- iptables -A INPUT -i $if -p $secproto -j ACCEPT
- iptables -A OUTPUT -o $if -p $secproto -j ACCEPT
- fi
-
-
- ########################################################################
- # DROP all RFC1918 addresses, martian networks, multicasts, ...
- # Credits to http://newartisans.com/2007/09/neat-tricks-with-iptables/
- # http://baldric.net/loose-iptables-firewall-for-servers/
-
- local ip
- if [ "$f" = 4 ] && [ "$ipsec" = y ]; then
- # Private-use networks (RFC 1918) and link local (RFC 3927)
- local MyIPsec="$( ip -4 -o route show table 220 dev $if | sed 's/\s.*//' )"
- local MyNetwork="$( ip -4 -o address show dev $if scope global \
- | sed -nr "s/^[0-9]+:\s+$if\s+inet\s(\S+).*/\1/p" \
- | while read ip; do
- for ips in $MyIPsec; do
- [ "$ips" = "$(netmask -nc "$ip" "$ips" | sed 's/^ *//')" ] || echo "$ip"
- done
- done
- )"
- [ "$MyNetwork" ] && \
- for ip in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16; do
- # Don't lock us out if we are behind a NAT ;-)
- for myip in $MyNetwork; do
- [ "$ip" = "$(netmask -nc "$ip" "$myip" | sed 's/^ *//')" ] || echo "$ip"
- done | uniq | while read ip; do iptables -A INPUT -i $if -s "$ip" -j DROP; done
- done
-
- # Other martian packets: "This" network, multicast, broadcast (RFCs
- # 1122, 3171 and 919).
- for ip in 0.0.0.0/8 224.0.0.0/4 240.0.0.0/4 255.255.255.255/32; do
- iptables -A INPUT -i $if -s "$ip" -j DROP
- iptables -A INPUT -i $if -d "$ip" -j DROP
- done
-
- elif [ "$f" = 6 ]; then
- # Martian IPv6 packets: ULA (RFC 4193) and site local addresses
- # (RFC 3879).
- for ip in fc00::/7 fec0::/10; do
- iptables -A INPUT -i $if -s "$ip" -j DROP
- iptables -A INPUT -i $if -d "$ip" -j DROP
- done
- fi
-
- # DROP INVALID packets immediately.
- iptables -A INPUT -m state --state INVALID -j DROP
- iptables -A OUTPUT -m state --state INVALID -j DROP
-
- # DROP bogus TCP packets.
- iptables -A INPUT -p tcp -m tcp --tcp-flags FIN,SYN FIN,SYN -j DROP
- iptables -A INPUT -p tcp -m tcp --tcp-flags SYN,RST SYN,RST -j DROP
- iptables -A INPUT -p tcp \! --syn -m state --state NEW -j REJECT --reject-with tcp-reset
-
- # Allow all input/output to/from the loopback interface.
- local localhost=$(inet46 $f '127.0.0.1/8' '::1/128')
- iptables -A INPUT -i lo -s "$localhost" -d "$localhost" -j ACCEPT
- iptables -A OUTPUT -o lo -s "$localhost" -d "$localhost" -j ACCEPT
- if [ "$f" = 4 ] && [ "$ipsec" = y ]; then
- # Allow local access to our virtual IP
- ip -4 -o route show table 220 dev $if \
- | sed -nr 's/.*\ssrc\s+([[:digit:].]{7,15})(\s.*)?$/\1/p' \
- | while read ips; do
- iptables -A INPUT -i lo -s "$ips" -d "$ips" -j ACCEPT
- iptables -A OUTPUT -o lo -s "$ips" -d "$ips" -j ACCEPT
- done
- fi
-
- # Prepare fail2ban. We make fail2ban insert its rules in a
- # dedicated chain, so that it doesn't mess up the existing rules.
- [ $fail2ban -eq 1 ] && iptables -A INPUT -i $if -j fail2ban
-
- if [ "$f" = 4 ]; then
- # Allow only ICMP of type 0, 3 and 8. The rate-limiting is done
- # directly by the kernel (net.ipv4.icmp_ratelimit and
- # net.ipv4.icmp_ratemask runtime options). See icmp(7).
- local t
- for t in 'echo-reply' 'destination-unreachable' 'echo-request'; do
- iptables -A INPUT -p icmp -m icmp --icmp-type $t -j ACCEPT
- iptables -A OUTPUT -p icmp -m icmp --icmp-type $t -j ACCEPT
- done
- elif [ $f = 6 ]; then
- iptables -A INPUT -p icmpv6 -j ACCEPT
- iptables -A OUTPUT -p icmpv6 -j ACCEPT
- fi
-
-
- ########################################################################
- # ACCEPT new connections to the services we provide, or to those we want
- # to connect to.
-
- sed -re 's/#.*//; /^\s*$/d' -e "s/^(in|out|inout)$f?(\s.*)/\1\2/" \
- /etc/iptables/services | \
- grep -Ev '^(in|out|inout)\S\s' | \
- while read dir proto dport sport; do
- # We add two entries per config line: we need to accept the new
- # connection, and latter the reply.
- local stNew=NEW,ESTABLISHED,RELATED
- local stEst=ESTABLISHED,RELATED
-
- # In-Out means full-duplex
- [[ "$dir" =~ ^inout ]] && stEst="$stNew"
-
- local iptNew= iptEst= optsNew= optsEst=
- case "$dport" in
- *,*|*:*) optsNew="--match multiport --dports $dport"
- optsEst="--match multiport --sports $dport";;
- ?*) optsNew="--dport $dport"
- optsEst="--sport $dport";;
- esac
- case "$sport" in
- *,*|*:*) optsNew+=" --match multiport --sports $sport"
- optsEst+=" --match multiport --dports $sport";;
- ?*) optsNew+=" --sport $sport"
- optsEst+=" --dport $sport";;
- esac
- case "$dir" in
- in|inout) iptNew="-A INPUT -i"; iptEst="-A OUTPUT -o";;
- out) iptNew="-A OUTPUT -o"; iptEst="-A INPUT -i";;
- *) fatal "Error: Unknown direction: '$dir'."
- esac
-
- iptables $iptNew $if -p $proto $optsNew -m state --state $stNew -j ACCEPT
- iptables $iptEst $if -p $proto $optsEst -m state --state $stEst -j ACCEPT
- done
-
- iptables -A OUTPUT -o $if -p tcp -j REJECT --reject-with tcp-reset
- iptables -A OUTPUT -o $if -p udp -j REJECT --reject-with port-unreach
- if [ "$f" = "4" ]; then
- iptables -A OUTPUT -o $if -p icmp -j REJECT --reject-with icmp-host-unreachable
- iptables -A OUTPUT -o $if -j REJECT --reject-with icmp-host-prohibited
- else
- iptables -A OUTPUT -o $if -j REJECT
- fi
-
- ########################################################################
- commit
-
-
- local rv1=0 rv2=0 persistent=/etc/iptables/rules.v$f
- local oldz=$(mktemp --tmpdir current-rules.v$f.XXXXXX)
-
- # Reset the counters. They are not useful for comparing and/or
- # storing persistent ruleset. (We don't use sed -i because we want
- # to restore the counters when reverting.)
- sed -r -e '/^:/ s/\[[0-9]+:[0-9]+\]$/[0:0]/' \
- -e 's/^\[[0-9]+:[0-9]+\]\s+//' \
- "$old" >"$oldz"
-
- ip netns exec $netns $ipt-restore <"$new" || ipt-revert
-
- for table in ${tables[$f]}; do
- ip netns exec $netns $ipt-save -t $table
- done >"$new"
-
- ipt-diff --label="a/$ipt-save" --label="b/$ipt-save" "$oldz" "$new" || rv1=$?
-
- if ! [ -f "$persistent" ] && [ -x /etc/network/if-pre-up.d/iptables ]; then
- rv2=1
- else
- ipt-trim <"$new" | ipt-diff --label="a/rules.v$f" --label="b/$ipt-save" "$persistent" - || rv2=$?
- fi
-
- local update="Please run '${0##*/}'."
- if [ $check -eq 0 ]; then
- uniq "$new" | $ipt-restore || ipt-revert
- else
- if [ $rv1 -ne 0 ]; then
- log "WARN: The IPv$f firewall is not up to date! $update"
- fi
- if [ $rv2 -ne 0 ]; then
- log "WARN: The current IPv$f firewall is not persistent! $update"
- fi
- fi
-
- rm -f "$oldz" "$new"
- return $(( $rv1 | $rv2 ))
-}
-
-
-# Parse options
-while [ $# -gt 0 ]; do
- case "$1" in
- -?*) for (( k=1; k<${#1}; k++ )); do
- o="${1:$k:1}"
- case "$o" in
- 4|6) addrfam="$o";;
- c) check=1;;
- f) force=1;;
- v) verbose=1;;
- *) usage;;
- esac
- done
- ;;
- *) usage;;
- esac
- shift
-done
-
-# If we are going to apply the ruleset, we should either have a TTY, or
-# use -f.
-if ! tty -s && [ $force -eq 0 ] && [ $check -eq 0 ]; then
- echo "Error: Not a TTY. Try with -f (at your own risks!)" >&2
- exit 1
-fi
-
-# Create an alternative net namespace in which we apply the ruleset, so
-# we can easily get a normalized version we can compare latter. See
-# http://bugzilla.netfilter.org/show_bug.cgi?id=790
-netns="ipt-firewall-test-$$"
-ip netns add $netns
-
-trap 'ip netns del $netns 2>/dev/null || true; ipt-revert' SIGINT
-trap 'ip netns del $netns; rm -f "${rss[@]}"' EXIT
-
-rv=0
-for f in ${addrfam:=4 6}; do
- run $f || rv=$(( $rv | $? ))
-done
-
-if [ $force -eq 1 ]; then
- # At the user's own risks...
- ipt-persist
-
-elif [ $check -eq 1 ] || [ $rv -eq 0 ]; then
- # Nothing to do, we're all set.
- exit $rv
-
-else
- echo "Try now to establish NEW connections to the machine."
-
- read -n1 -t$timeout \
- -p "Are you sure you want to use the new ruleset? (y/N) " \
- ret 2>&1 || { [ $? -gt 128 ] && echo -n "Timeout..."; }
- case "${ret:-N}" in
- [yY]*) echo; ipt-persist
- ;;
- *) echo; ipt-revert
- ;;
- esac
-fi
diff --git a/roles/common/tasks/firewall.yml b/roles/common/tasks/firewall.yml
index 133b631..fd1ad92 100644
--- a/roles/common/tasks/firewall.yml
+++ b/roles/common/tasks/firewall.yml
@@ -1,41 +1,27 @@
-- name: Install some packages required for the firewall
- apt: pkg={{ packages }}
- vars:
- packages:
- - iptables
- - netmask
- - bsdutils
+- name: Install nftables
+ apt: pkg=nftables
-- name: Create directory /etc/iptables
- file: path=/etc/iptables
- state=directory
- owner=root group=root
- mode=0755
-
-- name: Generate /etc/iptables/services
- template: src=etc/iptables/services.j2
- dest=/etc/iptables/services
- owner=root group=root
- mode=0600
-
-- name: Copy /usr/local/sbin/update-firewall.sh
- copy: src=usr/local/sbin/update-firewall.sh
- dest=/usr/local/sbin/update-firewall.sh
+- name: Copy /usr/local/sbin/update-firewall
+ copy: src=usr/local/sbin/update-firewall
+ dest=/usr/local/sbin/update-firewall
owner=root group=staff
mode=0755
-- name: Make the rulesets persistent
- copy: src=etc/network/{{ item }}
- dest=/etc/network/{{ item }}
- owner=root group=root
- mode=0755
- with_items:
- - if-pre-up.d/iptables
- - if-post-down.d/iptables
+- name: Copy /etc/nftables.conf
+ template: src=etc/nftables.conf.j2
+ dest=/etc/nftables.conf
+ owner=root group=root
+ mode=0644
- name: Ensure the firewall is up to date
- command: /usr/local/sbin/update-firewall.sh -c
+ command: /usr/local/sbin/update-firewall -c
register: rv
# A non-zero return value will make ansible stop and show stderr. This
# is what we want.
changed_when: rv.rc
+
+- name: Enable nftables.service
+ service: name=nftables enabled=yes
+
+- name: Start nftables.service
+ service: name=nftables state=started
diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml
index 7fa7b20..02a745c 100644
--- a/roles/common/tasks/main.yml
+++ b/roles/common/tasks/main.yml
@@ -12,6 +12,7 @@
tags:
- firewall
- iptables
+ - nftables
- import_tasks: stunnel.yml
tags: stunnel
diff --git a/roles/common/tasks/sysctl.yml b/roles/common/tasks/sysctl.yml
index ffda544..3bf3b4f 100644
--- a/roles/common/tasks/sysctl.yml
+++ b/roles/common/tasks/sysctl.yml
@@ -18,7 +18,7 @@
- { name: 'net.ipv4.icmp_ratemask', value: 6425 }
- { name: 'net.ipv4.icmp_ratelimit', value: 1000 }
- # Disable paquet forwarding between interfaces (we are not a router).
+ # Disable packet forwarding between interfaces (we are not a router).
- { name: 'net.ipv4.ip_forward', value: 0 }
- { name: 'net.ipv6.conf.all.forwarding', value: 0 }
diff --git a/roles/common/templates/etc/ipsec.conf.j2 b/roles/common/templates/etc/ipsec.conf.j2
index 0ff9fbb..6b3840f 100644
--- a/roles/common/templates/etc/ipsec.conf.j2
+++ b/roles/common/templates/etc/ipsec.conf.j2
@@ -20,7 +20,7 @@ conn %default
leftsubnet = {{ ipsec[inventory_hostname_short] | ipv4 }}/32
leftid = {{ inventory_hostname }}
leftsigkey = {{ inventory_hostname_short }}.pem
- leftfirewall = yes
+ leftfirewall = no
lefthostaccess = yes
rightauth = pubkey
auto = route
diff --git a/roles/common/templates/etc/iptables/services.j2 b/roles/common/templates/etc/iptables/services.j2
deleted file mode 100644
index 6dd5aae..0000000
--- a/roles/common/templates/etc/iptables/services.j2
+++ /dev/null
@@ -1,48 +0,0 @@
-# {{ ansible_managed }}
-# Do NOT edit this file directly!
-#
-# direction protocol destination port source port
-# (in|out|inout)[46]? (tcp|udp|..) (port|port:port|port,port) (port|port:port|port,port)
-
-{% if groups.all | length > 1 %}
-inout udp 500 500 # ISAKMP
-{% if groups.NATed | length > 0 %}
-inout4 udp 4500 4500 # IPsec NAT Traversal
-{% endif %}
-{% endif %}
-
-out tcp 80,443 # HTTP/HTTPS
-out udp 53 # DNS
-out tcp 53 # DNS
-out udp 67 # DHCP
-out tcp 22 # SSH
-out udp 123 123 # NTP
-
-in tcp {{ ansible_port|default('22') }} # SSH
-{% if 'LDAP-provider' in group_names %}
-in tcp 636 # LDAPS
-{% elif 'MX' in group_names or 'lists' in group_names or 'nextcloud' in group_names %}
-out tcp 636 # LDAPS
-{% endif %}
-{% if 'MX' in group_names %}
-in tcp 25 # SMTP
-{% endif %}
-{% if 'out' in group_names or 'MSA' in group_names %}
-out tcp 25 # SMTP
-{% endif %}
-{% if 'IMAP' in group_names %}
-in tcp 993 # IMAPS
-in tcp 4190 # MANAGESIEVE
-out tcp 2703 # Razor2
-{% endif %}
-{% if 'MSA' in group_names %}
-in tcp 465 # SMTP-AUTH
-in tcp 587 # SMTP-AUTH
-{% endif %}
-{% if 'webmail' in group_names or 'lists' in group_names or 'wiki' in group_names or 'nextcloud' in group_names %}
-in tcp 80,443 # HTTP/HTTPS
-{% endif %}
-{% if 'LDAP-provider' in group_names %}
-out tcp 11371 # HKP
-out tcp 43 # WHOIS
-{% endif %}
diff --git a/roles/common/templates/etc/network/if-up.d/ipsec.j2 b/roles/common/templates/etc/network/if-up.d/ipsec.j2
index caa5129..9f183d3 100755
--- a/roles/common/templates/etc/network/if-up.d/ipsec.j2
+++ b/roles/common/templates/etc/network/if-up.d/ipsec.j2
@@ -25,10 +25,9 @@ PATH=/usr/sbin:/usr/bin:/sbin:/bin
# Only the device with the default, globally-scoped route, is of
# interest here.
-ip="$( ip -4 -o route show to default scope global \
- | sed -nr '/^default via (\S+) dev (\S+).*/ {s//\2 \1/p;q}' )"
-[ "${ip% *}" = "$IFACE" ] || exit 0
-ip="${ip##* }"
+iface="$( ip -o route show to default scope global \
+ | sed -nr '/^default via \S+ dev (\S+).*/ {s//\1/p;q}' )"
+[ "$iface" = "$IFACE" ] || exit 0
vip="{{ ipsec[inventory_hostname_short] }}"
vsubnet="{{ ipsec_subnet }}"
@@ -39,9 +38,9 @@ case "$MODE" in
# in the absence of xfrm lookup (i.e., when there is no
# matching IPsec Security Association).
ip route replace prohibit "$vsubnet" proto static || true
- ip route replace table 220 to "$vsubnet" via "$ip" dev "$IFACE" proto static src "$vip" || true
+ ip route replace table 220 to "$vsubnet" dev "$IFACE" proto static src "$vip" || true
;;
- stop) ip route del table 220 to "$vsubnet" via "$ip" dev "$IFACE" proto static src "$vip" || true
+ stop) ip route del table 220 to "$vsubnet" dev "$IFACE" proto static src "$vip" || true
ip route del prohibit "$vsubnet" proto static || true
ip address del "$vip/32" dev "$IFACE" scope global || true
esac
diff --git a/roles/common/templates/etc/nftables.conf.j2 b/roles/common/templates/etc/nftables.conf.j2
new file mode 100755
index 0000000..1e1fde2
--- /dev/null
+++ b/roles/common/templates/etc/nftables.conf.j2
@@ -0,0 +1,193 @@
+#!/usr/sbin/nft -f
+
+define in-tcp-ports = {
+ {{ ansible_port|default(22) }}
+{% if 'MX' in group_names %}
+ , 25 # SMTP
+{% endif %}
+{% if 'LDAP-provider' in group_names %}
+ , 636 # ldaps
+{% endif %}
+{% if 'IMAP' in group_names %}
+ , 993 # imaps
+ , 4190 # ManageSieve
+{% endif %}
+{% if 'MSA' in group_names %}
+ , 587 # submission [RFC4409]
+ , 465 # submission over TLS [RFC8314]
+{% endif %}
+{% if 'webmail' in group_names or 'lists' in group_names or 'wiki' in group_names or 'nextcloud' in group_names %}
+ , 80 # HTTP
+ , 443 # HTTP over SSL/TLS
+{% endif %}
+}
+
+define out-tcp-ports = {
+ 22
+ , 80 # HTTP
+ , 443 # HTTP over SSL/TLS
+{% if 'out' in group_names or 'MSA' in group_names %}
+ , 25 # SMTP
+{% endif %}
+{% if 'LDAP-provider' in group_names %}
+ , 11371 # OpenPGP HTTP Keyserver
+ , 43 # whois
+{% elif 'MX' in group_names or 'lists' in group_names or 'nextcloud' in group_names %}
+ , 636 # ldaps
+{% endif %}
+{% if 'IMAP' in group_names %}
+ , 2703 # Razor2
+{% endif %}
+}
+
+
+###############################################################################
+
+flush ruleset
+
+table inet filter {
+ # blackholes
+ set fail2ban { type ipv4_addr; timeout 10m; }
+ set fail2ban6 { type ipv6_addr; timeout 10m; }
+
+ chain input {
+ type filter hook input priority 0
+ policy drop
+
+ iif lo accept
+
+ # XXX Bullseye: this is a hack for the lack of reqid matches in
+ # nftables: we mark the esp packet and accept after decapsulation
+ # https://serverfault.com/questions/971735/how-to-match-reqid-in-nftables
+ # https://blog.fraggod.net/2016/09/25/nftables-re-injected-ipsec-matching-without-xt_policy.html
+ define IPsec.mark = 0x220
+ meta l4proto esp mark set mark | $IPsec.mark accept
+ ip saddr 172.16.0.0/24 ip daddr 172.16.0.7 mark & $IPsec.mark == $IPsec.mark accept
+
+ # rate-limiting is done directly by the kernel (net.ipv4.icmp_{ratelimit,ratemask} runtime options)
+ icmp type { echo-reply, echo-request, destination-unreachable } counter accept
+ icmpv6 type { echo-reply, echo-request, destination-unreachable,
+ packet-too-big, time-exceeded, parameter-problem } counter accept
+
+ # accept neighbour discovery for autoconfiguration, RFC 4890 sec. 4.4.1
+ icmpv6 type { 133,134,135,136,141,142 } ip6 hoplimit 255 counter accept
+
+ jump martian
+ jump invalid
+
+ udp sport 123 udp dport 123 ct state related,established accept
+{% if groups.all | length > 1 %}
+ udp sport 500 udp dport 500 ct state new,related,established accept
+{% if groups.NATed | length > 0 %}
+ udp sport 4500 udp dport 4500 ct state new,related,established accept
+{% endif %}
+{% endif %}
+
+ udp sport 53 ct state related,established accept
+ tcp sport 53 ct state related,established accept
+{% if 'dhclient' in group_names %}
+ udp sport 67 ct state related,established accept
+{% endif %}
+
+ meta l4proto tcp ip saddr @fail2ban counter drop
+ meta l4proto tcp ip6 saddr @fail2ban6 counter drop
+
+ tcp dport $in-tcp-ports ct state related,established accept
+ tcp dport $in-tcp-ports ct state new counter accept
+ tcp sport $out-tcp-ports ct state related,established accept
+ }
+
+ chain output {
+ type filter hook output priority 0
+ policy drop
+
+ oif lo accept
+
+ # XXX Bullseye: unlike for input we can't use marks here,
+ # because by the time we see a packet to 172.16.0.0/24 we don't
+ # know if it'll be encapsulated
+ meta l4proto esp accept
+ ip saddr 172.16.0.7 ip daddr 172.16.0.0/24 accept
+
+ meta l4proto { icmp, icmpv6 } accept
+
+ jump martian
+ jump invalid
+
+ udp sport 123 udp dport 123 ct state new,related,established accept
+ udp sport 500 udp dport 500 ct state new,related,established accept
+ udp sport 4500 udp dport 4500 ct state new,related,established accept
+
+ udp dport 53 ct state new,related,established accept
+ tcp dport 53 ct state new,related,established accept
+{% if 'dhclient' in group_names %}
+ udp dport 67 ct state new,related,established accept
+{% endif %}
+
+ tcp sport $in-tcp-ports ct state related,established accept
+ tcp dport $out-tcp-ports ct state related,established accept
+ tcp dport $out-tcp-ports ct state new counter accept
+
+ meta l4proto tcp counter reject with tcp reset
+ meta l4proto udp counter reject
+ counter reject
+ }
+
+ chain martian {
+ # bogon filter (cf. RFC 6890 for non-global ip addresses)
+ define invalid-ip = {
+ 0.0.0.0/8 # this host, on this network (RFC 1122 sec. 3.2.1.3)
+{% if not ansible_default_ipv4.address | ipaddr('10.0.0.0/8') %}
+ , 10.0.0.0/8 # private-use (RFC 1918)
+{% endif %}
+ , 100.64.0.0/10 # shared address space (RFC 6598)
+ , 127.0.0.0/8 # loopback (RFC 1122, sec. 3.2.1.3)
+ , 169.254.0.0/16 # link local (RFC 3927)
+{% if not ansible_default_ipv4.address | ipaddr('172.16.0.0/12') %}
+ , 172.16.0.0/12 # private-use (RFC 1918)
+{% endif %}
+ , 192.0.0.0/24 # IETF protocol assignments (RFC 6890 sec. 2.1)
+ , 192.0.2.0/24 # documentation (RFC 5737)
+{% if not ansible_default_ipv4.address | ipaddr('192.168.0.0/16') %}
+ , 192.168.0.0/16 # private-use (RFC 1918)
+{% endif %}
+ , 198.18.0.0/15 # benchmarking (RFC 2544)
+ , 198.51.100.0/24 # documentation (RFC 5737)
+ , 203.0.113.0/24 # documentation (RFC 5737)
+ , 240.0.0.0/4 # reserved (RFC 1112, sec. 4)
+ , 255.255.255.255/32 # limited broadcast (RFC 0919, section 7)
+ }
+
+ define invalid-ip6 = {
+ ::1/128 # loopback address (RFC 4291)
+ , ::/128 # unspecified (RFC 4291)
+ , ::ffff:0:0/96 # IPv4-mapped address (RFC 4291)
+ , 100::/64 # discard-only address block (RFC 6666)
+ , 2001::/23 # IETF protocol assignments (RFC 2928)
+ , 2001::/32 # TEREDO (RFC 4380)
+ , 2001:2::/48 # benchmarking (RFC 5180)
+ , 2001:db8::/32 # documentation (RFC 3849)
+ , 2001:10::/28 # ORCHID (RFC 4843)
+ , 2002::/16 # 6to4 (RFC 3056)
+ , fc00::/7 # unique-local (RFC 4193)
+ , fe80::/10 # linked-scoped unicast (RFC 4291)
+ }
+
+ ip saddr $invalid-ip counter drop
+ ip daddr $invalid-ip counter drop
+
+ ip6 saddr $invalid-ip6 counter drop
+ ip6 daddr $invalid-ip6 counter drop
+ }
+
+ chain invalid {
+ ct state invalid counter reject
+
+ # drop bogus TCP packets
+ tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop # null packets
+ tcp flags != syn ct state new counter drop # SYN-flood attacks
+ tcp flags & (fin|syn|rst|psh|ack|urg) == fin|psh|urg counter drop # XMAS packets
+ tcp flags & (fin|syn) == fin|syn counter drop # bogus
+ tcp flags & (syn|rst) == syn|rst counter drop # bogus
+ }
+}