MySQL breaches rarely need a zero-day: a root account reachable from the network, an application holding global privileges, or a FILE grant that turns a SQL injection into file access on the host is usually enough. MySQL 8.4 LTS closed many historic gaps, with caching_sha2_password as the default, TLS 1.0 and 1.1 removed and mysql_native_password disabled by default, but many estates still run upgraded 8.0 instances that carry older accounts and settings. This guide covers MySQL Community and Enterprise 8.4 and the 9.x series: the account model, authentication plugins, password and lockout policy, privileges and roles, server options that limit file and plugin abuse, encryption, auditing and patching. It is for DBAs, platform engineers and security engineers running MySQL themselves or on a managed service. Engine-neutral principles live in the Database Security Guide; this page goes deeper on MySQL itself.
Key takeaways
- An account is user plus host.
'app'@'10.0.1.%'and'app'@'%'are different accounts with different passwords and grants, so wildcard hosts quietly widen who can log in. - Move off mysql_native_password now. It was deprecated in 8.0.34, is disabled by default in 8.4 and is removed in 9.0, so any account still using it blocks your upgrade path.
- Global privileges are the danger zone. Grants on
*.*, and especiallyFILE,SUPER,SYSTEM_USERandCREATE USER, give far more than any application needs. - Native lockout and rotation exist, use them.
FAILED_LOGIN_ATTEMPTS,PASSWORD_LOCK_TIMEand dual passwords withRETAIN CURRENT PASSWORDsupport safe service credential rotation without downtime. - Audit is an edition decision. MySQL Enterprise Audit is part of the commercial Enterprise Edition; Community users need an alternative audit plugin, managed service audit features or network-level monitoring.
The account model: user, host and the grant tables
MySQL identifies an account by both a user name and a host pattern, stored in the mysql.user table along with its authentication plugin and global privileges. When a client connects, the server picks the most specific matching host entry. This has three practical consequences.
- Wildcards widen exposure:
'%'matches any address. Prefer an exact IP or a narrow subnet such as'10.0.1.%', and remove'%'accounts that duplicate a narrower one. - Anonymous and test accounts: older installers created anonymous users (empty user name) and a
testdatabase open to all. Check withSELECT user, host, plugin FROM mysql.user;and drop any account with an empty user name. - Hostnames depend on DNS: with name resolution on, a host pattern such as
'%.corp.example'trusts reverse DNS. Settingskip_name_resolve = ONforces IP-based host matching and removes that dependency.
The MySQL security guidelines are blunt on the system tables: never give anyone except root accounts access to mysql.user, never grant privileges to all hosts, and test whether mysql -u root connects without a password. Use SHOW GRANTS FOR 'user'@'host'; to review each account and REVOKE what is not needed.
Authentication plugins and transport encryption
| Plugin | Status in 8.4 / 9.x | Use it for |
|---|---|---|
caching_sha2_password | Default. SHA-256 based, with a server-side cache for fast re-authentication. | All password-based accounts. Requires TLS or RSA key exchange for the first password exchange. |
mysql_native_password | Deprecated since 8.0.34, disabled by default in 8.4 (enable with mysql_native_password=ON), removed in 9.0.0. | Nothing new. Re-key remaining accounts before upgrading to 9.x. |
auth_socket | Available on Unix. | Local administration: the OS user name must match, so root needs no network password. |
| LDAP and Kerberos pluggable authentication | Enterprise Edition features. | Human DBA logins tied to the corporate directory. |
| Cloud IAM authentication | Provided by managed services (for example Amazon RDS, Cloud SQL). | Service and human logins with short-lived tokens instead of stored passwords. |
MySQL 8.4 accepts encrypted connections only over TLSv1.2 and TLSv1.3; the documentation states that TLSv1 and TLSv1.1 were deprecated and removed in MySQL 8.0. Set require_secure_transport = ON so the server rejects unencrypted TCP connections (local socket connections remain allowed), and for individual accounts add REQUIRE SSL or REQUIRE X509 with CREATE USER or ALTER USER for certificate-bound service accounts. On the client side, the default --ssl-mode=PREFERRED does not verify the server; use VERIFY_IDENTITY with a CA file in application connection strings. Keep port 3306 (and 33060 for the X Protocol, if enabled) behind a firewall, and bind bind_address to a private interface. See the mTLS Guide for certificate-based service authentication.
Password policy, lockout and rotation
MySQL 8.x has a richer native password policy than most databases. The settings below can be applied globally and overridden per account.
| Control | Setting or syntax | Guidance |
|---|---|---|
| Complexity | validate_password component: validate_password.policy (LOW, MEDIUM default, STRONG), validate_password.length (default 8) | Raise length to at least 15 for humans in line with current NIST guidance; length matters more than character classes. validate_password.check_user_name rejects passwords matching the user name. |
| Lockout | FAILED_LOGIN_ATTEMPTS N PASSWORD_LOCK_TIME {N | UNBOUNDED} | Both must be non-zero to take effect; counts consecutive failures only; locked accounts return error 3957. |
| Reuse | password_history, password_reuse_interval, or PASSWORD HISTORY per account | Block the last several passwords for human accounts. |
| Change verification | PASSWORD REQUIRE CURRENT, global password_require_current (default OFF) | Stops a hijacked session from silently changing the password. |
| Expiry | default_password_lifetime (default 0, disabled), PASSWORD EXPIRE INTERVAL N DAY | Prefer event-driven rotation for humans; scheduled rotation for service accounts via a vault. |
| Dual passwords | ALTER USER ... IDENTIFIED BY 'new' RETAIN CURRENT PASSWORD, then DISCARD OLD PASSWORD | Lets every application instance move to the new secret before the old one is removed. |
| Generated secrets | IDENTIFIED BY RANDOM PASSWORD, length from generated_random_password_length (default 20) | Use for service accounts provisioned by automation; the binary log records the hash, not the cleartext. |
Dual passwords are the most useful feature for non-human identities. A rotation job sets the new password with RETAIN CURRENT PASSWORD, updates the secret in the vault, waits for every application instance to reconnect, confirms in the connection logs that the old secret is no longer used, then runs DISCARD OLD PASSWORD. Managing your own secondary password needs APPLICATION_PASSWORD_ADMIN; managing other accounts' passwords needs CREATE USER. See the Secrets Management Guide and the Password Policy Guide (NIST-aligned).
Privileges, roles and partial revokes
MySQL privileges apply at global (*.*), database (db.*), table, column and routine level. Global static privileges such as FILE, PROCESS, RELOAD, SHUTDOWN and SUPER sit alongside dynamic privileges introduced in 8.0 that split SUPER into narrower rights.
| Privilege | Why it is dangerous | Who should hold it |
|---|---|---|
ALL PRIVILEGES ON *.* | Full control of every schema and of other accounts | Named break-glass DBA accounts only. |
FILE | Read and write files on the server host with LOAD_FILE(), LOAD DATA and SELECT ... INTO OUTFILE, within secure_file_priv | Nobody routinely. It turns SQL injection into host file access. |
SUPER (deprecated) | Bypasses many limits, changes global variables, kills any session | Nobody; replace with specific dynamic privileges. |
SYSTEM_USER | Marks an account that only other system users can modify | DBA and automation accounts that must be protected from ordinary admins. |
CREATE USER, GRANT OPTION | Create accounts and pass on privileges; ATT&CK persistence via new accounts | Identity provisioning automation and DBAs. |
CREATE ROUTINE, TRIGGER, EVENT | Persistent code that runs later, possibly with the definer's rights (T1505.001 SQL Stored Procedures) | Migration accounts, not runtime application accounts. |
PROCESS | See all running statements, which may include sensitive values | Monitoring accounts, reviewed. |
- Roles: MySQL 8 supports
CREATE ROLE,GRANT role TO userand default roles viaSET DEFAULT ROLE. Build roles such asapp_rw,app_roandapp_migrateper schema and grant them, rather than granting privileges directly to accounts. Note that granted roles are not active in a session unless set as default or activated, unlessactivate_all_roles_on_loginis ON. - Partial revokes: with
partial_revokes = ON(default OFF) you can grant a global privilege and revoke it for specific schemas, for example letting an operations account modify every schema exceptmysql. It applies to schema-level privileges only, not to global-only privileges such asFILE, and once any account uses it the variable cannot be switched off until those restrictions are removed. In replication it must be enabled on all hosts. - Definers: views, routines, triggers and events default to
SQL SECURITY DEFINER. UseSQL SECURITY INVOKERwhere the caller's rights should apply, and make sure definers are dedicated, locked accounts (ACCOUNT LOCK) so the code keeps working without a usable login.
Server hardening options
| Option | Hardened value | Effect |
|---|---|---|
local_infile | OFF | Disables LOAD DATA LOCAL, which a malicious server or injected statement can abuse to read client files. |
secure_file_priv | A dedicated directory, or NULL to disable | Restricts LOAD DATA, SELECT ... INTO OUTFILE and LOAD_FILE(). Empty means no restriction. |
skip_name_resolve | ON | Host matching by IP only; no reverse DNS trust. |
require_secure_transport | ON | Rejects unencrypted TCP connections. |
skip_show_database | ON on shared servers | Hides database names from accounts without SHOW DATABASES. |
allow-suspicious-udfs | Off (default) | Blocks loading arbitrary libraries as user-defined functions. |
bind_address | Private interface | Keeps the listener off public interfaces. |
sql_mode | Include STRICT_TRANS_TABLES | Rejects silently truncated or invalid data that can undermine application checks. |
Run mysqld as a dedicated, unprivileged OS user, keep the data directory and option files readable only by that user, and never store client passwords in plain ~/.my.cnf files on shared hosts; mysql_config_editor obfuscates rather than encrypts, so treat login path files as secrets too (ATT&CK T1552.001 Credentials In Files). See the Linux Server Hardening Guide for the host layer.
Encryption at rest and backups
MySQL supports InnoDB data-at-rest encryption for file-per-table and general tablespaces, redo and undo logs, and binary and relay log encryption (binlog_encryption), with master keys held by a keyring component. Keep the keyring outside the data directory and outside backup sets, or use a key management service through an Enterprise keyring, so a stolen backup does not carry its own key. Encryption at rest protects files, disks and backups; it does not restrict anyone who logs in with valid credentials. Backups from mysqldump or physical tools are full copies of the data: encrypt them, store them on immutable storage with separate credentials, and restore-test them. The JADEPUFFER agentic ransomware case shows why: an agent holding root MySQL credentials dropped configuration and history tables and whole databases, and inserted a backdoor admin directly into the database. See the Backup Security and Immutable Backup Guide.
Auditing and detection
MySQL Enterprise Audit is included only in the commercial Enterprise Edition. It is implemented as the audit_log plugin, writes new-style XML by default (JSON is selectable through audit_log_format) and supports rule-based filters per account and event class. Community Edition users can rely on the general query log (too verbose and unsafe for production because it records statements with values), the error log for failed logins, Performance Schema tables such as events_statements_history and host_cache, third-party audit plugins, or the audit features of their managed service.
- Authentication: bursts of access-denied errors per account or source (T1110 Brute Force) and any error 3957 lockout.
- Account changes:
CREATE USER,GRANTof global privileges,ALTER USERon root or definer accounts outside change windows (T1078 Valid Accounts persistence). - File and code paths:
SELECT ... INTO OUTFILE,LOAD_FILE(),CREATE FUNCTION ... SONAME, and new events or triggers. - Bulk access and destruction: row counts far above the account's baseline (T1213.006 Databases),
DROP DATABASEand mass deletes (T1485 Data Destruction). - Connections by root over TCP: should never happen if root is restricted to
localhostwithauth_socket.
Forward logs off host so a compromised administrator cannot erase them. See the Logging and Monitoring Policy Template.
Versions, patching and managed services
Oracle's end-of-life notice states that MySQL 8.0 moved to Sustaining Support on 21 April 2026 and recommends upgrading to MySQL 8.4 LTS or 9.7 LTS; innovation releases between LTS versions are short-lived. Sustaining Support means no new security fixes, so any 8.0 instance is now an unpatched asset. Upgrading to 8.4 is also where mysql_native_password accounts surface, because the plugin is disabled by default: inventory them with SELECT user, host FROM mysql.user WHERE plugin = 'mysql_native_password'; and re-key them with ALTER USER ... IDENTIFIED WITH caching_sha2_password BY ... before the upgrade. Track Oracle's quarterly Critical Patch Updates and the CISA KEV catalogue. On managed services the provider patches the engine and withholds true root, but you still own network rules, TLS enforcement, accounts and grants, and log export. See the Patch Management Guide.
Implementation roadmap
- Inventory: list every instance, version, edition,
bind_address, and every account with its host, plugin and global privileges (SELECT * FROM mysql.userreview plusSHOW GRANTS). - Close exposure: remove internet reachability on 3306 and 33060, drop anonymous and
'%'duplicate accounts, restrict root tolocalhost. Snapshot grants withSHOW GRANTSoutput first so any removed grant can be restored in seconds. - Turn on visibility: enable audit (Enterprise Audit, a plugin or managed service audit) in log-only mode and baseline which accounts and privileges are actually used for two weeks.
- Fix authentication: re-key
mysql_native_passwordaccounts, enablerequire_secure_transport, set clientVERIFY_IDENTITY, installvalidate_password, set lockout on human accounts. - Reduce privilege: replace global grants with schema roles, split runtime and migration accounts, remove
FILEandSUPER, lock definer accounts. Roll out one application at a time in pilot rings. - Rotate service credentials: move secrets into a vault and adopt dual-password rotation, or move to IAM tokens on managed services.
- Baseline and drift: apply a CIS MySQL Benchmark profile through configuration management and alert on drift; upgrade any 8.0 instance to an LTS release.
Choosing your approach
For a small team, a managed MySQL service with IAM authentication, enforced TLS, schema roles and the provider's audit log covers most risk with little operational load. Larger estates that self-host should weigh Enterprise Edition for native audit, LDAP or Kerberos authentication and KMS-backed keyrings against Community Edition plus third-party audit and a strong secrets manager. Use certificate-bound accounts (REQUIRE X509) where you already operate a private CA; use vault-issued passwords with dual-password rotation where you do not.
Metrics that show it is working
- Exposure: instances reachable from untrusted networks (target: zero).
- Legacy authentication: accounts still on
mysql_native_password(target: zero before any 9.x upgrade). - Privilege: accounts with global privileges,
FILEorSUPER, trending to named break-glass accounts only. - Encryption: percentage of sessions using TLS, from Performance Schema
status_by_threador connection logs. - Rotation: median age of service account passwords.
- Patch currency: instances on a supported LTS and latest patch release.
Framework mapping
| Control area | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | CIS Controls v8.1 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|---|
| Accounts, roles and grants | AC-2, AC-6 | PR.AA-05 | 5.4, 6.8 | A.5.15, A.8.2 |
| Authentication plugins, password policy, lockout | IA-5 | PR.AA-05 | 4.7, 5.2 | A.8.5 |
| TLS, tablespace and backup encryption | SC-8, SC-28, SC-12 | PR.DS-02, PR.DS-01 | 3.10, 3.11 | A.8.24 |
| Server options baseline | CM-6, CM-7 | PR.PS-01 | 4.1 | A.8.9 |
| Audit logging | AU-2, AU-12 | PR.PS-04, DE.CM-09 | 8.2 | A.8.15 |
| Patching and version support | SI-2, RA-5 | ID.RA-01 | 7.3 | A.8.8 |
Relevant ATT&CK techniques: T1190 Exploit Public-Facing Application, T1078.001 Default Accounts, T1110 Brute Force, T1505.001 SQL Stored Procedures, T1213.006 Databases, T1485 Data Destruction and T1486 Data Encrypted for Impact.
Common failure patterns
- Root from anywhere:
'root'@'%'with a reused password, often created by a container image environment variable and never removed (T1078.001 Default Accounts). - Application with ALL ON *.*: one account for the application, migrations and humans, so a SQL injection can read every schema and, with
FILE, write web shells throughINTO OUTFILE. See the SQL Injection Prevention Guide. - Unlimited secure_file_priv: an empty value left in place after a data load, removing the only directory restriction on file functions.
- TLS on the server, PREFERRED on clients: encrypted in theory, but no client checks the server certificate.
- Stuck on 8.0: instances past their support date because
mysql_native_passwordclients were never upgraded. - Single-copy backups: dumps on the same host or bucket the database credentials can delete, as in the JADEPUFFER destruction above.
Practitioner checklist
- No anonymous accounts, no
'root'@'%', root restricted tolocalhostwithauth_socketor a vaulted password. - No accounts on
mysql_native_password; all oncaching_sha2_password, certificates or IAM tokens. require_secure_transport = ONand client connection strings useVERIFY_IDENTITY.- Application accounts hold schema roles only, with no global privileges,
FILE,SUPERorGRANT OPTION, and a specific host pattern. local_infile = OFF,secure_file_privset to a dedicated directory orNULL,skip_name_resolve = ON.validate_passwordinstalled andFAILED_LOGIN_ATTEMPTSwithPASSWORD_LOCK_TIMEset on human accounts.- Service secrets in a vault with dual-password rotation and an owner recorded per account.
- Audit logging enabled for account, privilege and DDL events, shipped off host with alerts.
- Tablespace, binary log and backup encryption on, with keyring material stored apart from backups; restores tested quarterly.
- Every instance on a supported LTS release with the latest Critical Patch Update.
Standards and references
- MySQL: General Security Guidelines (8.4 Reference Manual)
- MySQL: Password Management
- MySQL: Native Pluggable Authentication
- MySQL: Encrypted Connection TLS Protocols and Ciphers
- MySQL: Privilege Restriction Using Partial Revokes
- MySQL: Security-Related mysqld Options and Variables
- MySQL: MySQL Enterprise Audit
- Oracle: MySQL Product Support EOL Announcements
Related NHI Mgmt Group resources: Database Security Guide · Database Credential and Access Security Guide · PostgreSQL Security Guide · Privileged Access Management Guide · Secrets Management Guide