MySQL with default settings is not secure. An open port 3306, a passwordless root account, anonymous users — all of these are gates for attackers who scan the internet specifically for these signs with automated bots, not by hand. Run through these steps right after installation, before the database fills up with real customer data.
Baseline protection: mysql_secure_installation
sudo mysql_secure_installation
# Answer the prompts:
# Set root password? YES -- pick a strong password
# Remove anonymous users? YES
# Disallow root login remotely? YES
# Remove test database? YES
# Reload privilege tables? YES
Stop listening on the external interface
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 127.0.0.1 # Localhost only
# or for IPv6:
bind-address = ::1
sudo systemctl restart mysql
After this change, nothing can connect to MySQL from outside even knowing the password — only from localhost or through an SSH tunnel. This closes port 3306 to the internet without a single firewall rule.
Create a separate user for every application
-- Create a user scoped to a single database
CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'StrongPassword123!';
CREATE DATABASE myapp_db;
GRANT ALL PRIVILEGES ON myapp_db.* TO 'myapp'@'localhost';
FLUSH PRIVILEGES;
-- Never grant applications GRANT OPTION:
-- GRANT ALL PRIVILEGES ON *.* TO ... -- FORBIDDEN!
An SSH tunnel instead of an open port
General MySQL/MariaDB installation on a VPS is covered in the article on MySQL/MariaDB on a VPS, and SSH access itself is worth hardening per the SSH Hardening article.
# Connect to MySQL through an SSH tunnel (without opening 3306)
ssh -L 3306:127.0.0.1:3306 user@your-vps-ip -N -f
# Now you can connect locally:
mysql -u myapp -p -h 127.0.0.1 -P 3306
Common errors and signs of a breach
- Host '1.2.3.4' is not allowed to connect to this MySQL server — actually good news: bind-address or user privileges are working as intended and blocking the external connection.
- Access denied for user 'root'@'localhost' (using password: YES) after many failed attempts in auth.log — a sign of a root password brute force; check
/var/log/mysql/error.logfor the frequency and source of the attempts. - Unfamiliar databases or tables appear, such as
readme_to_recover— a classic trace of a ransom attack, where a script drops the original tables and leaves a ransom note; recovery is only possible from a backup taken before the incident. - A sudden spike in general_log that no one enabled by hand — a sign that someone gained access and turned on logging to harvest application passwords passed in queries.
Monitoring suspicious activity
# Turn on the general log (briefly, for diagnostics only!)
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/log/mysql/general.log';
# Find slow queries
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
# View current connections
SHOW PROCESSLIST;
Do not leave general_log enabled permanently — it records EVERY query and fills up the disk quickly, and under heavy load it noticeably slows down the database engine itself.
Comparing protection measures
| Measure | Priority | What it closes |
|---|---|---|
| mysql_secure_installation | Critical | Empty root, anonymous users, the test database |
| bind-address = 127.0.0.1 | Critical | Direct access to port 3306 from the internet |
| Separate users per database | High | A breach of one application does not reach other databases |
| SSH tunnel | High | Remote administration without an open port |
| Regular backups | Medium | Recovery after a ransom attack or a mistake |
When these measures are not enough
- The application and MySQL live on separate servers in the same data center — then
bind-address = 127.0.0.1does not work; you need either a VPN between the servers or a firewall allow-listing specific IPs on port 3306. - Several developers connect to the database from different workstations — permanent SSH tunnels for each one are inconvenient; a bastion host with a single entry point and session logging fits better here.
- Master-slave replication is needed between data centers — replica traffic must run over a TLS connection between the MySQL instances; simply disabling the external interface is not enough.
How to verify the protection is working
# Confirm port 3306 is not visible from outside
nmap -p 3306 your-vps-ip
# Expect: filtered or closed, not open
# Confirm bind-address was applied
mysql -u root -p -e "SHOW VARIABLES LIKE 'bind_address';"
# Check the list of users and their access hosts
mysql -u root -p -e "SELECT user, host FROM mysql.user;"
If an external scan finds no open port 3306, and the user list has no anonymous entries and no % host entries for critical accounts, the baseline protection is set up correctly.