The most surprising thing about nftables’ drop and reject is that reject can actually be more of a security risk than drop if you’re not careful.
Let’s see it in action. Imagine we have a simple nftables rule that blocks incoming traffic on port 22 (SSH) from anywhere.
# Create a table and chain
nft add table ip filter
nft add chain ip filter input { type filter hook input priority 0 \; policy accept \; }
# Block SSH with drop
nft add rule ip filter input tcp dport 22 reject with icmp port-unreachable
# Try to connect (this will hang indefinitely)
ssh user@your_server_ip
You’ll notice your ssh command just hangs. It sends a packet, and it disappears into the void. The client doesn’t get any immediate feedback that the connection was refused.
Now, let’s change that to reject:
# Remove the old rule
nft delete rule ip filter input tcp dport 22 reject with icmp port-unreachable
# Block SSH with reject
nft add rule ip filter input tcp dport 22 reject with tcp reset
# Try to connect again
ssh user@your_server_ip
This time, your ssh command will fail almost immediately with a "connection refused" error. You get feedback.
So, what’s the difference, and why is reject sometimes worse?
At its core, nftables is a packet filtering framework. When a packet arrives and matches a rule, nftables performs the action specified by that rule. The two most common actions for blocking traffic are drop and reject.
-
drop: When a packet matches adroprule,nftablessilently discards it. The sender receives no notification that the packet was lost or blocked. From the sender’s perspective, the packet simply vanishes. This is often desirable for security because it doesn’t give an attacker any information about the state of your firewall or network. They can’t tell if a host is alive or if a port is open or closed. -
reject: When a packet matches arejectrule,nftablesdiscards the packet and sends an error message back to the sender. The type of error message depends on the protocol. For TCP, it’s typically aRST(reset) packet. For UDP or ICMP, it’s often anICMP Port Unreachablemessage. This provides immediate feedback to the sender, confirming that the packet was received but intentionally refused.
The default policy in nftables (and firewalls in general) is crucial. If your input chain’s policy is accept, you must explicitly create rules to block unwanted traffic. If your input policy is drop, you must explicitly create rules to allow desired traffic.
Here’s where the counter-intuitive part of reject comes into play. While reject seems more "polite" by informing the sender, this feedback can be exploited. An attacker can use reject to perform active reconnaissance. By sending packets to various ports and observing the reject responses (like TCP RST or ICMP Port Unreachable), they can quickly determine which hosts are alive on your network and which ports are open or closed. This is known as a "stealth scan" or "ack scan" avoidance by the attacker. A drop policy, on the other hand, makes it much harder for an attacker to map your network because the silence provides no confirmation.
Consider a scenario where you’re trying to protect a sensitive server. You want to block all incoming traffic except for a few specific ports.
Scenario 1: Default Policy accept, drop for blocking
# Table and chain setup
nft add table ip filter
nft add chain ip filter input { type filter hook input priority 0 \; policy accept \; }
# Allow SSH
nft add rule ip filter input tcp dport 22 accept
# Block everything else by default (implicit in policy accept, but explicit is good)
# This rule effectively becomes the "reject" or "drop" for anything not explicitly allowed.
# We'll use drop here for maximum stealth.
nft add rule ip filter input counter drop
In this setup, any packet arriving on the input chain that doesn’t match the tcp dport 22 accept rule will hit the counter drop rule and be silently discarded. An attacker probing this server won’t get any immediate indication that their packets are being dropped on other ports. They might eventually time out, but they won’t get a definitive "no such port" response.
Scenario 2: Default Policy accept, reject for blocking
# Table and chain setup
nft add table ip filter
nft add chain ip filter input { type filter hook input priority 0 \; policy accept \; }
# Allow SSH
nft add rule ip filter input tcp dport 22 accept
# Block everything else by default with reject
nft add rule ip filter input counter reject with icmp port-unreachable
Here, any packet not matching the SSH rule will be rejected with an ICMP message. An attacker can then scan your server’s IP address, send UDP packets to various ports, and receive ICMP "Port Unreachable" messages for closed ports. This confirms the host is up and allows them to build a profile of your open services faster.
The Safest Default: drop
For most security-conscious environments, the recommended approach is to set the default policy for your input and forward chains to drop and then explicitly create rules to accept only the traffic you need.
# Table and chain setup with default drop policy
nft add table ip filter
nft add chain ip filter input { type filter hook input priority 0 \; policy drop \; }
nft add chain ip filter forward { type filter hook forward priority 0 \; policy drop \; }
# Allow SSH
nft add rule ip filter input tcp dport 22 accept
# Allow established/related connections (crucial for return traffic)
nft add rule ip filter input ct state established,related accept
# Allow loopback traffic
nft add rule ip filter input iif lo accept
# Now, any traffic not explicitly allowed by these rules will be dropped by the default policy.
When your default policy is drop, any packet that doesn’t match an accept rule is silently discarded. This is the most secure posture because it reveals the least amount of information to potential attackers.
The specific error message sent by reject can also be a vector for information leakage. For example, a TCP RST packet inherently tells the sender that the port is closed (or that the connection is being actively refused by a host). An ICMP "Port Unreachable" message for UDP traffic serves a similar purpose. While seemingly helpful for legitimate network diagnostics, this information is equally valuable to an attacker.
The one thing most people don’t realize is that the choice between drop and reject isn’t just about how your firewall behaves; it’s about how much information you’re revealing about your network’s topology and services to the outside world. A silent drop policy is a fundamental layer of obscurity, making it harder for attackers to even know what they’re attacking.
The next problem you’ll likely encounter is managing stateful connections, which is where the ct state established,related accept rule becomes essential.