MySQL is the #1 target for database attacks. Port 3306 is scanned millions of times per day worldwide — attackers find exposed instances in minutes and exploit weak credentials, anonymous users, and default configurations. This guide covers every hardening step for MySQL 8.0+ in production.
The single most impactful change you can make is ensuring MySQL only listens on localhost. By default on many Linux distributions, MySQL binds to 0.0.0.0 — meaning it accepts connections from any IP on the internet. This must be changed immediately on any production server.
Edit /etc/mysql/mysql.conf.d/mysqld.cnf (Ubuntu/Debian) or /etc/my.cnf (CentOS/RHEL):
[mysqld]
# IMPORTANT: restrict MySQL to localhost only
# Change from 0.0.0.0 (all interfaces) to 127.0.0.1 (loopback only)
bind-address = 127.0.0.1
# If using MySQL 8.0.13+ with X Protocol, also restrict mysqlx
mysqlx-bind-address = 127.0.0.1
# Optional: disable the X Protocol entirely if not using it
# mysqlx = 0
# Disable local file loading (prevents LOAD DATA INFILE attacks)
local_infile = 0
# Disable symbolic link following (prevents symlink attacks)
symbolic-links = 0
After editing, restart MySQL:
sudo systemctl restart mysql
Verify that MySQL is no longer listening on 0.0.0.0:
sudo ss -tlnp | grep 3306
# Expected: 127.0.0.1:3306 (NOT 0.0.0.0:3306)
If you need remote access to MySQL, use an SSH tunnel instead of opening port 3306. Connect via ssh -L 3307:127.0.0.1:3306 user@server and then connect your local client to 127.0.0.1:3307.
A fresh MySQL installation creates anonymous user accounts and a test database that anyone can access. These must be removed before the server goes into production. Run mysql_secure_installation to handle this interactively, or run the SQL commands manually for automation.
Option A — Interactive (recommended for first setup):
sudo mysql_secure_installation
# Answer prompts:
# - Set root password: YES
# - Remove anonymous users: YES
# - Disallow root login remotely: YES
# - Remove test database: YES
# - Reload privilege tables: YES
Option B — SQL commands (for scripted/automated hardening):
-- Connect as root
mysql -u root -p
-- Remove all anonymous accounts
DELETE FROM mysql.user WHERE User = '';
-- Remove test database
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db = 'test' OR Db = 'test\_%';
-- Disable remote root login (root should only connect from localhost)
DELETE FROM mysql.user WHERE User = 'root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
-- Apply changes immediately
FLUSH PRIVILEGES;
Verify no anonymous accounts remain:
SELECT user, host, authentication_string FROM mysql.user WHERE user = '';
# Expected: Empty set (0 rows)
MySQL 8.0 ships with the validate_password component (previously a plugin in 5.7). Enable it and configure strict requirements so that weak passwords are rejected at creation time.
Install and configure the password validation component:
-- Install the validate_password component (MySQL 8.0+)
INSTALL COMPONENT 'file://component_validate_password';
-- Set policy to STRONG (requires: length, mixed case, digits, special chars)
SET GLOBAL validate_password.policy = STRONG;
-- Require at least 14 characters
SET GLOBAL validate_password.length = 14;
-- Require at least 2 uppercase, 2 lowercase, 2 digits, 2 special characters
SET GLOBAL validate_password.mixed_case_count = 2;
SET GLOBAL validate_password.number_count = 2;
SET GLOBAL validate_password.special_char_count = 2;
-- Block passwords that match the username
SET GLOBAL validate_password.check_user_name = ON;
Or persist settings in my.cnf under [mysqld]:
[mysqld]
# Load the validate_password component
early-plugin-load = validate_password.so
# Password policy: STRONG requires length + mixed case + digits + special chars
validate_password.policy = STRONG
validate_password.length = 14
validate_password.mixed_case_count = 2
validate_password.number_count = 2
validate_password.special_char_count = 2
validate_password.check_user_name = ON
# Use the modern caching_sha2_password authentication plugin
default_authentication_plugin = caching_sha2_password
Verify current password policy:
SHOW VARIABLES LIKE 'validate_password%';
Additionally, use caching_sha2_password (the MySQL 8.0 default) instead of the older mysql_native_password. Verify your root account uses it:
SELECT user, host, plugin FROM mysql.user WHERE user = 'root';
# Expected plugin: caching_sha2_password
Every application should connect with a dedicated MySQL user that has only the permissions it needs — nothing more. Never use the root account for application connections, and never grant GRANT ALL to application users.
Create a dedicated application user with minimal privileges:
-- Create a dedicated application database
CREATE DATABASE myapp_production;
-- Create user restricted to localhost with a strong password
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'Str0ng!P@ssw0rd#2026';
-- Grant ONLY what the application needs (not GRANT ALL)
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp_production.* TO 'myapp_user'@'localhost';
-- For apps that manage schema (migrations), also allow DDL
GRANT CREATE, DROP, ALTER, INDEX ON myapp_production.* TO 'myapp_user'@'localhost';
-- Apply changes
FLUSH PRIVILEGES;
-- Never do this for application users:
-- GRANT ALL PRIVILEGES ON *.* TO 'myapp_user'@'localhost' WITH GRANT OPTION; -- DANGEROUS
GRANT SELECT ON myapp.* TO
'reports'@'localhost'
IDENTIFIED BY '...';
GRANT SELECT, LOCK TABLES,
SHOW VIEW, EVENT,
TRIGGER ON *.* TO
'backup'@'localhost';
GRANT REPLICATION SLAVE,
REPLICATION CLIENT ON *.*
TO 'replica'@'10.0.0.%'
IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE,
DELETE, CREATE, DROP,
ALTER, INDEX ON myapp.*
TO 'deploy'@'localhost';
Audit all users and their grants to spot over-privileged accounts:
SELECT user, host FROM mysql.user;
SHOW GRANTS FOR 'appuser'@'localhost';
Logging database activity is essential for detecting unauthorized access and forensic investigation after a breach. MySQL Community Edition supports general_log for basic query logging. MySQL Enterprise Edition adds a full audit plugin. For most production setups, enabling the general log with rotation is a pragmatic baseline.
Enable query logging in my.cnf:
[mysqld]
# Enable general query log
general_log = ON
general_log_file = /var/log/mysql/general.log
# Enable slow query log (captures queries taking > 1 second)
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = ON
# Log all connections and disconnections
log_connections = ON
log_disconnections = ON
Or enable/disable at runtime without restart:
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/log/mysql/general.log';
Set up log rotation with logrotate — create /etc/logrotate.d/mysql-general:
/var/log/mysql/general.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
# Tell MySQL to re-open the log file after rotation
mysql -u root -p$(cat /etc/mysql/root.password) \
-e 'FLUSH LOGS;' 2>/dev/null || true
endscript
}
Note that general_log logs every query and can be very verbose on busy servers. Consider enabling it only temporarily for audits, or use the slow query log (slow_query_log) as a lighter alternative that captures queries above a time threshold.
Even with MySQL bound to localhost, you should enforce encrypted connections for any clients that connect over the network. MySQL 8.0 enables SSL/TLS by default, but you must verify it's working and optionally require it per user. For internet-facing MySQL (not recommended), firewall rules are mandatory.
Verify SSL is enabled in MySQL 8.0:
SHOW VARIABLES LIKE 'have_ssl';
# Expected: Value = YES
SHOW VARIABLES LIKE 'tls_version';
# Expected: TLSv1.2,TLSv1.3
Require secure transport globally in my.cnf:
[mysqld]
# Paths to SSL certificates (auto-generated by MySQL 8.0 if not specified)
ssl_ca = /var/lib/mysql/ca.pem
ssl_cert = /var/lib/mysql/server-cert.pem
ssl_key = /var/lib/mysql/server-key.pem
# Force all connections to use SSL/TLS (MySQL 8.0+)
require_secure_transport = ON
# Enforce minimum TLS version (disable old/vulnerable versions)
tls_version = TLSv1.2,TLSv1.3
Or require SSL per user account:
ALTER USER 'appuser'@'%' REQUIRE SSL;
FLUSH PRIVILEGES;
Firewall rules — block port 3306 from all external sources (iptables):
# Block all incoming traffic to port 3306 from external IPs
iptables -A INPUT -p tcp --dport 3306 -j DROP
# Allow MySQL only from localhost (loopback interface)
iptables -I INPUT -i lo -p tcp --dport 3306 -j ACCEPT
# If you have an application server on a private network (e.g., 10.0.0.0/8)
# allow it explicitly, and deny everything else
iptables -I INPUT -s 10.0.0.0/8 -p tcp --dport 3306 -j ACCEPT
# Save rules (Ubuntu/Debian)
sudo iptables-save > /etc/iptables/rules.v4
Or using nftables (modern Linux):
# /etc/nftables.conf — add inside the inet filter table
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow loopback
iif lo accept
# Block external access to MySQL port 3306
tcp dport 3306 iif != lo drop
# Allow established connections
ct state established,related accept
}
}
Manual hardening is a one-time task. Ongoing security requires continuous monitoring: detecting brute force attempts in real time, alerting on exposed ports, and receiving advisories when MySQL CVEs affect your installed version. This is where Defensia complements your manual hardening steps.
One curl command installs the Defensia agent as a systemd service. MySQL monitoring begins automatically — no configuration file to edit.
The agent checks if port 3306 is accessible from outside. If MySQL is bound to 0.0.0.0, you receive an advisory in your dashboard within seconds.
Defensia tails MySQL authentication logs and detects repeated "Access denied" failures. Attacking IPs are automatically banned via iptables after the threshold is crossed.
The agent reports your MySQL version via heartbeat. Defensia cross-references it against the NVD CVE database and surfaces critical advisories in your dashboard with CVSS scores.
All MySQL security events appear in your Defensia dashboard — attack timelines, blocked IPs, port exposure advisories, and CVE matches for your installed version.
Install the Defensia agent
curl -fsSL https://defensia.cloud/install.sh | sudo bash -s -- --token YOUR_TOKEN
Works on Ubuntu, Debian, CentOS, Rocky, AlmaLinux, Amazon Linux. MySQL monitoring starts automatically after installation.
Defensia's agent watches your MySQL server continuously — detecting threats that slip past manual hardening and reacting faster than any human can.
Detects if MySQL port 3306 is publicly accessible on startup and whenever the agent restarts. Creates a security advisory with remediation instructions if the port is exposed.
Monitors MySQL error logs for authentication failures. After repeated failures from the same IP, the attacker is automatically banned via iptables/nftables — no manual intervention needed.
Defensia tracks your installed MySQL version and alerts you when critical CVEs are published for it. You see the CVSS score, description, and fix version directly in your dashboard.
On-demand hardening scan checks your MySQL configuration against security best practices — bind address, anonymous users, remote root login, and more. Results include prioritized fixes.
Defensia auto-detects MySQL log paths across distributions. No need to specify log file locations — the agent finds and monitors them automatically after install.
Bans integrate with iptables or nftables. Banned IPs are synced to the dashboard so you can review, whitelist, or extend bans. Ban duration is configurable per server.
[DEFENSIA] MySQL auth failure: root@185.224.128.47 — attempt 1/5
[DEFENSIA] MySQL auth failure: root@185.224.128.47 — attempt 2/5
[DEFENSIA] MySQL auth failure: admin@185.224.128.47 — attempt 3/5
[DEFENSIA] MySQL auth failure: mysql@185.224.128.47 — attempt 4/5
[DEFENSIA] MySQL auth failure: root@185.224.128.47 — attempt 5/5
[DEFENSIA] BRUTE FORCE DETECTED — banning 185.224.128.47 via iptables
[DEFENSIA] IP 185.224.128.47 banned for 24h — event logged to dashboard
Restricting the bind address to 127.0.0.1 is the single most impactful change. If MySQL is bound to 0.0.0.0, it is accessible to the entire internet and will be attacked within minutes. All other hardening steps are secondary to ensuring MySQL only listens on the loopback interface.
It is a good starting point. mysql_secure_installation removes anonymous users, the test database, and optionally sets a root password and disables remote root login. However, it does not configure password validation, SSL/TLS, audit logging, or firewall rules — all of which are covered in this guide.
Use caching_sha2_password, which is the default in MySQL 8.0. It provides stronger password hashing (SHA-256) and supports a caching mechanism for performance. Only use mysql_native_password if you have legacy clients that do not support the newer plugin — and plan to migrate those clients as soon as possible.
Run `sudo ss -tlnp | grep 3306` on the server. If you see 0.0.0.0:3306, MySQL is listening on all interfaces and is publicly accessible. It should show 127.0.0.1:3306 instead. You can also use an external port scanner or install Defensia, which checks this automatically at startup and alerts you.
The Defensia agent tails MySQL error logs (typically /var/log/mysql/error.log or syslog) and looks for repeated "Access denied for user" messages from the same IP address. After a configurable threshold of failures (default: 5 within a short window), the attacking IP is banned via iptables/nftables and the event is reported to your dashboard.
Harden once, monitor forever. Defensia watches your MySQL server 24/7 — detecting brute force, exposed ports, and CVE advisories automatically.
Get Started Free