Building a server's outer defense layer with the Linux firewall: packet filtering concepts, UFW practice on Debian/Ubuntu, Firewalld zones on RHEL/Rocky, down to low-level iptables/nftables, along with the traps that lock you out of your own server.

After episode 18 where we covered Remote Access Using SSH — hardening sshd_config, key authentication, and tunnels — you can now enter a server securely. But entering securely is only half the journey. The next question: who else is allowed to try to enter? Your server is open to the internet, and every second bots scan ports 22, 80, 443, 3389 — trying millions of password combinations. Without defense, your server is like a house with all doors wide open, relying on the kindness of strangers.
This is where the firewall comes in. It's the gatekeeper that decides which packets may enter, exit, or be forwarded — based on the rules you set. A firewall isn't a "set and forget" tool: it's a policy that must be designed. What do you open? From which sources? To which destination? The answers to these questions are what separate a secure server from a leaking one.
In this episode 19, we'll cover the three firewall tools that dominate the Linux ecosystem: UFW (Ubuntu/Debian), Firewalld (RHEL/Rocky), and iptables/nftables as the low-level foundation behind them all. We'll discuss packet filtering concepts, practice each tool, and — just as importantly — avoid the classic traps that make you locked out of your own server.
The Linux firewall works at the packet level. Every passing packet is inspected and compared against sequential rules; the first matching rule determines the packet's fate: ACCEPT (allow), DROP (discard silently), or REJECT (refuse with a reply).
There are three main paths (or chains) you must memorize:
| Chain | Traffic Direction | Analogy |
|---|---|---|
| INPUT | Packets entering this machine | The house's entrance guard |
| OUTPUT | Packets leaving this machine | People leaving the house |
| FORWARD | Packets passing through this machine (routing) | A transit city's border post |
Why does a server need a firewall, when its services only use three or four ports? Because the firewall enforces a default-deny policy: close everything, open only what's needed. This reduces the attack surface, blocks unused ports, and narrows the impact if a service is hacked. Bots will find doors that are already closed, not doors waiting to be opened.
Important
The golden firewall rule: default-deny — close everything not explicitly opened. A server with an "open everything, close later when there's a problem" policy isn't a secured server; it's just postponing disaster. Start from "everything closed", then open ports one by one with a reason.
UFW (Uncomplicated Firewall) is built on top of iptables/nftables, but hides the complexity behind easy-to-read syntax. It's the primary choice on Ubuntu/Debian because of its philosophy: uncomplicated.
sudo ufw status verbosesudo ufw enablesudo ufw allow 22/tcpsudo ufw allow 80,443/tcpsudo ufw allow from 192.168.1.0/24 to any port 5432 proto tcpsudo ufw delete allow 80,443/tcpWarning
The number one trap in the UFW world: running ufw enable before allowing SSH. UFW activates the default deny incoming policy, so the moment you run ufw enable, your SSH connection drops — and you're locked out. The correct order: ufw allow 22/tcp first, then ufw enable. To avoid the drama, enable UFW while keeping another SSH session open as a rescue path.
For well-known services, UFW has built-in service names:
sudo ufw allow OpenSSH
sudo ufw allow 'Apache Full'
sudo ufw allow 'Nginx Full'sudo ufw status numberedThe status numbered output shows each rule's number — useful for deleting a specific rule with sudo ufw delete <number>.
Firewalld is the default firewall on the RHEL family (Rocky, AlmaLinux, CentOS Stream). Its core concept is zones — sets of rules applied based on network trust. The default public zone is for internet-facing interfaces, and internal/trusted for more trusted networks.
sudo firewall-cmd --get-default-zone
sudo firewall-cmd --statesudo firewall-cmd --list-all-zonessudo firewall-cmd --permanent --add-service=httpsudo firewall-cmd --permanent --add-port=2222/tcpsudo firewall-cmd --reloadfirewall-cmd flag | Function |
|---|---|
--permanent | Save the rule so it survives reload/reboot |
--reload | Apply permanent rules without dropping connections |
--add-service | Allow a named service (http, https, ssh) |
--add-port | Allow a raw port+protocol (80/tcp) |
--add-source | Allow all traffic from a specific IP/netmask |
--runtime-to-permanent | Make the current runtime rules permanent |
Warning
The classic Firewalld trap: adding a rule without --permanent, then running --reload. The runtime rule disappears on reload — you think the firewall is open, but it isn't. The standard rule: almost always use --permanent, then --reload. And remember the order above: open SSH first, then change the zone or default policy.
Changing an interface's zone and adding a new zone:
sudo firewall-cmd --permanent --zone=internal --change-interface=eth1sudo firewall-cmd --permanent --zone=internal --add-service=postgresql
sudo firewall-cmd --permanent --zone=internal --add-source=192.168.50.0/24sudo ufw status verbose
sudo ufw allow 22/tcp
sudo ufw allow 80,443/tcp
sudo ufw enablesudo ufw allow from 192.168.50.0/24 to any port 5432 proto tcpBoth tools answer the same question — who may access what — in different styles. UFW is simple and flat; Firewalld is zone-based, making it more suited for servers with many interfaces and different trust levels. Choose based on your distro's ecosystem.
UFW and Firewalld are just frontends. The engine actually doing the work is netfilter inside the kernel, controlled via iptables (legacy) or nftables (the modern replacement). Understanding this matters because: (1) sometimes you must deal directly with raw rules, and (2) debugging "weird" rules from UFW/Firewalld always ends up here.
iptables configuration is divided into tables (filter, nat, mangle) and chains (INPUT, OUTPUT, FORWARD). Most daily tasks only touch the filter table.
sudo iptables -L -n -vsudo iptables -A INPUT -p tcp --dport 22 -j ACCEPTsudo iptables -A INPUT -s 203.0.113.0/24 -j DROPsudo iptables-save > /etc/iptables/rules.v4Individual rules matter, but what matters more is the default policy — what the firewall does with packets that match no rule at all. -P sets the default policy on a chain:
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPTThe rules above state: drop all incoming packets not explicitly allowed, but let outgoing traffic keep flowing. This is the essence of default-deny discussed at the start of the episode — with -P, you apply it as a system policy, not just a collection of rules.
Caution
Changing -P INPUT DROP while there's not yet an ACCEPT rule for your SSH port = session dropped and you're locked out. The safe order: add -A INPUT ... --dport 22 -j ACCEPT first, confirm the rule shows up in iptables -L -n, then change the default policy. And don't forget iptables-save afterward — an unsaved policy disappears at reboot (and could drop your connection again at the worst possible time).
Masquerade (NAT): to make this machine a router/NAT for an internal subnet, you need the nat table — a concept that becomes very useful when episode 21 covers routing and network namespaces:
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE| iptables option | Function |
|---|---|
-A | Append (add a rule at the end of the chain) |
-I | Insert (add a rule at the start of the chain) |
-D | Delete (remove a rule) |
-s | Source IP/netmask |
-d | Destination IP |
-p | Protocol (tcp, udp, icmp) |
--dport | Destination port |
-j | Action (ACCEPT, DROP, REJECT) |
-L -n -v | List rules, no name resolution, verbose |
Every iptables command only changes the runtime configuration. You view rules with iptables -L, add them with -A, then save them with iptables-save. Without saving, everything disappears at reboot.
Tip
-n is very important when debugging: without -n, iptables tries to resolve IPs to hostnames — slow and potentially misleading. Get used to always iptables -L -n -v. And rule order matters: iptables processes top to bottom, the first matching rule wins. A specific -D rule must come before a general -A rule.
nftables is iptables' successor — more consistent syntax and one framework for all tables. New distros already make it the default (which is what UFW uses on modern Ubuntu):
sudo nft list rulesetnft add table inet filter
nft add chain inet filter input { type filter hook input priority 0; policy drop; }
nft add rule inet filter input tcp dport 22 accept
nft add rule inet filter input ip saddr 203.0.113.0/24 dropThe rules above form a default-deny policy: all incoming packets are dropped except SSH (tcp dport 22 accept) and specifically allowed sources.
After putting the rules together, don't stop there. Verify from a network perspective — exactly like you learned in episode 17:
sudo ss -tulpn | grep -E ":(22|80|443)"nc -zv 192.168.1.10 22
curl -v https://192.168.1.10nc (netcat) and curl give honest answers from the client side: is the port really reachable from outside, through all firewall layers? If nc succeeds but curl fails, the problem isn't the firewall — it's the service or application (the lessons of episodes 17 and 18).
1. ufw enable without opening SSH first. Locked out of your own server. The order: allow 22/tcp → enable.
2. firewall-cmd without --permanent then --reload. Rules vanish instantly. Always --permanent + --reload.
3. A default DROP policy that locks you out. Changing the INPUT policy to DROP without an ACCEPT rule for SSH = a decision you'll regret. Only change the policy after all critical services are already allowed.
4. Opening an SSH port in the firewall but forgetting to change the sshd port (or vice versa). You changed Port 2222 in sshd (episode 18) but the firewall only opens 22 — connections are immediately refused. Always keep these two layers in sync.
5. iptables without iptables-save. Runtime rules vanish at reboot. Or the reverse — you add rules with -A after a more general rule, so the new rule never gets used because the general one already matched first.
6. Testing the firewall only from localhost. Test from another machine. The firewall can look correct inside the server, but real traffic comes from outside. Use nc -zv from an external client.
In this episode 19, you've built the server's outer defense layer: understanding packet filtering concepts (ACCEPT/DROP/REJECT, INPUT/OUTPUT/FORWARD chains), practicing UFW on Debian/Ubuntu with a default-deny policy, managing Firewalld on RHEL/Rocky with zones and --permanent/--reload, and dismantling iptables/nftables as the engine behind all the frontends.
Key points to take home:
allow first, then enable. Firewalld: --permanent first, then --reload.iptables-save for persistence.nc -zv, curl) — not just from inside the server.In the next episode 20 we'll share files between machines through the topic Network File Sharing (NFS & Samba) & DNS Basics. You'll learn to set up an NFS server, share folders between Linux servers, prepare Samba shares for Windows/macOS clients, and understand the basics of DNS from /etc/hosts to systemd-resolved. See you there!