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

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.
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.
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
Free resources
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.
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.
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.

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.
Win a $2,495 Black Hat pass.
Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.
