How to Set Up a VPS Firewall with iptables: Advanced Rules and Best Practices
UFW is an excellent beginner-friendly firewall, but understanding iptables — the underlying Linux firewall — gives you granular control that UFW can’t match. From connection-state tracking and rate limiting to port forwarding and custom chains, iptables lets you express precisely what traffic is allowed, logged, or blocked.
This guide covers iptables fundamentals, building a complete production ruleset, persistent configuration, rate limiting, and advanced techniques for VPS security.
How iptables Works
iptables processes network packets through chains of rules. Each rule matches specific packet characteristics and specifies an action (ACCEPT, DROP, REJECT, LOG).
| Chain | When it’s evaluated | Common use |
|---|---|---|
| INPUT | Packets destined for this server | Allow/block inbound traffic |
| OUTPUT | Packets originating from this server | Control outbound traffic |
| FORWARD | Packets routed through this server | Router/VPN setups |
Tables
- filter — Default table. Accepts, drops, or rejects packets.
- nat — Network Address Translation. Port forwarding, masquerading.
- mangle — Packet modification (QoS, TTL changes).
Essential iptables Command Syntax
# Basic syntax
iptables -A CHAIN -options -j TARGET
# Common flags
-A # Append rule to chain
-I # Insert rule at position
-D # Delete rule
-L # List rules
-F # Flush (delete all) rules
-n # Numeric output (no DNS lookups)
-v # Verbose
--line-numbers # Show rule numbers
# Examples
iptables -A INPUT -p tcp --dport 22 -j ACCEPT # Allow SSH
iptables -D INPUT 3 # Delete rule #3
iptables -L INPUT -n -v --line-numbers # List INPUT rules
Step 1: View Current Rules
# View all tables
sudo iptables -L -n -v
# View specific table
sudo iptables -t nat -L -n -v
# View with line numbers (needed for deletion)
sudo iptables -L INPUT -n -v --line-numbers
Step 2: Build a Complete Production Ruleset
The safest approach: flush all rules and build from scratch. Run this as a script so you can revert if something goes wrong:
sudo nano /root/setup-iptables.sh
#!/bin/bash
# VPS iptables firewall setup
# IMPORTANT: Run this via SSH. If you get locked out,
# reboot from VPS.DO control panel to restore access.
IPT="sudo iptables"
YOUR_SSH_PORT=2222 # Your actual SSH port
YOUR_HOME_IP="" # Optional: restrict SSH to your IP
# ── Step 1: Set default policies ─────────────────────────
$IPT -P INPUT DROP # Drop all inbound by default
$IPT -P FORWARD DROP # Drop all forwarding by default
$IPT -P OUTPUT ACCEPT # Allow all outbound by default
# ── Step 2: Flush existing rules ─────────────────────────
$IPT -F # Flush filter table
$IPT -X # Delete custom chains
$IPT -t nat -F # Flush NAT table
$IPT -t mangle -F
# ── Step 3: Allow loopback (localhost) ───────────────────
$IPT -A INPUT -i lo -j ACCEPT
$IPT -A OUTPUT -o lo -j ACCEPT
# ── Step 4: Allow established/related connections ────────
# This allows return traffic for connections you initiated
$IPT -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# ── Step 5: Drop invalid packets ─────────────────────────
$IPT -A INPUT -m conntrack --ctstate INVALID -j DROP
# ── Step 6: Allow ICMP (ping) ────────────────────────────
$IPT -A INPUT -p icmp --icmp-type echo-request -m limit \
--limit 1/s --limit-burst 10 -j ACCEPT
# ── Step 7: SSH access ───────────────────────────────────
if [ -n "$YOUR_HOME_IP" ]; then
# Restrict SSH to specific IP
$IPT -A INPUT -p tcp --dport $YOUR_SSH_PORT \
-s $YOUR_HOME_IP -m conntrack --ctstate NEW -j ACCEPT
else
# Allow SSH from anywhere (with rate limiting)
$IPT -A INPUT -p tcp --dport $YOUR_SSH_PORT \
-m conntrack --ctstate NEW \
-m recent --set --name SSH_NEW --rsource
$IPT -A INPUT -p tcp --dport $YOUR_SSH_PORT \
-m conntrack --ctstate NEW \
-m recent --update --seconds 60 --hitcount 4 --name SSH_NEW -j DROP
$IPT -A INPUT -p tcp --dport $YOUR_SSH_PORT \
-m conntrack --ctstate NEW -j ACCEPT
fi
# ── Step 8: Web traffic ──────────────────────────────────
$IPT -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
$IPT -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
# ── Step 9: Additional service ports (uncomment as needed) ─
# $IPT -A INPUT -p tcp --dport 25 -j ACCEPT # SMTP
# $IPT -A INPUT -p tcp --dport 587 -j ACCEPT # SMTP submission
# $IPT -A INPUT -p tcp --dport 993 -j ACCEPT # IMAPS
# $IPT -A INPUT -p udp --dport 51820 -j ACCEPT # WireGuard VPN
# $IPT -A INPUT -p tcp --dport 25565 -j ACCEPT # Minecraft
# ── Step 10: Log dropped packets ─────────────────────────
$IPT -A INPUT -m limit --limit 5/min -j LOG \
--log-prefix "iptables-dropped: " --log-level 7
echo "Firewall rules applied successfully!"
$IPT -L -n -v
chmod +x /root/setup-iptables.sh
sudo bash /root/setup-iptables.sh
Step 3: Save Rules Persistently
iptables rules are lost on reboot without persistence configuration:
sudo apt install iptables-persistent -y
# Save current rules
sudo netfilter-persistent save
# Rules are saved to:
# /etc/iptables/rules.v4 (IPv4)
# /etc/iptables/rules.v6 (IPv6)
# Reload saved rules
sudo netfilter-persistent reload
After any rule change, save again:
sudo iptables-save > /etc/iptables/rules.v4
Step 4: Rate Limiting with iptables
Limit HTTP connections per IP (anti-DDoS)
# Allow max 25 new HTTP connections per minute per IP
sudo iptables -A INPUT -p tcp --dport 80 \
-m conntrack --ctstate NEW \
-m limit --limit 25/minute --limit-burst 100 \
-j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 \
-m conntrack --ctstate NEW -j DROP
Block port scanning (SYN flood protection)
# Limit SYN packets
sudo iptables -A INPUT -p tcp --syn \
-m limit --limit 1/s --limit-burst 3 -j ACCEPT
sudo iptables -A INPUT -p tcp --syn -j DROP
Block brute-force SSH with recent module
# Track IPs that try to connect to SSH
sudo iptables -N SSH_CHECK
sudo iptables -A INPUT -p tcp --dport 2222 -j SSH_CHECK
sudo iptables -A SSH_CHECK -m recent --set --name SSH
sudo iptables -A SSH_CHECK -m recent --update \
--seconds 60 --hitcount 4 --name SSH -j DROP
sudo iptables -A SSH_CHECK -j ACCEPT
Step 5: Block Specific IPs or Countries
Block a specific IP
# Block single IP
sudo iptables -A INPUT -s 45.83.91.14 -j DROP
# Block entire subnet
sudo iptables -A INPUT -s 45.83.0.0/16 -j DROP
# View blocked IPs
sudo iptables -L INPUT -n -v | grep DROP
Unblock an IP
# Find the rule number
sudo iptables -L INPUT -n --line-numbers | grep 45.83.91.14
# Delete by number (e.g., rule 5)
sudo iptables -D INPUT 5
GeoIP blocking with ipset (block entire countries)
sudo apt install ipset -y
# Create a set for blocked IPs
sudo ipset create blocked_countries hash:net
# Add CIDR ranges for a country (get from ipdeny.com)
# Example: add a range
sudo ipset add blocked_countries 1.2.3.0/24
# Block the entire set
sudo iptables -I INPUT -m set --match-set blocked_countries src -j DROP
Step 6: Port Forwarding with iptables
# Enable IP forwarding
sudo sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
# Forward external port 8080 to internal port 80
sudo iptables -t nat -A PREROUTING \
-p tcp --dport 8080 -j REDIRECT --to-port 80
# Forward to a different internal server
sudo iptables -t nat -A PREROUTING \
-p tcp --dport 3306 \
-j DNAT --to-destination 192.168.1.100:3306
sudo iptables -t nat -A POSTROUTING \
-j MASQUERADE
Step 7: Log and Monitor Dropped Traffic
# Watch firewall drops in real time
sudo tail -f /var/log/kern.log | grep "iptables-dropped"
# Count drops by IP (top offenders)
sudo grep "iptables-dropped" /var/log/kern.log | \
grep -oP 'SRC=\S+' | sort | uniq -c | sort -rn | head -20
Step 8: IPv6 Rules (ip6tables)
# Apply the same ruleset for IPv6
sudo ip6tables -P INPUT DROP
sudo ip6tables -P FORWARD DROP
sudo ip6tables -P OUTPUT ACCEPT
sudo ip6tables -A INPUT -i lo -j ACCEPT
sudo ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo ip6tables -A INPUT -p ipv6-icmp -j ACCEPT # Allow ICMPv6 (required for IPv6)
sudo ip6tables -A INPUT -p tcp --dport 2222 -j ACCEPT
sudo ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
# Save IPv6 rules
sudo ip6tables-save > /etc/iptables/rules.v6
iptables vs UFW: When to Use Each
| iptables | UFW | |
|---|---|---|
| Complexity | High | Low |
| Flexibility | Complete | Limited to common scenarios |
| Rate limiting | ✅ Built-in | Basic |
| Custom chains | ✅ Yes | No |
| Best for | Advanced production servers | Quick setup, basic protection |
UFW actually manages iptables rules underneath — you can use both together, but be careful about rule ordering conflicts.
Final Thoughts
A properly configured iptables ruleset gives you complete control over network traffic entering and leaving your VPS. The production ruleset in this guide — default deny, stateful connection tracking, rate limiting on SSH and HTTP, and persistent configuration — provides strong baseline protection against the most common attack vectors.