Linux Firewall Configuration: firewalld and UFW
At a glance#
- Purpose: Configure host firewalls on Linux servers using firewalld (RHEL family) and UFW (Debian/Ubuntu).
- Applies to: RHEL/Rocky/AlmaLinux 7+ and Ubuntu/Debian 18.04+.
- Risk: High — a wrong rule locks you out of a remote server instantly.
- Time: 30 minutes.
Overview#
Both tools are front ends to netfilter. Use whichever ships with the distribution rather than mixing them:
| firewalld | UFW | |
|---|---|---|
| Default on | RHEL, Rocky, AlmaLinux | Ubuntu, Debian |
| Model | Zones with assigned interfaces | Simple ordered rule list |
| Persistence | --permanent then --reload | Immediate and persistent |
| Backend | nftables (modern) / iptables | iptables / nftables |
Warning: Every command below can lock you out of a remote server. Before you start, open a second SSH session and leave it connected, and have console or IPMI access available. Add the SSH rule before enabling the firewall, never after.
Before you start#
- Root or
sudoaccess. - A list of ports the server genuinely needs to expose.
- A second SSH session already open.
- Console access as a fallback.
Record what is currently listening, so you know what actually needs allowing:
sudo ss -tulpnfirewalld (RHEL family)#
Basics#
sudo systemctl enable --now firewalld
sudo firewall-cmd --state
sudo firewall-cmd --list-all
sudo firewall-cmd --get-active-zonesThe permanent/runtime split#
This trips people up constantly. firewalld holds two rule sets:
- Runtime — active now, lost on reload or reboot.
- Permanent — written to disk, applied on reload.
# runtime only - disappears on reload
sudo firewall-cmd --add-service=http
# permanent - not active until reloaded
sudo firewall-cmd --permanent --add-service=http
# apply permanent rules to the running firewall
sudo firewall-cmd --reloadNote: The runtime-only form is genuinely useful. Add a risky rule to runtime, test it, and if it locks you out, a reboot or reload reverts it. Only commit with --permanent once proven.
Common operations#
# by service name
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=https
# by port
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --permanent --add-port=9000-9100/tcp
# remove
sudo firewall-cmd --permanent --remove-service=http
sudo firewall-cmd --permanent --remove-port=8080/tcp
# what services are defined
sudo firewall-cmd --get-services
sudo firewall-cmd --reloadRestricting a port to specific sources#
Rich rules handle "only the office may reach this":
# allow MySQL from one subnet only
sudo firewall-cmd --permanent --add-rich-rule='
rule family="ipv4" source address="192.168.10.0/24"
port port="3306" protocol="tcp" accept'
# block a specific host entirely
sudo firewall-cmd --permanent --add-rich-rule='
rule family="ipv4" source address="203.0.113.55" drop'
# log and reject
sudo firewall-cmd --permanent --add-rich-rule='
rule family="ipv4" source address="198.51.100.0/24"
port port="22" protocol="tcp" log prefix="SSH-BLOCKED " level="warning" reject'
sudo firewall-cmd --reload
sudo firewall-cmd --list-rich-rulesZones#
# what zone is an interface in
sudo firewall-cmd --get-zone-of-interface=eth0
# move an interface to a different zone
sudo firewall-cmd --permanent --zone=internal --change-interface=eth1
# add a source network to a zone
sudo firewall-cmd --permanent --zone=trusted --add-source=192.168.10.0/24
sudo firewall-cmd --reload| Zone | Typical use |
|---|---|
public | Default — internet-facing |
internal | Trusted LAN |
trusted | Everything allowed |
drop | Everything dropped silently |
UFW (Ubuntu / Debian)#
Basics#
sudo ufw status verbose
sudo ufw status numberedEnabling safely#
Order matters. Allow SSH first:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw enableWarning: Running ufw enable before allowing SSH drops your session immediately and needs console access to recover. UFW warns about this — do not dismiss it.
Common operations#
# by service name
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
# by port
sudo ufw allow 8080/tcp
sudo ufw allow 9000:9100/tcp
# from a specific source
sudo ufw allow from 192.168.10.0/24 to any port 3306 proto tcp
# on a specific interface
sudo ufw allow in on eth1 to any port 5432
# deny
sudo ufw deny from 203.0.113.55
# rate limit - blocks an IP after 6 connections in 30 seconds
sudo ufw limit sshufw limit ssh is worth applying on any internet-facing host; it stops most brute-force noise without extra tooling.
Removing rules#
sudo ufw status numbered
sudo ufw delete 3
# or by specification
sudo ufw delete allow 8080/tcpLogging#
sudo ufw logging on
sudo ufw logging medium
sudo tail -f /var/log/ufw.logVerification#
Confirm the ruleset:
# firewalld
sudo firewall-cmd --list-all
# ufw
sudo ufw status verboseThen test from another machine — testing from the server itself bypasses the firewall entirely and proves nothing:
# from a different host
nc -zv server.example.com 22
nc -zv server.example.com 443
nc -zv server.example.com 3306 # should fail if restricted
nmap -Pn -p 1-1000 server.example.comFinally, open a new SSH session before closing your existing one. That is the only real proof you have not locked yourself out.
Confirm the rules survive a reboot:
sudo reboot
# after it returns
sudo firewall-cmd --list-all # or: sudo ufw status verboseRollback#
If you lose access, connect via console and:
# firewalld - drop runtime changes back to the last permanent set
sudo firewall-cmd --reload
# or stop the firewall entirely while you fix it
sudo systemctl stop firewalld
# ufw
sudo ufw disableThen correct the rules and re-enable.
Note: Stopping the firewall is an emergency measure, not a fix. Re-enable it as soon as the rule is corrected — a server left with no firewall after a late-night incident is how hosts get compromised.
Troubleshooting#
| Symptom | Cause and fix |
|---|---|
| Locked out after enabling | SSH not allowed before enabling. Recover via console, add the rule, re-enable. |
| Rule added but no effect (firewalld) | --permanent used without --reload. Run firewall-cmd --reload. |
| Rules gone after reboot (firewalld) | Added to runtime only. Re-add with --permanent. |
| Port open in the firewall but still unreachable | Nothing listening, or bound to 127.0.0.1 only. Check ss -tulpn. |
| Works locally, fails remotely | Upstream network firewall, or the service binds only to loopback. |
| firewalld and iptables rules conflict | Both are managing netfilter. Use one; disable iptables.service. |
| UFW allows traffic that should be denied | Rule order — first match wins. Check ufw status numbered. |
| Docker containers bypass UFW | Docker writes its own iptables rules. Set iptables: false in daemon.json or manage exposure via Docker. |
| Service unreachable despite correct rules on RHEL | SELinux may block the port binding. Check with sudo ausearch -m avc -ts recent. |