This article documents how I'm currently building my firewalls. It
builds on
netfilter-based-port-knocking, and tries to
integrate several components of a firewall as gracefully as possible.
For some context: I'm getting involved with servers where the firewall
policy goes beyond a handful of SSH from my home and my other
servers rules. There are many different network streams that need to
be policed, some of them are common across several (potentially many)
servers, and so on. So I'm gradually giving up on my hand-made
scripts, and trying out higher-level tools. I settled on FWbuilder,
which seems nice. However, it only allows static policies, and I
still want to keep dynamic policies such as what
fail2ban
provides,
as well as my own port-knocking system.
The problem I had was that
fail2ban
isn't really made to play nice
as part of a complex firewalling setup, my port-knocking system was
too tightly integrated within my firewall script, and FWbuilder wasn't
too flexible when it came to delegating part of the firewall policy to
something external. Fortunately, this was only a perceived problem
(or a real problem in my understanding), because it is actually
possible to have all three blocks playing nicely together.
More context: as usual, I'm focusing on Debian-like systems. More
precisely, on those with a Linux kernel; it may be that FreeBSD's
firewalling subsystem has a feature comparable to Linux's
recent
module, but I don't know.
Let's start with FWbuilder. This is not the place for a manual, the
official documentation is rather complete. I'll assume you have
defined most relevant blocks in there: firewall, hosts, IP addresses,
services, and so on. You define your static policy with the
standard rules. From then on, we want to integrate the external tools
for dynamic rules.
Step 1: Integrating
fail2ban
We want
fail2ban
to have its own playground, so that it doesn't
overwrite anything in the standard policy. The trick is to define a
new policy rule set named
fail2ban
. Leave it empty in FWbuilder.
So far so good, but
fail2ban
(the daemon) still operates on the
INPUT
chain in the firewall, and could therefore still mangle the
static rules. Fortunately, starting with
fail2ban
0.8.5 (available
from Debian Wheezy, or in the backports for Squeeze), you can define
what chain to operate on: with a configuration item such as
chain =
fail2ban
,
fail2ban
(the daemon) will now only add its rules to
fail2ban
(the firewall chain), and won't be able do damage the other
chains.
The missing part is to send some of the traffic to it using the
standard policy: i defined a rule sending the incoming SSH connections
to the
fail2ban
policy ( branching in FWbuilder jargon).
Voil : the static policy delegates part of the decision-making to a
sub-policy controlled by the
fail2ban
daemon.
Step 2: Integrating port-knocking
This is a bit trickier, but we'll use a similar method.
First, the traffic used for port-knocking needs to be directed to the
chain that does the listening. Define a policy rule set named
portknocking
, and leave it empty in FWbuilder. It'll be used by the
dynamic rules to track progression of source IP addresses through the
port-knocking sequence, so you'll need to send ( branch ) incoming
traffic there, probably after the rules allowing incoming connections
from known hosts.
The dynamic part of this will only concern the refreshing of this
listening chain , which we assume will do its work and mark IP
addresses with
PK_ESTABLISHED
once the sequence is completed. What we
do with these marked IP addresses will still be defined within the
FWbuilder policy.
We're going to need some complex rules since we want to filter
according to this
PK_ESTABLISHED
bit
and according to destination
port, for instance; unfortunately FWbuilder doesn't allow combining
filter criteria with
and, so we define a new policy rule set called
accept_if_pk_ok
. This ruleset has two rules: the second is an
ACCEPT
and should be easy to understand. The first rule needs to
ensure the
ACCEPT
is only reached for connections coming from
PK_ESTABLISHED
addresses, so it's going to be a bit tricky.
- The service needs to be a custom service (I called it PK not
established ), since FWbuilder doesn't know about the marking
feature in
iptables
. Use -m recent ! --rcheck --seconds 86400
--name PK_ESTABLISHED
for the definition (change the duration to
the number of seconds the door should stay open after the
port-knocking sequence has been completed). Note the exclamation
mark.
- The action is also going to be custom, defined as
-j RETURN
.
Again, this feature is iptables
-specific and FWbuilder doesn't
provide any UI for it.
(Explanation: the first rule matches packets coming from IP addresses
not marked as
PK_ESTABLISHED
, and returns them to the calling
policy. Packets remaining after this rule are those coming from the
appropriate addresses, and they go on to the
ACCEPT
. We could have
had the first rule match on IP addresses that
are marked, and branch
to yet another ruleset with the
ACCEPT
part, but that would make it
harder to read, I feel.)
Now let's get back to the main policy and add rules concerning what
kind of traffic we want to allow once the port-knocking sequence
completed. For instance, we define a rule matching on the SSH
service , where the action is to branch to
accept_if_pk_ok
. When
an incoming packet tries to establish a connection to the SSH port,
it's passed to the
accept_if_pk_ok
ruleset. If it comes from the
same IP as a recent port-knocking sequence, it goes on to be
ACCEPT
ed. If not, it returns to the main policy. Maybe static
rules further on will allow it to go through.
Step 3: tying it all together
Now that we have all the pieces, the rest is plumbing.
- Get FWbuilder to compile a script from the data. I called mine
$hostname.fw
, and stored it into /usr/local/sbin
.
- Write a
/usr/local/sbin/port-knocking
script that operates on the
portknocking
chain and manages the PK_ESTABLISHED
bit. It need
not do more than what's described in
netfilter-based-port-knocking.
- Write an initialisation script that calls both
/usr/local/sbin/$hostname.fw
and /usr/local/sbin/port-knocking
.
I called mine /etc/init.d/firewall
.
- Make sure that
fail2ban
's initialisation script is called after
ours. Either with boot sequence numbers, or with the LSB dependency
pseudo-headers: I made my firewall
script to Provides: iptables
;
since fail2ban
's script declares that it Should-Start: [ ]
iptables
, we're fine.
- Run
/usr/local/sbin/port-knocking
every hour, or as often as
needed to recalculate the port numbers.
With this setup, at boot time, the
$hostname.fw
script creates the
static policy and the extra playgrounds; then the
port-knocking
script implements the listening for the magic sequence; then
fail2ban
inserts its own rules. And there we are: three different
parts for the firewall policy, all integrating nicely. Mission
accomplished!
Note: (Mostly copy-and-pasted from the previous article) This
article is deliberately short on details and ready-to-run scripts.
Firstly because firewall scripts vary wildly so any script would have
to be adapted anyway, but mostly because security is best handled with
one's brain switched on. Fiddling with a firewall can easily open
gaping holes or lock everyone out. So please make sure you understand
what goes on before blindly pasting stuff into your own setup. Some
bits are left as an exercise to the reader.