summaryrefslogtreecommitdiffstats
path: root/roles
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2013-10-30 21:06:51 +0100
committerGuilhem Moulin <guilhem@fripost.org>2015-06-07 02:50:28 +0200
commitfbde929fce7405f018fc66bb5796bf0a16292913 (patch)
tree25be7bfa8547295694be7658d41cdc9e33423b2a /roles
parente54c9bc8d96bdef1c9a5634f5cff3b66f38f487e (diff)
Configure v4 and v6 iptable rulesets.
Diffstat (limited to 'roles')
-rwxr-xr-xroles/common/files/etc/network/if-pre-up.d/iptables38
-rwxr-xr-xroles/common/files/usr/local/sbin/update-firewall.sh245
-rw-r--r--roles/common/handlers/main.yml5
-rw-r--r--roles/common/tasks/firewall.yml34
-rw-r--r--roles/common/tasks/main.yml1
-rw-r--r--roles/common/templates/etc/iptables/services.j213
6 files changed, 336 insertions, 0 deletions
diff --git a/roles/common/files/etc/network/if-pre-up.d/iptables b/roles/common/files/etc/network/if-pre-up.d/iptables
new file mode 100755
index 0000000..6a50948
--- /dev/null
+++ b/roles/common/files/etc/network/if-pre-up.d/iptables
@@ -0,0 +1,38 @@
+#!/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>
+#
+# Licensed under the GNU GPL version 3 or higher.
+#
+
+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 strip 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 syslog.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 syslog.err
+rv=$?
+
+[ $rv -gt 0 ] && $log -p syslog.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.sh b/roles/common/files/usr/local/sbin/update-firewall.sh
new file mode 100755
index 0000000..cefeea5
--- /dev/null
+++ b/roles/common/files/usr/local/sbin/update-firewall.sh
@@ -0,0 +1,245 @@
+#!/bin/bash
+#
+# Create iptables (v4 and v6) rules. Unless [-f] is given, 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 using iptables-persistent.
+#
+# 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>
+#
+# Licensed under the GNU GPL version 3 or higher.
+#
+
+set -ue
+PATH=/usr/sbin:/usr/bin:/sbin:/bin
+
+timeout=10
+force=0
+
+[ "${1:-}" = -f ] && force=1
+if ! /usr/bin/tty -s && [ $force -eq 0 ]; then
+ echo "Error: Not a TTY. Try with -f (at your own risks)!" >&2
+ exit 1
+fi
+
+getInteface() {
+ /sbin/ip -f "$1" route | sed -nr 's/^default via .*dev (\S+).*/\1/p' | head -1
+}
+
+WAN=$( getInteface inet )
+WAN6=$(getInteface inet6)
+
+oldv4table=$(mktemp)
+newv4table=$(mktemp)
+
+oldv6table=$(mktemp)
+newv6table=$(mktemp)
+
+iptables() {
+ [ -z "$WAN" ] || { echo "$@" >> "$newv4table"; }
+}
+ip6tables() {
+ [ -z "$WAN6" ] || { echo "$@" >> "$newv6table"; }
+}
+tgrep() {
+ [ -z "$WAN" ] || { /bin/grep -E -- "$@" "$oldv4table" >> "$newv4table" || true; }
+ [ -z "$WAN6" ] || { /bin/grep -E -- "$@" "$oldv6table" >> "$newv6table" || true; }
+}
+log() {
+ /usr/bin/logger -st firewall -p syslog.info -- "$@"
+}
+fatal() {
+ /usr/bin/logger -st firewall -p syslog.err -- "$@"
+ exit 1
+}
+
+[ -n "$WAN" -o -n "$WAN6" ] || fatal "Error: couldn't find a network interface"
+
+# Store the existing table
+/sbin/iptables-save -t filter > "$oldv4table"
+/sbin/ip6tables-save -t filter > "$oldv6table"
+
+# The usual chains in filter, along with the desired default policies.
+cat > "$newv4table" <<- EOF
+ *filter
+ :INPUT DROP [0:0]
+ :FORWARD DROP [0:0]
+ :OUTPUT DROP [0:0]
+EOF
+cp -f "$newv4table" "$newv6table"
+
+# Also, keep fail2ban chains
+tgrep ':fail2ban-'
+
+
+# (Host-to-host) IPSec tunnels come first. TODO: test IPSec on IPv6.
+tgrep ' -m policy --dir (in|out) --pol ipsec .* --proto esp -j ACCEPT$'
+
+
+# Allow any IPsec ESP protocol packets to be sent and received.
+iptables -A INPUT -i $WAN -p esp -j ACCEPT
+iptables -A OUTPUT -o $WAN -p esp -j ACCEPT
+
+ip6tables -A INPUT -i $WAN6 -p esp -j ACCEPT
+ip6tables -A OUTPUT -o $WAN6 -p esp -j ACCEPT
+
+
+# Then we have the fail2ban traps
+tgrep ' -j fail2ban-\S+$'
+
+
+##################################################################################
+# 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/
+
+if [ -n "$WAN" ]; then
+ # Private-use networks (RFC 1918) and link local (RFC 3927)
+ MyNetwork=$( /bin/ip addr show "$WAN" \
+ | sed -nr "s/^\s+inet\s(\S+).*\bscope global ($WAN)?$/\1/p")
+ [ -n "$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
+ [ "$ip" = "$(/usr/bin/netmask -nc $ip $MyNetwork | sed 's/ //g')" ] \
+ || iptables -A INPUT -i $WAN -s "$ip" -j DROP
+ 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 $WAN -s "$ip" -j DROP
+ iptables -A INPUT -i $WAN -d "$ip" -j DROP
+ done
+fi
+
+# Martian IPv6 packets: ULA (RFC 4193) and site local addresses (RFC
+# 3879).
+for ip6 in fc00::/7 fec0::/10
+do
+ ip6tables -A INPUT -i $WAN6 -s "$ip6" -j DROP
+ ip6tables -A INPUT -i $WAN6 -d "$ip6" -j DROP
+done
+
+
+# DROP INVALID packets immediately.
+for chain in INPUT OUTPUT; do
+ iptables -A $chain -m state --state INVALID -j DROP
+ ip6tables -A $chain -m state --state INVALID -j DROP
+done
+
+
+# DROP bogus TCP packets.
+iptables -A INPUT -p tcp -m tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
+iptables -A INPUT -p tcp -m tcp --tcp-flags SYN,RST SYN,RST -j DROP
+
+ip6tables -A INPUT -p tcp -m tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
+ip6tables -A INPUT -p tcp -m tcp --tcp-flags SYN,RST SYN,RST -j DROP
+
+
+# Allow all input/output to/from the loopback interface.
+iptables -A INPUT -i lo -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT
+iptables -A OUTPUT -o lo -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT
+
+ip6tables -A INPUT -i lo -s ::1 -d ::1 -j ACCEPT
+ip6tables -A OUTPUT -o lo -s ::1 -d ::1 -j ACCEPT
+
+
+# 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).
+for type in echo-reply destination-unreachable echo-request; do
+ iptables -A INPUT -i $WAN -p icmp -m icmp --icmp-type $type -j ACCEPT
+ iptables -A OUTPUT -o $WAN -p icmp -m icmp --icmp-type $type -j ACCEPT
+done
+ip6tables -A INPUT -i $WAN6 -p icmpv6 -j ACCEPT
+
+
+##################################################################################
+# 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)\b(.*)/\14\2\n\16\2/' \
+ /etc/iptables/services | \
+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.
+ stNew=NEW,ESTABLISHED
+ stEst=ESTABLISHED
+
+ # In-Out means full-duplex
+ [[ "$dir" =~ inout ]] && stEst="$stNew"
+
+ 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[46]|inout[46]) iptNew="-A INPUT -i"; iptEst="-A OUTPUT -o";;
+ out[46]) iptNew="-A OUTPUT -o"; iptEst="-A INPUT -i";;
+ *) fatal "Error: Unknown direction: '$dir'."
+ esac
+ case "$dir" in
+ *4) ipt="iptables"; if=$WAN;;
+ *6) ipt="ip6tables"; if=$WAN6;;
+ esac
+
+ $ipt $iptNew $if -p $proto $optsNew -m state --state $stNew -j ACCEPT
+ $ipt $iptEst $if -p $proto $optsEst -m state --state $stEst -j ACCEPT
+done
+
+
+##################################################################################
+# And last come the fail2ban rules.
+tgrep '^-[AI] fail2ban-\S+ '
+
+echo COMMIT >> "$newv4table"
+echo COMMIT >> "$newv6table"
+
+/usr/bin/uniq "$newv4table" | /sbin/iptables-restore
+/usr/bin/uniq "$newv6table" | /sbin/ip6tables-restore
+
+save() {
+ mkdir -p /etc/iptables
+ /sbin/iptables-save > /etc/iptables/rules.v4
+ /sbin/ip6tables-save > /etc/iptables/rules.v6
+}
+
+rv=0
+if [ $force -eq 1 ]; then
+ # At the user's own risks...
+ save
+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; save
+ ;;
+ *) log "Reverting to old ruleset... "; echo
+ /sbin/iptables-restore -c < "$oldv4table"
+ /sbin/ip6tables-restore -c < "$oldv6table"
+ rv=1
+ ;;
+ esac
+fi
+
+rm -f "$oldv4table" "$newv4table" "$oldv6table" "$newv6table"
+exit $rv
diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml
index c1f9137..b8108e1 100644
--- a/roles/common/handlers/main.yml
+++ b/roles/common/handlers/main.yml
@@ -4,3 +4,8 @@
- name: apt-get update
apt: update_cache=yes
+
+- name: Unsafe firewall update
+ fail: msg="The firewall has been updated, but not activated yet; an unsafe
+ update may lock you and others out! Please log in to '{{ ansible_fqdn }}' and
+ manually run 'sudo update-firewall.sh'."
diff --git a/roles/common/tasks/firewall.yml b/roles/common/tasks/firewall.yml
new file mode 100644
index 0000000..2913a9e
--- /dev/null
+++ b/roles/common/tasks/firewall.yml
@@ -0,0 +1,34 @@
+- name: Install some packages required for the firewall
+ apt: pkg={{ item }}
+ with_items:
+ - iptables
+ - netmask
+ - bsdutils
+
+- name: Create directory /etc/iptables
+ file: path=/etc/iptables
+ owner=root group=root
+ state=directory
+ mode=0755
+
+- name: Generate /etc/iptables/services
+ template: src=etc/iptables/services.j2
+ dest=/etc/iptables/services
+ owner=root group=root
+ mode=0600
+ notify:
+ - Unsafe firewall update
+
+- name: Copy /usr/local/sbin/update-firewall.sh
+ copy: src=usr/local/sbin/update-firewall.sh
+ dest=/usr/local/sbin/update-firewall.sh
+ owner=root group=root
+ mode=0755
+ notify:
+ - Unsafe firewall update
+
+- name: Make the iptable ruleset persistent
+ copy: src=etc/network/if-pre-up.d/iptables
+ dest=/etc/network/if-pre-up.d/iptables
+ owner=root group=root
+ mode=0755
diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml
index d6a4266..460ffdd 100644
--- a/roles/common/tasks/main.yml
+++ b/roles/common/tasks/main.yml
@@ -2,3 +2,4 @@
- include: sysctl.yml tags=sysctl
- include: hosts.yml
- include: apt.yml tags=apt
+- include: firewall.yml tags=firewall,iptables
diff --git a/roles/common/templates/etc/iptables/services.j2 b/roles/common/templates/etc/iptables/services.j2
new file mode 100644
index 0000000..b1b7f0f
--- /dev/null
+++ b/roles/common/templates/etc/iptables/services.j2
@@ -0,0 +1,13 @@
+# {{ 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)
+
+inout udp 500 500 # ISAKMP
+
+in tcp {{ ansible_ssh_port|default('22') }} # SSH
+
+out tcp 80,443 # HTTP/HTTPS
+out udp 53 # DNS
+out udp 67 # DHCP