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:
- Is MariaDB listening publicly?
- Which accounts can authenticate remotely?
- Which databases can they access?
- Which privileges do they have?
- 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