Firewall Best Practices for Linux
A firewall controls which network traffic is allowed to enter and leave a system. On Linux, the kernel's Netfilter framework provides the underlying packet filtering mechanism, with iptables, nftables, and firewalld as the primary user-space tools for configuring rules. A properly configured firewall is the first line of network defense. This guide covers defense-in-depth principles, default deny policies, connection tracking, rate limiting, logging, zone-based management with firewalld, and IPv6 considerations.
Hub: Linux Security Hardening | See also: SSH Hardening, Server Checklist
Defense in Depth
A firewall is only one layer. Defense in depth means combining the firewall with application-level controls, TLS, authentication, intrusion detection, and mandatory access control. If the firewall is bypassed -- for example through an application vulnerability on an allowed port -- the other layers still protect the system. Never rely on a single control, no matter how well configured.
Think of a firewall as the outer wall of a castle. Useful, but you still want guards, locks, and a vault inside.
Default Deny with iptables
The most important firewall rule is the default policy. A default deny (or default drop) policy rejects everything that is not explicitly permitted. This means that if you forget to add a rule, the traffic is blocked rather than allowed:
# Flush existing rules and delete custom chains
sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t mangle -F
# Set default policies to DROP
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
# Allow loopback traffic (essential for local services)
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A OUTPUT -o lo -j ACCEPT
# Allow established and related connections (stateful tracking)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow SSH (adjust port if using a non-standard port)
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# Allow HTTP and HTTPS
sudo iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
# Allow ICMP ping (useful for monitoring and diagnostics)
sudo iptables -A INPUT -p icmp --icmp-type echo-request \
-m limit --limit 1/second -j ACCEPT
# Drop invalid packets explicitly (rather than letting them hit the default)
sudo iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
The OUTPUT ACCEPT policy allows all outgoing traffic. For high-security
environments, set OUTPUT DROP and explicitly allow only the outbound
connections your server needs (DNS, HTTPS for updates, NTP, SMTP). Egress
filtering prevents data exfiltration and limits what a compromised process
can reach.
Connection Tracking (conntrack)
Netfilter's connection tracking module (conntrack) maintains state for every
connection passing through the firewall. The --ctstate ESTABLISHED,RELATED
rule is critical: it allows return traffic for connections your server
initiated or accepted, and related traffic such as FTP data connections or
ICMP errors associated with an existing session.
# View the current connection tracking table
sudo conntrack -L
# Count tracked connections
sudo conntrack -C
# View tracking table statistics
sudo conntrack -S
# Increase the maximum tracked connections (default is often 65536)
echo 262144 | sudo tee /proc/sys/net/netfilter/nf_conntrack_max
# Make the change persistent
echo "net.netfilter.nf_conntrack_max = 262144" | sudo tee -a /etc/sysctl.d/99-conntrack.conf
sudo sysctl --system
Without conntrack, you would need explicit rules for every direction of every conversation, which is both error-prone and inherently less secure.
Rate Limiting
Rate limiting mitigates brute-force attacks and SYN floods by restricting how many new connections per unit of time a source IP can establish:
# Global rate limit: accept up to 4 new SSH connections per minute
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m limit --limit 4/minute --limit-burst 4 -j ACCEPT
# Drop SSH connections that exceed the rate limit
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j DROP
# Per-IP rate limit with hashlimit (more precise than global limit)
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m hashlimit --hashlimit-name ssh --hashlimit-above 4/minute \
--hashlimit-mode srcip --hashlimit-burst 4 -j DROP
# Rate limit ICMP to prevent ping floods
sudo iptables -A INPUT -p icmp --icmp-type echo-request \
-m limit --limit 1/second --limit-burst 4 -j ACCEPT
The hashlimit module is preferred over limit for per-IP tracking because
the basic limit module applies globally -- one fast source can consume the
entire budget, blocking legitimate users.
Logging
Logging dropped packets helps you detect attacks and troubleshoot legitimate connection issues. Logs are essential for forensics after an incident:
# Log packets that will be dropped (add before the default DROP policy takes effect)
sudo iptables -A INPUT -m limit --limit 5/minute --limit-burst 10 \
-j LOG --log-prefix "IPT_DROP: " --log-level 4
# Create a dedicated chain for logging (cleaner rule management)
sudo iptables -N LOG_AND_DROP
sudo iptables -A LOG_AND_DROP -m limit --limit 5/minute \
-j LOG --log-prefix "IPT_DROP: " --log-level 4
sudo iptables -A LOG_AND_DROP -j DROP
# Use the chain instead of DROP in other rules
sudo iptables -A INPUT -p tcp --dport 23 -j LOG_AND_DROP
Logs appear in /var/log/kern.log or via journalctl -k. Rate-limit the
LOG target to avoid filling disk and creating a denial-of-service vector in
itself. Pipe firewall logs to a SIEM or logwatch for analysis.
Zone-Based Firewalling with firewalld
firewalld provides a higher-level, zone-based abstraction over nftables
(or iptables on older systems). Each network interface is assigned to a zone
with predefined trust levels:
# Check which zones are active and which interfaces belong to them
sudo firewall-cmd --get-active-zones
# Set the default zone to drop (deny all by default)
sudo firewall-cmd --set-default-zone=drop
# Allow specific services permanently in the public zone
sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --zone=public --add-service=https --permanent
# Allow a custom port
sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent
# Apply changes
sudo firewall-cmd --reload
# List all rules in the public zone
sudo firewall-cmd --zone=public --list-all
# List all available zones and their configurations
sudo firewall-cmd --list-all-zones
Zones make it easy to apply different policies to different interfaces -- a
strict drop zone for the public interface and a permissive trusted zone
for a management VLAN, for example.
Rich Rules and Port Forwarding
firewalld supports rich rules for more complex logic:
# Allow SSH only from a specific subnet
sudo firewall-cmd --zone=public --add-rich-rule='
rule family="ipv4"
source address="10.0.0.0/24"
service name="ssh"
accept' --permanent
# Rate limit connections to port 80
sudo firewall-cmd --zone=public --add-rich-rule='
rule family="ipv4"
service name="http"
limit value="25/m"
accept' --permanent
# Forward port 8080 to an internal server
sudo firewall-cmd --zone=public --add-forward-port=\
port=8080:proto=tcp:toport=80:toaddr=10.0.1.10 --permanent
sudo firewall-cmd --reload
IPv6 Considerations
Do not forget about IPv6. Many administrators carefully configure iptables but leave ip6tables wide open. Apply equivalent rules for IPv6:
# Set default DROP for IPv6
sudo ip6tables -P INPUT DROP
sudo ip6tables -P FORWARD DROP
sudo ip6tables -P OUTPUT ACCEPT
# Allow loopback
sudo ip6tables -A INPUT -i lo -j ACCEPT
# Allow established connections
sudo ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow ICMPv6 (required for Neighbor Discovery Protocol)
# DO NOT block all ICMPv6 -- it will break IPv6 connectivity
sudo ip6tables -A INPUT -p ipv6-icmp -j ACCEPT
# Allow SSH and HTTPS over IPv6
sudo ip6tables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
sudo ip6tables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
Blocking all ICMPv6 will break IPv6 entirely because Neighbor Discovery Protocol (the IPv6 equivalent of ARP) and Stateless Address Autoconfiguration both rely on ICMPv6 messages.
Persisting Rules
iptables rules exist only in kernel memory and are lost on reboot unless explicitly saved:
# Debian/Ubuntu -- install iptables-persistent
sudo apt install iptables-persistent
sudo netfilter-persistent save
sudo netfilter-persistent reload
# RHEL/Fedora (if using iptables-services)
sudo service iptables save
sudo service ip6tables save
firewalld persists rules automatically when you use the --permanent flag
followed by --reload.
A well-configured firewall is the first line of network defense. Combine it with SSH hardening, intrusion detection, and application-level controls for a robust, layered security posture.