Skip to content

Build a Minimal Packet Filter with nftables

Build a Minimal Packet Filter with nftables

Section titled “Build a Minimal Packet Filter with nftables”

On RHEL 8-compatible Linux systems, if firewalld and nftables are run simultaneously as separate management mechanisms, it can become difficult to determine which configuration is actually creating the packet filter. In this article, we stop firewalld and switch to a configuration where nftables is managed directly.

The input filter we build allows existing connections, loopback, ICMP/ICMPv6, DHCP client communication, SSH on TCP/22, and HTTP on TCP/80, while dropping everything else by default. We also make the HTTP service listen on TCP/8080 so that we can verify that the firewall blocks the port even though a service is present.

Because this procedure replaces the existing firewall, in production environments you should first check the current ruleset and management path, and ensure that you have a recovery path such as console access before proceeding.

Variable nameExample valueDescription
<<SERVER_IP>>192.0.2.10IP address of the server on which nftables is configured

In addition to nftables itself, install Apache HTTP Server so that listening can be verified on both TCP/80 and TCP/8080. Also install semanage so that port 8080 can be treated as an HTTP listening port in environments where SELinux is enabled.

Terminal window
sudo dnf install -y nftables httpd policycoreutils-python-utils

Step 2: Make the HTTP service listen on TCP/80 and TCP/8080

Section titled “Step 2: Make the HTTP service listen on TCP/80 and TCP/8080”

To handle cases where TCP/8080 is already registered with another SELinux port type, if adding it fails, change it to http_port_t. Then add port 8080 to Apache’s listening configuration and place sample content.

Terminal window
sudo semanage port -a -t http_port_t -p tcp 8080 2>/dev/null || sudo semanage port -m -t http_port_t -p tcp 8080
printf '%s\n' 'nftables packet filter example' | sudo tee /var/www/html/index.html >/dev/null
printf '%s\n' 'Listen 8080' | sudo tee /etc/httpd/conf.d/listen-8080.conf >/dev/null
sudo httpd -t
sudo systemctl enable --now httpd
sudo systemctl is-active httpd

At this point, the HTTP service itself can listen on both ports 80 and 8080. In the following steps, only port 80 will be allowed on the nftables side.

Step 3: Stop firewalld to avoid conflicts with direct management

Section titled “Step 3: Stop firewalld to avoid conflicts with direct management”

From this point onward, use the nftables service as the only firewall management mechanism. Stop firewalld and disable automatic startup.

Terminal window
sudo systemctl disable --now firewalld

In environments where you continue to use firewalld, do not mix it with direct nftables management from this step onward. Implement the rules through firewalld instead.

Use the inet family to handle both IPv4 and IPv6 input in the same table. Set the default policy of the input chain to drop, while allowing loopback, established connections, ICMP/ICMPv6, DHCP client communication, SSH, and HTTP. TCP/8080 is not accepted; instead, an explicit counter drop is added to make verification easier. All other unmatched packets are dropped by the chain’s default policy.

Back up the existing /etc/sysconfig/nftables.conf and replace the configuration so that only the ruleset from this article is loaded.

Terminal window
sudo cp -a /etc/sysconfig/nftables.conf /etc/sysconfig/nftables.conf.bak
sudo install -d -m 0755 /etc/nftables
sudo tee /etc/nftables/main.nft >/dev/null <<'EOF'
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iifname "lo" counter accept
ct state established,related counter accept
ip protocol icmp counter accept
ip6 nexthdr ipv6-icmp counter accept
udp sport 67 udp dport 68 counter accept
udp sport 547 udp dport 546 counter accept
tcp dport 22 counter accept
tcp dport 80 counter accept
tcp dport 8080 counter drop
}
}
EOF
printf '%s\n' 'include "/etc/nftables/main.nft"' | sudo tee /etc/sysconfig/nftables.conf >/dev/null
sudo nft -c -f /etc/nftables/main.nft

nft -c checks the syntax and rule interpretation, but does not modify the ruleset at this point.

Step 5: Apply the ruleset and enable automatic startup

Section titled “Step 5: Apply the ruleset and enable automatic startup”

Apply the syntax-checked ruleset and enable the nftables service. Existing SSH sessions are allowed by ct state established,related, while new SSH connections are allowed by the TCP/22 rule.

Terminal window
sudo nft -f /etc/nftables/main.nft
sudo systemctl enable --now nftables

Step 6: Verify exclusivity with firewalld and the active ruleset

Section titled “Step 6: Verify exclusivity with firewalld and the active ruleset”

Verify that nftables is active and enabled, and that firewalld is inactive and disabled. Then display the input chain actually registered in the kernel.

Terminal window
set -e
test "$(sudo systemctl show -p ActiveState --value nftables)" = "active"
test "$(sudo systemctl show -p UnitFileState --value nftables)" = "enabled"
test "$(sudo systemctl show -p ActiveState --value firewalld)" = "inactive"
test "$(sudo systemctl show -p UnitFileState --value firewalld)" = "disabled"
sudo nft -nn list chain inet filter input

In the displayed chain, verify policy drop, the accept rule for TCP/22, the accept rule for TCP/80, and the drop rule for TCP/8080. No accept rule is created for TCP/8080.

Step 7: Verify that new SSH connections are allowed

Section titled “Step 7: Verify that new SSH connections are allowed”

From another terminal, verify that a new connection can be established to TCP/22.

Terminal window
nc -vz <<SERVER_IP>> 22

If you can connect to TCP/22, a new administrative connection path remains available after the filter is applied.

Step 8: Verify HTTP communication on TCP/80

Section titled “Step 8: Verify HTTP communication on TCP/80”

Send an HTTP request to the allowed TCP/80 port and retrieve the sample content.

Terminal window
curl --connect-timeout 3 --max-time 5 --fail http://<<SERVER_IP>>/

If you receive an HTTP response, the accept rule for TCP/80 is working.

Step 9: Verify that TCP/8080 is blocked by nftables

Section titled “Step 9: Verify that TCP/8080 is blocked by nftables”

The same Apache service is also listening on TCP/8080, but this port is not allowed by nftables. Send an HTTP request to TCP/8080.

Terminal window
curl --connect-timeout 3 --max-time 5 --fail http://<<SERVER_IP>>:8080/

The success condition for this check is that the command fails to connect. Because TCP/80 is reachable while only TCP/8080 on the same server is unreachable, you can verify the difference in the input filter rather than merely the presence or absence of a service.

Step 10: Verify that the counters recorded the traffic

Section titled “Step 10: Verify that the counters recorded the traffic”

After generating traffic on TCP/22, TCP/80, and TCP/8080, verify that the packet counter for each corresponding rule is greater than 0.

Terminal window
set -e
RULES="$(sudo nft -nn list chain inet filter input)"
printf '%s\n' "$RULES"
printf '%s\n' "$RULES" | grep -E 'tcp dport 22 counter packets [1-9][0-9]*'
printf '%s\n' "$RULES" | grep -E 'tcp dport 80 counter packets [1-9][0-9]*'
printf '%s\n' "$RULES" | grep -E 'tcp dport 8080 counter packets [1-9][0-9]*'

If the counters have increased, this confirms not only the displayed configuration but also that actual packets passed through the intended rules.

To verify that the ruleset is loaded from /etc/sysconfig/nftables.conf and that automatic startup of the nftables service works, reboot the server.

Terminal window
sudo systemctl reboot

After the reboot, reconnect to the server and continue with the following checks.

Verify that nftables is still enabled after the reboot, that firewalld remains stopped, and that the contents of the input chain have been restored.

Terminal window
set -e
test "$(sudo systemctl show -p ActiveState --value nftables)" = "active"
test "$(sudo systemctl show -p UnitFileState --value nftables)" = "enabled"
test "$(sudo systemctl show -p ActiveState --value firewalld)" = "inactive"
test "$(sudo systemctl show -p UnitFileState --value firewalld)" = "disabled"
sudo nft -nn list chain inet filter input

Step 13: Verify that TCP/80 is still allowed after reboot

Section titled “Step 13: Verify that TCP/80 is still allowed after reboot”

Connect to the automatically started HTTP service over TCP/80.

Terminal window
curl --connect-timeout 3 --max-time 5 --fail http://<<SERVER_IP>>/

Step 14: Verify that TCP/8080 is still blocked after reboot

Section titled “Step 14: Verify that TCP/8080 is still blocked after reboot”

Finally, connect to TCP/8080 and verify that it remains blocked just as it was before the reboot.

Terminal window
curl --connect-timeout 3 --max-time 5 --fail http://<<SERVER_IP>>:8080/

If TCP/80 is reachable and TCP/8080 remains blocked, both ruleset persistence and restoration at system startup have been confirmed.

In this configuration, we stopped firewalld, consolidated firewall management under nftables, and created a minimal input filter using table, a base chain, accept/drop rule, counter, and a default drop policy.

In operation, prepare the rules that allow existing connections and administrative SSH access first, and check the syntax with nft -c before applying them. In addition, by testing both allowed and blocked ports from outside and checking the counters and ruleset after a reboot, you can verify the actual effective state, which cannot be determined from the configuration file contents alone.

Category: Linux