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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

1
service iptables start

You may want to periodically refresh your geo-blocking rules. This can be done with the following commands:

1
2
3
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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
}