How to keep MySQL and MariaDB safe on your own server
Your database holds the things attackers want most: customer details, orders and password hashes. A few simple settings keep it out of reach from the internet and limit the damage if one site is hacked.
The examples use a site called shop. Use your own names. The commands were tested on Ubuntu 24.04 (MySQL 8.0 and MariaDB 10.11) and Debian 13 (MariaDB 11.8).
MySQL or MariaDB? Most commands work on both. On MariaDB, the tools are called mariadb, mariadb-dump and mariadb-secure-installation. On MySQL, they are mysql, mysqldump and mysql_secure_installation. We show the MariaDB name first and the MySQL name after it.
1. Check what the database listens on
sudo ss -tlnp | grep -E ':3306|:33060'
Good: you only see 127.0.0.1:3306 (MySQL also shows 127.0.0.1:33060). That means only programs on the server itself can connect.
Bad: 0.0.0.0:3306, *:3306 or [::]:3306. Then the database is open to the network, and only your firewall stands in the way.
On older servers you may know netstat better. It shows the same thing: sudo netstat -tlnp | grep 3306 (it comes in the net-tools package).
2. Make it listen on localhost only
Find the setting:
sudo grep -rn bind-address /etc/mysql/
On MariaDB it is in /etc/mysql/mariadb.conf.d/50-server.cnf. On MySQL it is in /etc/mysql/mysql.conf.d/mysqld.cnf. It should say:
bind-address = 127.0.0.1
On MySQL there is a second line for its extra port 33060: mysqlx-bind-address = 127.0.0.1. Lines that start with # are switched off and don't count.
This is already the default on Ubuntu and Debian. If someone changed it to 0.0.0.0, change it back and restart:
sudo systemctl restart mariadb # or: sudo systemctl restart mysql
Then run the ss command from step 1 again.
3. Run the secure installation script
sudo mariadb-secure-installation # or: sudo mysql_secure_installation
It asks a few questions. Here is what they mean today:
- Enter current password for root (MariaDB): just press Enter.
- Switch to unix_socket authentication / Change the root password (MariaDB): answer
nto both. On Ubuntu and Debian, the database root user already logs in through your Linux root account (withsudo mariadb), not with a password. That is safer than a password. - VALIDATE PASSWORD component (MySQL): you can answer
nif you make long random passwords as shown below. MySQL then skips the root password for the same reason as above. - Remove anonymous users:
Y. - Disallow root login remotely:
Y. - Remove test database:
Y. - Reload privilege tables:
Y.
On new Ubuntu and Debian installs, there are no anonymous users and no test database to begin with, so the script mostly confirms that. It is still worth running on a server someone else set up. On Debian 13, only the new name mariadb-secure-installation exists.
4. Keep root local
Open the database as root:
sudo mariadb # or: sudo mysql
List all database users and where they may log in from:
SELECT user, host FROM mysql.user;
Every host should be localhost (or 127.0.0.1). If you see root with % (meaning "from anywhere") or an IP address, remove it:
DROP USER 'root'@'%';
Don't give root a password just so an app can use it. Apps should never log in as root.
5. Give each site its own database and user
Make a strong random password on the server:
openssl rand -base64 24
Then, in sudo mariadb (or sudo mysql), create the database and a user that can only use it. Put your new password where it says PASTE-PASSWORD:
CREATE DATABASE shop CHARACTER SET utf8mb4;
CREATE USER 'shop'@'localhost' IDENTIFIED BY 'PASTE-PASSWORD';
GRANT ALL PRIVILEGES ON shop.* TO 'shop'@'localhost';
shop.* means "everything inside the shop database, and nothing else". 'shop'@'localhost' means this user can only log in from the server itself.
Put the password in the site's own settings file (for example wp-config.php or .env). Make sure only the site's user can read that file. Our guide to one user and PHP-FPM pool per site shows how.
Check that it works. Log in as the new user:
mariadb -u shop -p # or: mysql -u shop -p
SHOW DATABASES;
You should see only shop and information_schema (MySQL also shows performance_schema). The other sites' databases are invisible to this user.
6. Give only the rights that are needed
- Never use
ON *.*for a site's user. That means "every database on the server". - Never add
WITH GRANT OPTION. Without it, the user can't hand out rights to anyone else. - A user that only reads (for reports or a dashboard) gets
SELECTonly:CREATE USER 'shop_read'@'localhost' IDENTIFIED BY 'PASTE-OTHER-PASSWORD'; GRANT SELECT ON shop.* TO 'shop_read'@'localhost'; - Check what a user can do at any time:
SHOW GRANTS FOR 'shop'@'localhost';
WordPress, Laravel and most other apps change their own tables during updates, so their main user needs full rights to their own database. That is fine: the limit is the one database, not the whole server.
7. If you really need to connect from outside
Best: use an SSH tunnel. The database stays on localhost, and you reach it through your SSH login. On your own computer, run:
ssh -N -L 3307:127.0.0.1:3306 you@your-server
Leave it running. In a second window, connect to port 3307 on your own computer:
mariadb -h 127.0.0.1 -P 3307 -u shop -p # or: mysql -h 127.0.0.1 -P 3307 -u shop -p
The traffic goes through SSH, so it is encrypted, and the server sees you as localhost. Most database apps on your computer (like DBeaver, HeidiSQL or MySQL Workbench) have an "SSH tunnel" option that does the same thing.
Only if a tunnel won't work: open the port to one IP address, with encryption. Say the other machine is 203.0.113.10.
- Set
bind-address = 0.0.0.0(step 2) and restart the database. - Allow only that one address in the firewall. With ufw:
Never usesudo ufw allow from 203.0.113.10 to any port 3306 proto tcpsudo ufw allow 3306: that opens it to the whole internet. See our ufw guide. - Create a user that may only log in from that address, and only over an encrypted connection:
CREATE USER 'report'@'203.0.113.10' IDENTIFIED BY 'PASTE-PASSWORD' REQUIRE SSL; GRANT SELECT ON shop.* TO 'report'@'203.0.113.10';
About encryption: MySQL 8 and MariaDB 11.4 or newer (Debian 13) turn it on by themselves. MariaDB 10.11 (Ubuntu 24.04) has it switched off until you add a certificate, so a REQUIRE SSL user can't log in there yet. Check with SHOW VARIABLES LIKE 'have_ssl';: it should say YES. The MariaDB guide to secure connections shows how to add one.
8. Back up every database, and test a restore
Make a private folder for the backups, then dump the database:
sudo mkdir -p /var/backups/mysql
sudo chmod 700 /var/backups/mysql
sudo mariadb-dump --single-transaction --routines --triggers shop | gzip | sudo tee /var/backups/mysql/shop-$(date +%F).sql.gz > /dev/null
On MySQL, use mysqldump instead of mariadb-dump. --single-transaction takes a clean copy without stopping the site (for normal InnoDB tables).
A good dump ends with a line like -- Dump completed on 2026-11-12. Check it:
sudo zcat /var/backups/mysql/shop-$(date +%F).sql.gz | tail -n 1
Now test a restore into a spare database, so you know the backup really works:
sudo mariadb -e "CREATE DATABASE shop_restore_test;"
sudo zcat /var/backups/mysql/shop-$(date +%F).sql.gz | sudo mariadb shop_restore_test
sudo mariadb -e "SHOW TABLES FROM shop_restore_test;"
sudo mariadb -e "DROP DATABASE shop_restore_test;"
(On MySQL, use mysql instead of mariadb.) Compare the tables, and the row count of one big table, with the real database. Because the dump was made of one database without --databases, it restores into whatever database you name and can't overwrite the real one by mistake.
To run it every night, add the dump line to root's cron with sudo crontab -e. In cron, write % as \%:
30 2 * * * mariadb-dump --single-transaction --routines --triggers shop | gzip > /var/backups/mysql/shop-$(date +\%F).sql.gz
45 2 * * * find /var/backups/mysql -name '*.sql.gz' -mtime +14 -delete
The second line deletes backups older than 14 days. A backup on the same server is lost if the server is, so also copy the files to another place every day.
9. Keep it updated
Ubuntu and Debian ship security fixes for MySQL and MariaDB like any other package. Check for waiting updates:
sudo apt update
apt list --upgradable 2>/dev/null | grep -iE 'mysql|mariadb'
Our guide to automatic security updates installs them for you every day. An update restarts the database, which takes a few seconds.
Good to know
- Backups hold everything your database holds, including customer data. Keep them private (
chmod 700on the folder) and encrypt copies you send elsewhere. - Remove users you no longer need, for example when you delete a site:
DROP USER 'shop'@'localhost'; - Change a password if it may have leaked:
ALTER USER 'shop'@'localhost' IDENTIFIED BY 'NEW-PASSWORD';and then update the site's settings file.
Official sources
- mariadb-secure-installation (MariaDB)
- GRANT (MariaDB)
- Remote connections (MariaDB)
- mariadb-dump (MariaDB)
- Install and configure MySQL (Ubuntu Server docs)
Next steps: give each site its own user and PHP-FPM pool, find the queries that slow your site down, then work through the security checklist for a new PHP server.
ServerNest is being built to set up databases this way for you: MariaDB on localhost only, a separate database user with a strong random password for every database, saved in a private file only the site can read, and databases included in backups. Join early access to try it first.