Skip to main content

Linux Firewalls: Controlling Traffic with iptables and nftables

Linux Learning Lab
Author
Linux Learning Lab
Writing about code, tools, and workflows.
Table of Contents
linux-networking - This article is part of a series.
Part 2: This Article

What is a Firewall?
#

A firewall inspects network traffic and decides what to allow or block based on rules you define. On Linux, the firewall operates in the kernel’s netfilter framework — iptables and nftables are the tools you use to configure it.

Every packet entering, leaving, or passing through your machine is evaluated against your rules. No rule match means the default policy applies.

iptables vs nftables
#

iptablesnftables
StatusLegacy (still widely used)Modern replacement
SyntaxSeparate commands per ruleStructured, scriptable
PerformanceSlower with many rulesBetter with large rulesets
Default onOlder systems, Ubuntu < 22.04Fedora, RHEL 9+, Debian 11+

Both do the same job. iptables is still everywhere in documentation and production, so you need to know it. nftables is the future. This post covers both.

iptables Fundamentals
#

Tables and chains
#

iptables organizes rules into tables, each containing chains:

TablePurposeCommon Chains
filterAccept/drop traffic (default)INPUT, OUTPUT, FORWARD
natRewrite addresses/portsPREROUTING, POSTROUTING, OUTPUT
mangleModify packet headersAll

The filter table is what you use 90% of the time:

ChainHandles traffic…
INPUTComing into this machine
OUTPUTGoing out from this machine
FORWARDPassing through this machine (routing)

How rules are evaluated
#

Packets hit chains top-to-bottom. The first matching rule wins:

Packet arrives → INPUT chain
  Rule 1: Allow SSH → no match, continue
  Rule 2: Allow HTTP → MATCH → accept
  Rule 3: Drop all → never reached for this packet

If no rule matches, the chain’s default policy applies.

Viewing current rules
#

# Show all rules (filter table)
sudo iptables -L -n -v

# Show with line numbers
sudo iptables -L -n --line-numbers

# Show NAT rules
sudo iptables -t nat -L -n
FlagMeaning
-LList rules
-nNumeric output (don’t resolve DNS)
-vVerbose (show counters)
--line-numbersShow rule positions

Setting the default policy
#

# Drop everything by default (whitelist approach — recommended)
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

Warning: Setting INPUT to DROP without first allowing SSH will lock you out of a remote server. Always add your allow rules first.

Adding rules
#

# Allow established/related connections (critical — lets responses come back)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow loopback (localhost)
sudo iptables -A INPUT -i lo -j ACCEPT

# Allow SSH (port 22)
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# Allow HTTP and HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# Allow ping
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

# Drop everything else (explicit, even if policy is DROP)
sudo iptables -A INPUT -j DROP
OptionMeaning
-A INPUTAppend to the INPUT chain
-p tcpMatch TCP protocol
--dport 22Match destination port 22
-j ACCEPTJump to ACCEPT (allow the packet)
-j DROPSilently discard the packet
-j REJECTDiscard and send an error back
-i loMatch traffic on the loopback interface
-m conntrackUse connection tracking module
--ctstateMatch connection state

Deleting rules
#

# Delete by line number
sudo iptables -D INPUT 3

# Delete by specification (exact match)
sudo iptables -D INPUT -p tcp --dport 80 -j ACCEPT

# Flush all rules in a chain
sudo iptables -F INPUT

# Flush everything
sudo iptables -F

Inserting rules at a position
#

# Insert at position 1 (top of chain)
sudo iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT

Allowing traffic from a specific IP or subnet
#

# Allow all traffic from a trusted IP
sudo iptables -A INPUT -s 192.168.1.100 -j ACCEPT

# Allow SSH only from a subnet
sudo iptables -A INPUT -s 10.0.0.0/24 -p tcp --dport 22 -j ACCEPT

# Block a specific IP
sudo iptables -A INPUT -s 203.0.113.50 -j DROP

Rate limiting
#

# Limit SSH to 3 new connections per minute (prevents brute force)
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m limit --limit 3/min --limit-burst 3 -j ACCEPT

nftables Fundamentals
#

nftables replaces iptables with a cleaner syntax. Instead of separate commands per rule, you define tables and chains in a structured format.

Viewing current rules
#

sudo nft list ruleset

Creating a basic firewall
#

# Create a table
sudo nft add table inet filter

# Create the input chain with a default drop policy
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }

# Create the output chain (allow by default)
sudo nft add chain inet filter output { type filter hook output priority 0 \; policy accept \; }

# Allow established connections
sudo nft add rule inet filter input ct state established,related accept

# Allow loopback
sudo nft add rule inet filter input iifname "lo" accept

# Allow SSH
sudo nft add rule inet filter input tcp dport 22 accept

# Allow HTTP/HTTPS
sudo nft add rule inet filter input tcp dport { 80, 443 } accept

# Allow ping
sudo nft add rule inet filter input icmp type echo-request accept

nftables advantages
#

# Multiple ports in one rule
sudo nft add rule inet filter input tcp dport { 22, 80, 443, 8080 } accept

# IP sets
sudo nft add set inet filter trusted_ips { type ipv4_addr \; }
sudo nft add element inet filter trusted_ips { 192.168.1.10, 192.168.1.20 }
sudo nft add rule inet filter input ip saddr @trusted_ips accept

# Rate limiting
sudo nft add rule inet filter input tcp dport 22 ct state new limit rate 3/minute accept

Deleting rules in nftables
#

# List rules with handles
sudo nft -a list chain inet filter input

# Delete by handle number
sudo nft delete rule inet filter input handle 7

# Flush a chain
sudo nft flush chain inet filter input

# Delete entire table
sudo nft delete table inet filter

nftables config file
#

Save your rules to a file for persistence:

# Export current rules
sudo nft list ruleset > /etc/nftables.conf

# Load rules from file
sudo nft -f /etc/nftables.conf

A complete /etc/nftables.conf:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        ct state established,related accept
        iifname "lo" accept

        tcp dport 22 accept
        tcp dport { 80, 443 } accept
        icmp type echo-request accept
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }

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

Enable on boot:

sudo systemctl enable nftables

Making iptables Rules Persistent
#

iptables rules are lost on reboot by default. To persist them:

Debian/Ubuntu
#

sudo apt install iptables-persistent

# Save current rules
sudo netfilter-persistent save

# Rules stored in:
# /etc/iptables/rules.v4
# /etc/iptables/rules.v6

RHEL/Fedora
#

# Save
sudo iptables-save > /etc/sysconfig/iptables

# Restore on boot (handled by iptables service)
sudo systemctl enable iptables

Manual save/restore
#

# Save to a file
sudo iptables-save > ~/firewall-rules.txt

# Restore from a file
sudo iptables-restore < ~/firewall-rules.txt

A Complete Server Firewall
#

iptables version
#

# Flush existing rules
sudo iptables -F
sudo iptables -X

# Default policies
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

# Allow established connections
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT

# Allow SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# Allow HTTP/HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# Allow ping
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

# Log dropped packets (optional, useful for debugging)
sudo iptables -A INPUT -j LOG --log-prefix "DROPPED: " --log-level 4

# Drop everything else
sudo iptables -A INPUT -j DROP

Checking logged drops
#

sudo journalctl -k | grep DROPPED
# or
dmesg | grep DROPPED

Common Patterns
#

Allow a service only from your network
#

# SSH only from local network
sudo iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT

# Database only from app servers
sudo iptables -A INPUT -s 10.0.1.0/24 -p tcp --dport 5432 -j ACCEPT

Block outgoing traffic to a host
#

sudo iptables -A OUTPUT -d 198.51.100.0/24 -j DROP

Port forwarding (NAT)
#

# Forward port 8080 to internal server on port 80
sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.10:80
sudo iptables -A FORWARD -p tcp -d 192.168.1.10 --dport 80 -j ACCEPT

Troubleshooting Firewalls
#

Check if a port is blocked
#

# From another machine
nc -zv yourserver.com 80

# Locally — check if something is listening
ss -tlnp | grep :80

If ss shows a listener but remote nc times out, the firewall is blocking it.

Watch packets hit rules
#

# Show packet and byte counters
sudo iptables -L -n -v

# Zero counters, wait, then check again
sudo iptables -Z
# ... send some traffic ...
sudo iptables -L -n -v

Temporarily disable the firewall
#

# iptables — accept everything
sudo iptables -P INPUT ACCEPT
sudo iptables -F

# nftables
sudo nft flush ruleset

Don’t forget to restore your rules after testing.

Best Practices
#

  • Always allow established/related connections first — without this, outbound connections won’t get responses back
  • Use a default DROP policy and explicitly allow what you need (whitelist approach)
  • Allow SSH before setting the default policy to DROP on remote servers — otherwise you lock yourself out
  • Use connection tracking (conntrack) rather than manually allowing return traffic
  • Log dropped packets during initial setup to see what’s being blocked unintentionally
  • Keep rules ordered — more specific rules first, broad catches at the end
  • Use nftables for new setups — it’s cleaner and better for complex rulesets
  • Persist your rules — iptables rules are lost on reboot unless explicitly saved
  • Test firewall changes with a timeout on remote servers: sudo bash -c "sleep 60 && iptables -F" & before applying restrictive rules
linux-networking - This article is part of a series.
Part 2: This Article