
A hosting firewall shouldn’t feel like trial and error. On a VPS that runs websites, mail, and maybe DNS, you want explicit rules with logging. You also want the rules to survive reboots. This iptables-nftables tutorial gives you a practical baseline for Ubuntu/Debian using the modern nftables backend—without relying on UFW.
You’ll keep SSH reachable and serve HTTP/HTTPS. You’ll also allow the outbound traffic a server actually needs. That helps you avoid the classic “mail suddenly stopped delivering” surprise.
If you’d rather not babysit firewall policy, persistence, and update coordination, managed VPS hosting from HostMyCode is the simplest way to keep security controls consistent through upgrades and migrations.
What you’ll build (and what you won’t)
- Default deny inbound, allow only required ports.
- Stateful rules so return traffic works.
- Basic anti-noise (drop invalid, rate-limit new SSH connections).
- Logging that won’t flood your disk.
- Persistence across reboots with a clear rollback plan.
This isn’t a WAF and it won’t stop volumetric DDoS on its own. It’s the “keep the server sane” firewall you put on every hosting VPS before customers touch it.
Prerequisites and safety checks (do these first)
Run these commands as root (or with sudo). Before you change any rules, confirm a few basics.
- You have console access (provider web console or rescue mode).
- You know your SSH port (default 22, or a custom port).
- You know which services are actually listening.
# show listening TCP/UDP ports
ss -tulpen
# identify your active network interface and default route
ip route
ip -br link
If your SSH sessions already drop, fix that first. A firewall won’t make flaky connectivity easier to debug.
See: VPS SSH timeout troubleshooting tutorial.
Understand iptables vs nftables on Ubuntu/Debian in 2026
On current Ubuntu and Debian releases, iptables commonly targets the nf_tables backend. You’ll often see “iptables v1.8.x (nf_tables)” in output. That’s normal.
You have two workable options:
- Use
iptablescommands (familiar and widely documented), or - Use native
nftsyntax (cleaner long-term, especially for complex policies).
This guide sticks to iptables for readability. It remains compatible with nftables-backed systems.
iptables --version
update-alternatives --display iptables 2>/dev/null || true
If you’re building a hosting VPS for client sites, start with predictable networking and a clean OS baseline.
HostMyCode HostMyCode VPS plans are a good fit when you want root access and stable behavior for web/mail/DNS stacks.
Plan your hosting ports (web, mail, DNS) before writing rules
Don’t open ports “just in case.” Open what your services actually use. Close everything else.
Typical inbound ports for a hosting VPS
- SSH: 22/tcp (or your custom port)
- Web: 80/tcp, 443/tcp
- DNS (only if you host authoritative/recursive DNS): 53/udp and 53/tcp
- Mail server (only if you host mail): 25/tcp, 587/tcp, 465/tcp, 143/tcp, 993/tcp
- Control panels: cPanel/WHM (2083/2087), DirectAdmin (2222), Plesk (8443) — open only if installed
Typical outbound ports you must allow
- DNS lookups: 53/udp, 53/tcp
- Web updates: 80/tcp, 443/tcp
- NTP time sync: 123/udp
- Mail delivery (if sending mail): 25/tcp (and sometimes 587/tcp)
If you run mail, deliverability lives and dies on DNS and hostname basics. Keep this handy: Reverse DNS setup tutorial.
iptables-nftables tutorial: apply a safe baseline ruleset (with rollback)
Make changes in a script you can re-run and version. Before you start, prep a “break glass” rollback command.
You should be able to paste the rollback into your provider console if you lock yourself out.
Step 1: Prepare a rollback command
This flushes filter rules and sets default policies to ACCEPT (temporary emergency mode):
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
iptables -F
iptables -X
Keep that within reach in your provider console before you proceed.
Step 2: Create a firewall script
Create /root/fw-apply.sh:
nano /root/fw-apply.sh
Paste this baseline. Then edit the variables at the top to match your server.
#!/usr/bin/env bash
set -euo pipefail
# === EDIT THESE ===
SSH_PORT="22" # set your SSH port
PUB_IFACE="eth0" # set your public interface (e.g. eth0, ens3)
ALLOW_DNS_IN="no" # yes if you host DNS on this VPS
ALLOW_MAIL_IN="no" # yes if you host mail on this VPS
ALLOW_PANEL_IN="no" # yes if you run a control panel here
# Optional: lock SSH to your office/home IP
# SSH_TRUSTED_IP="203.0.113.10/32"
SSH_TRUSTED_IP=""
# Flush existing rules
iptables -F
iptables -X
# Default policies: deny inbound, allow outbound
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Allow established/related
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Drop invalid packets
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# Basic ICMP (ping) rate-limited (helps PMTU discovery)
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/second --limit-burst 4 -j ACCEPT
iptables -A INPUT -p icmp -j ACCEPT
# SSH: allow new connections, rate-limited
if [[ -n "$SSH_TRUSTED_IP" ]]; then
iptables -A INPUT -i "$PUB_IFACE" -p tcp -s "$SSH_TRUSTED_IP" --dport "$SSH_PORT" -m conntrack --ctstate NEW -m limit --limit 15/minute --limit-burst 20 -j ACCEPT
else
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport "$SSH_PORT" -m conntrack --ctstate NEW -m limit --limit 15/minute --limit-burst 20 -j ACCEPT
fi
# Web
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
# DNS (authoritative/recursive) - only if enabled
if [[ "$ALLOW_DNS_IN" == "yes" ]]; then
iptables -A INPUT -i "$PUB_IFACE" -p udp --dport 53 -j ACCEPT
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 53 -j ACCEPT
fi
# Mail server - only if enabled
if [[ "$ALLOW_MAIL_IN" == "yes" ]]; then
# SMTP
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 25 -j ACCEPT
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 587 -j ACCEPT
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 465 -j ACCEPT
# IMAP
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 143 -j ACCEPT
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 993 -j ACCEPT
fi
# Control panel ports - only if enabled
if [[ "$ALLOW_PANEL_IN" == "yes" ]]; then
# cPanel/WHM (common)
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 2083 -j ACCEPT
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 2087 -j ACCEPT
# DirectAdmin
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 2222 -j ACCEPT
# Plesk
iptables -A INPUT -i "$PUB_IFACE" -p tcp --dport 8443 -j ACCEPT
fi
# Log and drop everything else (rate-limited to avoid disk spam)
iptables -A INPUT -m limit --limit 6/minute --limit-burst 10 -j LOG --log-prefix "IPTABLES DROP: " --log-level 4
iptables -A INPUT -j DROP
echo "Firewall rules applied."
Make it executable, then apply it:
chmod +x /root/fw-apply.sh
/root/fw-apply.sh
Step 3: Verify from a second SSH session
Don’t close your current session yet. Open a second terminal and confirm you can still log in.
Then inspect the rules:
iptables -S
iptables -L -n -v --line-numbers
From another machine, probe the ports you meant to expose:
# replace IP and ports as needed
nc -vz YOUR_SERVER_IP 22
nc -vz YOUR_SERVER_IP 80
nc -vz YOUR_SERVER_IP 443
Make rules persistent across reboots (Ubuntu/Debian)
Rules you apply with iptables disappear after a reboot unless you persist them. On Ubuntu/Debian, the straightforward approach is iptables-persistent (netfilter-persistent).
apt update
apt install -y iptables-persistent
You’ll be asked whether to save the current rules. Choose “Yes.”
To save or reload later:
netfilter-persistent save
netfilter-persistent reload
systemctl status netfilter-persistent --no-pager
Path check: persistent rule files are typically:
/etc/iptables/rules.v4/etc/iptables/rules.v6
Don’t forget IPv6 (either secure it or disable it deliberately)
A common hosting failure mode is simple: IPv4 is locked down, but IPv6 is wide open. If your VPS has global IPv6, decide what you’re doing with it.
- Option A: Build equivalent ip6tables rules.
- Option B: Disable IPv6 at the OS level (only if you don’t need it).
Option A: Mirror the rules for IPv6
Apply ip6tables rules that match your IPv4 baseline:
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT ACCEPT
ip6tables -F
ip6tables -X
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -m conntrack --ctstate INVALID -j DROP
# SSH/Web examples
ip6tables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
ip6tables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
ip6tables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
ip6tables -A INPUT -m limit --limit 6/minute --limit-burst 10 -j LOG --log-prefix "IP6TABLES DROP: " --log-level 4
ip6tables -A INPUT -j DROP
Then save:
netfilter-persistent save
Option B: Disable IPv6 (only if your stack doesn’t need it)
Set sysctl values:
cat > /etc/sysctl.d/99-disable-ipv6.conf <<'EOF'
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOF
sysctl --system
If you host email, pause before disabling IPv6. Some receivers prefer IPv6, and broken AAAA records can create confusing delivery delays.
Logging and quick diagnostics (so you can answer “what was blocked?”)
With the LOG rule in place, you can review drops through journald or syslog. Which one you use depends on how the distro is configured.
# Ubuntu/Debian with journald
journalctl -k -g "IPTABLES DROP" --since "1 hour ago" --no-pager
# If using rsyslog and kernel logs go to /var/log/kern.log
grep "IPTABLES DROP" /var/log/kern.log | tail -n 50
If you’re tightening retention, make sure firewall logs can’t fill /var. Related: VPS log rotation tutorial.
Common hosting pitfalls (and exact fixes)
1) DNS breaks: apt updates stall, Let’s Encrypt fails
DNS timeouts show up fast. Package installs hang, API calls fail, and ACME clients can’t validate.
In this tutorial, OUTPUT is ACCEPT. So the usual culprit is an external firewall/security group. Still, you can confirm resolution in seconds:
getent hosts deb.debian.org
resolvectl status 2>/dev/null || true
cat /etc/resolv.conf
For certificate renewals specifically, see: Let’s Encrypt renewal troubleshooting tutorial.
2) Email sending fails: outbound SMTP blocked
Many providers restrict outbound port 25 by default. Your firewall can be correct and mail still won’t leave the box.
Check connectivity:
nc -vz gmail-smtp-in.l.google.com 25
nc -vz smtp.office365.com 587
If port 25 is blocked, use an authenticated relay on 587 with your mail provider. Or request port 25 unblocking if you run a legitimate mail server.
If you host mail on cPanel/WHM, queue behavior matters too: cPanel mail queue tutorial.
3) You opened DNS inbound without actually running DNS
Exposing port 53 on a server that doesn’t answer authoritative or recursive queries just buys you noise. Leave ALLOW_DNS_IN="no" unless you’re intentionally running BIND/PowerDNS/CoreDNS.
4) Control panel ports exposed to the world
If you run a control panel, tighten access by restricting those ports to your admin IP range. Example for WHM (2087) from a single admin IP:
iptables -I INPUT 1 -p tcp -s 203.0.113.10/32 --dport 2087 -j ACCEPT
iptables -A INPUT -p tcp --dport 2087 -j DROP
On reseller nodes, that one tweak cuts background scanning dramatically.
If you’re building a reseller platform, pair this baseline with sane WHM defaults: cPanel reseller setup guide tutorial.
A practical “hosting profile” checklist (copy/paste decisions)
- Web-only VPS (WordPress, apps): SSH, 80/443 inbound. No DNS/mail/panel ports unless needed.
- Hosting VPS with control panel: SSH, 80/443 plus panel ports, but restrict panel access by IP if possible.
- Authoritative DNS node: SSH, 53/tcp+udp inbound, and lock recursion down in your DNS software.
- Mail server: SSH plus SMTP/Submission/IMAP ports inbound, and verify rDNS/SPF/DKIM/DMARC.
Hardening extras that pair well with this firewall
A firewall is one layer. Two changes consistently pay off on hosting servers.
- Disable SSH password logins and use keys.
- Keep security updates predictable (and reboot safely).
For cPanel users, the SSH path is straightforward: cPanel SSH key setup tutorial.
For patching hygiene: VPS security update tutorial.
Summary: a firewall you can explain to customers (and future you)
You now have a predictable hosting firewall. Inbound is denied by default. You explicitly allow the ports you run. Drops get logged without turning your disk into a landfill.
The rules survive reboots. You also have an immediate rollback if you make a bad change.
If you want a clean platform for this setup, start on a HostMyCode VPS. If you’d rather not maintain iptables policy, persistence, and update timing yourself, HostMyCode managed VPS hosting is the practical option for production sites.
If you’re putting a hosting VPS into production and want predictable security from day one, HostMyCode can help you pick a solid base and keep it stable. Choose a plan that fits your workload with HostMyCode VPS, or hand off routine hardening and upkeep to managed VPS hosting so your firewall policy stays consistent through updates and migrations.
FAQ
Is iptables still okay to use in 2026, or should I switch to nft?
It’s fine. On Ubuntu/Debian, iptables often runs on the nftables backend. Native nft syntax is cleaner, but iptables rules are still widely supported and easy for most admins to read.
Should I set OUTPUT to DROP too?
For most hosting VPS setups, OUTPUT=ACCEPT prevents surprise breakage (package updates, DNS, certificate renewals). If you need strict egress control, build a deliberate allowlist and document it.
Why allow ICMP at all?
ICMP isn’t just “ping.” Path MTU discovery depends on it. If you block ICMP, you can end up with strange HTTPS hangs or slow connections, especially through tunnels and certain networks.
How do I safely test changes without locking myself out?
Keep a second SSH session open, apply rules from a script, and have console access ready. If you want extra safety, schedule an automatic rollback with at and cancel it once you confirm access.
Do I need to open 53/tcp for DNS?
If you host DNS, yes. Large responses (DNSSEC, long TXT records) can fall back to TCP. If you don’t host DNS on the VPS, keep 53 closed.