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 notation
Section titled “Variable notation”| Variable name | Example value | Description |
|---|---|---|
<<SERVER_IP>> | 192.0.2.10 | IP address of the server on which nftables is configured |
Step 1: Install the required packages
Section titled “Step 1: Install the required packages”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.
sudo dnf install -y nftables httpd policycoreutils-python-utilsStep 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.
sudo semanage port -a -t http_port_t -p tcp 8080 2>/dev/null || sudo semanage port -m -t http_port_t -p tcp 8080printf '%s\n' 'nftables packet filter example' | sudo tee /var/www/html/index.html >/dev/nullprintf '%s\n' 'Listen 8080' | sudo tee /etc/httpd/conf.d/listen-8080.conf >/dev/nullsudo httpd -tsudo systemctl enable --now httpdsudo systemctl is-active httpdAt 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.
sudo systemctl disable --now firewalldIn 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.
Step 4: Create a minimal ruleset
Section titled “Step 4: Create a minimal ruleset”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.
sudo cp -a /etc/sysconfig/nftables.conf /etc/sysconfig/nftables.conf.baksudo install -d -m 0755 /etc/nftablessudo 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 }}EOFprintf '%s\n' 'include "/etc/nftables/main.nft"' | sudo tee /etc/sysconfig/nftables.conf >/dev/nullsudo nft -c -f /etc/nftables/main.nftnft -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.
sudo nft -f /etc/nftables/main.nftsudo systemctl enable --now nftablesStep 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.
set -etest "$(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 inputIn 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.
nc -vz <<SERVER_IP>> 22If 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.
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.
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.
set -eRULES="$(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.
Step 11: Reboot and verify persistence
Section titled “Step 11: Reboot and verify persistence”To verify that the ruleset is loaded from /etc/sysconfig/nftables.conf and that automatic startup of the nftables service works, reboot the server.
sudo systemctl rebootAfter the reboot, reconnect to the server and continue with the following checks.
Step 12: Verify the ruleset after reboot
Section titled “Step 12: Verify the ruleset after reboot”Verify that nftables is still enabled after the reboot, that firewalld remains stopped, and that the contents of the input chain have been restored.
set -etest "$(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 inputStep 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.
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.
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.
Summary
Section titled “Summary”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.