Linux Administration
Firewall Management
In this lesson
A firewall is the gatekeeper between a Linux server and the network — it inspects every packet and decides whether to allow or drop it based on a set of rules. A properly configured firewall enforces the principle of least privilege at the network layer: only the ports that a server needs to expose are open, and everything else is silently dropped. Linux provides three main tools for managing firewall rules, each suited to different distributions and use cases.
The Linux Firewall Architecture
All Linux firewall tools — regardless of whether you use ufw, firewalld, or iptables — ultimately write rules into the Linux kernel's netfilter framework. Understanding this layered architecture explains why different tools can coexist and conflict, and why a rule added by one tool might be overwritten by another.
Fig 1 — All firewall tools write rules into the kernel's netfilter framework — only use one tool per system
Analogy: The firewall tools are like different dashboards for the same building's security system. Whether you use the phone app, the wall panel, or the master console, you are all arming and disarming the same physical sensors and locks. Running two dashboards simultaneously creates conflicts — one might unlock a door the other just locked. Pick one tool and use it exclusively.
ufw — Uncomplicated Firewall (Debian / Ubuntu)
ufw (Uncomplicated Firewall) is the standard firewall interface on Ubuntu and Debian. It wraps iptables/nftables with a simpler command syntax designed for server administrators who need to manage common firewall rules without learning the full iptables command language. Its default-deny model is the correct starting point for any server.
# Check ufw status
sudo ufw status verbose
# Enable ufw with default-deny inbound, allow outbound
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
# Allow specific services by name (reads from /etc/services)
sudo ufw allow ssh # port 22/tcp
sudo ufw allow http # port 80/tcp
sudo ufw allow https # port 443/tcp
# Allow by port number and protocol
sudo ufw allow 8080/tcp
sudo ufw allow 53/udp
# Allow from a specific IP address
sudo ufw allow from 192.168.1.0/24
sudo ufw allow from 10.0.0.5 to any port 5432
# Allow on a specific interface only
sudo ufw allow in on eth0 to any port 80
# Deny a port (explicit deny — useful to override a broader allow)
sudo ufw deny 23/tcp
# Delete a rule by number
sudo ufw status numbered
sudo ufw delete 3
# Delete a rule by specification
sudo ufw delete allow 8080/tcp
# Reload rules without disabling the firewall
sudo ufw reload# sudo ufw status verbose Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp ALLOW IN Anywhere 80/tcp ALLOW IN Anywhere 443/tcp ALLOW IN Anywhere 5432/tcp ALLOW IN 10.0.0.5 22/tcp (v6) ALLOW IN Anywhere (v6) 80/tcp (v6) ALLOW IN Anywhere (v6) 443/tcp (v6) ALLOW IN Anywhere (v6)
What just happened? The status output shows a well-configured server firewall — three services open to the world (SSH, HTTP, HTTPS) and PostgreSQL restricted to a single trusted IP (10.0.0.5). Note that ufw automatically created IPv6 rules alongside IPv4 rules. The Default: deny (incoming) line confirms that any port not explicitly listed is silently dropped — no rule needed for each individual closed port.
firewalld — RHEL, Rocky Linux, and Fedora
firewalld is the default firewall on RHEL, Rocky Linux, CentOS Stream, and Fedora. It introduces the concept of zones — named trust levels assigned to network interfaces or source IP ranges. Each zone has a set of allowed services, so you can define "this interface is in the public zone where only SSH and HTTPS are allowed" and "this interface is in the internal zone where all services are trusted."
All inbound traffic dropped silently. No response. Most restrictive — use for untrusted networks.
Default for internet-facing interfaces. Allows only SSH and DHCPv6 by default. Add services explicitly.
For internal network interfaces. Allows more services — SSH, samba, mdns, ipp-client.
All inbound connections accepted. Use only for loopback or fully trusted private networks.
# Check firewalld status and active zones
sudo firewall-cmd --state
sudo firewall-cmd --get-active-zones
# List all rules in the default zone
sudo firewall-cmd --list-all
# List all rules in a specific zone
sudo firewall-cmd --zone=public --list-all
# Add a service to the public zone (runtime — lost on reload)
sudo firewall-cmd --zone=public --add-service=https
# Add permanently (survives reboot) — always use --permanent for production
sudo firewall-cmd --zone=public --add-service=https --permanent
# Add a specific port
sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent
# Remove a service
sudo firewall-cmd --zone=public --remove-service=telnet --permanent
# Reload to apply permanent rules
sudo firewall-cmd --reload
# Allow access from a specific IP range to a zone
sudo firewall-cmd --zone=internal --add-source=10.0.0.0/24 --permanent
# Block a specific IP (rich rule)
sudo firewall-cmd --add-rich-rule='rule family="ipv4" source address="185.220.101.42" reject' --permanent
sudo firewall-cmd --reload# sudo firewall-cmd --get-active-zones public interfaces: eth0 internal sources: 10.0.0.0/24 # sudo firewall-cmd --zone=public --list-all public (active) target: default icmp-block-inversion: no interfaces: eth0 sources: services: cockpit dhcpv6-client ssh https http ports: 8080/tcp protocols: rich rules:
What just happened? The active zones show a well-structured setup: eth0 is in the public zone with only necessary services allowed, and the internal zone is assigned to the 10.0.0.0/24 source range — all traffic from that network gets the more permissive internal policy automatically, without needing to specify the interface. The zone model makes multi-interface servers significantly easier to manage than individual iptables rules.
iptables Fundamentals
iptables is the low-level tool that both ufw and firewalld generate rules for. Understanding it is important because: iptables rules appear in security audit outputs, some automation tools write iptables rules directly, and reading iptables output is the only way to see the complete, unabstracted state of the firewall. On modern systems, iptables is being replaced by nftables, but the concepts are identical.
| Concept | Values | Purpose |
|---|---|---|
| Tables | filter nat mangle raw |
filter — allow/drop packets. nat — address translation. mangle — modify packet headers. |
| Chains | INPUT OUTPUT FORWARD |
INPUT — traffic destined for this host. OUTPUT — traffic from this host. FORWARD — traffic routed through this host. |
| Targets | ACCEPT DROP REJECT LOG |
ACCEPT — let it through. DROP — silently discard. REJECT — discard with an error response. LOG — log and continue. |
# View all iptables rules with line numbers and packet/byte counts
sudo iptables -L -n -v --line-numbers
# View only the filter table INPUT chain
sudo iptables -L INPUT -n -v
# View the NAT table
sudo iptables -t nat -L -n -v
# Add a rule — allow inbound TCP port 443
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Insert a rule at position 1 (before other rules)
sudo iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT
# Allow established connections (essential — allows return traffic for outbound connections)
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT
# Drop everything else (set default policy)
sudo iptables -P INPUT DROP
# Delete a specific rule by line number
sudo iptables -D INPUT 5
# Save rules (Debian/Ubuntu)
sudo apt install iptables-persistent -y
sudo netfilter-persistent save
# Save rules (RHEL/Rocky)
sudo service iptables save# sudo iptables -L INPUT -n -v --line-numbers Chain INPUT (policy DROP 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination 1 142 8520 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 2 9821 589260 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED 3 412 24720 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 4 283 16980 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 5 941 56460 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 6 18 1080 DROP all -- * * 0.0.0.0/0 0.0.0.0/0
What just happened? The iptables output showed the full INPUT chain with packet and byte counters per rule. Rule 2 — the ESTABLISHED,RELATED rule — has already processed 9,821 packets (589KB), confirming it is essential for return traffic. The final DROP rule (line 6) has caught 18 packets — blocked connection attempts. The packet counts are invaluable for confirming a rule is being hit and for quantifying attack volume.
Firewall Troubleshooting
Firewall misconfiguration is a frequent cause of connectivity failures. A systematic approach to firewall troubleshooting confirms whether a dropped packet is the result of a firewall rule, a missing rule, or something else entirely in the network stack.
Step 1 — Confirm the firewall is actually active
Rule mismatches are only relevant if a firewall tool is actually running. Check service status and whether the kernel tables have any rules loaded.
sudo ufw status # or
sudo firewall-cmd --state # or
sudo iptables -L -n | head -5
Step 2 — Confirm the service is actually listening
Before blaming the firewall, confirm the service is bound and listening. A refused connection when the firewall allows the port usually means the service is down or listening on the wrong address.
sudo ss -tlnp | grep :8080
Step 3 — Test from localhost to isolate the firewall layer
The firewall INPUT chain only applies to traffic arriving from outside. A connection from localhost bypasses INPUT rules. If localhost can reach the service but external cannot, the firewall is definitely the cause.
curl -v http://localhost:8080 # bypasses firewall
nc -zv localhost 8080
Step 4 — Use LOG target to see what the firewall is doing
Add a temporary LOG rule just before the DROP rule. The kernel will log every dropped packet to syslog — you can then see exactly what traffic is being blocked and refine your rules.
sudo iptables -I INPUT 5 -j LOG --log-prefix "FW-DROP: " --log-level 4
sudo tail -f /var/log/kern.log | grep "FW-DROP"
# Quick check — temporarily disable the firewall to test if it's the cause
# (on a test server — never do this on production without a maintenance window)
sudo ufw disable # re-enable with: sudo ufw enable
# or
sudo systemctl stop firewalld # restart with: sudo systemctl start firewalld
# Check if a specific port would be allowed by the current ufw rules
sudo ufw status | grep 8080
# Trace a packet through iptables rules (requires xtables-addons or kernel support)
# Simpler: add LOG rule before DROP to see what's being dropped
sudo iptables -I INPUT -j LOG --log-prefix "INPUT: " --log-level debug
sudo dmesg | grep "INPUT:"
# Remove the LOG rule when done
sudo iptables -D INPUT -j LOG --log-prefix "INPUT: " --log-level debugProduction Firewall Patterns
Real production servers follow consistent patterns that balance security with operational requirements. The following examples cover the three most common server archetypes — a web server, a database server, and a bastion host — each with its characteristic firewall profile.
If SSH access should only come from a management network, restrict port 22 to that CIDR rather than opening it to the world. Consider running SSH on a non-standard port to reduce log noise from automated scanners.
Database ports should never be open to the internet. Restrict to the specific application server IPs or subnet. If app servers change frequently, restrict to the VPC CIDR rather than individual IPs.
A bastion host's entire purpose is to be the single SSH entry point. Its firewall should be the strictest — restrict SSH to a list of known admin IP addresses or CIDR ranges, enable fail2ban, and run nothing else on the server.
# ── Web server — ufw ──────────────────────────────────────────────
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 10.0.0.0/8 to any port 22 comment "SSH from management network"
sudo ufw allow 80/tcp comment "HTTP"
sudo ufw allow 443/tcp comment "HTTPS"
sudo ufw enable
# ── Database server — ufw ─────────────────────────────────────────
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 10.0.0.5 to any port 5432 comment "PostgreSQL from app server"
sudo ufw allow from 10.0.0.0/8 to any port 22 comment "SSH from management network"
sudo ufw enable
# ── Database server — firewalld ───────────────────────────────────
sudo firewall-cmd --zone=public --remove-service=dhcpv6-client --permanent
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="10.0.0.5" port port="5432" protocol="tcp" accept' --permanent
sudo firewall-cmd --reload# sudo ufw status verbose (web server) Status: active Default: deny (incoming), allow (outgoing), disabled (routed) To Action From -- ------ ---- 22/tcp ALLOW IN 10.0.0.0/8 # SSH from management 80/tcp ALLOW IN Anywhere # HTTP 443/tcp ALLOW IN Anywhere # HTTPS # sudo ufw status verbose (database server) To Action From -- ------ ---- 22/tcp ALLOW IN 10.0.0.0/8 # SSH from management 5432/tcp ALLOW IN 10.0.0.5 # PostgreSQL from app server
What just happened? The two firewall profiles show a meaningful security difference — the web server allows SSH from the entire management /8 network (flexible for the admin team), while the database server restricts PostgreSQL to a single IP (10.0.0.5 — the known app server). Adding a comment to each ufw allow rule documents the business reason, making future audits far easier.
Never Mix ufw, firewalld, and iptables on the Same System
All three tools write rules into the same kernel netfilter tables. Using two simultaneously creates conflicts that are extremely difficult to debug — a rule added by ufw may be silently overridden by firewalld on reload, or vice versa. Pick one tool appropriate for your distribution (ufw for Debian/Ubuntu, firewalld for RHEL/Rocky) and disable the others. Check for conflicts with systemctl status ufw firewalld — only one should be active.
Lesson Checklist
default deny incoming and add explicit allow rules only for the ports a server needs — I never "open everything and restrict specific ports"
--permanent flag for all production rules and always run --reload after changes
Teacher's Note
The localhost isolation test (Step 3 in the troubleshooting sequence) is the single most useful firewall diagnostic technique. It takes five seconds: if curl localhost:8080 works but the external connection fails, the firewall is definitively the problem and you can stop investigating the application. If curl localhost:8080 also fails, the firewall is irrelevant — the service is the problem. This single question eliminates half the possible causes in seconds.
Practice Questions
1. You are configuring the firewall on a new Ubuntu 24.04 server that will run both an nginx web server and an internal monitoring agent that listens on port 9100 (Prometheus node_exporter). The monitoring agent should only be reachable from the monitoring server at 10.0.2.5. Write the complete ufw configuration.
sudo ufw default deny incoming → sudo ufw default allow outgoing → sudo ufw allow 22/tcp (SSH) → sudo ufw allow 80/tcp (HTTP) → sudo ufw allow 443/tcp (HTTPS) → sudo ufw allow from 10.0.2.5 to any port 9100 (restrict node_exporter to monitoring server only) → sudo ufw enable. Verify with sudo ufw status verbose.
2. A developer reports that their application on port 3000 is accessible from localhost but not from external clients. You have confirmed the service is running and the correct IP is bound. Walk through the diagnostic steps and explain how you would confirm the firewall is the cause and then fix it using ufw.
ss -tlnp | grep 3000 — ensure it shows 0.0.0.0:3000 not 127.0.0.1:3000. Then check the firewall: sudo ufw status — if port 3000 is not listed, the firewall is blocking it. Confirm by temporarily testing with ufw disabled: sudo ufw disable and retry the connection. If it works, re-enable and add the rule: sudo ufw enable && sudo ufw allow 3000/tcp.
3. Explain the difference between DROP and REJECT as iptables targets. In what scenario would you prefer REJECT over DROP, and when is DROP the better choice?
DROP silently discards the packet — the sender gets no response and must wait for a timeout. REJECT discards the packet and sends back an ICMP error (e.g. "port unreachable") so the sender knows immediately the connection was refused. Prefer REJECT for internal networks and development environments where fast failure feedback is useful and you trust the senders. Use DROP for internet-facing rules to slow down port scanners and attackers — no response gives them less information and forces them to wait for timeouts, consuming their resources.
Lesson Quiz
1. You add a service to firewalld with sudo firewall-cmd --zone=public --add-service=http (without --permanent). What happens to this rule after a reboot?
2. An iptables INPUT chain with policy DROP is missing the ESTABLISHED,RELATED rule. What breaks?
3. In firewalld, what is a zone and why is it more flexible than managing individual iptables rules per service?
Up Next
Lesson 30 — SELinux Basics
Mandatory access control, SELinux modes, contexts, and resolving AVC denials