Secure MySQL with cnspec
Scan MySQL, MariaDB, and Percona servers against security and compliance best practices with cnspec.
Scan your MySQL-compatible servers to find security risks before they become incidents. cnspec evaluates accounts and their host patterns, password state, global and schema privileges, roles, authentication plugins, transport encryption, stored routines, tablespace encryption, and configuration variables.
cnspec connects with a MySQL account and inventories the server through read-only queries against information_schema, performance_schema, and the system catalogs. It never touches your data.
MySQL, MariaDB, and Percona Server are all supported by one provider. The flavor is detected automatically and reported as mysqldb.instance.flavor.
If you're new to cnspec, start with the Quickstart. For an overview of every database cnspec can scan, see the database scanning overview.
Prerequisites
To scan a MySQL-compatible server with cnspec, you must have:
- cnspec installed on your workstation
- Network access to the server on its port (3306 by default)
- An account that can read
mysql.user,information_schema, andperformance_schema
Authenticate
cnspec authenticates with a MySQL account and password, and negotiates TLS according to --tls-mode:
cnspec shell mysqldb db.contoso.com --user root --ask-passFor a verified-TLS connection, add the certificate flags:
cnspec shell mysqldb db.contoso.com --user auditor --ask-pass --tls-mode true --tls-ca ca.pemPrefer a least-privileged account for auditing. Read access to mysql.user, information_schema, and performance_schema covers everything below. Without it those collections return empty rather than failing the scan, so confirm mysqldb.instance.users is populated before trusting a passing account check.
Connection options
| Option | Description |
|---|---|
--host | Server hostname or IP address (also accepted as the positional argument) |
--port | Server port (default 3306) |
--user, -u | Account to authenticate as |
--password, -p | Password, or --ask-pass to be prompted |
--database | Optional default schema for the connection |
--tls-mode | false, skip-verify, preferred (default), or true |
--tls-ca, --tls-cert, --tls-key | CA and client-certificate material for verified or mutual TLS |
--discover | auto (default), all, databases, or none |
Verify with a quick MySQL check
cnspec shell mysqldb db.contoso.com --user root --ask-passcnspec> mysqldb.instance { version flavor requireSecureTransport localInfile }
mysqldb.instance: {
version: "11.8.8-MariaDB-ubu2404"
flavor: "mariadb"
requireSecureTransport: false
localInfile: true
}If cnspec connects and shows the version, you're ready to scan.
Scan MySQL
By default cnspec discovers each schema as its own mysqldb-database asset alongside the server asset:
cnspec scan mysqldb db.contoso.com --user auditor --ask-passTo scan the server only:
cnspec scan mysqldb db.contoso.com --user auditor --ask-pass --discover noneWhen a scan completes, cnspec prints a summary of all the checks it ran, grouped by policy, along with a risk score from 0 (no risk) to 100 (highest risk). Failed checks include remediation guidance to help you fix issues. To learn more about reading scan results, read Understand cnspec Results.
Mondoo ships Mondoo MySQL Security and Mondoo MariaDB Security policies, but those assess the server's on-disk option files through the os provider, not a live connection. They can't see accounts, granted privileges, or the effective value of a dynamic variable, which is exactly what this provider reads. Run both: the option-file policy tells you what the server loads at startup, and the checks below tell you what the running server actually has in effect.
For the live connection there's no out-of-the-box policy yet. The resources are shaped around the CIS MySQL Benchmark, so the checks below map closely to its recommendations. Use them as a starting point and create your own policies.
Explore and test checks interactively
cnspec shell mysqldb db.contoso.com --user auditor --ask-passList accounts
cnspec> mysqldb.instance.users { user host authPlugin hasPassword accountLocked }Read a configuration variable
cnspec> mysqldb.instance.variables.where(name == "log_bin") { name value }List global privileges held by an account
cnspec> mysqldb.instance.users.where(user == "appuser").first.privileges.where(scope == "GLOBAL") { privilegeType scope }List installed plugins and components
cnspec> mysqldb.instance.plugins { name type status }
cnspec> mysqldb.instance.components { name urn }List stored routines in a schema
cnspec> mysqldb.instance.schemas.where(name == "appdb").first.routines { name type securityType definer }Example security checks
Ensure there are no anonymous accounts
An anonymous account has an empty user name and matches any name the client offers:
cnspec> mysqldb.instance.users.none(isAnonymous == true)
[ok] value: trueEnsure no account is reachable from any host
cnspec> mysqldb.instance.users.none(isWildcardHost == true)
[ok] value: trueEnsure every account has a password
cnspec> mysqldb.instance.users.where(accountLocked == false).all(hasPassword == true)
[ok] value: trueEnsure the server requires encrypted connections
cnspec> mysqldb.instance.requireSecureTransport == true
[ok] value: trueEnsure local_infile is disabled
local_infile lets a client instruct the server to read files from the client host, which turns a compromised server into a file-exfiltration tool:
cnspec> mysqldb.instance.localInfile == false
[ok] value: trueEnsure file access is confined to a directory
An empty secure_file_priv allows LOAD DATA and SELECT ... INTO OUTFILE anywhere the server user can reach:
cnspec> mysqldb.instance.secureFilePriv != ""
[ok] value: trueEnsure no account holds FILE
FILE reads and writes files on the server host as the server's OS user:
cnspec> mysqldb.instance.users.all(privileges.none(privilegeType == "FILE"))
[ok] value: trueEnsure SUPER is limited
cnspec> mysqldb.instance.users.where(privileges.any(privilegeType == "SUPER")) { user host }Ensure a password-validation component is installed
cnspec> mysqldb.instance.components.any(name == "component_validate_password") || mysqldb.instance.plugins.any(name == "validate_password" && status == "ACTIVE")
[ok] value: trueEnsure passwords expire
A passwordLifetime of -1 means the account falls back to the server default, so check the default too:
cnspec> mysqldb.instance.variables.where(name == "default_password_lifetime").all(value != "0")
[ok] value: trueEnsure replication channels verify the source certificate
A channel that allows TLS but skips certificate verification is open to an interposed source:
cnspec> mysqldb.instance.replicationChannels.all(sslAllowed == true && sslVerifyServerCert == true)
[ok] value: trueReview SECURITY DEFINER routines
A DEFINER routine runs as its definer, so one defined by a privileged account is a privilege-escalation surface:
cnspec> mysqldb.instance.schemas { name routines.where(securityType == "DEFINER") { name type definer } }Review unencrypted tables
cnspec> mysqldb.instance.schemas { name tables.where(encrypted == false) { name engine } }Learn more
- MySQL Resource Pack Reference: every MySQL resource and field cnspec can query
- Write Effective MQL: guide to authoring checks and queries