Firewall Rules and iptables
A firewall enforces a policy on network traffic - deciding what packets are allowed through and what’s dropped. This note covers the theory of packet filtering, the Linux iptables/nftables tool suite, and practical rule writing for both host and network firewall scenarios.
How Packet Filtering Works
Section titled “How Packet Filtering Works”A packet filter inspects each packet’s headers (IP source/destination, protocol, port, TCP flags) and matches them against an ordered list of rules. The first matching rule wins.
Packet arrives │ ▼Rule 1: src=10.0.0.0/24, dst=any, dport=22 → ACCEPT (matched → execute)Rule 2: src=any, dst=any, dport=22 → DROPRule 3: src=any, dst=any, proto=icmp → ACCEPT...Default policy: → DROP (implicit deny)Stateless vs Stateful
Section titled “Stateless vs Stateful”| Type | How it works | Limitation |
|---|---|---|
| Stateless | Matches each packet individually on header fields only | Can’t distinguish request from response; must explicitly allow return traffic |
| Stateful | Tracks connection state (NEW, ESTABLISHED, RELATED, INVALID); allows return traffic automatically | Requires more memory; state table can be exhausted (DoS) |
Stateful rule for HTTP: ALLOW outbound TCP to port 80 (NEW connection) ALLOW inbound TCP from port 80 that is ESTABLISHED,RELATED (return traffic automatically allowed without a separate inbound rule)Modern firewalls are always stateful. Use conntrack module in iptables.
iptables Architecture
Section titled “iptables Architecture”iptables organises rules into tables → chains → rules:
Tables: filter → packet accept/drop decisions (default; most common) nat → source/destination address translation mangle → packet header modification (TTL, QoS marks) raw → bypass conntrack (specialised)
Chains (filter table): INPUT → packets destined for THIS host FORWARD → packets passing THROUGH this host (routing/NAT gateway) OUTPUT → packets generated BY this host
Packet flow: Incoming: PREROUTING (nat) → INPUT (filter) → local process Forwarded: PREROUTING (nat) → FORWARD (filter) → POSTROUTING (nat) → egress Outgoing: local process → OUTPUT (filter) → POSTROUTING (nat)Rule Anatomy
Section titled “Rule Anatomy”iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT │ │ │ │ │ │ tool chain source IP protocol dest port target
# Targets:# ACCEPT - allow the packet# DROP - silently discard the packet# REJECT - discard and send ICMP/TCP RST to sender# LOG - log the packet (continue processing - not a terminating target)# RETURN - return to calling chainPractical iptables Reference
Section titled “Practical iptables Reference”Basic Host Firewall Setup
Section titled “Basic Host Firewall Setup”#!/bin/bash# Host firewall hardening script# Run as root
# 1. Flush existing rulesiptables -Fiptables -Xiptables -Z
# 2. Default policies - drop everything inbound, allow outboundiptables -P INPUT DROPiptables -P FORWARD DROPiptables -P OUTPUT ACCEPT
# 3. Always allow loopbackiptables -A INPUT -i lo -j ACCEPTiptables -A OUTPUT -o lo -j ACCEPT
# 4. Allow established/related return traffic (stateful)iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 5. Drop INVALID packets (malformed or unexpected state)iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# 6. Allow ICMP ping (optional - remove for stealth)iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/sec -j ACCEPT
# 7. Allow SSH only from management networkiptables -A INPUT -s 10.0.0.0/24 -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# 8. Allow HTTPS from anywhereiptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
# 9. Log and drop everything elseiptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "DROPPED: " --log-level 4iptables -A INPUT -j DROP
# 10. Persist rulesiptables-save > /etc/iptables/rules.v4# Load rules on boot (Debian/Ubuntu)apt install iptables-persistentnetfilter-persistent savenetfilter-persistent reload
# Load rules on boot (systemd approach)cat << 'EOF' > /etc/systemd/system/iptables-restore.service[Unit]Description=Restore iptables rulesBefore=network.target
[Service]ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4Type=oneshot
[Install]WantedBy=multi-user.targetEOFsystemctl enable --now iptables-restoreCommonly Used Rule Patterns
Section titled “Commonly Used Rule Patterns”# --- Block a specific IP ---iptables -I INPUT -s 203.0.113.50 -j DROP
# --- Rate limit SSH (brute force protection) ---iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -m recent --set --name SSH_BRUTEiptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \ -m recent --update --seconds 60 --hitcount 5 --name SSH_BRUTE -j DROP# Allows max 5 new SSH connections per 60 seconds per source IP
# --- Block outbound to suspicious IP range ---iptables -A OUTPUT -d 185.220.0.0/16 -j DROP
# --- Allow specific port range ---iptables -A INPUT -p tcp --dport 8000:8080 -j ACCEPT
# --- Allow multiple ports with multiport ---iptables -A INPUT -p tcp -m multiport --dports 80,443,8080 -j ACCEPT
# --- Block a country's IP range (using ipset for efficiency) ---apt install ipsetipset create russia hash:netipset add russia 5.8.0.0/16 # add each CIDR block# ... (or load from geolocation database)iptables -A INPUT -m set --match-set russia src -j DROP
# --- FORWARD rules for a NAT/router host ---# Allow forwarding from internal to externaliptables -A FORWARD -i eth1 -o eth0 -j ACCEPT# Allow return trafficiptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTNAT Rules
Section titled “NAT Rules”# --- Masquerade (dynamic NAT - Linux acting as a router) ---# All traffic from 192.168.1.0/24 going out eth0 appears to come from eth0's IPiptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
# Enable IP forwarding (required for NAT/routing)echo 1 > /proc/sys/net/ipv4/ip_forwardecho "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
# --- Port forwarding (DNAT - forward external port to internal host) ---# Forward external port 2222 on this host to 192.168.1.100:22iptables -t nat -A PREROUTING -p tcp --dport 2222 -j DNAT --to-destination 192.168.1.100:22
# --- Static SNAT (force a specific source IP, e.g., for multi-IP servers) ---iptables -t nat -A POSTROUTING -s 192.168.1.100 -o eth0 -j SNAT --to-source 203.0.113.5Viewing, Debugging, Flushing
Section titled “Viewing, Debugging, Flushing”# List all rules with line numbers and packet countsiptables -L -v -n --line-numbersiptables -t nat -L -v -n --line-numbers # NAT table
# Delete by line number (INPUT chain rule 3)iptables -D INPUT 3
# Delete by matching the rule exactlyiptables -D INPUT -s 203.0.113.50 -j DROP
# Insert rule at position 1 (start of chain)iptables -I INPUT 1 -s 10.0.0.0/8 -j ACCEPT
# Flush only one chainiptables -F INPUT
# Reset entire ruleset and default policiesiptables -F && iptables -X && iptables -P INPUT ACCEPT && iptables -P FORWARD ACCEPT
# Show packet/byte counters (useful for verifying rules are being hit)iptables -L -v -n | column -t
# Watch live rule hitswatch -n 1 "iptables -L INPUT -v -n --line-numbers"nftables - The Modern Replacement
Section titled “nftables - The Modern Replacement”nftables replaces iptables, ip6tables, arptables, and ebtables with a unified framework. Most distributions now default to nftables (Debian 10+, RHEL 8+).
# Check if nftables is the backendiptables --version # shows "iptables v1.8.x (nf_tables)" if nftables backend
# Basic nftables ruleset equivalent to the iptables script abovenft delete table ip filter # clear previousnft add table ip filternft add chain ip filter input { type filter hook input priority 0 \; policy drop \; }nft add chain ip filter output { type filter hook output priority 0 \; policy accept \; }
# Loopbacknft add rule ip filter input iif lo accept
# Established/relatednft add rule ip filter input ct state established,related acceptnft add rule ip filter input ct state invalid drop
# SSH from management networknft add rule ip filter input ip saddr 10.0.0.0/24 tcp dport 22 accept
# HTTPSnft add rule ip filter input tcp dport 443 accept
# Log and dropnft add rule ip filter input log prefix "DROPPED: "nft add rule ip filter input drop
# Savenft list ruleset > /etc/nftables.confsystemctl enable --now nftables# nftables syntax - view current rulesetnft list ruleset
# Delete a specific rule (use handle)nft list ruleset -a # shows handlesnft delete rule ip filter input handle 5
# Add to start (priority lower number = process first in nftables)# Use separate chains with priority numbers instead of -I orderingufw - Uncomplicated Firewall (Ubuntu/Debian)
Section titled “ufw - Uncomplicated Firewall (Ubuntu/Debian)”ufw is a friendlier wrapper around iptables, suitable for simpler host firewalls:
# Enable ufw (defaults: deny inbound, allow outbound)ufw default deny incomingufw default allow outgoing
# Allow common servicesufw allow ssh # port 22ufw allow 443/tcp # HTTPSufw allow from 10.0.0.0/24 to any port 22 # SSH from LAN only
# Rate limit SSHufw limit ssh # limits to 6 connections per 30 seconds
# Enable and check statusufw enableufw status verbose
# Delete a ruleufw delete allow 443/tcpufw delete 2 # delete rule #2 from numbered list
# Show with rule numbersufw status numberedfirewalld - Zone-Based Firewall (RHEL/Fedora)
Section titled “firewalld - Zone-Based Firewall (RHEL/Fedora)”firewalld uses the concept of zones - pre-defined trust levels for network interfaces:
# View available zonesfirewall-cmd --get-zones# public trusted home work internal dmz drop block external
# Check active zones and interfacesfirewall-cmd --get-active-zones
# Allow a service (uses /etc/firewalld/services/ definitions)firewall-cmd --zone=public --add-service=https --permanentfirewall-cmd --reload
# Allow a portfirewall-cmd --zone=public --add-port=8443/tcp --permanent
# Allow only from specific sourcefirewall-cmd --zone=internal --add-source=10.0.0.0/24 --permanentfirewall-cmd --zone=internal --add-service=ssh --permanent
# Remove a service from zonefirewall-cmd --zone=public --remove-service=dhcpv6-client --permanent
# View all rules in a zonefirewall-cmd --zone=public --list-all
# Create a rich rule (more granular control)firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="203.0.113.50" reject' --permanentWindows Firewall (PowerShell)
Section titled “Windows Firewall (PowerShell)”# Check firewall profiles statusGet-NetFirewallProfile | Select-Object Name, Enabled, DefaultInboundAction, DefaultOutboundAction
# Block inbound by default on all profilesSet-NetFirewallProfile -Profile Domain,Public,Private -DefaultInboundAction Block
# Allow inbound SSH (OpenSSH)New-NetFirewallRule -DisplayName "Allow SSH" -Direction Inbound ` -Protocol TCP -LocalPort 22 -Action Allow
# Allow inbound from specific IP onlyNew-NetFirewallRule -DisplayName "Allow SSH from LAN" -Direction Inbound ` -Protocol TCP -LocalPort 22 -RemoteAddress 10.0.0.0/24 -Action Allow
# View all inbound allow rulesGet-NetFirewallRule -Direction Inbound -Action Allow | Select-Object DisplayName, Enabled
# Delete a ruleRemove-NetFirewallRule -DisplayName "Allow SSH"
# List rules with port detailsGet-NetFirewallRule | Get-NetFirewallPortFilter | Select-Object LocalPort, ProtocolFirewall Hardening Principles
Section titled “Firewall Hardening Principles”1. Default deny (implicit deny) → Start with DROP/BLOCK all; add allow rules selectively
2. Least privilege → Allow exactly what's needed, from exactly whom, to exactly where
3. Stateful inspection always → Use conntrack; never write stateless rules for TCP services
4. Egress filtering matters too → Outbound filtering stops C2 callbacks, data exfiltration, lateral movement → Block outbound to known-bad IPs; restrict what protocols servers can initiate
5. Log before drop → LOG target before DROP gives visibility into what's being blocked → Rate-limit logging to avoid log flood (--limit 5/min)
6. Rules are evaluated in order → More specific rules before general ones → ALLOW before DROP for the same traffic
7. Segment with firewall, not just VLAN → VLANs separate L2 broadcast domains → Firewalls enforce L3/L4 access control between segments
8. Test after change → nmap from attacker perspective after applying rules → Verify blocked traffic is actually blocked; verify allowed traffic works# Verify your firewall from the outside (test from a different machine)# Test that SSH is blocked when it should benmap -p 22 -sS target-ip # should show "filtered" if dropped, "closed" if rejected
# Test connectivity to an allowed portnmap -p 443 -sS target-ip # should show "open"
# Simulate an attack - check if suspicious ports are opennmap -sV -p- target-ip # full port scan with service detection