Skip to main content

iptables: setting up a Linux firewall from scratch

Security · 21.02.2025 · 5 min read
Illustration for “iptables: setting up a Linux firewall from scratch”

iptables is the classic interface to netfilter, the Linux kernel's packet filtering subsystem. It works with chains of rules that are checked in order from top to bottom: as soon as a packet matches a rule, later rules in that chain are not checked.

The three main chains

Each chain is checked independently and does not affect the others' decisions: allowing something in INPUT does not automatically allow it in FORWARD. On a plain web server that does not forward someone else's traffic, the FORWARD chain can stay closed entirely — it is only needed when the server itself acts as a gateway or router for other machines.

ChainWhen it appliesTypical task
INPUTPackets addressed to the server itselfAllow SSH, HTTP, HTTPS; block the rest
OUTPUTPackets leaving the serverUsually left as ACCEPT unless the server proxies someone else's traffic
FORWARDTransit packets passing through the serverRelevant for routers, VPN gateways and Docker hosts

Basic iptables rules

#!/bin/bash
# Flush all rules
iptables -F
iptables -X

# Default policy: block incoming traffic
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

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

# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# SSH (replace 22 with your own port)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# HTTP and HTTPS
iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT

# ICMP (ping)
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

Common mistakes on first run

Forgetting the SSH rule before the DROP policy

If you run iptables -P INPUT DROP before the rule -A INPUT -p tcp --dport 22 -j ACCEPT is added, an active SSH session drops and a new one will not connect. On a VPS with no provider console this means a complete loss of access. The right order is to add every ACCEPT rule first and set the DROP policy as the very last command.

Forgetting to allow loopback

Without iptables -A INPUT -i lo -j ACCEPT under an INPUT DROP policy, local connections between processes on the server itself break — for example, PHP-FPM can no longer reach MySQL over 127.0.0.1, even though nobody touched the port from outside.

Protection against port scanning

# Block NMAP SYN scanning
iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP

# Limit new connections (SYN flood protection)
iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT

The rule with --state ESTABLISHED,RELATED from the basic set relies on the conntrack connection tracking module, which has a limit on table size. During mass port scanning or a flood of new connections the table can fill up, and the system log shows a message:

kernel: nf_conntrack: table full, dropping packet

After this message, new legitimate connections start being dropped too, not just the attacker's. The new-connection limit in the rule above reduces the risk of such an overflow, but for a genuinely large attack iptables alone is not enough — filtering needs to happen before the server, at the provider or CDN level.

Saving the rules

apt install iptables-persistent -y
netfilter-persistent save
# Rules are saved to /etc/iptables/rules.v4
⚠️ Before applying the rules, check that you have allowed your own SSH port, otherwise you will lose access to the server.
💡 For simpler management, use UFW — it is a layer on top of iptables with friendlier syntax.

When iptables alone is not enough

  • You need filtering at the application level (analysing HTTP requests, not just ports and IPs) — iptables works at the L3/L4 level, L7 needs a WAF like ModSecurity.
  • Many tables and chains with overlapping logic — modern distributions are gradually moving to nftables, which has more readable syntax and a single set of tables instead of separate utilities for IPv4 and IPv6.
  • You need protection against password brute force based on failed login attempts — iptables does not read application logs, that requires Fail2ban on top of it.

Checking that the rules were applied

  1. Run iptables -L -v -n — confirm the INPUT chain contains the expected rules and the default policy is indeed DROP.
  2. From another device, check a closed port: nc -zv YOUR_IP 3306 should end in a refusal.
  3. Reboot the server and run iptables -L -v -n again — if the rules are gone, netfilter-persistent save was not run or the iptables-persistent package is not installed.
  4. Check the packet counters in iptables -L -v -n: rising numbers next to the SYN flood rule confirm the limit is actually filtering traffic, not just sitting unused in the config.

A simpler interface to the same rules is covered in UFW for Ubuntu and Debian, and automatic blocking based on logs is covered in setting up Fail2ban.

Was this article helpful?
← Back to Knowledge Base Ask Support