diff options
| -rw-r--r-- | production | 4 | ||||
| -rwxr-xr-x | roles/common/files/etc/network/if-post-down.d/iptables | 36 | ||||
| -rwxr-xr-x | roles/common/files/etc/network/if-pre-up.d/iptables | 47 | ||||
| -rwxr-xr-x | roles/common/files/usr/local/sbin/update-firewall | 61 | ||||
| -rwxr-xr-x | roles/common/files/usr/local/sbin/update-firewall.sh | 445 | ||||
| -rw-r--r-- | roles/common/tasks/firewall.yml | 48 | ||||
| -rw-r--r-- | roles/common/tasks/main.yml | 1 | ||||
| -rw-r--r-- | roles/common/tasks/sysctl.yml | 2 | ||||
| -rw-r--r-- | roles/common/templates/etc/ipsec.conf.j2 | 2 | ||||
| -rw-r--r-- | roles/common/templates/etc/iptables/services.j2 | 48 | ||||
| -rwxr-xr-x | roles/common/templates/etc/network/if-up.d/ipsec.j2 | 11 | ||||
| -rwxr-xr-x | roles/common/templates/etc/nftables.conf.j2 | 193 | 
12 files changed, 283 insertions, 615 deletions
| @@ -65,20 +65,24 @@ antilop  [wiki:children]  civett  [git:children]  wiki  [nextcloud:children]  calima  [munin_master:children]  benjamin  # machines behind NAT  [NATed:children]  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 @@ -1,34 +1,35 @@  ---  - import_tasks: sysctl.yml    tags: sysctl  - import_tasks: hosts.yml  - import_tasks: apt.yml    tags: apt  - name: Install intel-microcode    apt: pkg=intel-microcode    when: "ansible_processor[1] is search('^(Genuine)?Intel.*') and not ansible_virtualization_role == 'guest'"    tags: intel  - import_tasks: firewall.yml    tags:      - firewall      - iptables +    - nftables  - import_tasks: stunnel.yml    tags: stunnel    when: "'webmail' in group_names and 'LDAP-provider' not in group_names"  - import_tasks: auditd.yml    tags: auditd  - import_tasks: unbound.yml    tags:      - unbound      - dns    when: "ansible_processor[1] is search('^(Genuine)?Intel.*') and not ansible_virtualization_role == 'guest'"  - import_tasks: rkhunter.yml    tags: rkhunter  - import_tasks: clamav.yml    tags: clamav  - import_tasks: fail2ban.yml    tags: fail2ban  - import_tasks: smart.yml    tags:      - smartmontools 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 @@ -1,41 +1,41 @@  - sysctl: name={{ item.name }} value={{ item.value }} sysctl_set=yes    with_items:      - { name: 'kernel.domainname', value: '{{ ansible_domain }}' }        # Networking. See        # https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt        # Enable Spoof protection (reverse-path filter). Turn on Source        # Address Verification in all interfaces to prevent some spoofing        # attacks.      - { name: 'net.ipv4.conf.default.rp_filter', value: 1 }      - { name: 'net.ipv4.conf.all.rp_filter',     value: 1 }        # Enable TCP/IP SYN cookies to avoid TCP SYN flood attacks. We        # rate-limit not only the default ICMP types 3, 4, 11 and 12        # (0x1818), but also types 0 and 8. See icmp(7).      - { name: 'net.ipv4.tcp_syncookies',  value: 1 }      - { 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 }        # Do not accept ICMP redirects (prevent MITM attacks).      - { name: 'net.ipv4.conf.all.accept_redirects', value: 0 }      - { name: 'net.ipv6.conf.all.accept_redirects', value: 0 }        # Do not send ICMP redirects (we are not a router).      - { name: 'net.ipv4.conf.default.send_redirects', value:  0 }      - { name: 'net.ipv4.conf.all.send_redirects',     value:  0 }        # Do not accept IP source route packets (we are not a router).      - { name: 'net.ipv4.conf.all.accept_source_route', value:  0 }      - { name: 'net.ipv6.conf.all.accept_source_route', value:  0 }        # Log Martian Packets.      - { name: 'net.ipv4.conf.all.log_martians', value: 1 }        # Ignore ICMP broadcasts.      - { name: 'net.ipv4.icmp_echo_ignore_broadcasts', value: 1 } 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 @@ -3,41 +3,41 @@  config setup      charondebug = "dmn 0, lib 0, cfg 0, ike 0, enc 0, net 0"  conn %default      keyexchange    = ikev2      keyingtries    = %forever      ike            = aes256gcm16-prfsha384-ecp384!      esp            = aes256gcm16-ecp384!  {% if 'NATed' not in group_names %}      mobike         = no  {% endif %}  {% if 'DynDNS' in group_names %}      leftallowany   = yes  {% endif %}      leftauth       = pubkey      left           = %defaultroute      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      dpdaction      = hold      inactivity     = 30m      modeconfig     = push  {% for host in groups.all | difference([inventory_hostname]) | sort %}  conn {{ hostvars[host].inventory_hostname_short }}      right         = {{ hostvars[host].inventory_hostname }}  {% if 'DynDNS' in hostvars[host].group_names %}      rightallowany = yes  {% endif %}      rightsigkey   = {{ hostvars[host].inventory_hostname_short }}.pem      rightsubnet   = {{ ipsec[ hostvars[host].inventory_hostname_short ] | ipv4 }}/32  {% if 'NATed' not in group_names and 'NATed' in hostvars[host].group_names %}      mobike        = yes  {% endif %} 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 @@ -8,40 +8,39 @@  # 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 and non inet4 families.  [ "$IFACE" != lo -a "$ADDRFAM" = inet ] || exit 0  # 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 }}"  case "$MODE" in      start) ip address add "$vip/32" dev "$IFACE" scope global || true             # Nullroute the subnet used for IPsec to avoid data leaks             # 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 +    } +} | 
