Database audit fix

How to Configure and Secure MySQL Network Bindings

MySQL and MariaDB accept TCP connections on whatever bind-address names, and MySQL 8.0.13 and later default to *, which is every IPv4 and IPv6 interface. Set bind-address = 127.0.0.1, or disable TCP altogether with skip-networking.

Hiren KalariyaLast reviewed: Sep 25, 2026Check mysql-network-binding
High
severity
Yes
needs root or sudo
5
results it can return
2 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 severityNeeds sudo

What this check reads

With socket access (MYOK=1) it asks the running server two questions, SELECT @@skip_networking and SELECT @@bind_address. Without that, it greps bind-address out of /etc/mysql, /etc/my.cnf and /etc/my.cnf.d and keeps the last match, reading through as_root when root is available and with the audit user's own permissions when it is not. skip_networking reported as 1 or ON passes outright because TCP is off; otherwise the address has to match 127.0.0.1, localhost or ::1 exactly to pass, and any other non-empty value is a WARN. An absent bind-address on the root config path is also a WARN, since MySQL 8 defaults to *.

When it applies

Runs only when MySQL/MariaDB is detected (HAS_MY=1); otherwise SKIP. It has three paths of decreasing reliability: with socket access (MYOK=1) it asks the running server (@@skip_networking, @@bind_address) - authoritative; with root but no socket access it greps bind-address from /etc/mysql, /etc/my.cnf, /etc/my.cnf.d; unprivileged it greps the same paths but only what it can read, and says so in the message. The config paths take the last match, which mimics but does not guarantee MySQL's own precedence, and !includedir files outside those three locations are not followed. A config value that has not been restarted into effect will differ from the live server.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
PASS`skip_networking` on (socket only)TCP is switched off entirely, so the only route into the server is the local Unix socket.
PASSBound to `127.0.0.1`, `localhost` or `::1`The server only accepts connections that originate on this host.
WARNBound to anything else (`0.0.0.0`, `*`, `::`, a LAN IP)The listener reaches past localhost, so anything that can route to that address can reach 3306 unless a firewall stops it.
WARNRoot, config fallback, no `bind-address` at all (MySQL 8 defaults to `*`)No directive was found, which on MySQL 8 means the permissive all-interfaces default is in force.
SKIPNon-root and no readable `bind-address`; or could not read the variableNothing was verified. The binding could not be read at all, so treat it as unknown rather than safe.

Why it matters

The fewer interfaces a database listens on, the less a password matters. MySQL 8.0.13+ defaults bind_address=* (all interfaces, IPv4+IPv6); MariaDB and Debian/Ubuntu MySQL packages set 127.0.0.1. OWASP: bind to localhost or disable TCP where the application is local. CIS MySQL "Ensure 'bind_address' is set to a specific address".

Why it fails, and when it is wrong

  • The app runs on another host, so a LAN bind is necessary. Then the finding is mitigated by a host firewall rule restricting 3306 to the app server, TLS (require_secure_transport), and host-scoped accounts. The check still WARNs because it cannot see the firewall.
  • MySQL 8.0.13+ supports a comma-separated list (bind_address=127.0.0.1,10.0.0.2); the regex for loopback fails on a list, so it lands in WARN even if all entries are private.
  • mysqlx_bind_address (X Protocol on 33060) is not checked.
  • The config-file fallback picks the last bind-address across files, which is normally the effective one but ignores !includedir ordering subtleties.

How to fix it

Ask the running server, not the config

The live values are what matter, because a config file can be overridden by a drop-in you forgot about. Ask the server before you edit anything.

sudo mysql -N -e "SELECT @@bind_address, @@skip_networking"

Set the bind address

Add bind-address = 127.0.0.1 under the [mysqld] section of /etc/mysql/mysql.conf.d/mysqld.cnf. On MySQL 8 an absent directive means all interfaces, so leaving it out is not the safe default. Where the application runs on this same host, skip-networking is stronger still: it switches TCP off entirely and leaves only the Unix socket.

[mysqld]
bind-address = 127.0.0.1
# or socket-only:
skip-networking

Or use the private address for a remote app

If the app must connect from another host, bind to the private network address rather than 0.0.0.0, require TLS, and let only that host through the firewall.

[mysqld]
bind-address = <private IP>
require_secure_transport = ON
ufw allow from <app-ip> to any port 3306 proto tcp

Restart and confirm

Nothing above takes effect until the server restarts, and the socket table is the only proof that it did.

sudo systemctl restart mysql
ss -tln | grep 3306

Verify the fix

# Authoritative - ask the running server:
sudo mysql -N -e "SELECT @@bind_address, @@skip_networking"
# expected: 127.0.0.1 (or skip_networking = 1)

# What is actually listening:
ss -tlnp | grep -E ':3306|:33060'
# expected: 127.0.0.1:3306 only - nothing on 0.0.0.0 or ::

# From another host, the port must not answer:
nc -zv <server-ip> 3306      # expected: connection refused/timed out

Debugging

The server was reachable but the variable query returned nothing. Run the first command above by hand to see the error.

An unprivileged run that could not read the config. Re-run as root or with sudo, or query the server directly.

MySQL 8 defaults to * (all interfaces), so an absent directive is genuinely permissive. Confirm the reality with ss -tlnp | grep 3306 and set bind-address=127.0.0.1 explicitly.

You are comparing different sources. The MYOK path reads the live server; a config edit that has not been restarted is not in effect yet. systemctl restart mysql, then re-check both.

The check reports the configured binding, not reality. ss -tlnp | grep 3306 is the ground truth; also remember MySQL's X Protocol on 33060 has its own mysqlx_bind_address and is not covered here. A container publishing 3306 also bypasses the host binding entirely.

The app is on another host and needs either a bind to the private interface plus a firewall rule, or an SSH/TLS tunnel. Do not go back to 0.0.0.0 without one.

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