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 | When | What 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. |
| PASS | Bound to `127.0.0.1`, `localhost` or `::1` | The server only accepts connections that originate on this host. |
| WARN | Bound 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. |
| WARN | Root, 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. |
| SKIP | Non-root and no readable `bind-address`; or could not read the variable | Nothing 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-addressacross files, which is normally the effective one but ignores!includedirordering 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-networkingOr 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 = ONufw allow from <app-ip> to any port 3306 proto tcpRestart 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 3306Verify 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 outDebugging
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
- MySQL 8.4: bind_address
- MySQL 8.4: skip_networking
- MariaDB KB: Configuring MariaDB for Remote Client Access
- MySQL 8.4: Connecting to the Server Using Sockets vs TCP
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