What this check reads
It lists TCP listeners with ss -tln, falling back to netstat -tln when ss is missing, and takes the local-address column from every line. Addresses starting with 127., [::1] or ::1 are dropped as local; from what remains it keeps the text after the last colon, discards anything that is not all digits, and sorts the rest into a unique list of externally reachable ports. That list is then intersected with exactly seven well-known ports: 3306 (MySQL), 5432 (PostgreSQL), 27017 (MongoDB), 6379 (Redis), 1433 (SQL Server), 9200 (Elasticsearch) and 11211 (memcached). A single match is enough to FAIL, an empty intersection is the PASS, and neither tool being installed is the SKIP.
When it applies
Runs on any host, needs no root, and requires either ss or netstat - with neither it SKIPs. It inspects TCP listeners only (-tln); UDP services (memcached over UDP) are not seen, and without root it cannot map sockets to processes, so it reports ports, not owners. Addresses beginning 127., [::1] or ::1 are treated as local and dropped; everything else counts as external, including private LAN addresses. Exactly seven ports are matched: 3306, 5432, 27017, 6379, 1433, 9200, 11211. A database on a non-standard port is invisible, as is one bound to an address like 0.0.0.0 but blocked by a firewall - this check measures listeners, not reachability.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | Neither `ss` nor `netstat` available | Nothing was measured. With no way to list sockets, the check made no claim about which ports are open. |
| FAIL | Any of those ports listens on a non-loopback address | A database port is accepting connections from off the host, and the message names the ports it found. |
| PASS | None | No socket on any of the seven watched ports is bound beyond loopback. |
Why it matters
This is the effective-state counterpart to the config checks: whatever the config says, the kernel socket table is the truth. 0.0.0.0:5432 or *:3306 means reachable from every interface unless a firewall intervenes. OWASP: expose databases only to application hosts.
Why it fails, and when it is wrong
- Docker containers with
-p 5432:5432show as0.0.0.0:5432(docker-proxy) and are reported, correctly: Docker bypasses UFW (see the Docker Firewall Bypass check). - Listening on a private LAN IP (
10.0.0.2:3306) is reported too. That is intended if the app is remote; document the firewall rule that scopes it. - A host firewall that blocks the port does not change the result; the check measures listeners, not reachability. Confirm externally with
nmap -p 3306 <public-ip>from another host. - IPv6
[::]:3306is reported (it is not[::1]). ss -tlndoes not need root to list sockets (only-pneeds it).
How to fix it
List external listeners
Find out which of the seven ports is actually bound past loopback before you change anything, because the socket table is the finding, not the config.
ss -tln | grep -vE '127\.0\.0\.1:|\[::1\]:' \
| grep -E ':(3306|5432|27017|6379|1433|9200|11211)\b'Fix the bind address first
Bind each engine to loopback (see the MySQL, PostgreSQL, MongoDB and Redis Network Binding checks), or bind to a private IP. This is the primary control, because it removes the socket rather than filtering it. For Docker, publish on loopback: -p 127.0.0.1:5432:5432 or do not publish at all (use a user-defined network).
Add the firewall rule second
Allow the application host explicitly, then deny the port, and repeat for each database port in use. This is the layer that catches the day someone edits a config and restarts the service without thinking.
sudo ufw allow from 10.0.0.5 to any port 5432 proto tcp
sudo ufw deny 5432Verify from outside
A local command cannot tell you what the internet sees, so finish from another machine.
nmap -Pn -p 3306,5432,27017,6379 <server-ip>Verify the fix
# Reproduce the check:
ss -tln | awk 'NR>1{print $4}' | grep -vE '^(127\.|\[::1\]|::1)' | sed 's/.*://' | sort -un
# expected: none of 3306 5432 27017 6379 1433 9200 11211
# With owners (needs root) - far more useful when triaging:
sudo ss -tlnp | grep -E ':(3306|5432|27017|6379|1433|9200|11211)\b'
# Reachability from outside is the real question:
nmap -Pn -p 3306,5432,27017,6379,1433,9200,11211 <server-ip>
# expected: filtered or closedDebugging
Neither ss nor netstat is installed. apt install iproute2 / dnf install iproute.
Re-run with root to get process names: sudo ss -tlnp | grep :6379. Without root the socket-to-process mapping is unavailable.
The check looks at listeners, not firewall rules. Binding to loopback is still the better fix (defence in depth), but if the service must listen on a private interface, confirm the firewall with sudo ufw status / sudo nft list ruleset and record it.
Containers publishing a port create a host listener on 0.0.0.0 that bypasses the database's own bind-address, and Docker's rules sit in front of ufw. Check with docker ps --format '{{.Names}}\t{{.Ports}}' and publish to 127.0.0.1:3306:3306 instead. See the Docker Port Publishing and Docker Port Exposure checks.
It is on a non-standard port, or reachable over UDP or IPv6-only in a form the filter missed. Widen the check by hand: sudo ss -tulnp | grep -v '127\.0\.0\.1'.
Sources
- ss(8)
- Docker: Packet filtering and firewalls (published ports bypass host firewall)
- Ubuntu: UFW
- Shodan database exposure trends (background)
- OWASP Database Security Cheat Sheet
How the script reads this
Next
Re-run the Network Isolation audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 7 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.
All 7 Network Isolation fixes