Creating knockd with iptables

UPDATE 2017-MAR-12: Removed references to fail2ban, following up a massive change in ubuntu’s fail2ban package that broke this knock. Now it’s not dependent on fail2ban, but seamlessly aligns behind it.

Having a knock daemon in front of your sensitive service is always a good idea. It won’t protect against a direct, planned attack, but it will avert random port scanners looking for vulnerabilities at random IP addresses.

Note that here I was focusing on the simplest knock daemon only, one that opens up a port if a valid sequence of connection attempts arrive. So far I’ve been using an ancient knockd for that (see debian/ubuntu repo), however, it being a userspace netfilter, all traffic passed through it and was consuming a huge amount of CPU (cca. 45 minutes a day). Since this is such a simple task that even an iptables rule set could handle it, I found this disproportionate and decided to dust off my iptables skills.

There are a lot more powerful knock daemons available too. For example, one that hides a one time pad in an ICMP payload. Or encrypt the current hour+minute+some secret with a secret RSA key and send that in an ICMP ping. And then you could combine this with some innocent-looking traffic, e.g. hiding the above in a regular looking web traffic. These are only the tip of the iceberg of what you can do, just what I could think of while typing this paragraph. Here, we will focus on the simplest knock ‘daemon’ possible, in order to preserve resources and to keep it painfully simple.

I’ve looked into this on google as well, of course, and boiled it down that I want:

  • ipv4 and ipv6 support;
  • tcp/udp/… support;
  • drop client if wrong sequence was hit;
  • integrate it with fail2ban-ssh;
  • don’t interfere with normal traffic (no port ranges reserved for knockd, …);
  • make no performance impact on normal traffic.

Below I made iptables chains for a knock sequence of 3, with a maximum of 10 second delay in between. How this works is a list is made for each state (using the iptables recent module). When the first port is hit, we add the source IP to the knock1 list. When an IP is in the knock1 list, and the second port in the sequence is hit, we add it to the knock2 list, and so on, until the source IP makes it into the knockp list (p for passed), which would simply allow accessing port 22 (SSH). If a client makes any other interaction in the watched ports that does not match the next port in the sequence, it is simply removed from all lists and it will have to start over again. So no brute forcing.

Here we only react on a specific port range only (51000 to 55000 in the below example), so that other traffic from the same IP would normally have no effect on our knocking. Also, I’ve made a knock chain and only route ‘knocking’ traffic trough our rules so that regular traffic would be almost unaffected. By using ACCEPT target, any service that actually would make use of these ports is not affected in any way. I happen to use high port numbers to avoid getting too many packets through these rules for no reason. Also, iptables’ recent module has a (configurable) limit of IP addresses on each list, which, if exhausted within the 10-second timeframe, would make knocking impossible.

So here it goes:

#!/bin/bash

function knock() {
IPTABLES=$1
LOCALNET=$2

# create/flush chains
$IPTABLES -N knock
$IPTABLES -N knock1
$IPTABLES -N knock2
$IPTABLES -N knockp

$IPTABLES -F knock
$IPTABLES -F knock1
$IPTABLES -F knock2
$IPTABLES -F knockp

## INPUT
# accept established connections (don't use conntrack here, as it terminates ssh connections after 5-15 mins, and is generally very resource-inefficient)
$IPTABLES -A INPUT -p tcp ! --syn --dport 22 -j ACCEPT
# accept ssh from local network
$IPTABLES -A INPUT -p tcp -s "$LOCALNET" --dport 22 -j ACCEPT

# if 'knock passed', go to pass-through chain
$IPTABLES -A INPUT -m recent --name knockp --rcheck --seconds 10 --reap -j knockp

# if within knock range, jump to knock chain
$IPTABLES -A INPUT -p tcp --dport 51000:55000 -j knock
$IPTABLES -A INPUT -p udp --dport 51000:55000 -j knock

# drop the rest of ssh initiation traffic
$IPTABLES -A INPUT -p tcp --dport 22 --syn -j DROP

## KNOCK
# route incoming packets by source IP state
$IPTABLES -A knock -m recent --name knock1 --rcheck --seconds 10 --reap -j knock1
$IPTABLES -A knock -m recent --name knock2 --rcheck --seconds 10 --reap -j knock2

# set new state on sequence hit
$IPTABLES -A knock -p udp --dport 51001 -m recent --name knock1 --set -j ACCEPT
$IPTABLES -A knock -p tcp --dport 51001 -m recent --name knock1 --set -j ACCEPT

$IPTABLES -A knock1 -m recent --name knock1 --remove
$IPTABLES -A knock1 -p udp --dport 51002 -m recent --name knock2 --set -j ACCEPT
$IPTABLES -A knock1 -p tcp --dport 51002 -m recent --name knock2 --set -j ACCEPT

$IPTABLES -A knock2 -m recent --name knock2 --remove
$IPTABLES -A knock2 -p udp --dport 51003 -m recent --name knockp --set -j ACCEPT
$IPTABLES -A knock2 -p tcp --dport 51003 -m recent --name knockp --set -j ACCEPT

# passed chain: allow ssh
$IPTABLES -A knockp -p tcp --dport 22 --syn -j ACCEPT
}
### v4
knock iptables 10/24

### v6
knock ip6tables 2001:981:a07b:1::/64

I’ve extracted the actual knock rules creation in a bash function, so that I could easily repeat this for v4 and v6 without added maintenance and testing. Comments should offer reasonable explanation of the rules.

Since this box is behind a firewall/router already (on the internal network), I did not add extra firewall rules here, only the ones necessary for knock-proofing the ssh port.

Hope this helped. Feel free to leave a comment.

Advertisements