By default, a fresh MySQL install on Ubuntu only listens on localhost — nothing outside the server can reach it. That is the safe default, but the moment you need a second app server, a developer laptop, or a reporting tool to talk to your database, you have to open remote access. Done carelessly, that is how databases end up exposed to the whole internet. Done right, it is a tight, controlled channel: the server listens only where it should, only specific IPs get through the firewall, and each remote machine gets its own limited database user. This guide walks you through exactly that, on Ubuntu 24.04 (works on 22.04 too) with MySQL 8.x, including the optional TLS encryption step most tutorials skip.

By the end you will have a remote user that can connect from one trusted IP, a firewall that drops everything else, and a verified encrypted connection.

Prerequisites

  • An Ubuntu 24.04 or 22.04 server with MySQL 8.x installed and running. (If you have not installed it yet, install MySQL 8.4 LTS — the current long-term support release.)
  • sudo access on the server.
  • The IP address (or private network range) of the remote machine that needs access, e.g. 203.0.113.25. In the examples below I use 203.0.113.25 as the remote IP and 192.0.2.10 as the database server — swap in your real values.
  • A MySQL client on the remote machine to test with (mysql-client package on Ubuntu: sudo apt install mysql-client).
  • UFW installed for the firewall steps (sudo apt install ufw). If you use a cloud provider, the same rules apply to its security groups.
Golden rule: never open MySQL port 3306 to the entire internet (0.0.0.0/0). You are whitelisting individual trusted IPs — nothing more.

Step 1 — Check how MySQL is listening right now

First, confirm the current state: MySQL should be bound to 127.0.0.1 (localhost only).

sudo ss -tlnp | grep 3306

ss -tlnp lists listening TCP sockets with the owning process; the grep 3306 filters to MySQL's default port. Expect a line like 127.0.0.1:3306 — remote connections cannot reach this. If you see 0.0.0.0:3306 already, the server is exposed; keep reading — the firewall and user steps still apply to you.

Also check the bind setting itself:

sudo grep -R "bind-address" /etc/mysql/

This searches MySQL's config tree for the bind-address directive, which controls which network interface MySQL accepts connections on. On Ubuntu it is usually in /etc/mysql/mysql.conf.d/mysqld.cnf, set to 127.0.0.1.

Step 2 — Change the bind-address

Open the MySQL server config:

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf

Find this line under [mysqld]:

bind-address = 127.0.0.1

You have two safe choices:

  • bind-address = 0.0.0.0 — listen on all interfaces. Simplest; the firewall (Step 5) decides who actually gets in. Use this when the remote machine reaches the server over the public internet.
  • bind-address = 10.0.0.5 — bind only to the server's private/VPN interface (replace with your server's private IP). Best option when the remote machine is on the same VPC or VPN: MySQL never even listens on the public interface. You do not need the firewall rule on the public side at all.

Change the line, save, and exit (Ctrl+O, Enter, Ctrl+X in nano). Note that the older skip-networking option must be off (it is off by default) — if you ever see it in your config, comment it out, because it disables TCP networking entirely.

Step 3 — Restart MySQL and confirm it listens on the network

MySQL only reads bind-address at startup, so restart the service:

sudo systemctl restart mysql
sudo systemctl status mysql --no-pager

The restart picks up the new bind address; the status check confirms the service came back up cleanly with no config errors.

Now verify with the same command as Step 1:

sudo ss -tlnp | grep 3306

You should now see 0.0.0.0:3306 (all interfaces) or 10.0.0.5:3306 (private interface only), depending on your choice. If it still shows 127.0.0.1, you edited the wrong file or the restart did not pick it up — re-check sudo grep -R "bind-address" /etc/mysql/ for duplicates; the last one wins.

Step 4 — Create a dedicated remote user (never open up root)

Ubuntu's MySQL root authenticates via auth_socket and only from localhost — leave it that way. Create one purpose-built user for the remote machine, restricted to that machine's IP:

sudo mysql

sudo mysql logs you in as root locally without a password (socket auth). Then run:

CREATE USER 'appuser'@'203.0.113.25' IDENTIFIED BY 'S3cure!Passw0rd#2026';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'203.0.113.25';
FLUSH PRIVILEGES;

What this does:

  • CREATE USER ... @'203.0.113.25' — the @'host' part is MySQL's built-in IP whitelist. This user cannot log in from any other address, even with the right password.
  • IDENTIFIED BY — sets a strong password. Use a generated one in production, not the example above.
  • GRANT ... ON appdb.* — least privilege: only the four data operations on one database. If the app needs to alter tables, add CREATE, ALTER — never grant ALL PRIVILEGES unless you have a reason.
  • FLUSH PRIVILEGES — reloads the grant tables immediately.

Confirm the user exists with the right host lock:

SELECT user, host FROM mysql.user WHERE user = 'appuser';

You should see appuser | 203.0.113.25. If you manage several app servers, repeat the CREATE USER/GRANT pair per server IP — one credential per machine, so you can revoke one without touching the others.

Step 5 — Lock the firewall down to that IP

The @'host' clause is your second line of defence; the firewall is the first. With UFW, allow port 3306 only from the trusted IP:

sudo ufw allow from 203.0.113.25 to any port 3306
sudo ufw status numbered

The first command adds a targeted rule instead of opening the port globally; ufw status numbered shows your rule list so you can confirm no blanket 3306 ALLOW Anywhere rule exists. If UFW was not enabled before, enable it now — but first make sure SSH is allowed or you will lock yourself out:

sudo ufw allow OpenSSH
sudo ufw enable

If you use AWS, DigitalOcean, Hetzner, or similar, apply the identical rule in the cloud security group / cloud firewall: inbound TCP 3306, source = the remote IP only.

Step 6 — Test the connection from the remote machine

From the remote machine (the one at 203.0.113.25), connect:

mysql -h 192.0.2.10 -u appuser -p

-h points at the database server's IP, -u selects the new user, and -p prompts for its password. Once in, run a quick sanity query:

SELECT USER(), DATABASE();

You should see appuser@203.0.113.25 — proof you authenticated as the restricted user, from the whitelisted IP. Run a real query against your app database (a SELECT on a small table) to confirm privileges are correct.

Step 7 (recommended) — Require TLS encryption on the wire

By default, MySQL credentials and queries travel in plain text. MySQL 8 supports TLS out of the box (it ships with auto-generated certificates), and forcing it costs one statement:

sudo mysql
ALTER USER 'appuser'@'203.0.113.25' REQUIRE SSL;
FLUSH PRIVILEGES;

REQUIRE SSL makes the server reject any non-encrypted session for this user. Then connect with encryption enforced:

mysql -h 192.0.2.10 -u appuser -p --ssl-mode=REQUIRED

Verify the session is actually encrypted:

SHOW STATUS LIKE 'Ssl_cipher';

If Ssl_cipher shows a value like TLS_AES_256_GCM_SHA384, traffic is encrypted. If it is empty, the connection fell back to plain text — do not use it until the cipher shows up.

Verify it worked

  • sudo ss -tlnp | grep 3306 on the server shows the new bind address, not 127.0.0.1.
  • mysql -h 192.0.2.10 -u appuser -p connects from the trusted IP and fails from any other IP.
  • SHOW STATUS LIKE 'Ssl_cipher'; returns a cipher name (if you did Step 7).
  • An online port scan of your server IP (e.g. from canyouseeme.org on port 3306) either times out or is rejected — better still, ask a friend on a different network to try connecting: they should get ERROR 2003 (HY000): Can't connect, not a password prompt.

Common issues and fixes

1. ERROR 2003 (HY000): Can't connect to MySQL server

The classic. Work through the chain in order: is MySQL running (sudo systemctl status mysql)? Is bind-address still 127.0.0.1 (sudo ss -tlnp | grep 3306)? Is the firewall blocking it (temporarily test with sudo ufw allow 3306 from a test IP, then re-tighten)? Nine times out of ten it is one of those three.

2. ERROR 1130 (HY000): Host '203.0.113.99' is not allowed to connect

You reached the server, but the user was created for a different host. The remote machine's outbound IP is not what you assumed — cloud VMs and NAT gateways often present a different public IP than you expect. Check the actual source IP in the server log (sudo tail /var/log/mysql/error.log shows the rejected host) and re-create the user with the correct @'host'.

3. ERROR 1045 (28000): Access denied for user 'appuser'

Wrong password, wrong username spelling, or — sneaky one — an anonymous user entry matching first. Re-run ALTER USER 'appuser'@'203.0.113.25' IDENTIFIED BY 'new-password'; FLUSH PRIVILEGES; to reset the password cleanly, then check SELECT user, host FROM mysql.user; for stray empty-user rows.

4. The server is behind NAT / a cloud security group and UFW rules do nothing

If UFW says the rule exists but connections still time out, the cloud provider's security group is dropping packets before they reach your VM. Add the same inbound rule (TCP 3306, source = remote IP) in the provider console. Also confirm the remote machine is hitting the server's public IP, not a private one it cannot route to.

5. bind-address change had no effect after restart

Duplicate bind-address lines: MySQL reads every .cnf under /etc/mysql/, and the last occurrence wins. Run sudo grep -R "bind-address" /etc/mysql/ — if a file in conf.d/ or the main my.cnf overrides yours, remove or align it, then restart.


Remote MySQL access is not complicated — it is three locks working together: the server only listens where you tell it, the firewall only admits the IPs you name, and each remote machine gets its own least-privilege user. Add TLS on top and you have a setup that is genuinely safe to run in production. If you would rather have an expert handle your server setup, hardening, and database tuning, XpertBees does exactly that — reach out and we will get your infrastructure production-ready.