Linux iptables and firewalld: Complete Firewall Configuration Guide

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:

TablePurposeChains Available
filterDefault — packet filtering (ACCEPT/DROP/REJECT)INPUT, OUTPUT, FORWARD
natNetwork Address TranslationPREROUTING, OUTPUT, POSTROUTING
mangleModify packet headers (TTL, TOS, marks)All chains
rawSkip 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)

ZoneDefault PolicyUse Case
trustedAccept allCompletely trusted networks
homeReject (with selected services)Home networks
internalReject (with selected services)Internal corporate network
workReject (with selected services)Work networks
publicReject (with ssh allowed)Default zone — internet-facing
externalReject (with masquerade)Router external interface
dmzReject (with ssh allowed)Demilitarized zone
blockReject all (with icmp-host-prohibited)Near-total blocking
dropDrop all incomingTotal 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

Featureiptablesfirewalld
RHEL version6 (primary), 7 (available)7+ (primary)
Rule changesRequire full reload or manual insertionDynamic (no connection drops)
ConceptsChains, tables, rulesZones, services, rich rules
PersistenceManual save requiredBuilt-in (--permanent)
D-Bus integrationNoYes (NetworkManager integration)
Managementiptables CLIfirewall-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