Skip to main content

Configuring Linux Firewalls with iptables and ufw

Configure production Linux firewalls using iptables and ufw — implement default-deny rules, protect SSH, handle Docker port exposure, and persist rules across reboots.

30 Terms

Overview and What You Will Learn

A freshly provisioned Linux server accepts connections on every port from anywhere on the internet. PostgreSQL, Redis, application APIs — all reachable by anyone who scans your IP. A firewall closes every door and opens only the ones that should be open.

By the end of this lab you will:

  • Understand how netfilter, iptables, and ufw relate to each other
  • Read and write iptables rules using correct syntax
  • Build a complete production ruleset from scratch
  • Configure ufw for simpler firewall management on Ubuntu servers
  • Handle the Docker and ufw bypass problem correctly
  • Persist firewall rules across server reboots

Why This Matters in Production

A PhonePe payment server had its database port (5432) briefly exposed to the internet due to a missing firewall rule after a server rebuild. Automated scanners found the open port within 4 minutes and began brute-force attempts. A properly configured firewall with default-deny would have made the port invisible — not just closed, but completely unresponsive to external probes.

Core Principles

iptables tables and chains — the packet path:

◈ DIAGRAM
+------------------------------------------+
| Packet arrives at server NIC |
+------------------------------------------+
|
v
+------------------------------------------+
| PREROUTING (nat table) |
| DNAT -- redirect to different port/IP |
+------------------------------------------+
|
routing decision
/ \
for this host forward to other host
| |
v v
+------------------+ +------------------+
| INPUT chain | | FORWARD chain |
| (filter table) | | (filter table) |
+------------------+ +------------------+
|
accepted packets
|
v
+------------------------------------------+
| Local process (nginx, postgres, etc.) |
+------------------------------------------+
|
v
+------------------------------------------+
| OUTPUT chain (filter table) |
+------------------------------------------+
|
v
+------------------------------------------+
| POSTROUTING (nat table) |
| SNAT -- rewrite source IP for outbound |
+------------------------------------------+

Rule anatomy:

◈ DIAGRAM
iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
| | | | | | |
tool action chain proto dest port source target
Actions: ACCEPT DROP REJECT LOG RETURN
Chains: INPUT OUTPUT FORWARD
Protocols: tcp udp icmp all

Detailed Step-by-Step Practical Lab

Milestone 1 — Read current iptables rules

Bash
## List all rules with packet/byte counters
sudo iptables -L -n -v
## List specific chain
sudo iptables -L INPUT -n -v
## List with line numbers (needed for deleting specific rules)
sudo iptables -L INPUT -n -v --line-numbers
## Output on a clean server:
## Chain INPUT (policy ACCEPT)
## num pkts bytes target prot opt in out source destination
## (empty -- no rules, everything accepted by default)
## Check current default policies
sudo iptables -L | grep 'Chain.*policy'
## Chain INPUT (policy ACCEPT) <- dangerous default
## Chain FORWARD (policy ACCEPT)
## Chain OUTPUT (policy ACCEPT)
## List nat table (for port forwarding / Docker)
sudo iptables -t nat -L -n -v
## List all tables
for table in filter nat mangle; do
echo "=== Table: $table ==="
sudo iptables -t $table -L -n -v
done

Milestone 2 — Build a production iptables ruleset

Apply rules in this exact order. Rules are evaluated top-to-bottom — the first match wins.

Bash
## Step 1: Allow established/related connections first
## This is stateful firewall -- allows return traffic for outgoing connections
## Without this, you would need explicit rules for every response
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
## Step 2: Allow loopback (127.0.0.1 -- local traffic)
## Services communicating with themselves must work
sudo iptables -A INPUT -i lo -j ACCEPT
## Step 3: Allow SSH from internal network only
## Replace 10.0.0.0/8 with your VPN/office CIDR
sudo iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
## Step 4: Allow HTTP and HTTPS from anywhere
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
## Step 5: Allow your application port from internal network only
sudo iptables -A INPUT -p tcp --dport 4000 -s 10.0.0.0/8 -j ACCEPT
## Step 6: Allow ICMP ping (useful for monitoring)
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
## Step 7: Log dropped packets before the default policy
## This captures what is being blocked for security analysis
sudo iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables-dropped: "
## Step 8: Set default policies to DROP
## DO THIS LAST -- if done first, you lock yourself out
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT ## outbound is usually unrestricted
## Verify the ruleset
sudo iptables -L INPUT -n -v --line-numbers
Common Mistake

Setting iptables -P INPUT DROP before adding allow rules. This immediately blocks all incoming traffic including your SSH session. Always build the full ruleset with ACCEPT rules first, verify it in a second terminal, then set the DROP default policy as the final step.

Security

Always keep a second terminal open when changing firewall rules. If your rule changes block your SSH connection, the existing session is your only way back in. Test from a second connection before closing the first.

Milestone 3 — Manage individual rules

Bash
## Add a rule at the top (INSERT) instead of bottom (APPEND)
## Useful for allowing a specific IP with higher priority
sudo iptables -I INPUT 1 -p tcp --dport 22 -s 203.0.113.10 -j ACCEPT
## Delete a rule by line number
sudo iptables -D INPUT 3
## Delete a rule by specification (exact match required)
sudo iptables -D INPUT -p tcp --dport 80 -j ACCEPT
## Flush (delete all rules) from a chain
## WARNING: if default policy is DROP, this blocks all traffic
sudo iptables -F INPUT
## Replace a rule at a specific position
sudo iptables -R INPUT 2 -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
## Check if a specific packet would match
## (test without actually adding a rule)
sudo iptables -C INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
## exit code 0 = rule exists, exit code 1 = rule does not exist

Milestone 4 — Persist rules across reboots

iptables rules are stored in memory and lost on reboot by default.

Bash
## Method 1: iptables-persistent (Debian/Ubuntu -- recommended)
sudo apt install iptables-persistent
## During install, it asks to save current rules -- say yes
## Save current rules manually
sudo iptables-save | sudo tee /etc/iptables/rules.v4
sudo ip6tables-save | sudo tee /etc/iptables/rules.v6
## Restore rules from file (done automatically at boot by iptables-persistent)
sudo iptables-restore < /etc/iptables/rules.v4
## Method 2: systemd service (RHEL/CentOS/Amazon Linux)
sudo yum install iptables-services
sudo systemctl enable iptables
sudo service iptables save
## Saves to /etc/sysconfig/iptables
## Verify rules load on boot
sudo reboot
## After reboot:
sudo iptables -L -n -v
## Rules should be present

Milestone 5 — Configure ufw for simpler management

ufw (Uncomplicated Firewall) writes iptables rules with a simpler interface. Standard on Ubuntu servers.

Bash
## Check ufw status
sudo ufw status verbose
## Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw default deny routed
## Allow services by name (uses /etc/services for port numbers)
sudo ufw allow ssh ## port 22
sudo ufw allow http ## port 80
sudo ufw allow https ## port 443
## Allow specific port/protocol
sudo ufw allow 4000/tcp
## Allow from specific IP
sudo ufw allow from 10.0.0.0/8 to any port 22
sudo ufw allow from 52.66.1.100
## Allow range of ports
sudo ufw allow 8000:9000/tcp
## Enable the firewall
sudo ufw enable
## View rules with numbers
sudo ufw status numbered
## Status: active
## To Action From
## [1] 22/tcp ALLOW 10.0.0.0/8
## [2] 80/tcp ALLOW Anywhere
## [3] 443/tcp ALLOW Anywhere
## Delete a rule by number
sudo ufw delete 3
## Disable and reset all rules
sudo ufw disable
sudo ufw reset

Milestone 6 — Fix the Docker and ufw bypass problem

Docker adds iptables rules that bypass ufw. A container with -p 80:80 becomes reachable from the internet even if ufw blocks port 80.

Bash
## Verify the problem: Docker adds to DOCKER chain
sudo iptables -L DOCKER -n -v
## Rules here bypass ufw INPUT chain
## Fix 1: Use DOCKER-USER chain (Docker-supported solution)
## Rules in DOCKER-USER are evaluated BEFORE Docker's rules
sudo iptables -I DOCKER-USER -p tcp --dport 80 -s 203.0.113.0/24 -j DROP
## This blocks port 80 for a specific IP even though Docker exposed it
## Block all external access to Docker ports except from internal network
sudo iptables -I DOCKER-USER -i eth0 ! -s 10.0.0.0/8 -j DROP
## ! -s 10.0.0.0/8 means: NOT from internal network
## Fix 2: Bind Docker container to localhost only (preferred)
## Instead of: docker run -p 80:80 nginx
## Use: docker run -p 127.0.0.1:80:80 nginx
## Then nginx only accepts connections from localhost
## Put nginx reverse proxy in front for external access
## Fix 3: Disable Docker iptables manipulation (not recommended)
## Edit /etc/docker/daemon.json:
## {"iptables": false}
## Then manage Docker networking manually -- complex

Production Best Practices and Common Pitfalls

Mistake Problem Fix
Setting default DROP before adding allow rules Locked out immediately Add all allow rules first, set DROP policy last
Not saving rules after changes Rules lost on reboot Run iptables-save after every change
Assuming ufw covers Docker ports Docker bypasses ufw Use DOCKER-USER chain or bind to localhost
Blocking ICMP entirely Monitoring tools stop working Allow icmp echo-request explicitly
No logging before DROP Cannot audit what was blocked Add LOG rule before default DROP policy

Quick Reference and Troubleshooting Commands

Task Command
List all rules sudo iptables -L -n -v
List with numbers sudo iptables -L INPUT --line-numbers
Add allow rule sudo iptables -A INPUT -p tcp --dport PORT -j ACCEPT
Delete by number sudo iptables -D INPUT LINE_NUMBER
Save rules sudo iptables-save > /etc/iptables/rules.v4
ufw status sudo ufw status numbered
ufw allow port sudo ufw allow PORT/tcp
Docker-aware block sudo iptables -I DOCKER-USER -i eth0 ! -s 10.0.0.0/8 -j DROP
Tip

After building your iptables ruleset, run sudo iptables -L INPUT -n -v --line-numbers and read it top to bottom as if you are a packet arriving at the server. Does every legitimate traffic source match a rule before hitting the DROP policy? This mental walkthrough catches most firewall misconfigurations before they cause incidents.

Remember

iptables rules are evaluated top-to-bottom and the first matching rule wins. Order matters enormously. An ACCEPT rule for port 22 at line 5 never fires if a DROP rule at line 3 matches first. Always verify rule order after any changes with --line-numbers.

Resources

Explore More in Linux Networking and Security

All 4 Topics

Frequently Asked Questions

Is Configuring Linux Firewalls with iptables and ufw free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Configuring Linux Firewalls with iptables and ufw topic cover?

Configure production Linux firewalls using iptables and ufw — implement default-deny rules, protect SSH, handle Docker port exposure, and persist rules across reboots.