MHOSTMHOST

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 root or a user with sudo.

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:

bash
iptables -V

If the command isn't found, install the package:

Ubuntu / Debian

bash
sudo apt update
sudo apt install iptables

AlmaLinux / Rocky Linux

bash
sudo dnf install iptables
Important: AlmaLinux and Rocky Linux use firewalld (on top of nftables) by default, not iptables — check whether it's active with systemctl status firewalld. firewalld and iptables can't run at the same time — they're two conflicting managers for the same netfilter subsystem. As of version 9, the iptables utilities themselves are officially deprecated on AlmaLinux/Rocky. The concepts and command syntax in this article (steps 2–5) hold for iptables on any distro, but as a way to actually configure the firewall on a production server, this article is written for Ubuntu/Debian; on AlmaLinux/Rocky, use firewalld instead.

Step 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:

bash
sudo iptables -L -n

The 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:

bash
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:

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

A 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):

bash
sudo iptables -S

Step 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:

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

Allow 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:

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

Now allow SSH itself and HTTP:

bash
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
Tip: if you've already changed the default SSH port (see the "SSH hardening" article), use your own port instead of 22. If the server also serves HTTPS, add port 443 the same way: sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT.
Tip: ufw does the same thing more briefly — ufw allow ssh and ufw allow 80/tcp. If the iptables syntax feels like overkill, check out the "Setting up the UFW firewall" article: it's a friendlier front-end over the same iptables/nftables engine, with simpler commands and ready-made application profiles.

Check that the rules were added:

bash
sudo iptables -L -v -n

Step 5. Turn on the default DROP policy

Important: this is the riskiest step in the article — a mistake here can cut off your own SSH access. Don't close your current SSH session — open a separate, second connection to test with, and keep the VNC console in VMmanager 6 handy: it lets you get onto the server and fix the rules even if SSH is unreachable.
Tip: there's one more safety net — until you save the rules (step 6), rebooting the server will usually reset the policies back to ACCEPT. So even a completely broken configuration at this step can be fixed with a reboot from VMmanager 6, no OS reinstall required.

Once the rules from step 4 are in place, turn on default DROP for incoming traffic and forwarding:

bash
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP

We 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:

bash
ssh root@185.246.66.12

And check the resulting state:

bash
sudo iptables -L -v -n

The 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.

Done: a new SSH connection opens, the site on port 80 responds, and everything else inbound gets dropped by default.

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:

bash
sudo apt update
sudo apt install iptables-persistent

During 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:

bash
sudo systemctl enable --now netfilter-persistent

If 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:

bash
sudo netfilter-persistent save

You can check that the saved rules load correctly without rebooting the server:

bash
sudo netfilter-persistent reload
Tip: running sudo dpkg-reconfigure iptables-persistent at any time asks the same save-current-rules questions again, just like on first install.

(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".