Linux Firewall (Part 2) : nftables

Intro

In the previous post, we installed RHEL and setup the networks for our firewall. In this post, we will start using nftables. First we will setup a basic skeleton and then add some rules later.

nftables Setup

Before using nftables directly, we will stop and disable the default firewall

systemctl stop firewalld
systemctl disable firewalld

Next, we will create configuration file(s) for nftables

nano /etc/nftables/myrules.nft

Flush the default/previous rules. Create a new ip table named fire. Put our configuration files in it. These files will have our rule chains (Netfilter hooks). For more information about them check out Netfilter hooks.

We only have one output rule for traffic originating from the firewall, so we can add that chain directly. We will allow all traffic to go out.

/etc/nftables/myrules.nft

flush ruleset

include "./defines.nft"

table ip fire {
    include "./input.nft"
    include "./nat.nft"
    include "./forward.nft"
    
    chain output {
        type filter hook output priority 0; policy accept;  # let this machine's traffic out
    }
}

Create familiar names for things we are going to use frequently

/etc/nftables/defines.nft

# interfaces
define if_wan = "enp1s0"
define if_dmz = "enp2s0"
define if_lan = "enp3s0"

# networks
define net_dmz = 10.1.1.0/24
define net_lan = 10.1.2.0/24

# machines
define desktop = 10.1.2.10

Create an input chain. This will have rules for traffic coming into the firewall. We can create smaller chains to jump to from the input chain. This will let us manage a bit more efficiently. Our default rule will be to drop traffic not explicitly allowed. We will also log the traffic that is allowed with the appropriate prefixes.

/etc/nftables/input.nft

# input from lan
chain in_from_lan {
		# allow ssh port
    ip protocol . th dport { tcp . 22 } log prefix "[NFTABLES] [SSH] "accept
    # allow DNS ports
    ip protocol . th dport { udp . 53 } log prefix "[NFTABLES] [DNS] " accept
    ip protocol . th dport { tcp . 53 } log prefix "[NFTABLES] [DNS] " accept
    # allow DHCP port
    ip protocol . th dport { udp . 67 } log prefix "[NFTABLES] [DHCP] " accept
}

chain in_from_dmz {
		# allow DNS ports
    ip protocol . th dport { udp . 53 } log prefix "[NFTABLES] [DNS] " accept
    ip protocol . th dport { tcp . 53 } log prefix "[NFTABLES] [DNS] " accept
    # allow DHCP port
    ip protocol . th dport { udp . 67 } log prefix "[NFTABLES] [DHCP] " accept
}

chain in_from_wan {
}

chain input {
		# default policy drop
    type filter hook input priority 0; policy drop;

		# drop invalid packets 
    ct state vmap { established : accept, related : accept, invalid : drop }
    
    # ping requests
    icmp type echo-request limit rate 5/second accept
    
    # allow all loopback traffic
    iifname lo accept

		# jump to chains based on the interface the traffic originated from
    iifname vmap { $if_lan: jump in_from_lan, $if_dmz: jump in_from_dmz, $if_wg0: jump in_from_lan, $if_wan: jump in_from_wan }
}

Create chains for Network Address Translation (NAT). We can use the prerouting hook for Desination NAT (DNAT) and postrouting hook for Source NAT (SNAT). Right now, we don’t have any special rules for the prerouting hook. For postrouting hook, we will use masquerade for routing packages between different interfaces/subnets. Masquerading automatically sets the source address to the address of the output interface.

[!Note]

The post routing hooks runs after the output and forward hooks, so masquerade plays well with them.

/etc/nftables/nat.nft

chain prerouting {
		# default accept all
    type nat hook prerouting priority 0; policy accept;
}

chain postrouting {
		# default accept all
    type nat hook postrouting priority 100; policy accept;

    ip saddr $net_lan oifname $if_wan masquerade
    ip saddr $net_dmz oifname $if_wan masquerade
    
    iifname $if_wan oifname $if_lan masquerade
}

Next, we will create a forward chain. Here we will create rules to decide what traffic to forward to different interfaces/subnets. Our default rule will be to drop traffic not explicitly allowed. This will allow us to create isolation between networks.

We will allow all traffic from DMZ and LAN to go out to WAN for internet access. We can also allow traffic from LAN to DMZ, so we can access the servers from our devices in LAN subnet.

/etc/nftables/forward.nft

chain forward {
		# default drop all
    type filter hook forward priority 0; policy drop;
    
    ct state vmap { established : accept, related : accept, invalid : drop }

		# accept lan to dmz
    iifname $if_lan oifname $if_dmz accept
		# accept lan to wan
    iifname $if_lan oifname $if_wan accept
		# accept dmz to wan
    iifname $if_dmz oifname $if_wan accept
}

Enable and start nftables service

systemctl enable nftables
systemctl start nftables

When we make changes later, we can restart nftables service with

systemctl restart nftables

At this point we should have a functional firewall/router.

Applications

Let’s take a look at some common application and how to configure the firewall for it.

Remote Desktop

If we want to access our desktop from a remote location (internet), we can enable Remote Desktop on windows. Let’s add rules to enable it.

Add rule to change the destination address of the traffic coming in at port 3389 to the ip address of the desktop.

Add in prerouting chain, /etc/nftables/nat.nft

iifname $if_wan tcp dport { 3389 } dnat to $desktop:3389

[!Note]

Remember that the masquerade will automatically take care of SNAT, so we only need to worry about DNAT.

Add rule to allow the traffic from WAN interface to LAN interface with destination desktop and port 3389.

Add in forward chain, /etc/nftables/forward.nft

iifname $if_wan oifname $if_lan ip daddr $desktop tcp dport { 3389 } accept

Reverse Proxy

We can run a reverse proxy on the firewall itself (more on this in another post). We want to allow client from WAN and LAN to connect to the reverse proxy on port 80 and 443. Let’s see how to create rules for them. The reverse proxy is running on the firewall, so we will use the input chains.

Add rules in in_from_lan chain, /etc/nftables/input.nft

ip protocol . th dport { tcp . 80 } log prefix "[NFTABLES] [HTTP] " accept
ip protocol . th dport { tcp . 443 } log prefix "[NFTABLES] [HTTPS] " accept

Add rules in in_from_wan chain, /etc/nftables/input.nft

ip protocol . th dport { tcp . 80 } log prefix "[NFTABLES] [HTTP] " accept
ip protocol . th dport { tcp . 443 } log prefix "[NFTABLES] [HTTPS] " accept

Thank you for reading. Check out the other parts in the series below.