MySQL bind-address

Upstream MySQL listens on every interface by default. Debian and Ubuntu packages usually change that to localhost, until someone comments the line out to get a remote client working. Here is how to check what yours does, and how to lock it down without breaking your app.

Why it matters

The bind_address setting (written bind-address in config files) decides which network addresses the server accepts TCP connections on. In upstream MySQL the default is *: every IPv4 interface and, where available, every IPv6 interface. If the server has a public address, port 3306 answers the whole internet, and bots start guessing passwords for root and every other account.

The password is then the only thing in the way. One account with a weak password and a host of '%' is enough.

Check from outside, then on the server

From another machine:

nmap -Pn -p 3306,33060 your.server.example

Port 33060 is MySQL's X Protocol listener. It has its own setting, mysqlx_bind_address, which also defaults to all interfaces, so it can be open even when 3306 isn't. On the server:

sudo ss -tlnp | grep -E ':(3306|33060)\b'
sudo mysql -e "SHOW VARIABLES LIKE '%bind_address%';"

0.0.0.0, * or [::] means every interface. 127.0.0.1 means local only.

Find the line that actually wins

On Debian and Ubuntu, /etc/mysql/my.cnf is managed by update-alternatives and points to a MySQL or MariaDB variant that pulls in more files from directories under /etc/mysql/. Ubuntu's MySQL server keeps its network settings in /etc/mysql/mysql.conf.d/mysqld.cnf. MariaDB packages use their own drop-in directory. Rather than guess, search all of them:

sudo grep -rnE 'bind[-_]address|skip[-_]networking' /etc/mysql/

Files in an included directory are read in file-name order, and when an option is set more than once, the last one wins. A stray bind-address = 0.0.0.0 in a file that sorts later overrides your fix. To see the values the server will really start with:

# MySQL
mysqld --print-defaults
# MariaDB
mariadbd --print-defaults

Bind to localhost

If the app runs on the same server, set this under [mysqld] in the file you found:

[mysqld]
bind-address        = 127.0.0.1
mysqlx-bind-address = 127.0.0.1

The second line is for MySQL only; leave it out on MariaDB. If an app server on a private network needs access, use the server's private address instead of 127.0.0.1. Don't use 0.0.0.0 or * for that. The setting is only read at startup:

sudo systemctl restart mysql

On MariaDB the service is usually called mariadb. If MySQL fails to bind to an address you gave it, it refuses to start, so check systemctl status straight after.

Check who connects before you restart. Anything that reaches MySQL over the network, such as an app on another host or in a container that talks to the host's address, loses its connection when you bind to localhost. Run SELECT host, user FROM information_schema.processlist; first, and keep a working SSH session open while you change things.

Accounts with host '%'

A MySQL account is a user name plus a host. A host of '%' matches any host, and an account created without a host part gets '%' automatically. Even with the right bind-address, you don't want those accounts around on a single server. List them:

SELECT user, host FROM mysql.user WHERE host = '%' OR user = '';

If an app account only connects locally, move it to localhost. Its privileges move with it:

RENAME USER 'appuser'@'%' TO 'appuser'@'localhost';

If it connects from one app server, use that address instead, for example 'appuser'@'198.51.100.20'. Never keep 'root'@'%'. Note that RENAME USER fails if the account is the DEFINER of views or stored routines, so check that before you plan the change.

Remove anonymous users and the test database

A user with an empty name is an anonymous user: it matches any user name. Some installs still have them, and some have a test database that every user, anonymous ones included, can access. mysql_secure_installation walks you through removing both, removing root accounts reachable from outside, and setting a root password:

sudo mysql_secure_installation

On MariaDB the tool is now called mariadb-secure-installation; the old name still works. Debian and Ubuntu MariaDB packages log root in through the Unix socket, so sudo mariadb works without a password. That is fine and safer than a root password, as long as root is not reachable over TCP. If you'd rather drop a single anonymous account by hand:

DROP USER ''@'localhost';

Docker and remote admin

In a container, MySQL needs to listen on the container's network, and app containers reach it by service name. The risk is publishing it on the host with ports: - "3306:3306": Docker's firewall rules let that traffic in even when UFW blocks 3306. Remove the ports: entry, or publish on loopback only with "127.0.0.1:3306:3306", then recreate the container. See Docker bypasses UFW.

To use a GUI client from your laptop, you don't need 3306 open. Forward a local port over SSH:

ssh -N -L 3307:127.0.0.1:3306 [email protected]

Connect your client to 127.0.0.1 port 3307 while that runs. Afterwards, run the nmap check again from outside: both 3306 and 33060 should no longer show as open.

Stenvakt checks this on every server. Its Review is a read-only check of each server, and the database guard finds MySQL and MariaDB servers that answer from outside and risky settings, with Back up now before you change anything. You see a Preview of every fix before it runs, and fixes run in safe mode: a snapshot first, a rollback timer and a fresh-connection test. It's a Mac app and isn't out yet: get early access.

Sources

  1. MySQL 8.4 manual: Server System Variables (bind_address)
  2. MySQL 8.4 manual: X Plugin options and variables (mysqlx_bind_address)
  3. Ubuntu Server docs: Install and configure a MySQL server
  4. MariaDB docs: Configuring MariaDB with option files
  5. MySQL 8.4 manual: mysql_secure_installation
Next guideIs your PostgreSQL exposed?