MySQL Security

Securing MySQL — Hardening Checklist

Youssef
17 May 2026

Securing MySQL — Complete Hardening Checklist

Initial Setup — mysql_secure_installation

-- Run immediately after installation
sudo mysql_secure_installation

-- This will:
-- 1. Set root password
-- 2. Remove anonymous users
-- 3. Disable remote root login
-- 4. Remove test database
-- 5. Reload privilege tables

User & Privilege Hardening

-- Principle of least privilege: app users get ONLY what they need
CREATE USER 'webapp'@'127.0.0.1' IDENTIFIED BY 'StrongPass!2024';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'webapp'@'127.0.0.1';
FLUSH PRIVILEGES;

-- Never grant these to app users:
-- FILE, SUPER, PROCESS, RELOAD, SHUTDOWN, CREATE USER, GRANT OPTION

-- Remove anonymous users
DELETE FROM mysql.user WHERE User='';
FLUSH PRIVILEGES;

-- Remove remote root access
DELETE FROM mysql.user WHERE User='root' AND Host != 'localhost';
FLUSH PRIVILEGES;

-- Check remaining users
SELECT user, host, authentication_string, plugin FROM mysql.user;

-- Remove test database
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\_%';
FLUSH PRIVILEGES;

Network Hardening

# /etc/mysql/mysql.conf.d/mysqld.cnf

# Bind only to localhost (prevents external connections)
bind-address = 127.0.0.1

# Disable networking entirely if only local access needed
skip-networking

# Use Unix socket instead of TCP
# socket = /var/run/mysqld/mysqld.sock

# If remote access IS needed: use firewall instead
sudo ufw allow from 192.168.1.0/24 to any port 3306
sudo ufw deny 3306

File System Hardening

# Restrict file operations to a safe directory (or disable completely)
# In /etc/mysql/mysql.conf.d/mysqld.cnf:
secure_file_priv = /var/lib/mysql-files   # only allow in this directory
# OR
secure_file_priv = NULL                    # completely disable LOAD_FILE / OUTFILE

# Disable LOAD DATA LOCAL INFILE
local_infile = OFF

# Run MySQL as non-root dedicated user (default: mysql)
user = mysql

# Verify
SHOW VARIABLES LIKE 'secure_file_priv';
SHOW VARIABLES LIKE 'local_infile';

Authentication Hardening

# Use strong authentication plugin (MySQL 8.0+)
# In mysqld.cnf:
default_authentication_plugin = caching_sha2_password

# Enforce password policy
INSTALL PLUGIN validate_password SONAME 'validate_password.so';
SET GLOBAL validate_password.policy = STRONG;
SET GLOBAL validate_password.length = 12;
SET GLOBAL validate_password.mixed_case_count = 1;
SET GLOBAL validate_password.number_count = 1;
SET GLOBAL validate_password.special_char_count = 1;

# Set password expiry
ALTER USER 'webapp'@'localhost' PASSWORD EXPIRE INTERVAL 90 DAY;

# Lock accounts after failed attempts (MySQL 8.0.19+)
ALTER USER 'webapp'@'localhost' FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1;

Logging & Monitoring

# Enable general query log (audit trail — careful, impacts performance)
general_log = ON
general_log_file = /var/log/mysql/mysql.log

# Enable slow query log
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2

# Enable error log
log_error = /var/log/mysql/error.log

# Audit plugin (MySQL Enterprise or MariaDB audit)
INSTALL PLUGIN audit_log SONAME 'audit_log.so';

# Monitor for suspicious activity:
-- Multiple failed auth attempts
-- Queries using LOAD_FILE(), INTO OUTFILE
-- Queries to information_schema by app users
-- SLEEP() in queries (blind SQLi indicator)

Connection Security

# Enforce SSL/TLS for all connections
# In mysqld.cnf:
require_secure_transport = ON
ssl_ca = /etc/mysql/certs/ca.pem
ssl_cert = /etc/mysql/certs/server-cert.pem
ssl_key = /etc/mysql/certs/server-key.pem

# Create user that requires SSL
CREATE USER 'secure_app'@'%' IDENTIFIED BY 'pass' REQUIRE SSL;

# Verify SSL is working
SHOW VARIABLES LIKE '%ssl%';
SHOW STATUS LIKE 'Ssl_cipher';

Hardening Checklist

-- Run this audit script to check your configuration:

SELECT 'Empty password users' AS check_name, count(*) AS result
FROM mysql.user WHERE authentication_string = '' OR authentication_string IS NULL
UNION
SELECT 'Anonymous users', count(*) FROM mysql.user WHERE user = ''
UNION
SELECT 'Remote root access', count(*) FROM mysql.user WHERE user='root' AND host != 'localhost'
UNION
SELECT 'FILE privilege users', count(*) FROM mysql.user WHERE file_priv='Y' AND user != 'root'
UNION
SELECT 'Insecure file priv', IF(@secure_file_priv='','INSECURE','OK');

-- Also check:
SHOW VARIABLES LIKE 'local_infile';      -- should be OFF
SHOW VARIABLES LIKE 'bind_address';      -- should NOT be 0.0.0.0
SELECT @version;                        -- should be latest patched

Priority Order: (1) Set strong root password, (2) remove anonymous users, (3) set secure_file_priv=NULL, (4) bind to localhost only, (5) apply least-privilege to app users. These 5 steps eliminate 90% of common MySQL attack vectors.