The MySQL Mistake I Would Fix First

Why I stopped exposing MariaDB just because a temporary server needed database access

By SepedaTua — CrushEdge.com


This was one of the parts of the incident that bothered me after everything was restored.

Not because MariaDB itself was necessarily the problem.

The problem was how convenient I had allowed remote database access to become.

At some point, I had a legitimate requirement:

A temporary cloud VPS needed to connect to a database on my main server.

The temporary VPS didn’t have a permanent IP.

So the convenient solution was to allow a MySQL account to connect remotely.

That works.

But there is a big difference between:

"remote access is required"

and:

"MariaDB should be available to the entire Internet."

Those are not the same thing.


First, find out what MySQL is actually doing

Before changing anything, I would check:

ss -lntp | grep ':3306'

You might see:

LISTEN 0 80 0.0.0.0:3306

or:

LISTEN 0 80 127.0.0.1:3306

Those are very different situations.

127.0.0.1:3306

Only local connections can reach MariaDB directly.

That’s generally what I want for a normal WordPress server.

0.0.0.0:3306

MariaDB is listening on all IPv4 interfaces.

That means firewall rules become extremely important.

And if the firewall is wrong, your database is sitting on the Internet.


Then inspect the MySQL accounts

Inside MariaDB:

SELECT User, Host
FROM mysql.user
ORDER BY User, Host;

The interesting part is the Host.

For example:

wordpress_user     localhost
wordpress_user     %
temporary_app      %

The % means:

this account is allowed to authenticate from any host, subject to the server’s other controls.

That isn’t automatically malicious.

But it is broader than I usually want.


Then inspect the privileges

For every remotely accessible account:

SHOW GRANTS FOR 'temporary_app'@'%';

This is where things can get ugly.

If you see something equivalent to:

GRANT ALL PRIVILEGES ON *.* ...

that’s far more powerful than an application normally needs.

Compare that with:

GRANT SELECT ON some_database.* ...

The second one has a much smaller blast radius.


The important distinction

There are actually two separate controls:

Can this account connect?
        +
What can this account do?

You need both.

Restricting the source isn’t enough if the account has global privileges.

And least privilege isn’t enough if the database is exposed unnecessarily.


What I would do for normal WordPress databases

For ordinary WordPress:

Internet
   │
   X
  3306
   │
   ▼
MariaDB
   │
   ├── database A
   ├── database B
   └── database C

The web application talks to MariaDB locally.

There is no reason for a random Internet host to connect directly.

So I’d normally make MariaDB listen only locally:

bind-address = 127.0.0.1

The exact configuration location depends on the Linux/MariaDB version.

After changing it, verify:

ss -lntp | grep ':3306'

I want to see:

127.0.0.1:3306

not:

0.0.0.0:3306

But what about my temporary VPS?

This is where the SSH tunnel becomes useful.

Suppose:

MAIN SERVER
    MariaDB
    127.0.0.1:3306

and:

TEMPORARY VPS
    application

The temporary server doesn’t need to know that MariaDB is publicly accessible.

Instead:

TEMP VPS
   │
   │ SSH
   ▼
MAIN SERVER
   │
   ▼
127.0.0.1:3306

The application can connect to its own local tunnel endpoint.

For example:

ssh -N \
-L 3307:127.0.0.1:3306 \
user@main-server

Then configure the temporary application to use:

host: 127.0.0.1
port: 3307

The connection is carried through SSH.


The temporary VPS doesn’t need a fixed IP

This is the part I like most.

The temporary VPS can be:

VPS A
    created today
    public IP: 203.0.113.41

VPS B
    created next month
    public IP: 198.51.100.77

VPS C
    created six months later
    public IP: 192.0.2.55

It doesn’t matter.

The important authentication is:

SSH key

rather than:

database accepts every IP on Earth

Give the tunnel its own SSH key

On the temporary machine:

ssh-keygen -t ed25519 \
-f ~/.ssh/db-tunnel

Don’t reuse your personal SSH key if you can avoid it.

That gives you a credential specifically associated with:

temporary application → database tunnel

When the temporary project is finished:

remove SSH key
remove account
destroy VPS

Done.


Better still: create a dedicated SSH user

I wouldn’t necessarily give the temporary application your normal administrative SSH account.

Create a dedicated account intended for the tunnel.

The exact configuration depends on your SSH setup, but conceptually:

temporary application
       │
       ▼
dedicated SSH account
       │
       ▼
SSH tunnel
       │
       ▼
MariaDB

The account exists for one purpose.

That’s much easier to reason about later.


And the database user should also be dedicated

Don’t do:

temporary application
       │
       ▼
root
       │
       ▼
all databases

Instead:

temporary application
       │
       ▼
temporary_db_user
       │
       ▼
one database

For example:

CREATE USER 'report_reader'@'localhost'
IDENTIFIED BY 'LONG_RANDOM_PASSWORD';

GRANT SELECT
ON shop_database.*
TO 'report_reader'@'localhost';

If the application needs writes, grant the minimum additional privileges it actually requires.


What if the application genuinely needs several databases?

That’s fine.

Grant those specific databases.

For example:

GRANT SELECT
ON database_one.*
TO 'report_reader'@'localhost';

GRANT SELECT
ON database_two.*
TO 'report_reader'@'localhost';

That’s still much better than:

GRANT ALL PRIVILEGES ON *.*

What about temporary access?

This is where I would be disciplined.

Suppose I need the temporary VPS for three hours.

I create:

temporary SSH account
temporary SSH key
temporary DB account
temporary tunnel

Then I put a note somewhere:

START: 2026-08-24 09:00
PURPOSE: temporary reporting job
REMOVE: when job completes

When the job finishes:

remove SSH key
remove SSH account
remove DB account
close tunnel
destroy VPS

The temporary infrastructure leaves no permanent door behind.


Don’t rely on IP addresses if you don’t control them

This is especially important with disposable cloud servers.

You could do:

allow 203.0.113.41 → 3306

and that works until:

VPS destroyed

Then the next temporary VPS gets:

198.51.100.77

Now you have to change the firewall.

Worse, if you forget old rules, they accumulate.

Authentication-based access is much cleaner for this use case.


If you absolutely must expose 3306

Sometimes you have a legacy application that simply cannot use an SSH tunnel.

Fine.

Then I’d layer the controls.

Layer 1 — dedicated database account

not root
not admin
not shared with WordPress

Layer 2 — specific database

database_one.*

rather than:

*.*

Layer 3 — minimum privileges

For reporting:

SELECT

For an application:

SELECT
INSERT
UPDATE
DELETE

as appropriate.

Layer 4 — firewall

Don’t allow arbitrary Internet traffic if you can avoid it.

Layer 5 — TLS

If direct remote database traffic is required, configure encrypted database connections appropriately.

The important thing is that “it has a password” isn’t the entire security model.


Don’t forget the application credentials

Suppose the temporary application has:

DB_HOST=...
DB_USER=...
DB_PASSWORD=...

If that VPS is compromised, those credentials are compromised too.

So after the temporary job finishes:

rotate or delete the database credential.

This is another reason temporary accounts are useful.


A disposable account is better than a permanent account

Compare:

Bad lifecycle

2024
create remote_db_user

2025
still exists

2026
nobody remembers why

2026
server gets compromised

with:

Better lifecycle

2026-08-24
create temporary_db_user

2026-08-24
run job

2026-08-24
delete temporary_db_user

The second one is much easier to audit.


After cleanup, check again

Run:

SELECT User, Host
FROM mysql.user
ORDER BY User, Host;

Then:

ss -lntp | grep ':3306'

I want to know:

  1. Is MariaDB listening publicly?
  2. Which accounts can authenticate remotely?
  3. Which databases can they access?
  4. Which privileges do they have?
  5. Why does each remote account exist?

If I cannot answer those five questions, the configuration isn’t finished.


My preferred final configuration

For the kind of VPS I run, I’d aim for:

                    INTERNET
                       │
              ┌────────┴────────┐
              │                 │
             80                443
              │                 │
              └────────┬────────┘
                       │
                    Websites
                       │
                       ▼
                    PHP-FPM
                       │
                       ▼
                   MariaDB
                 127.0.0.1:3306

Administration:

My PC
  │
  ▼
SSH
  │
  ▼
VPS

Temporary application:

Temporary VPS
      │
      │ SSH tunnel
      ▼
Main VPS
      │
      ▼
127.0.0.1:3306

No public database port required.


One last check after changing it

This is the part I would absolutely not skip.

From another machine, test whether port 3306 is actually reachable.

For example:

nc -vz YOUR_SERVER 3306

If you’ve intentionally made MariaDB private, the expected result should be that the connection is refused or filtered.

Then test your application through the SSH tunnel.

Don’t assume the configuration worked.

Verify it.


The lesson I took from this

I didn’t need remote MySQL because:

“MySQL should be remote.”

I needed it because:

“One temporary application needs database access.”

Those are very different requirements.

Once I separated the requirement from the implementation, the solution became much cleaner.

The application needs database access.

It doesn’t necessarily need:

Internet → 3306 → MariaDB

It can have:

Application
    │
    ▼
authenticated SSH tunnel
    │
    ▼
private MariaDB

That’s the kind of change I like.

No complicated security product.

No giant infrastructure project.

Just one less door exposed to the Internet.

And after spending a couple of nights looking at what happens when too many doors are available, I’ll gladly take one less door.

No Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.