I was going to start this series with explaining how I did the remote set-up, but instead I will share something that happened today.

One of the first things you want to do when putting a server directly connected to the Internet is some filtering. You don't want to have an application listening on the network by mistake, so a simple netfilter firewall is a good way to ensure you are only accepting connections on ports you explicitly allowed.

I have been a long-time user of ferm, a simple tool that will read a configuration file written in a special structured syntax, and generates iptables commands from it. I have used it successfully to build very complex firewalls in previous jobs, and it had the huge benefit of keeping your firewall description readable and easy to modify by other people.

This time I thought I may go with something simpler, as I only wanted a handful of very simple netfilter rules. I looked at Shorewall, and browsed a bit a few others. But in the end I decided against them: there was the need to learn the tools' concepts about different parts of the network, or there were more slanted towards command-line commands, so your actual configuration will be some files in /var/lib, totally managed by the tool. With ferm, I just need to write a very small configuration file, which reads almost like iptables commands, and that's it.

In fact, the default configuration placed by the Debian package, already did 90% of what I wanted: accept incoming SSH connections, ICMP packets, and reject everything else. I took the example IPv6 configuration from /usr/share/doc/ferm/examples/ipv6.ferm and in 10 minutes it was ready:

table filter {
    chain INPUT {
        policy DROP;
        mod state state INVALID DROP;
        mod state state (ESTABLISHED RELATED) ACCEPT;

        interface lo ACCEPT;
        proto icmp ACCEPT; 

        # allow IPsec
        proto udp dport 500 ACCEPT;
        proto (esp ah) ACCEPT;

        proto tcp dport ssh ACCEPT;
        proto tcp dport (http https) ACCEPT;
    }
    chain OUTPUT policy ACCEPT;
    chain FORWARD policy DROP;
}

domain ip6 table filter {
    chain INPUT {
        policy DROP;
        mod state state INVALID DROP;
        mod state state (ESTABLISHED RELATED) ACCEPT;

        interface lo ACCEPT;
        proto ipv6-icmp ACCEPT;

        proto tcp dport ssh ACCEPT;
        proto tcp dport (http https) ACCEPT;
    }
    chain OUTPUT policy ACCEPT;
    chain FORWARD policy DROP;
}

It is important to note than when doing this kind of thing on a remote machine, you want to make sure you don't get locked out by accident. My method is that before activating any dangerous change, I drop an at job to disable the firewall in a few minutes:

# echo /etc/init.d/ferm stop | at now +10min
warning: commands will be executed using /bin/sh
job 4 at Mon Apr 29 02:47:00 2013

And if everything goes well, I just remove the job:

# atrm 4

Update: As paravoid pointed out in the comments, now (read: since many years ago, but I've never noticed) ferm has a --interactive mode which will revert the changes if you get locked out, much like the screen resolution changing dialog in Gnome.


Another thing that you definitely want to do, is to have some kind of protection against the almost constant influx of brute-force attacks against SSH. Apart from the obvious PermitRootLogin=no setting, there are a couple of popular methods to stop people probing random username/password combinations (I am assuming here that you actually have sensible passwords, or no passwords at all): running SSH in a non-standard port, and the great fail2ban daemon.

Since I don't like non-standard stuff, I installed fail2ban, which by default it will inspect /var/log/auth.log for SSH login failures and insert netfilter rules to block the offenders.

Problem is, I don't like much how fail2ban inserts rules and chains into my very tidy netfilter configuration which I had just created. So, I added an "action" to do things my way: only create a service-related chain and insert rules there, I will call that chain from my main ferm.conf. Ferm runs early in the boot sequence, so this won't be a problem during normal operation. The only caveat is that after changing a configuration in ferm, I need to restart fail2ban so it will recreate the netfilter chains and rules, which were wiped by ferm.

This is my configuration, note that I am ignoring the port and protocol: the whole IP is blocked for a few minutes.

# cat /etc/fail2ban/jail.local 
[DEFAULT]
action = iptables-fixed[name=%(__name__)s]

# cat /etc/fail2ban/action.d/iptables-fixed.conf
[Definition]
actionstart = iptables -N fail2ban-<name>
              iptables -I fail2ban -j fail2ban-<name>
actionstop = iptables -D fail2ban -j fail2ban-<name>
             iptables -F fail2ban-<name>
             iptables -X fail2ban-<name>
actioncheck = iptables -n -L | grep -q fail2ban-<name>
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j DROP
actionunban = iptables -D fail2ban-<name> -s <ip> -j DROP

[Init]
name = default
ferm & fail2ban

Also, when I once needed scripts to dynamically alter the configuration of an otherwise ferm-managed firewall, I did the following steps:

a) Have ferm jump to a new DYNAMIC chain (that alone will fix the "ferm flushes the rules on reload" issue).

b) Have the scripts (e.g. fail2ban) put a ferm snippet in /var/lib/ferm, then execute ferm for that file alone, effectively adding just that rule. Optionally make the snippets be just a call to a previously-defined ferm function, as to put the policy on the main ferm config (if e.g. you want to LOG, then DROP) instead of the snippets themselves.

c) Have the main ferm config in /etc/ferm create the DYNAMIC chain and then @include /var/lib/ferm/, essentially reloading all of the custom rules on /etc/init.d/ferm reload.

Note that for the last step to work, you need to set CACHE=no on /etc/default/ferm, as the init script caches generated rules on /var/cache/ferm based on the mtime of files under /etc/ferm.

Comment by paravoid
ferm --interactive

Oh, that's a great feature, I didn't know about it.. Thanks for the tip!

I guess I've been using ferm for too long, and never really looked at the new features :)

Comment by Martín
comment 4
I used to block all the malicious SSH traffic by implementing port knocking (via knockd), but I was unhappy with that approach as it involved running yet another daemon as root. Since you're using ferm, here's my config for a pure-iptables based port knocking implementation:
# Pure iptables port-knocking implementation
# based on http://www.debian-administration.org/articles/268
#
# Opens SSH, OpenVPN after knocking on TCP 7100, 7200, 7300
# 10 secs time between knocks, 120 secs after opening ports


table filter {
    chain KNOCK2 {
        protocol tcp {
            mod recent name "knock1" remove NOP;
            mod recent name "knock2" set NOP;
        }
        DROP;
    }
    chain KNOCK3 {
        protocol tcp {
            mod recent name "knock2" remove NOP;
            mod recent name "knock3" set NOP;
        }
        DROP;
    }

    chain INPUT {
        protocol tcp {
            dport 7100 mod recent set name "knock1" NOP;
            dport 7200 mod recent rcheck name "knock1" seconds 10 jump KNOCK2;
            dport 7300 mod recent rcheck name "knock2" seconds 10 jump KNOCK3;
            dport (ssh openvpn) mod recent rcheck name "knock3" seconds 120 ACCEPT;
        }
    }
}
All the hardcoded stuff is easily parametrized, of course. Pros: doesn't need extra software, especially not one running as root. Cons: You need a knocking client. I was lazy and just used the one from package "knockd".
Comment by Christian