geo blocking with iptables/ipset
In this post, I'll go over how to use iptables and ipset to create a basic firewall with ssh brute force protection and geo-blocking. I'm assuming CentOS here, adjust paths/commands accordingly for other distributions.
Ipset is a tool to create and maintain IP sets in the Linux kernel. The advantage of using ipset over setting up a bunch of individual rules is one of CPU utilization. Ipset can handle thousands of entries without CPU degradation, wheras introducing thousands of rules in iptables will have a noticeable impact on packet processing speeds.
First we'll install the tool and create an initial ipset and persist it.
yum install ipset iptables iptables-services
cat <<'EOF' >/root/refresh-geoblock.sh
#!/bin/bash
mkdir /tmp/geoblocking
cd /tmp/geoblocking
ipset destroy geoblock
ipset -N geoblock nethash
for i in cn kr pk tw sg kh pe; do
echo $i
wget -q http://www.ipdeny.com/ipblocks/data/countries/$i.zone
for k in `cat $i.zone`; do
ipset -A geoblock $k
done
done
cd /tmp
rm -rf /tmp/geoblocking
ipset save geoblock >/etc/sysconfig/ipset-geoblock
'EOF'
chmod +x /root/refresh-geoblock.sh
/root/refresh-geoblock.sh
Next up, a basic firewall configuration. Some things to note about these rules:
- Default input/output policy is set to drop. I highly recommend you leave it this way and only open up what you really need.
- Poking holes for inbound SSH. These rules use the recent match and will only allow 4 connections per source IP every five minutes, which will greatly reduce the number of brute force attempts that hit the server.
- Poking holes for outbound ports for github/bitbucket
- Poking holes for some yum repositories
- Poking holes for outbound DNS (using google's recursive DNS resolver in this example)
If you want to use this as-is, make sure you update your /etc/resolv.conf to have the correct DNS entries.
cat <<'EOF' >/etc/sysconfig/iptables
# Generated by iptables-save v1.4.21 on Thu Feb 12 15:15:51 2015
*filter
:INPUT DROP [70:4342]
:FORWARD ACCEPT [0:0]
:OUTPUT DROP [0:0]
-A INPUT -m set --match-set geoblock src -j REJECT --reject-with icmp-port-unreachable
-A INPUT -i lo -p tcp -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -m recent --set --name SSH --mask 255.255.255.255 --rsource
-A INPUT -p tcp -m tcp --dport 22 -m recent --rcheck --seconds 300 --hitcount 4 --rttl --name SSH --mask 255.255.255.255 --rsource -j REJECT --reject-with tcp-reset
-A INPUT -p tcp -m tcp --dport 22 -m recent --rcheck --seconds 300 --hitcount 3 --rttl --name SSH --mask 255.255.255.255 --rsource -j LOG --log-prefix "SSH brute force "
-A INPUT -p tcp -m tcp --dport 22 -m recent --update --seconds 300 --hitcount 3 --rttl --name SSH --mask 255.255.255.255 --rsource -j REJECT --reject-with tcp-reset
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -d 192.30.252.0/24 -p tcp -m multiport --dports 80,443,22,9418 -m comment --comment "Allow to talk to github" -j ACCEPT
-A OUTPUT -d 70.38.0.137/32 -p tcp -m tcp --dport 80 -m comment --comment "epel mirror" -j ACCEPT
-A OUTPUT -d 31.172.186.53/32 -p tcp -m tcp --dport 80 -m comment --comment "erlang mirror" -j ACCEPT
-A OUTPUT -d 209.132.181.16/32 -p tcp -m tcp --dport 80 -m comment --comment "epel mirror" -j ACCEPT
-A OUTPUT -d 66.135.62.201/32 -p tcp -m tcp --dport 80 -m comment --comment "epel mirror" -j ACCEPT
-A OUTPUT -d 66.35.62.166/32 -p tcp -m tcp --dport 80 -m comment --comment "epel mirror" -j ACCEPT
-A OUTPUT -d 152.19.134.146/32 -p tcp -m tcp --dport 80 -m comment --comment "epel mirror" -j ACCEPT
-A OUTPUT -d 140.211.169.197/32 -p tcp -m tcp --dport 80 -m comment --comment "epel mirror" -j ACCEPT
-A OUTPUT -d 67.203.2.67/32 -p tcp -m tcp --dport 80 -m comment --comment "epel mirror" -j ACCEPT
-A OUTPUT -d 70.38.0.136/32 -p tcp -m tcp --dport 80 -m comment --comment "centos mirror" -j ACCEPT
-A OUTPUT -d 131.103.20.168/32 -p tcp -m multiport --dports 80,443,22 -m comment --comment "Allow to talk to bitbucket" -j ACCEPT
-A OUTPUT -d 131.103.20.167/32 -p tcp -m multiport --dports 80,443,22 -m comment --comment "Allow to talk to bitbucket" -j ACCEPT
-A OUTPUT -d 4.2.2.2/32 -p udp -m udp --dport 53 -m comment --comment "google DNS 1" -j ACCEPT
-A OUTPUT -d 8.8.8.8/32 -p udp -m udp --dport 53 -m comment --comment "google DNS 2" -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
-A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
COMMIT
# Completed on Thu Feb 12 15:15:51 2015
'EOF'
Once this is in place, we're good to go. You can start your fresh firewall with
service iptables start
You may want to periodically refresh your geo-blocking rules. This can be done with the following commands:
service iptables stop
/root/refresh-geoblock.sh
service iptables start
There's one last thing that hasn't been addressed, it's reloading the ipset after a reboot before the firewall gets reloaded. To address this, I've edited /usr/libexec/iptables/iptables.init and changed the following function:
start() {
# Do not start if there is no config file.
[ ! -f "$IPTABLES_DATA" ] && return 6
# check if ipv6 module load is deactivated
if [ "${_IPV}" = "ipv6" ] \
&& grep -qIsE "^install[[:space:]]+${_IPV}[[:space:]]+/bin/(true|false)" /etc/modprobe.conf /etc/modprobe.d/* ; then
echo $"${IPTABLES}: ${_IPV} is disabled."
return 150
fi
echo -n $"${IPTABLES}: Applying firewall rules: "
OPT=
[ "x$IPTABLES_SAVE_COUNTER" = "xyes" ] && OPT="-c"
$IPTABLES-restore $OPT $IPTABLES_DATA
if [ $? -eq 0 ]; then
success; echo
else
failure; echo;
if [ -f "$IPTABLES_FALLBACK_DATA" ]; then
echo -n $"${IPTABLES}: Applying firewall fallback rules: "
$IPTABLES-restore $OPT $IPTABLES_FALLBACK_DATA
if [ $? -eq 0 ]; then
success; echo
else
failure; echo; return 1
fi
else
return 1
fi
fi
# Load additional modules (helpers)
if [ -n "$IPTABLES_MODULES" ]; then
echo -n $"${IPTABLES}: Loading additional modules: "
ret=0
for mod in $IPTABLES_MODULES; do
echo -n "$mod "
modprobe $mod > /dev/null 2>&1
let ret+=$?;
done
[ $ret -eq 0 ] && success || failure
echo
fi
# Load sysctl settings
load_sysctl
touch $VAR_SUBSYS_IPTABLES
return $ret
}
To:
start() {
# Do not start if there is no config file.
[ ! -f "$IPTABLES_DATA" ] && return 6
# check if ipv6 module load is deactivated
if [ "${_IPV}" = "ipv6" ] \
&& grep -qIsE "^install[[:space:]]+${_IPV}[[:space:]]+/bin/(true|false)" /etc/modprobe.conf /etc/modprobe.d/* ; then
echo $"${IPTABLES}: ${_IPV} is disabled."
return 150
fi
echo -n $"${IPTABLES}: Loading ipset: "
ipset restore </etc/sysconfig/ipset-geoblock
echo "ok"
echo -n $"${IPTABLES}: Applying firewall rules: "
OPT=
[ "x$IPTABLES_SAVE_COUNTER" = "xyes" ] && OPT="-c"
$IPTABLES-restore $OPT $IPTABLES_DATA
if [ $? -eq 0 ]; then
success; echo
else
failure; echo;
if [ -f "$IPTABLES_FALLBACK_DATA" ]; then
echo -n $"${IPTABLES}: Applying firewall fallback rules: "
$IPTABLES-restore $OPT $IPTABLES_FALLBACK_DATA
if [ $? -eq 0 ]; then
success; echo
else
failure; echo; return 1
fi
else
return 1
fi
fi
# Load additional modules (helpers)
if [ -n "$IPTABLES_MODULES" ]; then
echo -n $"${IPTABLES}: Loading additional modules: "
ret=0
for mod in $IPTABLES_MODULES; do
echo -n "$mod "
modprobe $mod > /dev/null 2>&1
let ret+=$?;
done
[ $ret -eq 0 ] && success || failure
echo
fi
# Load sysctl settings
load_sysctl
touch $VAR_SUBSYS_IPTABLES
return $ret
}