iptables basics
iptables is the classic Linux firewall tool: it decides which packets to let through and which to drop by running them through chains of rules. In this article you'll get familiar with chains and policies, learn to list the current rules, allow SSH and HTTP, lock everything else down with a default DROP policy, and make the ruleset survive a server reboot.
What you'll need
- An mHost VPS running Ubuntu 22.04/24.04, Debian 11/12, or AlmaLinux/Rocky Linux 8/9.
- SSH access as
rootor a user withsudo.
Step 1. Check that iptables is installed
On Ubuntu and Debian, the iptables command is usually already there out of the box (as a wrapper over nftables — iptables-nft — but with the familiar syntax). Check the version:
iptables -VIf the command isn't found, install the package:
Ubuntu / Debian
sudo apt update
sudo apt install iptablesAlmaLinux / Rocky Linux
sudo dnf install iptablesStep 2. Understand chains and policies
iptables filters traffic through tables, and inside a table, through chains of rules. For a firewall you need the filter table (it's the default, so you don't need to specify it separately), which has three built-in chains:
- INPUT — packets addressed to the server itself: incoming connections over SSH, HTTP, and so on;
- OUTPUT — packets the server itself generates: outgoing requests;
- FORWARD — packets the server forwards on elsewhere. On a regular VPS (not a router), nothing goes through this chain.
Each built-in chain has a default policy — what to do with a packet if no rule in the chain matched it. The policy can only be ACCEPT (let it through) or DROP (silently discard it) — iptables won't accept REJECT as a chain policy, only as the target of an individual rule.
Until you've configured any rules, the policy on all three chains is ACCEPT — meaning everything is allowed. Check the current policies:
sudo iptables -L -nThe first line of each chain in the output looks like Chain INPUT (policy ACCEPT) — that's the current policy.
Step 3. List the current rules
For more detail — with packet/byte counters and interface names:
sudo iptables -L -v -n-L lists the rules (with no argument, all chains in the filter table), -v adds counters and interfaces, -n skips resolving addresses and ports into names (without it, the output can be noticeably slower).
To see line numbers — useful before deleting a specific rule:
sudo iptables -L -v -n --line-numbersA rule is deleted by its line number from that output and the chain name: sudo iptables -D <chain> <number> — for example, sudo iptables -D INPUT 3 deletes the third rule in the INPUT chain.
And to get the rules as ready-to-run iptables commands (handy for copying or comparing against what you're about to apply):
sudo iptables -SStep 4. Allow SSH and HTTP
Before turning on the DROP policy (step 5), add the rules that allow the traffic you need — otherwise, once DROP is on, the server becomes unreachable on absolutely everything, including SSH.
Allow traffic on the loopback interface (lo) — many local services rely on it:
sudo iptables -A INPUT -i lo -j ACCEPTAllow already-established connections and traffic related to them — otherwise responses to the server's own outgoing requests (to DNS or a package repository, for example) get cut off:
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTNow allow SSH itself and HTTP:
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPTCheck that the rules were added:
sudo iptables -L -v -nStep 5. Turn on the default DROP policy
Once the rules from step 4 are in place, turn on default DROP for incoming traffic and forwarding:
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROPWe deliberately leave the OUTPUT policy at ACCEPT — filtering outgoing traffic (egress filtering) is useful, but it's easy to get wrong and break your own DNS, package updates, and the like; that's a separate, more advanced topic outside this article.
Because the ESTABLISHED,RELATED rule is already in place, your current SSH session won't drop — the DROP policy only applies to new connections that don't match any rule. Still, from that second, new session, check that SSH still works:
ssh root@185.246.66.12And check the resulting state:
sudo iptables -L -v -nThe output should show Chain INPUT (policy DROP) and Chain FORWARD (policy DROP), with the ACCEPT rules for loopback, ESTABLISHED/RELATED, SSH, and HTTP listed above them.
Step 6. Persist the rules: iptables-persistent / netfilter-persistent
The rules and policies you've configured live only in kernel memory — after a server reboot they're gone, and the default ACCEPT policies take over again. To make the rules survive a reboot, Ubuntu/Debian uses the iptables-persistent package, which installs the netfilter-persistent system service:
sudo apt update
sudo apt install iptables-persistentDuring installation, the package asks twice whether to save the current rules — once for IPv4, once for IPv6. Say yes both times: the IPv4 rules are exactly what you configured in steps 4–5, and an empty IPv6 ruleset won't cause any harm even if you don't use IPv6.
The rules are saved to /etc/iptables/rules.v4 (and /etc/iptables/rules.v6 for IPv6). Make sure the service that loads them at boot is enabled:
sudo systemctl enable --now netfilter-persistentIf you change the rules again later (say, to open another port), remember to save them again — otherwise the old ruleset comes back after a reboot:
sudo netfilter-persistent saveYou can check that the saved rules load correctly without rebooting the server:
sudo netfilter-persistent reload(This method is for Ubuntu/Debian. On AlmaLinux/Rocky, as noted in step 1, the firewall is managed by firewalld by default, not iptables-persistent.)
What's next
You've set up a basic firewall by hand: chains, a default DROP policy, and the permissions you need (SSH, HTTP) now survive a reboot. You can reach the same result more briefly through ufw — a friendlier front-end over the same iptables/nftables engine. From here it's worth locking down SSH brute-forcing with the "Fail2ban" article, hardening SSH itself — "SSH hardening" — and checking your setup against the general "Server security checklist".