DevTools Hub

Search tools

Search for a developer tool

Linux Firewall Guide

Part of the Linux Security Toolkit

A Linux firewall's entire job can be stated in one sentence: deny everything incoming by default, then explicitly allow only what this specific server actually needs to expose. Getting that one sentence backwards — or forgetting one specific rule at the wrong moment — is responsible for most of the real-world firewall incidents that actually happen, which are far more often "I locked myself out" than "an attacker got through." This guide covers how UFW and firewalld actually work, the rule-ordering mistake that causes lockouts, and what else needs opening beyond SSH.

Two tools, one underlying mechanism

Both UFW (Ubuntu, Debian) and firewalld (Fedora, RHEL) are front ends over the same underlying kernel packet filter — neither is a separate firewall "engine," just a different command syntax and configuration philosophy layered over the same netfilter subsystem the kernel already provides. UFW favors simple, direct commands (ufw allow 22/tcp); firewalld favors a zone-based model where each network interface belongs to a zone with its own rule set, more suited to a machine that moves between different network contexts (a laptop that's sometimes on a trusted office network and sometimes on public Wi-Fi) than a typical single-purpose server.

The setup sequence, and why order matters

Setting up either tool follows the same shape:

# UFW
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable

# firewalld
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --set-default-zone=public
sudo firewall-cmd --reload

The detail that actually matters: the SSH allow rule is added before the firewall is enabled or reloaded with its new default-deny policy. Reverse that order — enable the default-deny policy first, add the SSH rule after — and the connection you're configuring the server over gets cut immediately, often with no way back except console access from the hosting provider. This single ordering mistake is, by a wide margin, the most common real-world firewall incident — not a misconfigured allow rule letting something through, but a correctly-configured deny rule applied one command too early.

Deciding what actually needs to be open

A firewall is only as good as knowing what genuinely needs to be reachable. Guessing from memory or documentation is unreliable; the ground truth is on the server itself:

ss -tulnp

This lists every port actually listening for connections and which process owns it. Only ports that show up here — and that genuinely need to accept connections from outside the machine — belong in the firewall's allow list. A service that only needs to talk to other processes on the same machine (a local cache, a database only the application server on the same box connects to) should bind to 127.0.0.1 rather than 0.0.0.0 in its own configuration, making it unreachable from the network layer entirely — a stronger guarantee than a firewall rule, since it removes the exposure at the source rather than relying on a rule to block it.

Restricting by source, not just by port

Not every open port needs to accept connections from the entire internet. A database listening on 5432 might only ever need connections from a specific application server on a private network — restricting the allow rule to that source address, rather than "anyone," meaningfully narrows what a scan of the open port can actually accomplish:

# UFW — allow only from a specific subnet
sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcp

# firewalld — the same restriction needs a rich rule
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" port protocol="tcp" port="5432" accept'

firewalld's plain --add-port has no concept of a source restriction on its own — a rich rule is specifically what adds one, at the cost of noticeably more verbose syntax than the equivalent UFW command.

IPv6 is a second, easy-to-forget front door

On a dual-stack host (one with both an IPv4 and an IPv6 address, increasingly the default), a firewall configured carefully for IPv4 but left untouched for IPv6 leaves a second, fully open path to the same services. Both UFW and firewalld manage IPv6 rules alongside IPv4 ones, but it's a genuinely common gap: a server hardened thoroughly on one protocol while the other is simply never considered, either because IPv6 isn't actually in active use (in which case disabling it outright is a reasonable alternative to firewalling a protocol nobody needs) or because it was overlooked entirely.

Logging what actually got blocked

A default-deny firewall silently dropping unwanted traffic is working correctly, but "working correctly and invisible" also means a genuine attack attempt produces no record unless logging is explicitly enabled. Both tools support it:

# UFW
sudo ufw logging on

# firewalld — logging denied packets specifically
sudo firewall-cmd --set-log-denied=all

This matters for the same reason logging matters everywhere else in a security setup: without it, there's no way to later distinguish "nobody tried" from "something tried and the firewall quietly handled it" — a distinction worth having during an actual incident review, even though the moment-to-moment behavior of the firewall itself is identical either way.

Firewalls and mandatory access control are different layers

A firewall controls what can reach a service from the network. It says nothing about what that service is allowed to do once a connection is accepted and it's actually processing a request. That's the layer SELinux (enforcing by default on Fedora and RHEL) and AppArmor (Ubuntu's default) exist for — confining what an already-running, already-reachable process can actually access on the machine itself. The scenario where this distinction matters concretely: a web server with a genuine vulnerability, fully reachable because the firewall correctly allowed port 443 for exactly the reason it's open. The firewall did its job; whether the compromise spreads beyond that one process is what the mandatory access control layer determines instead.

Common mistakes

  • Enabling default-deny before adding the SSH rule. Covered above, and worth repeating since it's the single most common firewall lockout.
  • Opening a port range for convenience. Widens the attack surface proportionally to however many ports are opened, whether or not something is actually listening on most of them.
  • Configuring IPv4 carefully and forgetting IPv6 entirely. A dual-stack host with an unfirewalled IPv6 address has a second, fully open path to the same services.
  • Letting firewall rules accumulate indefinitely. A rule added for a service decommissioned months ago doesn't remove itself — a periodic review catches what's actually still needed.
  • Assuming default-deny blocks outgoing connections too. Both UFW and firewalld's typical setup allows outgoing traffic by default while denying incoming — worth confirming explicitly rather than assuming, since a compromised host reaching out to an attacker-controlled server is a real, separate concern from unwanted incoming connections.

FAQ

Do I need both a firewall and a mandatory access control system like SELinux or AppArmor?

Yes — they defend against different things. A firewall controls what can reach the machine over the network in the first place; SELinux and AppArmor confine what an already-running process is allowed to do on the machine itself. A compromised network-facing service is exactly the scenario where the second layer matters, since the firewall already let the connection through by design.

Should I disable the firewall if I'm behind a cloud provider's network security group?

No — treat them as independent layers, not substitutes. A cloud security group can be misconfigured, changed by someone else on the team, or simply not cover every path (some cloud networking allows traffic between instances in the same group or VPC to bypass the security group entirely). The host's own firewall is what's actually guaranteed to apply regardless of what happens at the network layer above it.

What's the actual difference between UFW and firewalld beyond syntax?

Functionally, both configure the same underlying kernel packet filter (netfilter) and accomplish the same default-deny-then-allow model — the difference is almost entirely in command syntax and configuration philosophy (UFW's simple allow/deny commands vs. firewalld's zone-based model), not in what's actually achievable with each.

Is it safe to allow a port range instead of individual ports?

It works, but it widens the attack surface proportionally — allowing 1000 ports at once for convenience opens 1000 potential listeners on that host, whether or not something is actually running on all of them at the moment. Allowing exactly the ports a service currently uses, and revisiting the rule set when that changes, keeps the firewall's actual protection matched to what's really running.

How do I know which ports my server actually needs open?

Run ss -tulnp (or the older netstat -tulnp) on the server itself — it lists every port actually listening and which process owns it, which is the ground truth for what the firewall needs to cover, rather than guessing from documentation or memory.

Try it yourself

Firewall Rule Generator builds the full command sequence for either UFW or firewalld from a list of rules, always ordering SSH safely before the firewall enables — entirely in your browser. For the complete server security picture, see Linux Security.

Related tools