Kennisbank › Firewall › iptables: Building a Firewall on Linux
firewall starter 07 October 2026

iptables: Building a Firewall on Linux

iptables: Building a Firewall on Linux A packet arrives on eth0, and the kernel has to decide — accept, drop, or forward it somewhere else. That decision is

This article walks you through setting up a working firewall on a Linux server with iptables, from a clean default policy to persistent rules that survive a reboot. It's aimed at anyone running a VPS or bare-metal box who needs to control what reaches the machine.

Requirements

  • A Linux system with the netfilter framework in the kernel. Any mainstream distribution from the last decade qualifies: Debian 12, Ubuntu 22.04 or 24.04, RHEL 9, Rocky 9, AlmaLinux 9.
  • Root access, or sudo. Firewall rules are a kernel-level concern; there is no unprivileged path.
  • The `iptables` userspace binary. On Debian and Ubuntu: `apt install iptables`. On RHEL-family: `dnf install iptables-services`.
  • A second way into the machine. If you're working over SSH and you lock yourself out, you'll need console access through your hosting panel. This is not optional.
One thing to decide before you start: are you managing IPv4 only, or also IPv6? The two stacks are separate. `iptables` handles IPv4, `ip6tables` handles IPv6. Rules in one do not apply to the other, and a server with IPv6 enabled but no `ip6tables` rules is wide open on that stack. Most people forget this.

How iptables is organised

Before the commands, the model. iptables rules live in tables, tables contain chains, and chains contain rules that end in a target.

The table you'll spend nearly all your time in is `filter`. It has three built-in chains: `INPUT` for packets destined for the local machine, `OUTPUT` for packets leaving it, and `FORWARD` for packets routed through it. A server that isn't a router can leave `FORWARD` at DROP and forget about it.

The other tables exist for narrower jobs. `nat` rewrites addresses and ports, `mangle` alters packet headers, `raw` bypasses connection tracking. You'll touch `nat` if you run Docker or set up a router. The rest you can ignore until you need them.

Rules are evaluated top to bottom. The first match wins, and evaluation stops there — unless the target explicitly says to continue. That single fact explains most firewall bugs: a broad ACCEPT near the top makes every rule below it dead code.

Targets you'll use: `ACCEPT` lets the packet through, `DROP` discards it silently, `REJECT` discards it and sends an ICMP error back, `LOG` writes to the kernel log and continues to the next rule. `DROP` versus `REJECT` is a real choice. `DROP` makes your host look like a black hole to a port scanner, which slows them down. `REJECT` tells legitimate clients immediately that the port is closed, which is friendlier but leaks that something is there.

Steps

1. Check what's already loaded

Never start writing rules on a box you haven't looked at. Something may already be there — a cloud-init script, a Docker install, a hosting provider's default.

```bash sudo iptables -L -n -v --line-numbers ```

The `-n` flag stops iptables from doing DNS lookups on every address, which turns a fast command into a slow one on a busy server. `--line-numbers` gives you numbers you can reference when deleting rules later.

If the output is empty chains with policy ACCEPT, you're starting clean. If there are rules, read them before you touch anything.

2. Set the default policies

The default policy is what happens to a packet that matches no rule in the chain. Set it before you add anything, and set it to DROP.

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

`INPUT DROP` is the important one. It means anything you don't explicitly allow is refused. That's the posture you want: deny by default, permit by exception.

`OUTPUT ACCEPT` is a deliberate compromise. Locking down outbound traffic is meaningful security — it stops a compromised process from phoning home — but it's also the fastest way to break package managers, DNS, NTP, and every application that talks to an API. For a general-purpose server, leave it open and revisit if you have a specific threat model. For a hardened appliance, close it and expect a week of debugging.

At this exact moment, before you've allowed anything, your SSH session is still alive — existing connections are tracked, and the policy only applies to new packets. Don't close the terminal.

3. Allow loopback and established connections

Two rules go in before anything else. Loopback first: a huge number of local services talk to each other over `127.0.0.1`, and dropping that traffic breaks things in ways that are hard to diagnose.

```bash sudo iptables -A INPUT -i lo -j ACCEPT ```

Then connection tracking. This is the rule that makes a stateful firewall work: once a connection is established, replies are allowed back in without a matching inbound rule.

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

`ESTABLISHED` covers ongoing connections. `RELATED` covers the odd cases — FTP data channels, ICMP errors tied to a connection you started. Without this rule, every outbound request you make would have its reply dropped, and the server would look like it has no network at all.

4. Allow SSH

If you're on a remote box, this is the rule that keeps you logged in. Put it before any broad DROP, and be specific about the port.

```bash sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT ```

`--ctstate NEW` means only new connection attempts match. The replies are already covered by the ESTABLISHED rule above, so you don't need a separate outbound rule.

If you run SSH on a non-standard port, change `22` to match. If you've restricted SSH to a jump host, add `-s 203.0.113.10` to limit the source. That's worth doing: an SSH port open to the world gets scanned constantly, and while key-only auth holds up fine, there's no reason to invite the traffic.

5. Allow the services you actually run

Now the specific ports. Web server, mail, whatever the box is for.

```bash sudo iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT sudo iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT ```

Each rule is one service. That's the cost of this approach: it's explicit, and explicit means you have to remember what you're running. Run `ss -tulpn` to see what's actually listening before you decide. A port you forgot to open is a service that looks broken; a port you opened by accident is an attack surface you'll forget about.

6. Add rate limiting for the noisy stuff

ICMP and SSH both attract automated traffic. A modest rate limit keeps the logs readable and blunts the laziest attacks.

```bash sudo iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s --limit-burst 4 -j ACCEPT sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name SSH sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP ```

The first rule allows ping at one per second with a burst of four. Blocking ICMP entirely is a common mistake — it breaks path MTU discovery, and you'll spend an afternoon wondering why large packets vanish on one route.

The next two use the `recent` module to track SSH attempts. Four new connections from the same source within 60 seconds and the rest are dropped. This is crude compared to fail2ban, but it's stateless from your side and needs no daemon. It also won't stop a distributed attempt, and it will annoy you if you legitimately reconnect four times in a minute.

7. Save the rules

iptables rules live in memory. Reboot the machine and they're gone — you'll come back to a wide-open server and no indication that anything changed.

```bash sudo apt install iptables-persistent sudo netfilter-persistent save ```

On Debian and Ubuntu, `iptables-persistent` writes the current ruleset to `/etc/iptables/rules.v4` and `/etc/iptables/rules.v6` and restores them at boot. On RHEL-family systems, use `service iptables save` with the `iptables-services` package, or write the output of `iptables-save` to a file and restore it from a systemd unit.

If you'd rather not add a package, the manual version is two commands:

```bash sudo iptables-save | sudo tee /etc/iptables/rules.v4 sudo ip6tables-save | sudo tee /etc/iptables/rules.v6 ```

And a unit or cron job to run `iptables-restore < /etc/iptables/rules.v4` at boot. That's more moving parts than the package, and you own the failure mode.

8. Handle IPv6 separately

If the machine has a public IPv6 address, `iptables` did nothing for it. Repeat the entire ruleset against `ip6tables`, or disable IPv6 if you don't use it.

```bash sudo ip6tables -P INPUT DROP sudo ip6tables -P FORWARD DROP sudo ip6tables -P OUTPUT ACCEPT sudo ip6tables -A INPUT -i lo -j ACCEPT sudo ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT sudo ip6tables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT sudo ip6tables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT sudo ip6tables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT sudo ip6tables -A INPUT -p ipv6-icmp --icmpv6-type echo-request -m limit --limit 1/s --limit-burst 4 -j ACCEPT ```

Note the ICMP type: IPv6 uses `ipv6-icmp` and `--icmpv6-type`, not `icmp` and `--icmp-type`. Getting this wrong means either no ping or no rules at all.

Verification

Check the ruleset, then test from outside.

```bash sudo iptables -L -n -v --line-numbers ```

You should see your rules in order, with packet counters. Counters that stay at zero after you've hit the service mean the rule isn't matching — usually a wrong port or a wrong interface.

From another machine, test what's open:

```bash nmap -Pn -p 22,80,443,3306 your-server-ip ```

Ports 22, 80, and 443 should show `open`. Anything you didn't allow should show `filtered` (DROP) or `closed` (REJECT). If a port you meant to close shows `open`, you have a rule you forgot about — go back to step 1 and read the output properly this time.

For IPv6, add `-6` to the nmap command and target the IPv6 address.

Common problems

You locked yourself out of SSH. The rules are in memory, not on disk. Reboot the machine from your hosting panel's console and the old ruleset is gone — provided you didn't save. If you did save, boot into rescue mode, mount the disk, and delete `/etc/iptables/rules.v4`. This is why step 1 exists and why you keep console access open.

Docker containers can't reach the network. Docker writes its own iptables rules on startup, and a `FORWARD` policy of DROP will break container networking unless Docker's rules are in place. Docker inserts them at the top of the `FORWARD` chain, ahead of yours, which is why it usually works. If you've flushed the chain after Docker started, restart the daemon: `sudo systemctl restart docker`. Do not set `iptables=false` on the daemon unless you've built your own firewall management — the Docker documentation is explicit that this breaks container networking for most setups.

Rules vanish after reboot. You didn't save them, or you saved them but nothing restores them at boot. Check `systemctl status netfilter-persistent` on Debian-family systems. On RHEL-family, confirm `iptables-services` is installed and enabled.

Where this leaves you

iptables has been the standard for long enough that a generation of tooling assumes it. Docker, Kubernetes, and most configuration management systems either write iptables rules directly or wrap them. That integration is the reason the tool is still everywhere, and also the reason it's getting harder to reason about — your ruleset is no longer only yours.

nftables is the designated successor and has been in the kernel since 3.13. It merges iptables, ip6tables, arptables, and ebtables into one framework, with a cleaner syntax and better performance on large rulesets. Docker added experimental nftables support via `--firewall-backend=nftables` in version 29.0.0, though overlay networks in Swarm mode still require iptables, so you can't switch wholesale yet.

The honest position: if you're starting fresh on a modern distribution and don't depend on Docker's iptables integration, nftables is the better choice. If you're maintaining existing servers, scripts, or container hosts, iptables will be with you for a while, and understanding the chain model above is what lets you debug it when something silently stops working at 2 a.m.

← Terug naar kennisbank
← Terug naar kennisbank