Firewalls: iptables & nftables

Why This Matters

You have just deployed a web server. It is serving pages beautifully on port 80 and 443. But it is also running MySQL on port 3306, an unprotected Redis on port 6379, and SSH on port 22 -- all exposed to the entire internet. Within hours, bots will find the open ports. Within days, someone will brute-force your SSH or exploit your unprotected Redis.

A firewall is the gatekeeper that decides which network traffic is allowed in, allowed out, and allowed to pass through your machine. On Linux, that gatekeeper lives inside the kernel itself, and it is called Netfilter. Everything in this chapter -- iptables, nftables, ufw, firewalld -- is just a different way to talk to Netfilter.


Try This Right Now

# Check if iptables rules exist
sudo iptables -L -n -v

# Check if nftables rules exist
sudo nft list ruleset

# Check if ufw is active (Ubuntu/Debian)
sudo ufw status

# Check if firewalld is active (RHEL/Fedora)
sudo firewall-cmd --state

At least one of these should show you the current firewall state of your system. If all of them return empty results or "inactive," your machine currently has no firewall rules -- all traffic is allowed.


The Netfilter Framework

Netfilter is the packet-filtering framework built into the Linux kernel. It provides hooks at various points in the networking stack where code can inspect and manipulate packets.

                        Incoming Packet
                              |
                              v
                      +---------------+
                      |  PREROUTING   |  (NAT, mangle)
                      +-------+-------+
                              |
                    +----Is it for us?----+
                    |                     |
                    v                     v
            +-------+-------+    +-------+-------+
            |    INPUT      |    |    FORWARD     |
            | (filter)      |    | (filter)       |
            +-------+-------+    +-------+-------+
                    |                     |
                    v                     v
             Local Process         +-----+------+
                    |               | POSTROUTING|
                    v               |  (NAT)     |
            +-------+-------+      +-----+------+
            |    OUTPUT     |            |
            | (filter, NAT) |            v
            +-------+-------+     Outgoing Packet
                    |
                    v
            +-------+-------+
            | POSTROUTING   |
            |  (NAT)        |
            +-------+-------+
                    |
                    v
             Outgoing Packet

The five hooks (also called chains in iptables) are:

ChainWhen it fires
PREROUTINGAs soon as a packet arrives, before routing decision
INPUTPacket is destined for this machine
FORWARDPacket is passing through this machine to another
OUTPUTPacket originates from this machine
POSTROUTINGJust before a packet leaves this machine

iptables: The Classic Firewall

iptables has been the standard Linux firewall tool since 2001. Even though nftables is its successor, iptables remains widely deployed and its concepts are essential knowledge.

Tables and Chains

iptables organizes rules into tables, each containing chains:

+------------------------------------------------------------------+
|  Table      | Purpose                  | Chains                  |
|-------------|--------------------------|-------------------------|
|  filter     | Packet filtering         | INPUT, FORWARD, OUTPUT  |
|             | (default table)          |                         |
|  nat        | Network Address          | PREROUTING, OUTPUT,     |
|             | Translation              | POSTROUTING             |
|  mangle     | Packet alteration        | All five chains         |
|  raw        | Connection tracking      | PREROUTING, OUTPUT      |
|             | exemptions               |                         |
+------------------------------------------------------------------+

Most of the time you will work with the filter table (the default) and occasionally the nat table.

Rule Anatomy

Every iptables rule has this structure:

iptables -t <table> -A <chain> <match-conditions> -j <target>

Where:

  • -t <table>: Which table (default: filter)
  • -A <chain>: Append to which chain
  • Match conditions: What to match (source, destination, port, protocol, etc.)
  • -j <target>: What to do with matching packets

Common targets:

TargetAction
ACCEPTAllow the packet through
DROPSilently discard the packet (sender gets no response)
REJECTDiscard and send an error back (connection refused)
LOGLog the packet, then continue processing the next rule
MASQUERADEReplace source IP with the outgoing interface's IP (NAT)

Viewing Current Rules

# List all rules in the filter table with line numbers
sudo iptables -L -n -v --line-numbers

# List rules in the nat table
sudo iptables -t nat -L -n -v

# List rules in the raw iptables format (best for scripts)
sudo iptables-save

The -n flag prevents DNS lookups (much faster), and -v shows packet/byte counters.

Essential iptables Examples

Allow established connections (stateful firewall)

This is almost always the first rule you should add:

sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

This allows return traffic for connections your machine initiated, and related traffic like ICMP error messages.

Allow SSH

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 loopback traffic

sudo iptables -A INPUT -i lo -j ACCEPT

Allow ping (ICMP)

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

Allow SSH only from a specific network

sudo iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT

Drop everything else (default deny)

sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

Safety Warning: Setting the INPUT policy to DROP before adding an ACCEPT rule for SSH will immediately lock you out of a remote server. Always add your allow rules first, then set the default policy.

Think About It: Why do we set OUTPUT policy to ACCEPT instead of DROP? Think about what would break if outgoing traffic were blocked by default.

If you set OUTPUT to DROP, your server could not make DNS queries, download package updates, reach NTP servers, or initiate any outgoing connection. While a DROP OUTPUT policy is more secure, it requires carefully whitelisting every outbound connection your server needs, which is difficult to maintain.

A Complete iptables Setup for a Web Server

#!/bin/bash
# Flush existing rules
sudo iptables -F
sudo iptables -t nat -F

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

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

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

# Allow SSH (from management network only)
sudo iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/24 -j ACCEPT

# Allow HTTP and HTTPS from anywhere
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 "iptables-dropped: " --log-level 4

Inserting, Deleting, and Replacing Rules

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

# Delete a rule by specification
sudo iptables -D INPUT -p tcp --dport 8080 -j ACCEPT

# Delete a rule by line number
sudo iptables -D INPUT 3

# Replace a rule at position 2
sudo iptables -R INPUT 2 -p tcp --dport 8443 -j ACCEPT

Saving and Restoring Rules

iptables rules are stored in kernel memory and are lost on reboot. You must save them explicitly.

# Save current rules to a file
sudo iptables-save > /etc/iptables/rules.v4

# Restore rules from a file
sudo iptables-restore < /etc/iptables/rules.v4

Distro Note:

  • Debian/Ubuntu: Install iptables-persistent to auto-load rules on boot: sudo apt install iptables-persistent Rules are stored in /etc/iptables/rules.v4 and /etc/iptables/rules.v6.

  • RHEL/CentOS 7: Use systemctl enable iptables and save with sudo service iptables save. Rules go to /etc/sysconfig/iptables.

  • RHEL/CentOS 8+, Fedora: firewalld is the default; if you use raw iptables, disable firewalld first.


nftables: The Modern Successor

nftables replaces iptables, ip6tables, arptables, and ebtables with a single unified framework. It is the default on Debian 10+, RHEL 8+, and Fedora.

Why nftables?

  • Single tool for IPv4, IPv6, ARP, and bridging (no more separate ip6tables)
  • Better syntax -- more readable and consistent
  • Better performance -- rules are compiled into a virtual machine in the kernel
  • Atomic rule updates -- load an entire ruleset at once, not rule by rule
  • Built-in sets and maps -- native support for collections of addresses/ports

nft Basics

# List the entire ruleset
sudo nft list ruleset

# List tables
sudo nft list tables

# Flush all rules
sudo nft flush ruleset

nftables Structure

nftables uses a hierarchy: tables contain chains, which contain rules.

ruleset
  +-- table inet filter
        +-- chain input
        |     +-- rule: accept established
        |     +-- rule: accept ssh
        |     +-- rule: drop
        +-- chain forward
        +-- chain output

The inet family handles both IPv4 and IPv6 simultaneously.

Creating Rules with nft

# Create a table
sudo nft add table inet filter

# Create a chain (base chain attached to a hook)
sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }

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

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

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

# Allow HTTP and 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
sudo nft add rule inet filter input icmpv6 type echo-request accept

Notice how nftables uses sets ({ 80, 443 }) natively -- no need for multiport match extensions like in iptables.

A Complete nftables Configuration File

The cleanest way to use nftables is with a configuration file. Create /etc/nftables.conf:

#!/usr/sbin/nft -f

flush ruleset

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

        # Allow loopback
        iif lo accept

        # Allow established/related connections
        ct state established,related accept

        # Drop invalid connections
        ct state invalid drop

        # Allow ICMP and ICMPv6
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # Allow SSH from management network
        ip saddr 10.0.0.0/24 tcp dport 22 accept

        # Allow HTTP and HTTPS
        tcp dport { 80, 443 } accept

        # Log everything else
        log prefix "nftables-dropped: " counter drop
    }

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

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

Load it:

sudo nft -f /etc/nftables.conf

# Enable nftables service to load on boot
sudo systemctl enable nftables

Named Sets

One of the most powerful nftables features is named sets:

table inet filter {
    set allowed_ssh {
        type ipv4_addr
        elements = { 10.0.0.5, 10.0.0.10, 192.168.1.0/24 }
    }

    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        ip saddr @allowed_ssh tcp dport 22 accept
        tcp dport { 80, 443 } accept
    }
}

You can dynamically add and remove elements from a set:

# Add an address to the set
sudo nft add element inet filter allowed_ssh { 10.0.0.20 }

# Remove an address
sudo nft delete element inet filter allowed_ssh { 10.0.0.20 }

Think About It: You are migrating from iptables to nftables. You have 200 iptables rules across three scripts. Is there a way to translate them automatically?

Yes! The iptables-translate command converts iptables rules to nftables syntax:

iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# Output: nft add rule ip filter INPUT tcp dport 22 counter accept

And iptables-restore-translate converts an entire iptables-save dump:

iptables-save | iptables-restore-translate

ufw: Uncomplicated Firewall (Ubuntu/Debian)

ufw is a user-friendly frontend to iptables/nftables. It is the default firewall management tool on Ubuntu.

# Enable ufw
sudo ufw enable

# Check status
sudo ufw status verbose

# Allow SSH (always do this BEFORE enabling!)
sudo ufw allow ssh

# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Allow from a specific network
sudo ufw allow from 10.0.0.0/24 to any port 22

# Deny a specific port
sudo ufw deny 3306/tcp

# Delete a rule
sudo ufw delete allow 80/tcp

# Reset to defaults
sudo ufw reset

# Show numbered rules for deletion
sudo ufw status numbered
sudo ufw delete 3

Safety Warning: Always sudo ufw allow ssh (or sudo ufw allow 22/tcp) before running sudo ufw enable on a remote server. Enabling ufw without an SSH rule will lock you out immediately.


firewalld: Zone-Based Firewall (RHEL/Fedora)

firewalld is the default on RHEL, CentOS, and Fedora. It uses the concept of zones to group interfaces and apply different trust levels.

+--------------------------------------------------------------+
|  Zone          | Default behavior   | Typical use            |
|----------------|--------------------|-----------------------|
|  drop          | Drop all incoming  | Maximum restriction   |
|  block         | Reject all incoming| Similar to drop       |
|  public        | Reject, allow SSH  | Default zone          |
|  external      | Masquerade, SSH    | External-facing NAT   |
|  dmz           | Allow SSH only     | DMZ servers           |
|  work          | Allow SSH, some    | Work network          |
|  home          | Allow more         | Home network          |
|  internal      | Allow more         | Internal LAN          |
|  trusted       | Accept all         | Maximum trust         |
+--------------------------------------------------------------+

firewalld Commands

# Check state
sudo firewall-cmd --state

# Show default zone
sudo firewall-cmd --get-default-zone

# List all zones and their settings
sudo firewall-cmd --list-all-zones

# List current zone rules
sudo firewall-cmd --list-all

# Add a service (temporary, until reload)
sudo firewall-cmd --add-service=http

# Add a service permanently
sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-service=https --permanent

# Add a specific port
sudo firewall-cmd --add-port=8080/tcp --permanent

# Allow from a specific source
sudo firewall-cmd --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" port port="22" protocol="tcp" accept' --permanent

# Reload to apply permanent changes
sudo firewall-cmd --reload

# Remove a service
sudo firewall-cmd --remove-service=http --permanent
sudo firewall-cmd --reload

Distro Note: On RHEL/Fedora systems, avoid using raw iptables/nftables commands when firewalld is running -- they will conflict. Either use firewalld exclusively or disable it and manage rules directly.


Hands-On: Build a Firewall for an SSH-Only Server

Let's build a lockdown firewall that only allows SSH. We will do this three ways.

Method 1: iptables

# Flush
sudo iptables -F

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

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

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

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

# Verify
sudo iptables -L -n -v

Method 2: nftables

sudo nft flush ruleset
sudo nft add table inet filter
sudo nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
sudo nft add rule inet filter input iif lo accept
sudo nft add rule inet filter input ct state established,related accept
sudo nft add rule inet filter input tcp dport 22 accept

# Verify
sudo nft list ruleset

Method 3: ufw

sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw enable

# Verify
sudo ufw status verbose

All three produce effectively the same result: only SSH on port 22 is allowed in, everything else is dropped.


NAT with iptables/nftables

Network Address Translation allows machines on a private network to reach the internet through a Linux gateway.

Source NAT (Masquerade) with iptables

# Enable IP forwarding
sudo sysctl -w net.ipv4.ip_forward=1

# Masquerade outgoing traffic on eth0
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# Allow forwarding from internal network
sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o eth1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Port Forwarding (DNAT) with iptables

Forward incoming traffic on port 8080 to an internal server:

sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.50:80
sudo iptables -A FORWARD -p tcp -d 10.0.0.50 --dport 80 -j ACCEPT

NAT with nftables

table ip nat {
    chain prerouting {
        type nat hook prerouting priority -100;
        tcp dport 8080 dnat to 10.0.0.50:80
    }

    chain postrouting {
        type nat hook postrouting priority 100;
        oifname "eth0" masquerade
    }
}

Debug This

Scenario: You set up a firewall on your web server. Users report that the website is not loading, but you can SSH in just fine. You check:

$ sudo iptables -L -n --line-numbers
Chain INPUT (policy DROP)
num  target  prot  opt  source       destination
1    ACCEPT  all   --   0.0.0.0/0    0.0.0.0/0    ctstate ESTABLISHED,RELATED
2    ACCEPT  tcp   --   0.0.0.0/0    0.0.0.0/0    tcp dpt:22
3    ACCEPT  tcp   --   0.0.0.0/0    0.0.0.0/0    tcp dpt:443

$ curl -I http://localhost
HTTP/1.1 200 OK

The web server is running (curl from localhost works), and HTTPS (443) is allowed. But users still cannot load the site via HTTP. What is wrong?

Diagnosis: Port 80 (HTTP) is not in the rules. Many users will type http:// or their browser will try HTTP first. There is a rule for port 443 but not for port 80.

Fix:

sudo iptables -I INPUT 3 -p tcp --dport 80 -j ACCEPT

Or if the web server should redirect HTTP to HTTPS, you still need to allow port 80 so the redirect can happen:

sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT

What Just Happened?

+-------------------------------------------------------------------+
|                     Chapter 34 Recap                               |
+-------------------------------------------------------------------+
|                                                                   |
|  * Netfilter is the kernel framework. iptables and nftables are   |
|    userspace tools that configure it.                             |
|                                                                   |
|  * iptables uses tables (filter, nat, mangle) and chains          |
|    (INPUT, OUTPUT, FORWARD, PREROUTING, POSTROUTING).             |
|                                                                   |
|  * Rules match packets and jump to targets: ACCEPT, DROP,         |
|    REJECT, LOG, MASQUERADE.                                       |
|                                                                   |
|  * nftables is the modern replacement. It offers a cleaner        |
|    syntax, native sets, atomic updates, and unified IPv4/IPv6.    |
|                                                                   |
|  * ufw (Ubuntu) and firewalld (RHEL/Fedora) are user-friendly    |
|    frontends -- they still use Netfilter under the hood.          |
|                                                                   |
|  * ALWAYS add SSH allow rules before setting default DROP policy. |
|                                                                   |
|  * Save your rules! iptables rules vanish on reboot unless        |
|    you explicitly save them.                                      |
|                                                                   |
+-------------------------------------------------------------------+

Try This

  1. Lockdown exercise: Starting from a machine with no firewall rules, build a firewall that allows only SSH from your network and ping. Test by scanning yourself with nmap from another machine.

  2. iptables to nftables: Write five iptables rules, save them with iptables-save, and then translate them with iptables-restore-translate. Compare the two syntaxes.

  3. ufw practice: Using ufw, allow HTTP, HTTPS, and SSH. Then add a rule to deny all traffic from a specific IP address. Verify with sudo ufw status numbered.

  4. Rate limiting: Use iptables to limit SSH connections to 3 per minute per source IP (hint: use the limit or recent match module). Test by trying to connect rapidly.

  5. Bonus challenge: Set up a Linux machine as a NAT gateway. Configure two VMs: one as the gateway with two interfaces (internal and external), and one as an internal client. The client should be able to reach the internet through the gateway using masquerade NAT. Verify with curl from the internal client.