Linux firewall is one of the most critical security layers for any production server. Understanding both iptables (the low-level netfilter framework) and firewalld (the high-level management tool) at a deep level enables you to design secure network access policies, troubleshoot connectivity issues, and protect servers from network attacks.
Linux Networking and Packet Flow
Before diving into firewall configuration, you need to understand how packets flow through the Linux networking stack. The Linux kernel uses netfilter as the packet filtering framework. iptables and firewalld are both management tools that write rules into netfilter.
Packet Journey Through netfilter
Incoming packet
↓
PREROUTING chain (NAT table) # modify destination before routing
↓
Routing decision: Is packet for local system or forward?
↓ ↓
INPUT chain FORWARD chain # packets for this system | packets being routed through
↓ ↓
Local process POSTROUTING chain (NAT)
↓ ↓
OUTPUT chain Network (forwarded)
↓
POSTROUTING chain (NAT) # modify source after routing
↓
Outgoing packet
iptables Tables
iptables uses multiple tables, each for a different purpose:
| Table | Purpose | Chains Available |
|---|---|---|
| filter | Default — packet filtering (ACCEPT/DROP/REJECT) | INPUT, OUTPUT, FORWARD |
| nat | Network Address Translation | PREROUTING, OUTPUT, POSTROUTING |
| mangle | Modify packet headers (TTL, TOS, marks) | All chains |
| raw | Skip connection tracking (performance) | PREROUTING, OUTPUT |
iptables Chains and Default Policies
Each chain has a default policy — what happens when no rule matches. For security, the INPUT chain default policy should be DROP (deny by default), with specific rules allowing legitimate traffic.
# View current iptables rules:
# iptables -L # list rules
# iptables -L -n # numeric output (no DNS lookup)
# iptables -L -n -v # with packet/byte counters
# iptables -L -n --line-numbers # with rule numbers
# Set default policies:
# iptables -P INPUT DROP # deny all incoming by default
# iptables -P OUTPUT ACCEPT # allow all outgoing by default
# iptables -P FORWARD DROP # deny all forwarding by default
iptables Rule Syntax
# Basic syntax:
# iptables [-t table] ACTION chain rule-specification -j target
# Actions:
# -A = append (add to end of chain)
# -I = insert (add to beginning or specific position)
# -D = delete rule
# -R = replace rule
# -F = flush (delete all rules in chain)
# -Z = zero counters
# -N = new chain
# -X = delete chain
# Targets (what to do with matching packet):
# ACCEPT = allow packet through
# DROP = silently discard (no response sent to sender)
# REJECT = discard and notify sender (sends ICMP port unreachable or TCP RST)
# LOG = log packet then continue (doesn't stop processing)
# DNAT = Destination NAT (redirect to different IP:port)
# SNAT = Source NAT (rewrite source IP)
# MASQUERADE = SNAT for dynamic IPs (e.g., shared internet)
Building a Secure Firewall Rule Set
# Step 1: Allow established connections (critical — must be first):
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Without this, server can initiate connections but replies are blocked
# Step 2: Allow loopback:
iptables -A INPUT -i lo -j ACCEPT
# Step 3: Allow ICMP (ping):
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
# Step 4: Allow SSH from trusted network:
iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT
# Step 5: Allow HTTP and HTTPS:
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Step 6: Allow DNS queries (if this is a DNS server):
iptables -A INPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p tcp --dport 53 -j ACCEPT
# Step 7: Log dropped packets (for debugging):
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPT DROP: " --log-level 4
# Step 8: Drop everything else (default policy already DROP, but explicit is clearer):
iptables -A INPUT -j DROP
Saving and Restoring iptables Rules
# RHEL 6 — save rules to persistent file:
# service iptables save # saves to /etc/sysconfig/iptables
# iptables-save > /etc/sysconfig/iptables # manual save
# RHEL 6 — restore rules at boot:
# chkconfig iptables on # iptables service reads /etc/sysconfig/iptables
# RHEL 7 — install iptables-services for persistent iptables:
# yum install iptables-services -y
# systemctl start iptables
# systemctl enable iptables
# iptables-save > /etc/sysconfig/iptables
# service iptables reload
# Apply saved rules:
# iptables-restore < /etc/sysconfig/iptables
# Example /etc/sysconfig/iptables:
*filter
:INPUT DROP [0:0] # default policy DROP
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -p icmp -j ACCEPT
-A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT
-A INPUT -p tcp --dport 80 -j ACCEPT
-A INPUT -p tcp --dport 443 -j ACCEPT
COMMIT
NAT Configuration with iptables
# Source NAT (MASQUERADE) — share internet via one IP:
# Enable IP forwarding:
# echo 1 > /proc/sys/net/ipv4/ip_forward
# echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
# Masquerade outgoing traffic:
# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
# iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
# Destination NAT (port forwarding):
# Forward external port 80 to internal web server:
# iptables -t nat -A PREROUTING -d 203.0.113.1 -p tcp --dport 80 \
-j DNAT --to-destination 192.168.1.10:80
# iptables -A FORWARD -p tcp -d 192.168.1.10 --dport 80 -j ACCEPT
firewalld — Zone-Based Firewall Management
firewalld is the default firewall management tool on RHEL 7+. It uses a zone concept — each network interface is assigned to a zone, and each zone has a set of allowed services/ports.
firewalld Zones (Default to Most Restrictive)
| Zone | Default Policy | Use Case |
|---|---|---|
| trusted | Accept all | Completely trusted networks |
| home | Reject (with selected services) | Home networks |
| internal | Reject (with selected services) | Internal corporate network |
| work | Reject (with selected services) | Work networks |
| public | Reject (with ssh allowed) | Default zone — internet-facing |
| external | Reject (with masquerade) | Router external interface |
| dmz | Reject (with ssh allowed) | Demilitarized zone |
| block | Reject all (with icmp-host-prohibited) | Near-total blocking |
| drop | Drop all incoming | Total blocking |
firewalld — Comprehensive Command Reference
# Status and zones:
# firewall-cmd --state # running or not
# firewall-cmd --get-default-zone # current default zone
# firewall-cmd --get-active-zones # zones with interfaces
# firewall-cmd --list-all # all rules in default zone
# firewall-cmd --list-all-zones # all zones, all rules
# Change default zone:
# firewall-cmd --set-default-zone=internal
# Assign interface to zone:
# firewall-cmd --zone=public --add-interface=eth0 --permanent
# firewall-cmd --zone=internal --change-interface=eth1 --permanent
# ── SERVICES (named services like http, ssh, nfs) ──
# Allow a service:
# firewall-cmd --permanent --add-service=http
# firewall-cmd --permanent --add-service=https
# firewall-cmd --permanent --add-service=mysql
# Remove a service:
# firewall-cmd --permanent --remove-service=http
# List available service definitions:
# firewall-cmd --get-services
# ls /usr/lib/firewalld/services/ # XML definitions
# ── PORTS (specific port numbers) ──
# Allow a port:
# firewall-cmd --permanent --add-port=8080/tcp
# firewall-cmd --permanent --add-port=5000-5010/tcp # port range
# Remove a port:
# firewall-cmd --permanent --remove-port=8080/tcp
# ── APPLY CHANGES ──
# firewall-cmd --reload # apply --permanent rules
# firewall-cmd --complete-reload # full reload (breaks connections)
# ── RUNTIME vs PERMANENT ──
# Without --permanent: change applies now, lost on reload/reboot
# With --permanent: change saved to config, applied after reload
# Best practice: use --permanent and then --reload
Rich Rules — Fine-Grained Control
# Rich rules allow conditions like source/destination IP, port, limit:
# Allow SSH only from specific subnet:
# firewall-cmd --permanent --add-rich-rule=\
'rule family="ipv4" source address="192.168.1.0/24" service name="ssh" accept'
# Reject all traffic from specific IP:
# firewall-cmd --permanent --add-rich-rule=\
'rule family="ipv4" source address="10.0.0.5" reject'
# Rate limit SSH (5 connections per minute):
# firewall-cmd --permanent --add-rich-rule=\
'rule service name="ssh" limit value="5/m" accept'
# Log dropped packets:
# firewall-cmd --permanent --add-rich-rule=\
'rule family="ipv4" source address="203.0.113.0/24" log prefix="BLOCKED: " level="warning" drop'
# Port forwarding with firewalld:
# firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
# firewall-cmd --permanent --add-masquerade # enable masquerade in zone
Creating Custom Services
# Custom service file:
# vim /etc/firewalld/services/myapp.xml
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>MyApp</short>
<description>My application on port 8765</description>
<port protocol="tcp" port="8765"/>
<port protocol="udp" port="8765"/>
</service>
# Use the custom service:
# firewall-cmd --permanent --add-service=myapp
# firewall-cmd --reload
Comparing iptables and firewalld
| Feature | iptables | firewalld |
|---|---|---|
| RHEL version | 6 (primary), 7 (available) | 7+ (primary) |
| Rule changes | Require full reload or manual insertion | Dynamic (no connection drops) |
| Concepts | Chains, tables, rules | Zones, services, rich rules |
| Persistence | Manual save required | Built-in (--permanent) |
| D-Bus integration | No | Yes (NetworkManager integration) |
| Management | iptables CLI | firewall-cmd, firewall-config, cockpit |
Common Firewall Troubleshooting
# Is service accessible?
# telnet server 80 # or nc -zv server 80
# curl http://server
# Check if firewall is the problem:
# Temporarily allow all (testing only):
# iptables -F # flush all rules
# iptables -P INPUT ACCEPT
# If it works after flush → firewall was blocking
# Check firewalld logs:
# journalctl -u firewalld
# firewall-cmd --get-log-denied # check denied log level
# View all active rules (iptables underneath firewalld):
# iptables -L -n -v # see actual netfilter rules
# nft list ruleset # RHEL 8+ uses nftables instead
# Common mistakes:
# Port 80 open but service fails → SELinux (httpd_can_network_connect)
# Firewall shows allow but still blocked → check if service is listening: ss -tulnp
# Different behavior per interface → check zone assignment: firewall-cmd --get-active-zones