Your UFW says Status: active. Your AWS security group has three rules. You feel good. You shouldn’t — not yet.
A firewall policy is not the same thing as actual exposure. A service can bind to 0.0.0.0 before UFW even starts. A Docker container can create its own bridge network and listen on an interface UFW doesn’t manage. A background job can bind to a port you forgot you exposed six months ago.
The only way to know what’s actually reachable is to check from the ground up: what’s bound, what’s allowed, and what an attacker can see from the outside.
This is a read-only audit. Six commands. No changes. Run it on a quiet Sunday, fix what you find, then sleep better.
Command 1: ss -tulpn — what’s actually listening
This is the ground truth. Everything listening on TCP or UDP, and which process owns it.
sudo ss -tulpn
Look for these red flags:
| Port | What it usually is | Why it matters |
|---|---|---|
0.0.0.0:6379 |
Redis | Exposed to the world, no auth by default |
0.0.0.0:5432 |
Postgres | Database reachable from anywhere |
0.0.0.0:3306 |
MySQL | Same |
0.0.0.0:2375 |
Docker API | Root shell on your server if exposed |
0.0.0.0:9200 |
Elasticsearch | Usually unauthenticated |
0.0.0.0:11211 |
Memcached | Often no auth |
0.0.0.0:27017 |
MongoDB | Historically exposed to the internet |
If you see any of these bound to 0.0.0.0 or :: instead of 127.0.0.1 or a specific interface, that’s a finding. Fix the binding first, then worry about the firewall.
For anything you don’t recognize, check the process:
sudo ss -tulpn | grep ':PORT'
# or
sudo lsof -i :PORT
Then trace back: what service owns it, when did it start, and do you still need it?
Command 2: iptables -L -n -v — the actual firewall rules
UFW, firewalld, and security groups all ultimately write iptables rules. This is what the kernel enforces.
sudo iptables -L -n -v
Look at the Chain INPUT section. Every packet that reaches your server goes through these rules in order.
Common gaps:
- A rule that allows
0.0.0.0/0to a port that should be internal - A rule added by Docker that bypasses your UFW policy entirely
- A rule you added during debugging and never removed
If you see target prot opt source destination where source is 0.0.0.0/0 and destination is a service port, verify that was intentional.
Command 3: ufw status verbose — your policy vs reality
Now check what UFW thinks it’s doing:
sudo ufw status verbose
Look for:
- Status: active — good
- Default: deny (incoming), allow (outgoing) — the safe default
- Any ALLOW rules for ports that should be internal
Compare this to what ss -tulpn showed. If UFW says deny for port 6379 but Redis is bound to 0.0.0.0:6379, that’s a contradiction. Fix the binding, not the firewall rule.
Command 4: nmap -sT -O your-server-ip — what attackers see
Run this from a different machine, not the server itself. This is what an attacker on the internet sees.
# From your laptop or another server
nmap -sT -O your-server-ip
Or if you don’t have nmap:
# Basic TCP connect scan
for port in 22 80 443 3000 3306 5432 6379 9200 11211 2375 27017; do
(echo > /dev/tcp/your-server-ip/$port) 2>/dev/null && echo "Port $port is open"
done
Compare this to what you expected. Every open port is an attack surface. If you see something you didn’t expect, go back to Command 1 and trace it.
Command 5: sudo lsof -i -P -n — process-to-port mapping
This shows which process owns each open port. Useful when ss doesn’t give enough detail.
sudo lsof -i -P -n
Look for:
- Processes you don’t recognize
- Processes running as root that shouldn’t be
- Processes listening on * (all interfaces) instead of 127.0.0.1
For anything suspicious, get more detail:
sudo lsof -i :PORT
sudo cat /proc/PID/cmdline
sudo systemctl status PID
Command 6: sudo iptables -L DOCKER-USER -n -v — the Docker gap
This is the most commonly missed one. Docker creates its own iptables chains that bypass UFW entirely.
sudo iptables -L DOCKER-USER -n -v
If you see rules here that allow traffic to containers, those rules bypass your UFW policy. Docker manages its own networking and doesn’t respect ufw rules.
Fix: either use ufw-docker (a community package that makes Docker respect UFW) or explicitly bind containers to 127.0.0.1 unless they need external access.
What a clean result looks like
After running all six commands, you should have:
ss -tulpnshowing only the ports you expect, bound to the right interfacesiptablesrules that match your intent (no0.0.0.0/0to internal services)ufw statusmatchingiptables(no contradictions)nmapshowing only ports 22, 80, 443 (or whatever you deliberately expose)lsofshowing only recognized processes owning portsDOCKER-USERchain empty or carefully scoped
The fix pattern
For every finding, the pattern is the same:
- Fix the binding first. Change
bind 0.0.0.0tobind 127.0.0.1in the service config. - Then fix the firewall. Add an explicit
denyrule if needed. - Verify. Run
nmapagain from outside.
Don’t just add a firewall rule and call it done. The service will still be exposed to anything that can reach it before the rule applies.
If you want a second set of eyes
This audit takes about 30 minutes. If you want someone else to run it and hand you a prioritized report, that’s what the QuickCheck does — a read-only posture review of a single Linux box. You get a report with what I found, why it matters, and what to do next. No production changes, no credentials stored.
See what a report looks like before deciding.
Read-only audit, not a pentest. Your mileage depends on your setup. Not compliance certification.