Firewall

Firewall rule management across iptables, nftables, ufw, and firewalld.

firewalld — Zone-Based Firewall

firewalld uses zones to define trust levels. Each network interface is assigned to exactly one zone. Traffic entering that interface is subject to that zone’s rules.

Zone Management

List all zones and their active settings
sudo firewall-cmd --list-all-zones
Show the active zone for each interface
sudo firewall-cmd --get-active-zones
Show rules for a specific zone
sudo firewall-cmd --zone=public --list-all
Show the default zone
sudo firewall-cmd --get-default-zone
Change the default zone
sudo firewall-cmd --set-default-zone=drop

Common Zones

Zone Default Behavior Use Case

drop

Drop all incoming, no reply

Untrusted networks, internet-facing

block

Reject all incoming (ICMP reject)

Similar to drop but polite

public

Reject incoming except allowed

Default — servers with selective access

internal

More permissive than public

Trusted LAN segments

trusted

Accept everything

Loopback only — never assign a real interface

Adding Services and Ports

Allow SSH — using service name (preferred)
sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --reload
Allow a specific port
sudo firewall-cmd --zone=public --add-port=8443/tcp --permanent
sudo firewall-cmd --reload
Allow multiple ports at once
sudo firewall-cmd --zone=public --add-port={80,443,8080}/tcp --permanent
sudo firewall-cmd --reload
Remove a service
sudo firewall-cmd --zone=public --remove-service=cockpit --permanent
sudo firewall-cmd --reload
List available services
sudo firewall-cmd --get-services

The --permanent flag writes to config. Without it, the rule is runtime-only and lost on reload. Always use --permanent + --reload together.

Rich Rules — Granular Control

Rich rules are firewalld’s answer to complex iptables rules. They combine source, destination, port, and action.

Allow SSH only from a specific subnet
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="10.50.1.0/24" service name="ssh" accept' --permanent
sudo firewall-cmd --reload
Rate-limit SSH connections — 3 per minute per source IP
sudo firewall-cmd --zone=public --add-rich-rule='rule service name="ssh" accept limit value="3/m"' --permanent
sudo firewall-cmd --reload
Log and drop traffic from a specific IP
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.100" log prefix="BLOCKED: " level="warning" drop' --permanent
sudo firewall-cmd --reload
Allow RADIUS from ISE to this host only
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="10.50.1.20" port port="1812-1813" protocol="udp" accept' --permanent
sudo firewall-cmd --reload
List rich rules
sudo firewall-cmd --zone=public --list-rich-rules

ufw — Uncomplicated Firewall

ufw is the Ubuntu/Debian frontend for iptables. Simpler than firewalld but less flexible.

Enable ufw with default deny incoming
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
Allow SSH
sudo ufw allow ssh
Allow from a specific subnet only
sudo ufw allow from 10.50.1.0/24 to any port 22
Allow a port range
sudo ufw allow 8000:8100/tcp
Deny a specific IP
sudo ufw deny from 192.168.1.100
Show numbered rules — for deletion
sudo ufw status numbered
Delete a rule by number
sudo ufw delete 3
Show verbose status
sudo ufw status verbose

nftables — The Modern Backend

firewalld and ufw are frontends. nftables is the actual kernel framework (replacing iptables). Direct nftables rules are for when you need performance or features the frontends don’t expose.

List all rules
sudo nft list ruleset
Show a specific table
sudo nft list table inet filter
Flush all rules (DESTRUCTIVE — you lose everything)
sudo nft flush ruleset

Hardened Server Baseline

A minimal firewall for a server that only runs SSH + a web app.

firewalld hardened baseline
# Set default to drop — deny everything first
sudo firewall-cmd --set-default-zone=drop

# Allow SSH from management subnet only
sudo firewall-cmd --zone=drop --add-rich-rule='rule family="ipv4" source address="10.50.1.0/24" service name="ssh" accept' --permanent

# Allow HTTPS from anywhere
sudo firewall-cmd --zone=drop --add-port=443/tcp --permanent

# Allow ICMP ping from local net (monitoring)
sudo firewall-cmd --zone=drop --add-rich-rule='rule family="ipv4" source address="10.50.0.0/16" protocol value="icmp" accept' --permanent

# Apply
sudo firewall-cmd --reload
sudo firewall-cmd --zone=drop --list-all

Defense in Depth — Layered Rules

Firewall rules are one layer. A hardened server combines multiple layers:

Layer Tool What It Controls

Network firewall

pfSense, Palo Alto

Inter-VLAN, internet egress

Host firewall

firewalld, ufw

Per-server ingress/egress

Application

nginx, Apache

TLS, request filtering, rate limiting

Authentication

SSH keys, Vault

Who can access

Monitoring

Wazuh, journald

Detect and alert on anomalies

Verify Wazuh is monitoring firewall events
sudo grep -c "firewalld" /var/ossec/logs/alerts/alerts.json

Port Knocking — Concept

Port knocking hides services behind a sequence of connection attempts. The firewall only opens the real port after seeing the correct knock sequence.

Concept — NOT a production recommendation
1. Client sends SYN to port 7000
2. Client sends SYN to port 8000
3. Client sends SYN to port 9000
4. Firewall opens port 22 for that source IP for 30 seconds
5. Client connects to SSH

Port knocking is security through obscurity. It adds a layer but should never be the only defense. Prefer VPN or Vault-signed SSH certificates instead.

Troubleshooting

Check if a port is reachable — from another host
nc -zv target-host 443
Check what’s listening locally
ss -tlnp
Trace firewall drops in the journal
sudo journalctl -k | grep -i "DROPPED\|REJECTED\|BLOCK" | tail -20
Check firewalld logs specifically
sudo journalctl -u firewalld --since "1 hour ago"
Verify-before/change/verify-after pattern — always
# Before
sudo firewall-cmd --zone=public --list-all

# Change
sudo firewall-cmd --zone=public --add-service=https --permanent
sudo firewall-cmd --reload

# After
sudo firewall-cmd --zone=public --list-all