Database audit fix

How to Block Exposed Database Ports on Linux VPS

An exposed database port is one listening on an address other than loopback, which makes the database reachable from every network the host touches and puts it in the path of constant internet-wide scanning. Rebind to 127.0.0.1 or firewall it.

Hiren KalariyaLast reviewed: Sep 25, 2026Check exposed-database-ports
High
severity
No
needs root or sudo
3
results it can return
6 of 7
checks in this audit

Every threshold on this page is transcribed from the database-security-network-isolation audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 7 Network Isolation checks, or how the audit runs.

High severityNo sudo needed

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 thresholds this check applies
ResultWhenWhat it means
SKIPNeither `ss` nor `netstat` availableNothing was measured. With no way to list sockets, the check made no claim about which ports are open.
FAILAny of those ports listens on a non-loopback addressA database port is accepting connections from off the host, and the message names the ports it found.
PASSNoneNo 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:5432 show as 0.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 [::]:3306 is reported (it is not [::1]).
  • ss -tln does not need root to list sockets (only -p needs 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 5432

Verify 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 closed

Debugging

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

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
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals