Your Server Is Wide Open and Nobody Knows It
Last week I reviewed a client’s Ubuntu server running a PHP web app with MySQL. I ran nmap -sV <ip> from the public internet and got back a full list: port 3306 (MySQL), 6379 (Redis), 8080 (dev server)… all exposed to the outside world. No firewall blocking any of it.
A familiar story. The server was running fine, no warnings anywhere, so the firewall kept getting pushed further down the todo list. Until MySQL gets brute-forced or Redis gets hijacked to run a cryptominer — that’s when panic sets in.
Why Is the Server “Open” When Nobody Intended It to Be?
When you install MySQL, Redis, or any service, they bind to 0.0.0.0 by default — listening on all network interfaces, including the one connected to the internet. Without a firewall blocking access, anyone can attempt a connection.
The Linux kernel comes with netfilter — a packet filtering framework at the kernel level. iptables is the command-line tool for configuring netfilter. On a fresh Ubuntu or CentOS install, iptables typically defaults to ACCEPT everything — blocking nothing.
Another issue: many admins use ufw (Ubuntu) or firewalld (CentOS/RHEL) as a black box — knowing the commands without understanding what’s underneath. When you need NAT, port forwarding, or advanced rate limiting, these wrappers start showing their limits. Understanding iptables directly helps you debug in the right place when you hit those situations.
Understanding iptables Structure Before You Start
iptables organizes rules by tables and chains. The most commonly used table is filter, which has 3 chains:
- INPUT: packets coming into this server
- OUTPUT: packets going out from this server
- FORWARD: packets passing through the server (used when the server acts as a router/gateway)
Each rule has a target that determines the fate of the packet:
ACCEPT— let it throughDROP— silently block it, no notificationREJECT— block it and send back an error responseLOG— log it and continue processing the next rule
Rules are evaluated top to bottom. The first matching rule is applied — rules further down are skipped.
ufw or Direct iptables?
Option 1: Using ufw — Simple but Limited
Sufficient for small servers that only need a few fixed ports open:
sudo ufw enable
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status verbose
Pros: fast and easy to remember. Cons: hard to handle NAT, rate limiting, or advanced rules. When you run into edge cases, you’ll end up falling back to iptables anyway.
Option 2: Configuring iptables Directly — Full Control
This is what I use for production servers. A bit more complex, but gives you full control over every detail.
Complete iptables Script for Production Web Servers
Instead of typing individual commands and losing track of what was done, I write a script for easier management and redeployment. On the Ubuntu 22.04 server I manage, hundreds of bot connections get blocked every hour — without a firewall, all of that would reach the application directly.
#!/bin/bash
# firewall.sh — iptables config for web server
# Clear all existing rules
iptables -F
iptables -X
iptables -Z
# Default policy: DROP all INPUT and FORWARD
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow loopback (localhost — required)
iptables -A INPUT -i lo -j ACCEPT
# Stateful firewall: allow already established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow SSH (MUST be added before setting DROP policy)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow HTTP and HTTPS
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Allow ping
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Log dropped packets for debugging
iptables -A INPUT -j LOG --log-prefix "IPT-DROP: " --log-level 4
iptables -A INPUT -j DROP
echo "Firewall configured!"
iptables -L -v --line-numbers
Important warning: If you’re SSH’d into the server, the --dport 22 rule must be added before setting the default DROP policy. Getting the order wrong means locking yourself out immediately.
Safety tip for your first test: schedule an auto-reset command 5 minutes out, in case you accidentally lock yourself out of SSH:
echo "iptables -F && iptables -P INPUT ACCEPT" | at now + 5 minutes
(Install at first if not already available: sudo apt install at)
SSH Brute-Force Protection with Rate Limiting
Instead of ACCEPTing all SSH connections, add rate limiting to block password-scanning bots:
# Replace the simple SSH rule above with:
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --set --name SSH
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
This rule blocks any IP that attempts more than 4 new SSH connections within 60 seconds.
Locking Down MySQL and Redis — Localhost or Internal IPs Only
# MySQL: allow only from localhost
iptables -A INPUT -p tcp --dport 3306 -s 127.0.0.1 -j ACCEPT
# Or from an internal IP range (VPC, private network)
iptables -A INPUT -p tcp --dport 3306 -s 10.0.0.0/8 -j ACCEPT
# Redis: same approach
iptables -A INPUT -p tcp --dport 6379 -s 127.0.0.1 -j ACCEPT
# Block everything else for these two ports
iptables -A INPUT -p tcp --dport 3306 -j DROP
iptables -A INPUT -p tcp --dport 6379 -j DROP
Saving Rules to Auto-Load on Reboot
iptables rules are lost after a reboot if not saved — this is a detail many people overlook:
# Ubuntu/Debian
sudo apt install iptables-persistent
sudo netfilter-persistent save
# Verify the saved file
cat /etc/iptables/rules.v4
# CentOS/RHEL 7
sudo service iptables save
# Rules are saved at /etc/sysconfig/iptables
Useful Commands for Checking and Debugging
# View all rules with line numbers and packet/byte counts
iptables -L -v --line-numbers
# View rules for a specific table
iptables -t nat -L -v
# Delete a rule by line number (e.g., delete rule 3 in INPUT)
iptables -D INPUT 3
# View DROP logs
dmesg | grep "IPT-DROP"
# or
tail -f /var/log/kern.log | grep "IPT-DROP"
# Reset everything to default (when starting over)
iptables -F && iptables -P INPUT ACCEPT && iptables -P FORWARD ACCEPT
Classic Pitfalls When Configuring iptables
- Locking yourself out of SSH: Always test inside tmux/screen, combined with the auto-reset trick above.
- Rules lost after reboot: Remember to install
iptables-persistentand runnetfilter-persistent save. - Conflicts with ufw/firewalld: If you’re using ufw, disable it before using iptables directly:
sudo ufw disable. - Docker bypassing the firewall: Docker adds its own rules to iptables and can override your FORWARD policy. If you’re running Docker, you’ll need to configure the
DOCKER-USERchain as well. - Missing the ESTABLISHED rule: If you forget the
--ctstate ESTABLISHED,RELATEDline, existing connections will be DROPped — your website will load the header then hang.
Conclusion
iptables isn’t a set-it-and-forget-it task. Every time you add a new service, ask yourself: does this port need to be publicly accessible? The default answer is no. The template script above covers most web servers — add or remove rules based on your specific application.
My go-to rule when taking over a new server: run nmap -sV <ip> from the outside first. Any port that shows up without a good reason — close it immediately. MySQL, Redis, Elasticsearch, and Memcached should only listen on localhost or an internal network.
