sysadmin · June 26, 2026 · 14 min read

firewalld from scratch.
zones, services, rich rules, and timed rules.

secadmin wants a clean firewall: web open to everyone, SSH only from the management subnet, a custom port for one host, and Cockpit open for an hour. All permanent, all reloaded.

// what we’re getting into
  1. The ticket
  2. Concept review. Zones and the two flags.
  3. Inventory the current firewall
  4. Open HTTP and HTTPS
  5. Restrict SSH with a rich rule
  6. A custom port for one host
  7. A timed rule for Cockpit
  8. Generate the report
  9. Exam questions
  10. Final checklist: confirm everything works

secadmin wants the firewall on server1 rebuilt from a clean sheet, and the rules map straight onto who should reach what. Web open to the world on 80 and 443. SSH only from the management subnet, 10.0.50.0/24, refused from everywhere else. A management UI on 8443 open to exactly one host, 10.0.50.5. Cockpit cracked open for a single one-hour maintenance window. And a snapshot of every rule for the change ticket. All permanent, reloaded after.

Two flags decide whether any of this survives. —permanent writes the rule to disk, —reload pushes the saved rules into the running firewall. Forget —permanent and the rule evaporates at the next reboot. Forget —reload and it sits on disk doing nothing while you swear the change is not working. You will type both more today than anything else.

The ticket

bash
sudo systemctl is-active firewalld || sudo systemctl enable --now firewalld
sudo firewall-cmd --state
sudo firewall-cmd --get-default-zone
sudo firewall-cmd --list-all

Concept review. Zones and the two flags.

bash
man firewall-cmd
man firewalld.richlanguage

A packet is matched to a zone, usually by the interface or its source address, and the zone decides what is allowed through named services, ports, or rich rules. Anything not allowed is dropped. The default zone on RHEL 9 is public. The trusted zone allows everything, drop silently discards, and block rejects with a reply.

the two flags you use constantly
--permanent   write the rule to disk, so it survives a reboot
--reload      apply the on-disk rules to the running firewall

# without --permanent the rule vanishes on reboot
# without --reload a permanent rule does not take effect yet

Inventory the current firewall

Know your baseline before you change anything.

bash
sudo firewall-cmd --get-default-zone
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all
sudo firewall-cmd --get-services | tr ' ' '\n' | head -20

Open HTTP and HTTPS

Named services are easier than raw ports, because they already map to the standard port numbers. Add them permanently, then reload.

bash
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-services

Restrict SSH with a rich rule

This is the step people get subtly wrong. Adding a rich rule that allows SSH from one subnet does nothing if the global ssh service is still open, because that still lets anyone in. Remove the global allow first, then add the scoped rule.

bash
sudo firewall-cmd --permanent --zone=public --remove-service=ssh

sudo firewall-cmd --permanent --zone=public \
    --add-rich-rule='rule family="ipv4" source address="10.0.50.0/24" service name="ssh" accept'

sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-rich-rules

A custom port for one host

Same idea, scoped to a single address and a raw port instead of a named service.

bash
sudo firewall-cmd --permanent --zone=public \
    --add-rich-rule='rule family="ipv4" source address="10.0.50.5" port port="8443" protocol="tcp" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-rich-rules

A timed rule for Cockpit

A maintenance window wants a rule that cleans itself up. A timeout rule is runtime only, not permanent, and firewalld removes it automatically when the clock runs out.

bash
sudo firewall-cmd --zone=public --add-service=cockpit --timeout=3600
sudo firewall-cmd --zone=public --list-services
# cockpit is removed from the runtime config automatically after an hour

Generate the report

bash
sudo mkdir -p /srv/audit_reports
sudo firewall-cmd --list-all-zones | sudo tee /srv/audit_reports/firewall_$(date +%Y%m%d).txt

Exam questions

Write the command first.

Q1. Permanently open port 8080/tcp in the default zone and reload.

Q2. Open HTTPS in the public zone permanently and verify it is listed.

Q3. Create a rich rule allowing SSH only from 10.0.0.5, permanent and applied immediately.

Answers.

bash
# A1
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
# A2
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload; sudo firewall-cmd --zone=public --list-services
# A3 (remember to remove the global ssh service too)
sudo firewall-cmd --permanent --zone=public --remove-service=ssh
sudo firewall-cmd --permanent --zone=public \
    --add-rich-rule='rule family="ipv4" source address="10.0.0.5" service name="ssh" accept'
sudo firewall-cmd --reload

Final checklist: confirm everything works

If every check passes, the ticket is done.

bash
# 1. http and https open publicly
sudo firewall-cmd --zone=public --list-services

# 2. SSH scoped to the management subnet, custom port to one host
sudo firewall-cmd --zone=public --list-rich-rules

# 3. cockpit opened with a 1 hour timeout (runtime only)
# 4. snapshot saved, every permanent change followed by --reload
# 5. all three exam commands written from scratch
# 6. tracker entry checked off

Reply to secadmin: firewalld configured. Web public, SSH limited to the management subnet, port 8443 to the single management host, Cockpit timed open for an hour. Snapshot in /srv/audit_reports/.

next post
SELinux part one: diagnosing denials and fixing file contexts