Skip to main content

Security Hardening

Learning Focus

Use this lesson to harden your MySQL installation beyond basic user management — covering TLS encryption, password policies, network access controls, and security monitoring.

Concept Overview

Why Security Hardening?

Creating users and granting privileges is necessary but not sufficient. A hardened MySQL installation also:

  • Encrypts connections — prevents eavesdropping on queries and results
  • Enforces strong passwords — blocks weak, easily-guessed credentials
  • Restricts network access — limits which IPs can even attempt to connect
  • Monitors activity — detects suspicious behavior before damage occurs

The Security Layers

LayerWhat It ProtectsTools
NetworkWho can reach the MySQL portFirewall rules, bind-address, host restrictions
AuthenticationWho can log inPassword policies, account lockout, TLS certificates
AuthorizationWhat authenticated users can doPrivileges, roles, views
EncryptionData in transitTLS/SSL connections
MonitoringDetection of suspicious activityAudit log, connection log, query log

Basic Syntax & Rules

TLS/SSL Configuration

Check TLS status:

SHOW VARIABLES LIKE '%ssl%';
SHOW VARIABLES LIKE 'require_secure_transport';

Require TLS for a user:

ALTER USER 'app_user'@'%' REQUIRE SSL;

-- Require specific certificate
ALTER USER 'secure_user'@'%'
REQUIRE X509;

Require TLS globally:

# my.cnf
[mysqld]
require_secure_transport = ON

Password Policy (MySQL 8.0+)

-- Check current policy
SHOW VARIABLES LIKE 'validate_password%';
VariableRecommended ValuePurpose
validate_password.policySTRONGRequire mixed case, numbers, special chars
validate_password.length12Minimum password length
validate_password.mixed_case_count1At least 1 uppercase + 1 lowercase
validate_password.number_count1At least 1 digit
validate_password.special_char_count1At least 1 special character

Set password policy:

SET GLOBAL validate_password.policy = 'STRONG';
SET GLOBAL validate_password.length = 12;

Or in my.cnf:

[mysqld]
validate_password.policy = STRONG
validate_password.length = 12

Account Lockout

-- Lock after 5 failed attempts, lock for 2 days
CREATE USER 'secure_user'@'%'
IDENTIFIED BY 'Str0ng_P@ss!'
FAILED_LOGIN_ATTEMPTS 5
PASSWORD_LOCK_TIME 2;

Network Restrictions

Bind to specific interface:

# my.cnf — only listen on private network
[mysqld]
bind-address = 10.0.1.100

# Or localhost only (most secure)
bind-address = 127.0.0.1

Firewall rules (iptables/ufw):

# Allow MySQL port only from application servers
ufw allow from 10.0.1.0/24 to any port 3306
ufw deny 3306

Step-by-Step Examples

Example 1: Security Audit Checklist

Run these checks on any MySQL installation:

-- 1. Check for users with no password
SELECT User, Host FROM mysql.user
WHERE authentication_string = '' OR authentication_string IS NULL;

-- 2. Check for users accessible from anywhere
SELECT User, Host FROM mysql.user WHERE Host = '%';

-- 3. Check for users with SUPER or ALL PRIVILEGES
SHOW GRANTS FOR 'root'@'localhost';

-- 4. Check if remote root login is possible
SELECT User, Host FROM mysql.user WHERE User = 'root' AND Host != 'localhost';

-- 5. Check TLS enforcement
SHOW VARIABLES LIKE 'require_secure_transport';

-- 6. Check password policy
SHOW VARIABLES LIKE 'validate_password%';

-- 7. Check for anonymous users
SELECT User, Host FROM mysql.user WHERE User = '';

Example 2: Secure a Fresh Installation

Step 1: Run the security script

mysql_secure_installation

This interactively:

  • Sets root password
  • Removes anonymous users
  • Disables remote root login
  • Removes test database
  • Reloads privilege tables

Step 2: Remove remaining risks

-- Remove anonymous users (if any remain)
DROP USER IF EXISTS ''@'localhost';
DROP USER IF EXISTS ''@'%';

-- Ensure root can only connect locally
DROP USER IF EXISTS 'root'@'%';

-- Verify
SELECT User, Host FROM mysql.user;

Step 3: Enable TLS

SET GLOBAL require_secure_transport = ON;

Step 4: Set password policy

INSTALL COMPONENT 'file://component_validate_password';
SET GLOBAL validate_password.policy = 'STRONG';
SET GLOBAL validate_password.length = 12;

Example 3: Monitor Connection Activity

-- See currently connected users
SELECT
USER AS connected_user,
HOST AS from_host,
DB AS current_database,
COMMAND,
TIME AS seconds_active,
STATE
FROM INFORMATION_SCHEMA.PROCESSLIST
ORDER BY TIME DESC;

-- Check failed login attempts (MySQL 8.0+)
SELECT * FROM performance_schema.host_cache
WHERE COUNT_AUTHENTICATION_ERRORS > 0;

Example 4: Disable Dangerous Features

-- Disable LOCAL INFILE (prevents loading local files into MySQL)
SET GLOBAL local_infile = OFF;

-- Disable LOAD DATA LOCAL (in my.cnf)
-- [mysqld]
-- local_infile = OFF

-- Check symbolic links (should be disabled)
SHOW VARIABLES LIKE 'have_symlink';

Practical Use Cases

1. Production Database Lockdown

Remove anonymous users, disable remote root, enforce TLS, restrict to application server IPs.

2. Compliance (PCI-DSS, HIPAA, GDPR)

Password policies, encrypted connections, audit logging, and access reviews satisfy regulatory requirements.

3. Multi-Tenant Isolation

Each tenant's application user is restricted to their database only — with TLS required.

4. Development Environment

Less restrictive but still secure — use a strong root password and restrict to localhost only.

5. Cloud Deployments (AWS RDS, Cloud SQL)

Managed databases handle some hardening automatically but still require proper user management and privilege scoping.


Common Mistakes & Troubleshooting

MistakeWhat HappensHow to Fix
Leaving default root passwordAnyone can gain full controlChange root password immediately on setup
Not running mysql_secure_installationAnonymous users, test DB, remote root existRun it on every new installation
Allowing root@'%'Root accessible from anywhereDrop root@'%', keep root@'localhost' only
No TLS enforcementCredentials and data sent in plaintextSet require_secure_transport = ON
Weak password policyUsers set "password123"Install validate_password component
No firewall on port 3306Anyone on the network can attempt loginRestrict with ufw or security groups

Best Practices

  • Run mysql_secure_installation on every new server
  • Require TLS for all non-localhost connections
  • Enforce strong passwords with the validate_password plugin
  • Restrict bind-address to private IPs or localhost
  • Use firewall rules to limit access to the MySQL port
  • Disable dangerous featureslocal_infile = OFF, remove symlinks
  • Audit quarterly — review all users, privileges, and password expiry status
  • Rotate credentials regularly — at least every 90 days for sensitive accounts
  • Use separate accounts — never share MySQL users between applications or people

Hands-On Practice

Exercise 1: Security Scan (Easy)

Run the security audit queries from Example 1 on your MySQL server. How many issues do you find?

Exercise 2: Harden a Development Server (Medium)

  1. Create a strong root password
  2. Remove anonymous users
  3. Disable remote root
  4. Enable the password policy plugin
  5. Set minimum password length to 12
  6. Verify all changes

Exercise 3: Full Production Security (Advanced)

Set up a production-ready MySQL installation:

  1. Enable TLS (generate certificates if needed)
  2. Require TLS for all non-localhost users
  3. Set up account lockout (5 attempts, 1 day lock)
  4. Enable password expiry (90 days)
  5. Create application-specific users with minimal privileges
  6. Set up monitoring queries for failed logins

Connection to Other Concepts

Related ConceptHow It Connects
Users and PrivilegesHardening builds on top of proper user management
ViewsViews help implement column-level access control
Stored ProceduresEXECUTE-only access hides table structure from users
Backup and RecoveryBackup users need specific, limited privileges
PerformanceTLS adds slight overhead (~5–10%) but is necessary for security

What to Learn Next


Visual Learning Diagram

flowchart TD
A["Connection attempt"] --> B["Network layer\n(firewall, bind-address)"]
B -->|Blocked| C["❌ Connection refused"]
B -->|Allowed| D["Authentication\n(password, TLS)"]
D -->|Failed| E["❌ Access denied"]
D -->|Success| F["Authorization\n(privileges, roles)"]
F -->|No privilege| G["❌ Permission denied"]
F -->|Has privilege| H["✅ Query executes"]

I["Monitoring"] --> D
I --> F
I --> H

classDef block fill:#dc3545,stroke:#fff,color:#fff
classDef pass fill:#28a745,stroke:#fff,color:#fff
class C,E,G block
class H pass

Quick Reference

-- Security audit
SELECT User, Host FROM mysql.user WHERE Host = '%';
SELECT User, Host FROM mysql.user WHERE authentication_string = '';
SHOW VARIABLES LIKE 'require_secure_transport';
SHOW VARIABLES LIKE 'validate_password%';

-- Harden
SET GLOBAL require_secure_transport = ON;
SET GLOBAL local_infile = OFF;
ALTER USER 'user'@'host' REQUIRE SSL;

-- Password policy
SET GLOBAL validate_password.policy = 'STRONG';
SET GLOBAL validate_password.length = 12;

-- Account lockout
ALTER USER 'user'@'host' FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1;

-- Monitor
SELECT USER, HOST, COMMAND, TIME FROM INFORMATION_SCHEMA.PROCESSLIST;

What's Next