
Firewall rules are only half the story. The other half is what your server is actually listening on, and whether those ports are reachable from the internet.
This firewall audit tutorial gives you a repeatable workflow. You’ll inventory open ports, map them to real services, confirm reachability from outside, and close what you don’t need. You’ll do it without breaking web, DNS, or email.
The workflow below fits Ubuntu 24.04/26.04 LTS, Debian 12/13, AlmaLinux 9/10, Rocky Linux 9/10, and other RHEL-compatible systems.
Use it on a single VPS, a reseller node, or a dedicated server.
What you’ll achieve (and what you won’t)
This is an audit-and-cleanup workflow, not “install a firewall and hope.” By the end, you’ll have:
- A clean list of listening ports and the processes behind them
- A simple service-to-port map for your hosting stack (web, mail, DNS, control panel)
- External validation from a second host (so you don’t rely on local output)
- Safer exposure: public ports only where required, internal-only where possible
- Change control: a rollback plan and quick checks after every change
You won’t get a universal “best” rule set here. A shared hosting node with mail and DNS needs different exposure than a WordPress-only VPS.
The goal is to verify what your server is truly exposing, then tighten it without guessing.
Prerequisites and safe-change habits
Before you change anything, set yourself up so a mistake isn’t a disaster:
- Have an out-of-band path: provider console, rescue mode, or at least a second SSH session kept open.
- Capture a snapshot or rollback point if you can. On VPSs, snapshots are usually the fastest recovery option.
If you host client sites and want an easy escape hatch, run through HostMyCode’s snapshot workflow first: VPS snapshot backup tutorial.
Hosting note: if you want a clean baseline for this audit, a fresh HostMyCode VPS is often faster than untangling years of inherited rules. It also avoids one-off “temporary” exceptions that never got removed.
Step 1: Inventory what’s listening locally (TCP/UDP) — your ground truth
Start with a full list of listening sockets. This is your ground truth.
Anything listening could become reachable if a firewall rule (or a future change) allows it.
On Ubuntu/Debian
sudo ss -tulpn
sudo ss -tulpn | awk 'NR==1 || /LISTEN/ {print}'
sudo ss -uapn
On AlmaLinux/Rocky/RHEL
sudo ss -tulpn
sudo ss -uapn
Interpretation tips:
- 0.0.0.0:PORT or [::]:PORT means the service is bound to all interfaces. If your firewall allows it, it’s often internet-reachable.
- 127.0.0.1:PORT means loopback-only. This is what you want for many internal services (Redis, local-only DBs, exporters).
- Private IP bindings (10.x/172.16-31/192.168.x) usually mean internal exposure, not public. Still verify NAT, port forwarding, and routing.
Save the output so you can compare “before vs after” later:
sudo ss -tulpn > /root/port-audit-ss-tcp.txt
sudo ss -uapn > /root/port-audit-ss-udp.txt
Step 2: Map ports to services (hosting-specific cheat sheet)
Use this as a recognition guide for what you see in ss.
Don’t treat it as an “open these ports” list.
| Service | Port(s) | Should it be public? | Notes |
|---|---|---|---|
| HTTP / HTTPS | 80, 443 | Yes | Web traffic |
| SSH | 22 (or custom) | Prefer “admin IPs only” | Consider a jump host and restrict source IPs |
| DNS (authoritative) | 53 TCP/UDP | Yes, if you host DNS | TCP needed for zone transfers/large answers; keep AXFR tight |
| SMTP | 25 | Yes, if you receive mail | Outbound-only servers may still need 25 depending on relay policy |
| Submission | 587 | Usually yes | Client sending with auth; enforce TLS |
| SMTPS | 465 | Optional | Legacy but still used by some clients |
| IMAP/IMAPS | 143/993 | Yes, if you host mailboxes | Prefer 993 |
| POP3/POP3S | 110/995 | Optional | Disable if you don’t support POP3 |
| Control panel (cPanel/WHM) | 2083, 2087 | Prefer “admin IPs only” | Client cPanel vs WHM admin; restrict hard |
| Webmail (cPanel) | 2096 | Yes, if you provide webmail | Or use 443 reverse proxying if configured |
| MySQL/MariaDB | 3306 | No (usually) | Bind to 127.0.0.1 unless you have a dedicated DB network |
If you run cPanel/WHM, you’ll also see supporting services (Dovecot, Exim, spam filters).
Keep mail configuration separate from firewall work. This makes troubleshooting cleaner: cPanel Mail Server Setup Guide.
Step 3: Verify firewall framework and current policy (don’t assume)
Servers often have more than one filtering layer.
Confirm what is actually enforcing rules before you change anything.
Check for UFW (Ubuntu/Debian often)
sudo ufw status verbose
sudo ufw show added
Check for firewalld (AlmaLinux/Rocky/RHEL often)
sudo firewall-cmd --state
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all
Check for raw nftables / iptables
sudo nft list ruleset | sed -n '1,200p'
sudo iptables -S
sudo iptables -L -n -v
Two common real-world surprises:
- UFW reports “inactive,” but nftables rules still filter traffic (or the reverse).
- Control panels ship their own firewall management. On cPanel that’s often CSF; on Plesk it may be firewalld-based.
Step 4: External scan from a second machine (the reality check)
Local listening sockets tell you what could be exposed.
An external scan tells you what is exposed.
Best practice: scan from a separate VPS on a different network. If you don’t have one, create a tiny instance temporarily. Run the scan, then destroy it.
A second HostMyCode VPS in another location is enough for this audit.
Install nmap on the scanner host
# Ubuntu/Debian
sudo apt update && sudo apt install -y nmap
# AlmaLinux/Rocky
sudo dnf install -y nmap
Run a targeted scan first (safer, faster)
export TARGET_IP="203.0.113.10"
nmap -Pn -sS -p 22,80,443,53,25,465,587,110,995,143,993,2083,2087,2096 $TARGET_IP
Then do a full TCP scan (schedule this off-peak)
nmap -Pn -sS -p- --open --reason -T3 $TARGET_IP
Check UDP for DNS (and anything else you expect)
nmap -Pn -sU -p 53 --open --reason $TARGET_IP
Compare results:
- If
ssshows a service listening on 0.0.0.0 but nmap can’t reach it, your firewall is blocking it (good). - If nmap shows an unexpected open port, something is exposed. It may be a forgotten rule, a second firewall layer, or an unexpected service.
Step 5: Decide what should be public vs. restricted (quick decision matrix)
This is what keeps you from “closing everything” and taking down production.
Use these rules as a starting point:
- Public web server: 80/443 open to all. Admin panels restricted. Databases private.
- Mail server: 25 + 587 + 993 open to all. Add 465/995 only if you truly support legacy clients.
- DNS server: 53 TCP/UDP open to all. Restrict zone transfers to specific IPs only.
- Control panel host (cPanel/WHM): restrict WHM (2087) to your admin IP ranges; keep cPanel (2083) tighter if possible.
If you already use a jump host, SSH restrictions are easier to live with day-to-day.
See: SSH jump host setup.
Step 6: Close exposure at the service level (often the real fix)
Firewall rules help, but service bindings prevent “oops” moments later.
If a service should never be public, bind it to localhost (or a private interface). A future firewall change can’t accidentally publish it.
Bind MySQL/MariaDB to localhost
Check what it’s bound to:
sudo ss -tulpn | grep -E ':3306\b' || true
Then edit your config (path varies by distro):
- Ubuntu/Debian:
/etc/mysql/mysql.conf.d/mysqld.cnf - AlmaLinux/Rocky:
/etc/my.cnfor/etc/my.cnf.d/mariadb-server.cnf
# inside [mysqld]
bind-address = 127.0.0.1
sudo systemctl restart mysql || sudo systemctl restart mariadb
Bind Redis/Memcached to localhost (common WordPress performance add-ons)
sudo ss -tulpn | grep -E ':(6379|11211)\b' || true
Redis config usually at /etc/redis/redis.conf:
bind 127.0.0.1 ::1
protected-mode yes
sudo systemctl restart redis-server || sudo systemctl restart redis
DNS: allow queries, restrict zone transfers (AXFR)
If you run BIND, don’t rely on the firewall alone for AXFR.
Lock it down in named.conf:
// /etc/bind/named.conf.options (Debian/Ubuntu) or /etc/named.conf (RHEL-family)
options {
allow-transfer { none; };
};
If you have secondary nameservers, specify their IPs instead of none.
Step 7: Apply firewall changes safely (UFW, firewalld, and CSF patterns)
This section sticks to audit-driven changes. Remove exposure you don’t need, then restrict admin ports to known IPs.
Keep changes small. Test after each one.
UFW: restrict SSH and WHM to admin IPs
Replace the example IP with your real office/VPN/jump host address.
# Allow web
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Restrict SSH to your admin IP
sudo ufw allow from 198.51.100.25 to any port 22 proto tcp
# Restrict WHM (cPanel admin) to your admin IP
sudo ufw allow from 198.51.100.25 to any port 2087 proto tcp
# Optional: restrict cPanel client login too
sudo ufw allow from 198.51.100.25 to any port 2083 proto tcp
# Remove broad allows (examples)
sudo ufw delete allow 22/tcp
sudo ufw delete allow 2087/tcp
Validate from your admin network after each change.
Do this before you close your current SSH session.
firewalld: rich rules for source-IP restrictions
# Allow web
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
# Restrict SSH to a source IP (rich rule)
sudo firewall-cmd --permanent \
--add-rich-rule='rule family="ipv4" source address="198.51.100.25" port protocol="tcp" port="22" accept'
# Remove public SSH if present
sudo firewall-cmd --permanent --remove-service=ssh || true
sudo firewall-cmd --reload
cPanel servers: don’t fight the panel
On WHM servers, CSF or WHM tooling often manages firewall state.
Make changes through the panel so they survive updates and service restarts.
If you need to add CSF cleanly and avoid lockouts, follow this separate tutorial: cPanel CSF firewall setup.
Step 8: Re-scan and prove you didn’t break hosting services
An audit isn’t finished until you can show what changed.
You also need to confirm key services still work.
External scan (from your scanner host)
nmap -Pn -sS -p 22,80,443,53,25,465,587,110,995,143,993,2083,2087,2096 $TARGET_IP
Functional checks (quick and practical)
- Web:
curl -I http://yourdomainandcurl -I https://yourdomain - DNS:
dig @YOUR_SERVER_IP yourdomain A +short(only if your server is authoritative) - Mail submission: confirm your mail client can send via 587 (or test with
openssl s_client) - WHM/cPanel: login from allowed admin IP ranges only
Check logs for blocked traffic and surprises
On systemd systems, this is a fast way to spot obvious drops and daemon errors after changes:
sudo journalctl -p warning --since "30 min ago" --no-pager
If you want a lightweight baseline for catching auth failures, mail spikes, and repeated 404s, set up log monitoring separately: log monitoring setup guide.
Step 9: Create an “approved ports” policy you can re-run monthly
A one-time audit helps, but drift is what gets you later.
Put your expected exposure in a small file, then re-check it on a schedule.
Create /root/approved-ports.txt like this:
# Public ports
80/tcp # HTTP
443/tcp # HTTPS
25/tcp # SMTP inbound
587/tcp # Submission
993/tcp # IMAPS
53/tcp # DNS (if authoritative)
53/udp # DNS (if authoritative)
# Restricted ports (admin IPs only)
22/tcp # SSH
2087/tcp # WHM admin
2083/tcp # cPanel
Then add a monthly check (cron) that emails you what’s listening.
This isn’t perfect diffing, but it will catch surprise installs and “temporary” changes that stuck.
sudo crontab -e
# Run on the 1st of every month at 03:20
20 3 1 * * ss -tulpn; echo '---'; ss -uapn | mail -s "Monthly port audit: $(hostname)" you@example.com
If you don’t want the server sending email, write the output to a file and pull it via SFTP.
The important part is making the check routine.
Common audit findings on hosting servers (and how to fix them fast)
- “Why is 3306 open?” Usually MySQL is bound to 0.0.0.0. Fix with
bind-address=127.0.0.1and block it at the firewall. - Unexpected panel ports exposed (2087/8443/etc). Restrict to admin IPs or a VPN. Don’t leave admin interfaces open to the world.
- Port 53 open but you don’t host DNS. Close it. Resolver access for the server is outbound and doesn’t require public 53.
- Mail ports open but no mail service configured. Disable the daemon and close the ports. Open mail ports attract brute-force attempts.
- IPv6 forgotten. You locked down IPv4 but left IPv6 wide open. Confirm with
ssand nmap from an IPv6-capable scanner.
Summary: a firewall audit is a workflow, not a config paste
If you keep one habit: measure, change one thing, re-measure.
Start with what’s listening, confirm from outside, then decide what must be public.
Bind internal services to localhost, restrict admin ports to known IPs, and re-scan every time.
If you’re doing this on a production hosting node, consider running it on managed VPS hosting. You get a second set of eyes on firewall changes, service bindings, and post-change validation.
For self-managed builds, a clean HostMyCode VPS gives you predictable networking and a simpler baseline for audits.
If you’re hardening a hosting server and want fewer surprises, start with infrastructure you can control. A HostMyCode VPS gives you root access for clean firewall audits, and managed VPS hosting helps when you want changes reviewed before they hit production.
FAQ
How is a firewall audit different from “setting up UFW”?
UFW setup is mainly rule syntax.
A firewall audit checks exposure end-to-end: listening sockets, service bindings, the active firewall layers, and external reachability scans. It’s a repeatable process, not a one-time install.
Should I hide SSH on a custom port instead of restricting by IP?
Changing the port cuts down on noise, but it doesn’t remove exposure.
Restricting SSH to known admin IPs (or using a jump host) blocks most scanning and brute-force traffic outright.
My server is behind a CDN or reverse proxy. Do I still need a firewall audit?
Yes. A CDN can protect your web ports, but it won’t automatically protect SSH, mail, DNS, or control panel ports.
An audit makes sure the origin isn’t exposed in places you didn’t intend.
What’s the fastest way to verify I didn’t lock myself out?
Keep an existing SSH session open. Test a new SSH login from your admin network.
Only then remove broad allow rules. If possible, have a provider console ready as a backstop.