PRACTITIONER GUIDE
Practitioner Guide11 min read

MySQL Security Hardening: CIS Benchmark Controls, Least Privilege, and Audit Logging

127.0.0.1
is the bind-address value that restricts MySQL to localhost only, eliminating all remote network access to port 3306.
Level 1+2
CIS MySQL Benchmark tiers cover network binding, account hygiene, password policy, and audit logging controls for production databases.
12 chars
minimum password length recommended by validate_password plugin configuration for MySQL production user accounts.
90 days
standard password expiration interval for DBA accounts; application service accounts should use long random secrets instead.

SponsoredRetool

Retool's new app builder is where AI-generated code ships safely

Building apps with AI is easy. Getting them to production safely is another story.

Start building for free today

MySQL's default configuration prioritizes ease of initial setup over security — which is appropriate for development but inappropriate for production. A production MySQL server needs network binding, authentication enforcement, audit logging, and least-privilege application accounts that the default install does not configure. The CIS MySQL Benchmark provides a comprehensive control list; this guide covers the controls with the highest security impact and the implementation specifics that the benchmark abstracts.

The hardening sequence matters: bind the network interface before creating application accounts (to confirm the correct host restriction), install the validate_password plugin before creating application passwords (so the policy is enforced from creation), and configure audit logging before auditors ask for it.

Network and authentication hardening: the highest-priority controls

Network binding and authentication controls prevent the class of vulnerability that has exposed tens of thousands of publicly accessible databases: remote unauthenticated access caused by a combination of wildcard bind-address, anonymous accounts, and no password policy. Setting bind-address to 127.0.0.1 or a private IP, removing anonymous and test accounts via mysql_secure_installation, and enforcing password complexity through the validate_password plugin each eliminate a distinct risk class. These three changes take under 30 minutes to implement and should be applied before any application connects to the database. The verification steps after each change confirm the control is effective and provide evidence for compliance audits.

Harden the bind-address and verify no anonymous or root network accounts exist

After setting bind-address in my.cnf and restarting MySQL: verify the network exposure with netstat -tlnp | grep 3306 — confirm MySQL is only listening on 127.0.0.1 or your private IP, not on 0.0.0.0 or ::. Then verify user accounts: run the user audit queries from the FAQ to confirm no anonymous accounts, no root with wildcard host, and no empty passwords. These three verifications confirm the network and authentication hardening is effective. For cloud-deployed MySQL (EC2, GCP VM, Azure VM with external IP): even if MySQL binds to a private IP, confirm the cloud security group/firewall rule blocks port 3306 from internet sources — network binding is the first line of defense but should be combined with network-layer firewall controls.

Application account design for long-running services

Application user accounts are the most frequently misconfigured MySQL security control: teams create a single highly privileged account shared across multiple applications and forget to restrict it by host or database scope. The correct design gives each application its own dedicated MySQL user scoped to the exact databases and DML operations it requires, with a host restriction limiting connections to the application server's IP range. This scoping limits the blast radius if application credentials are compromised: an attacker with the credentials can only reach the databases that application already accesses, and cannot drop tables, read system databases, or grant permissions to other accounts. Testing the account from the application server before go-live confirms that the GRANT statement is correct and that no additional permissions were accidentally omitted.

Create and test application accounts before deploying to production

Create the application user with the least-privilege GRANT statement, then test it from the application server before the application goes to production: mysql -u appuser -p -h DB_IP specific_database -e 'SELECT 1;' should succeed. mysql -u appuser -p -h DB_IP mysql -e 'SELECT User FROM user;' should fail with 'Access denied' — the application user should not be able to query the MySQL system database. mysql -u appuser -p -h DB_IP specific_database -e 'DROP TABLE sensitive_table;' should fail — the application user should not have DROP privilege. This three-test sequence confirms: the account works for normal operations, cannot access system databases, and cannot perform destructive operations. Document the test results and include them in the application deployment checklist.

Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

The bottom line

MySQL security hardening is a sequence of configuration changes that each eliminate a specific class of risk: network binding eliminates remote access risk, anonymous account removal eliminates unauthenticated local access, password policy enforcement eliminates weak credential risk, least-privilege application accounts limit the blast radius of application compromise, and audit logging creates the forensic record that makes security incident investigations possible. The CIS MySQL Benchmark covers all of these controls and more. Start with the network binding and authentication controls on day one, add application account least-privilege before the application connects to production, and configure audit logging before the next compliance audit. MariaDB users: most of these controls apply equally to MariaDB, which includes a built-in audit plugin that simplifies the logging configuration.

Frequently asked questions

What are the first MySQL security hardening steps for a freshly installed server?

First-day MySQL security hardening sequence: (1) Run mysql_secure_installation: this interactive script prompts you to set the root password, remove anonymous users, disallow root login remotely, and remove the test database. Run it before any application connects. (2) Set bind-address in /etc/mysql/mysql.conf.d/mysqld.cnf (Ubuntu) or /etc/my.cnf (RHEL): bind-address = 127.0.0.1 for local-only access, or bind-address = 10.0.1.5 (your private IP) for network access restricted to your private network. Restart MySQL after this change: systemctl restart mysql. (3) Verify no anonymous accounts remain: SELECT User, Host FROM mysql.user WHERE User=''; should return no rows. (4) Verify root is not accessible remotely: SELECT User, Host FROM mysql.user WHERE User='root'; should show only 'localhost' and '127.0.0.1' hosts, not '%' (any host). If 'root'@'%' exists: DROP USER 'root'@'%'; FLUSH PRIVILEGES;. (5) Install and configure the validate_password plugin (see dedicated FAQ). (6) Enable the general log temporarily for 24 hours to review all queries and confirm no unexpected access is occurring: SET GLOBAL general_log = 'ON'; SET GLOBAL general_log_file='/var/log/mysql/general.log';. Review and then disable.

How do I configure MySQL user accounts with least privilege for each application?

MySQL least-privilege user account design: Each application should connect to MySQL with a dedicated user account that has only the permissions required for that application's normal operation. Design principles: (1) One user per application: do not share MySQL credentials between applications. (2) Restrict to specific databases: GRANT SELECT, INSERT, UPDATE, DELETE ON specific_database.* TO 'appuser'@'10.0.1.%' IDENTIFIED BY 'strong-password'; — this grants only DML permissions on one specific database from a specific source IP range. (3) Use specific host restrictions: 'appuser'@'10.0.1.%' is more restrictive than 'appuser'@'%' (any host). (4) Do not grant GRANT OPTION: GRANT OPTION allows the user to grant their permissions to other users — application accounts should not have this. (5) Do not grant administrative privileges to application accounts: CREATE USER, DROP, ALTER, PROCESS, FILE, SUPER are for DBA accounts, not application accounts. (6) Create separate read-only accounts for reporting: if your application has a reporting module that only reads data, create a read-only account: GRANT SELECT ON specific_database.* TO 'appuser_ro'@'10.0.1.%' IDENTIFIED BY 'separate-password';. Verify the permissions after grant: SHOW GRANTS FOR 'appuser'@'10.0.1.%';

How do I enable and configure MySQL audit logging?

MySQL audit logging options: (1) MySQL Enterprise Audit Plugin (requires MySQL Enterprise license): INSTALL PLUGIN audit_log SONAME 'audit_log.so'; Configure in my.cnf: [mysqld] audit_log_policy=ALL (or LOGINS for authentication-only logging), audit_log_format=JSON, audit_log_file=/var/log/mysql/audit.log. The plugin logs: login success/failure, DDL operations (CREATE, ALTER, DROP), DML operations (configurable), connection events. (2) For MySQL Community Edition (open source): use McAfee MySQL Audit Plugin (open source, installable on Community Edition): download from github.com/mcafee/mysql-audit, install following the plugin install procedure. Alternative: use MariaDB's built-in audit plugin if using MariaDB (INSTALL PLUGIN server_audit SONAME 'server_audit.so'). (3) Log review: audit log files grow rapidly in production. Set up log rotation: logrotate configuration for /var/log/mysql/audit.log with daily rotation and 90-day retention. (4) SIEM ingestion: forward audit logs to your SIEM using a file-based log shipper (Filebeat, Fluentd, or rsyslog). Configure alerts for: multiple failed login attempts from the same user/host, schema changes outside change management windows, SELECT from sensitive tables by non-application accounts.

How do I enforce password policies for MySQL user accounts?

MySQL password policy configuration: (1) Install the validate_password plugin: INSTALL PLUGIN validate_password SONAME 'validate_password.so'; (MySQL 5.7). In MySQL 8.0, the component replaces the plugin: INSTALL COMPONENT 'file://component_validate_password'; (2) Configure in my.cnf under [mysqld]: validate_password.policy=MEDIUM (requires mixed case, digit, special character), validate_password.length=12 (minimum length), validate_password.mixed_case_count=1, validate_password.number_count=1, validate_password.special_char_count=1. Restart MySQL after adding to my.cnf. (3) Verify the policy is applied: SHOW VARIABLES LIKE 'validate_password%'; should show your configured values. (4) Set account lockout for failed attempts (MySQL 8.0.19+): ALTER USER 'appuser'@'10.0.1.%' FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1 (locks the account for 1 day after 5 consecutive failures). (5) Set password expiration: ALTER USER 'appuser'@'10.0.1.%' PASSWORD EXPIRE INTERVAL 90 DAY; or set the default globally: default_password_lifetime=90 in my.cnf. Application accounts typically should have password expiration disabled (for stability) and should instead rely on strong initial passwords stored in a secrets manager.

How do I configure SSL/TLS encryption for MySQL client connections?

MySQL SSL/TLS configuration: (1) Generate or obtain certificates: MySQL can use self-signed certificates (acceptable for transit encryption between application and database on a private network) or CA-signed certificates. Generate with MySQL's built-in tool: mysql_ssl_rsa_setup --datadir=/var/lib/mysql. This creates ca.pem, server-cert.pem, server-key.pem, client-cert.pem, client-key.pem in the data directory. (2) Configure MySQL server in my.cnf: [mysqld] ssl-ca=/var/lib/mysql/ca.pem, ssl-cert=/var/lib/mysql/server-cert.pem, ssl-key=/var/lib/mysql/server-key.pem. (3) Require SSL for specific users: ALTER USER 'appuser'@'10.0.1.%' REQUIRE SSL; — this prevents the application from connecting without SSL even if it is misconfigured to skip SSL. (4) Require SSL for all users by default: require_secure_transport=ON in my.cnf (MySQL 5.7.8+). (5) Verify SSL is in use for a connection: STATUS; in the MySQL shell shows 'SSL: Cipher in use' for SSL connections. (6) For AWS RDS MySQL: SSL is available by default using the AWS RDS CA certificate — configure your application to use the RDS CA bundle (available from the AWS documentation) and enable require_secure_transport if all applications support SSL.

What my.cnf settings should I include in the MySQL security configuration?

Security-relevant my.cnf settings under [mysqld]: local_infile=0: disables the LOAD DATA LOCAL INFILE command, which can be used to read arbitrary files from the client host. log_error=/var/log/mysql/error.log: ensures error logging is to a file, not stdout. skip-symbolic-links: prevents use of symbolic links to database files, which can be used to access files outside the database directory. secure_file_priv=/var/lib/mysql-files: restricts file import/export operations to a specific directory, preventing MySQL from reading or writing to arbitrary file system paths. skip_name_resolve: prevents MySQL from resolving hostnames for connection validation (uses only IP addresses), which avoids DNS-based attacks and improves connection performance. max_connect_errors=10: locks out hosts after 10 consecutive connection errors. log_bin_trust_function_creators=0: prevents non-SUPER users from creating stored functions that could be used to bypass security controls. event_scheduler=OFF: disables the MySQL event scheduler unless you explicitly need it (reduces attack surface). After making changes, validate with: mysqladmin variables | grep -E 'local_infile|secure_file|skip_name'.

How do I audit existing MySQL user accounts and permissions for over-privilege?

MySQL user permission audit: (1) List all users and their host restrictions: SELECT User, Host, authentication_string, account_locked FROM mysql.user ORDER BY User; — review for: anonymous users (User=''), wildcard hosts (Host='%'), unused accounts. (2) Review permissions for each user: SELECT * FROM information_schema.USER_PRIVILEGES; and SELECT * FROM information_schema.SCHEMA_PRIVILEGES; — look for: GRANT OPTION on any user (ability to grant privileges to others), ALL PRIVILEGES grants (overly broad), SUPER privilege on non-DBA accounts, FILE privilege on application accounts. (3) Check for accounts with no password: SELECT User, Host FROM mysql.user WHERE authentication_string=''; — empty passwords are unacceptable for any production account. (4) Check for root accounts with network access: SELECT User, Host FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost','127.0.0.1','::1'); — any root account with a network-accessible host is a critical finding. (5) Identify unused accounts: compare the user list against application connection string configurations and remove any account that no longer has a corresponding application. Stale accounts are access control risk.

Sources & references

  1. CIS Benchmark for MySQL Community Server 8.0
  2. MySQL 8.0 Security Guide
  3. MySQL Enterprise Audit Plugin
  4. MySQL SSL/TLS Configuration

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.