When nftables silently drops HTTP traffic: a step‑by‑step port‑filter investigation

HTTP is the backbone of most web services. When a client can’t reach a web server, the first instinct is to look at the application logs, DNS, or the network path. A silent drop by nftables is a common, yet often overlooked, culprit. This post walks through a systematic approach to uncovering why nftables is silently discarding HTTP packets, with a focus on the port‑filtering rules that most administrators set up for hardening.

1. Verify the symptom with tcpdump

Before diving into nftables, confirm that packets are arriving at the host and that the drop is happening locally.

# Capture only TCP traffic destined for port 80 or 443
sudo tcpdump -i eth0 -nn -v tcp port 80 or 443

If you see SYN packets from the client but no ACKs from the server, the problem is likely on the server side. If you see no traffic at all, the issue could be upstream (firewall, NAT, or ISP). The key is to isolate the drop to the server’s nftables.

2. Check the default policy on the inet table

Modern distributions ship with the inet table pre‑loaded. A default drop policy on the input chain will silently drop any packet that doesn’t match an earlier rule.

sudo nft list table inet

Look for a line like:

chain input { type filter hook input priority 0; policy drop; }

If the policy is drop, you must explicitly allow HTTP traffic. A common mistake is to add a rule to the filter table instead of inet, or to forget to add a rule to the input chain.

3. Inspect the port‑filter chain

Many hardening guides create a dedicated port-filter chain to keep the main chain tidy. Verify that the chain exists and that it contains the expected rules.

sudo nft list chain inet filter port-filter

Typical rules for HTTP/HTTPS look like:

# Allow HTTP
tcp dport 80 accept
# Allow HTTPS
tcp dport 443 accept

If these rules are missing or commented out, packets will fall through to the default drop policy.

4. Confirm rule order and priorities

nftables evaluates rules in the order they appear. A more general rule placed before the specific HTTP rules can swallow the traffic. For example:

# This rule will drop all TCP traffic before the HTTP rules run
tcp dport != 80,443 drop

If such a rule exists, move it after the HTTP accept rules or delete it.

5. Test with a minimal rule set

Create a temporary rule set that only allows HTTP/HTTPS and drops everything else. This isolates the problem.

sudo nft flush chain inet filter input
sudo nft add rule inet filter input tcp dport 80 accept
sudo nft add rule inet filter input tcp dport 443 accept
sudo nft add rule inet filter input drop

Try accessing the web server again. If it works, the issue lies in the original rule set’s ordering or missing rules. If it still fails, the problem may be elsewhere (e.g., SELinux, AppArmor, or the application itself).

6. Examine stateful inspection rules

nftables can track connection state. A common pattern is to allow established connections but drop new ones unless they match specific ports.

sudo nft add rule inet filter input ct state established,related accept
sudo nft add rule inet filter input ct state new tcp dport 80 accept
sudo nft add rule inet filter input ct state new tcp dport 443 accept
sudo nft add rule inet filter input drop

If the ct state established,related accept rule is missing, a return packet from the server may be dropped, causing the client to time out. Ensure that the connection tracking module is loaded:

sudo modprobe nf_conntrack

7. Check for IPv6 interference

If the server has both IPv4 and IPv6, a rule that only allows IPv4 traffic will silently drop IPv6 packets. Verify that you have corresponding rules in the inet table, which handles both families, or duplicate the rules in the ip6 table.

sudo nft list table inet
# Look for tcp dport 80 accept in both ip and ip6 families

8. Look for masquerading or NAT rules that interfere

Sometimes a NAT rule can rewrite the source or destination address, causing the packet to be misrouted or dropped later. Inspect the nat table:

sudo nft list table nat

If you have a masquerade rule that rewrites traffic destined for 80/443, it may be overriding the port filter. Adjust the rule or add a meta l4proto tcp condition to limit it.

9. Verify that the service is listening on the expected interface

A common oversight is that the web server is bound to 127.0.0.1 or a specific interface, while nftables allows traffic on all interfaces. Use ss or netstat to confirm:

sudo ss -tuln | grep ':80\|:443'

If the server is only listening on 127.0.0.1, external traffic will never reach it, regardless of nftables. Bind the service to 0.0.0.0 or the correct interface.

10. Enable nftables logging for debugging

Add a temporary log rule before the drop rule to see what packets are being discarded.

sudo nft add rule inet filter input log prefix "nft-drop: " level warning
sudo nft add rule inet filter input drop

Check /var/log/syslog or journalctl -f for log entries. The log will show the packet’s source, destination, protocol, and port. This is invaluable for confirming that the drop is indeed happening at the nftables layer.

11. Consider the impact of SELinux or AppArmor

Even if nftables allows the packet, SELinux or AppArmor can silently block the connection. Temporarily set SELinux to permissive mode to test:

sudo setenforce 0

If the HTTP traffic starts working, the issue is an SELinux policy. Re‑enable enforcing mode:

sudo setenforce 1

Similarly, check AppArmor profiles for the web server (/etc/apparmor.d/usr.sbin.nginx, for example).

12. Persist the rule set

Once you’ve identified the missing or misordered rule, add it to the persistent configuration. On most distributions, nftables loads /etc/nftables.conf at boot. Edit that file:

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        tcp dport 80 accept
        tcp dport 443 accept
        drop
    }
}

Reload the configuration:

sudo nft -f /etc/nftables.conf

13. Automate sanity checks

Create a small script that verifies the presence of critical rules and alerts you if they’re missing. For example:

#!/usr/bin/env bash
set -euo pipefail

REQUIRED_RULES=(
    "tcp dport 80 accept"
    "tcp dport 443 accept"
    "ct state established,related accept"
)

for rule in "${REQUIRED_RULES[@]}"; do
    if ! sudo nft list chain inet filter input | grep -q "$rule"; then
        echo "Missing rule: $rule" >&2
        exit 1
    fi
done

echo "All critical rules present."

Schedule it with cron or systemd to run at boot.

14. Trade‑offs and best practices

ApproachProsCons
Explicit accept rules for each serviceClear intent, easy to auditMore rules, potential for ordering errors
Stateful tracking onlyMinimal rule setRequires conntrack module; may miss new connections
Logging before dropImmediate visibilityGenerates noise; requires log rotation
Using inet tableHandles IPv4/IPv6 uniformlySlightly more complex syntax

For most production systems, a hybrid approach works best: stateful tracking for established traffic, explicit accept rules for critical services, and a default drop policy. Logging should be enabled only during troubleshooting to avoid log bloat.

15. Common pitfalls to avoid

PitfallFix
Rules in the wrong table (filter vs inet)Use nft list table inet to verify
Missing ct state established,related acceptAdd it before port rules
Overly broad drop rule before specific acceptsRe‑order or delete the broad rule
IPv6 traffic not coveredDuplicate rules in inet or ip6 tables
Service bound to localhostReconfigure the web server to listen on the correct interface

16. Quick checklist

  1. Confirm traffic reaches the host – tcpdump.
  2. Verify default policy – nft list table inet.
  3. Inspect port‑filter chain – nft list chain inet filter port-filter.
  4. Check rule order – look for broad drops before accepts.
  5. Test minimal rule set – isolate the issue.
  6. Verify stateful rules – ct state entries.
  7. Check IPv6 coverage – inet table handles both.
  8. Look for NAT interference – nft list table nat.
  9. Confirm service listening – ss -tuln.
  10. Enable logging if needed – log rule before drop.
  11. Check SELinux/AppArmor – permissive mode test.
  12. Persist changes – /etc/nftables.conf.
  13. Automate sanity checks – script + cron.

By following this step‑by‑step workflow, you can quickly pinpoint why nftables is silently dropping HTTP traffic and apply a robust, auditable rule set that balances security with operational reliability.

See also