New firewall system for Sympl using nftables

I move the log_selector into its own file because that removes it from being linked with the tls options. Also I think that the exim docs said can only appear once, but this belief may be incorrect in practice. The log_selector in tls_options gets commented out in CH3. The new file is 25_logging and gets several comments explaining the options. It also stops annoying message generated by sympl’s monitoring system.

My log_selector says

log_selector = +tls_sni +smtp_protocol_error +incoming_interface +smtp_mailauth

because these deliver good indications of unwanted behaviour.

P.S The change in 50-tls-options that was adopted was to change to

auth_advertise_hosts = localhost : ${if eq{$tls_cipher}{}{localhost}{*}}

which originally said

auth_advertise_hosts = localhost : ${if eq{$tls_cipher}{}{nomatchinghosts}{*}}

and gave rise to more erroneous error messages about nomatchinghosts not being a legal host. Using localhost here did the trick and stopped the messages.

Hi, I’ve just signed up to say I’m looking to test nftfw on my home server. It serves as a gateway for my lan and a bunch of lxc containers in a bridged dmz on the same host. It’s still running Ubuntu 16.04 and the upgrade will be quite the mammoth undertaking, going from lxc to lxd on bookworm and from Shorewall to native nftables!

Shorewall makes complex systems much more manageable. I’m using ipset for general black/whitelisting with fail2ban and lists downloaded via firehol’s update-ipsets tool, xt_geoip for black/whitelisting by geolocation and have a bunch of traffic shaping controls to boot. nftfw appears to be best-fit framework available for nftables.

I’ve just had a peek at your router template and one thing jumped out immediately. Why are you doing universal blacklisting in the input chain? Don’t do that! :smiley: nftables provides an ingress hook that is akin to prerouting in iptables. From the performance tests I’ve seen by guys at Cloudfare, dropping at nft ingress is not quite as fast as dropping packets even earlier via tc, but it’s significantly more performant than jumping to drop targets after connection state checks in the input chain. You can define an ingress chain with “type filter hook ingress” to handle universal blacklisting. The ingress chain is also the pace to drop xmas scans etc. Full schematic for netfilter hooks: Netfilter hooks - nftables wiki

It might also be a good idea to add a means of whitelisting by geoip. For example, I only allow access to ssh and ipsec on the gateway to IPs in Great Britain and this, along with changing the ssh port, reduces miscreant activity at minimal cost.

All that said, I’m impressed with your efforts! I can’t say the whole team at Redhat have done a better job with the documentation than you have here.

Ingress chain

The ingress chain needs to know the name of the device that it’s filtering. Currently the tables generated by nftfw are essentially device independent, and this makes them easy to drop in and use without any specific hardware configuration. That being said, on my systems I change nftfw_init.nft to include some ingress filtering, but have not published it as ‘standard’ because it complicates matters. Sometimes simple systems that work are better than complicated ones that need a lot of understanding from users.

Here’s the code I put at the top of my nftfw_init.nft:

# DDOS hardening
# From https://blog.samuel.domains/blog/security/nftables-hardening-rules-and-good-practices
table netdev filter {

      # set to look for BOGONS, ie packets from outside that use
      # public local ip address range
      # may need adjustment if the router uses an internal address
      # often 192.168.1.0/24
      set ipv4_bogons {
      	  type ipv4_addr
	  flags interval
	  elements = {
		 0.0.0.0/8,
		 10.0.0.0/8,
		 100.64.0.0/10,
		 127.0.0.0/8,
		 169.254.0.0/16,
		 172.16.0.0/12,
		 192.0.0.0/24,
		 192.0.2.0/24,
		 192.168.0.0/16,
		 198.18.0.0/15,
		 198.51.100.0/24,
		 203.0.113.0/24,
		 224.0.0.0/3
		 }
      }

      # chain looking at packets directly from the device
      # the eth0 here should be replaced by your WAN connection device
      # this table looks at both ipv4 and ipv6 packets
      chain ingress {
      	    type filter hook ingress device eth0 priority -500;

	    # IP fragments
	    # not sure about this so it's just counted for now
	    ip frag-off & 0x1fff != 0 counter

	    # Track bogons
	    ip saddr @ipv4_bogons meta protocol ip counter drop

	    # TCP XMAS attack
	    tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg counter drop
	    # TCP NULL
	    tcp flags & (fin|syn|rst|psh|ack|urg) == 0x0 counter drop
	    # TCP MSS
  	    tcp flags syn tcp option maxseg size 1-535 meta protocol ip counter drop
	}
}
table inet mangle {
      chain prerouting {
            type filter hook prerouting priority -150;

            # CT INVALID
	    ct state invalid counter drop

	    # TCP SYN (CT NEW)
	    tcp flags & (fin|syn|rst|ack) != syn ct state new counter drop
      }
}

You have to add the name of the device you are filtering, and on a home network you may need to adjust the bogons code to remove any public address range you are using.

Whitelisting sets

I think that this would be possible using the extant blacknets system, assuming that you don’t want to use that. Caveat: I’ve not tried this.

The current lookup chain generated for blacknets is:

chain blacknets {
  ip saddr @blacknets_set counter jump dropcounter
 }

You are whitelisting and will want to drop the packet if the ip is not in the set, so you need:

chain blacknets {
  ip saddr != @blacknets_set counter jump dropcounter
 }

All blacknets files are used to create a single set, so this will work.

Rules are generated by small shell scripts in rule.d. Currently the config.ini file is telling the system to use the standard ‘drop’ rule for blacknets.

So copy the current drop.sh to say exclude.sh and change

if [ "$IPS" != "" ]; then
    IPSWITHDIRECTION="$PROTO $ADDRCMD $IPS"
fi

to

if [ "$IPS" != "" ]; then
    IPSWITHDIRECTION="$PROTO $ADDRCMD != $IPS"
fi

Check that your script is syntactically OK by running it through bash.

Now alter config.ini to change the blacknets setting:

;incoming = accept
;outgoing = reject
;whitelist = accept
;blacklist = drop
blacknets = exclude

and I think that would do it.

Thanks
For the comments.

1 Like

Elegant stuff!

Note that a variable can be passed to the device defined for the ingress hook since nftables 0.9ish, so it could easily be integrated into a config file. The inet table now also supports the ingress hook.

I’m looking to whitelist UK traffic specifically to private service ports such as ssh, imap, submission and sieve, but wouldn’t want to invert the blacknets set. I’ve managed to translate the bulk of my Shorewall rules automatically, apart from more complicated stuff involving ipsec and ipsets, but there’s over 1000 lines. :joy: I’ve spent the evening with my head in the nftables wiki and am thinking I will take deep breath, cut down the cruft as much as I can and optimise the rest, once I’ve gotten my head around concatenation etc.