What this check reads
Queries the running server over the Unix socket as root and reads three variables: @@require_secure_transport, have_ssl (falling back to have_openssl when the first query returns nothing) and @@skip_networking. The branches are evaluated in that order. require_secure_transport reading 1 or ON is a PASS, and so is skip_networking reading 1 or ON, because a server with TCP switched off has no network traffic left to encrypt. If neither is set, have_ssl reading exactly YES means TLS exists but is optional and gives a WARN; anything else, an empty value included, is a FAIL that says TLS is not available.
When it applies
Runs only when MySQL/MariaDB is detected (HAS_MY=1) and reachable as root over the socket (MYOK=1); otherwise SKIP. It reads live server variables, so it reflects the running configuration. It judges only whether TLS is available and required for the server as a whole - per-account requirements (CREATE USER … REQUIRE SSL) are not examined, so a server can FAIL or WARN here while every account is individually forced onto TLS. skip_networking short-circuits to PASS because socket-only access makes network TLS irrelevant.
Known accuracy issue: the FAIL branch depends on have_ssl/have_openssl, which were deprecated in MySQL 8.0.26 and removed in MySQL 8.4.0. On MySQL 8.4 and 9.x both queries return nothing, so the check reports FAIL "TLS not available" even when TLS is working. This is recorded in the README's known-issues list.
What each result means
| Result | When | What it means |
|---|---|---|
| PASS | `require_secure_transport=ON` | The server refuses plaintext TCP connections, so every network client is already on TLS. |
| PASS | `skip_networking` on | The server accepts no TCP connections at all, so there is no wire traffic left to encrypt. |
| WARN | TLS available but optional | TLS works but nothing insists on it, so a client that asks for plaintext still gets plaintext. |
| FAIL | `have_ssl` not `YES` | The server reports no usable TLS, so clients authenticate and query in clear text - but confirm this by hand on MySQL 8.4 and later, where the variable this branch reads no longer exists. |
| SKIP | Not accessible | Nothing was verified: MySQL was either not detected or not reachable as root, so its transport is unknown rather than safe. |
Why it matters
Without TLS, MySQL credentials (mysql_native_password challenge/response is replayable offline; caching_sha2_password falls back to RSA or sends plaintext over non-TLS in some paths) and every query result cross the network in clear. require_secure_transport refuses non-TLS TCP connections while still allowing the Unix socket. OWASP: only allow encrypted connections. CIS MySQL "Ensure 'require_secure_transport' is set to 'ON'". MySQL auto-generates a self-signed CA and server cert at first start (8.0+), so TLS is normally available but not required.
Why it fails, and when it is wrong
- False FAIL on MySQL 8.4 and 9.x.
have_sslandhave_opensslwere removed in 8.4.0 (deprecated since 8.0.26). The query returns no row,HSis empty, and the check reports "TLS not available". Verify withSHOW VARIABLES LIKE 'tls_version';(non-empty means TLS is on) orSHOW STATUS LIKE 'Ssl_server_not_after';. MariaDB still hashave_ssl. - MariaDB with TLS compiled but not configured reports
have_ssl=DISABLED→ FAIL is correct: setssl_cert/ssl_key/ssl_ca. - Local-only deployments (
bind-address=127.0.0.1) get WARN even though TLS on loopback adds little; the check only passes loopback viaskip_networking. - Enforcing TLS breaks clients that connect with
--ssl-mode=DISABLEDor old drivers. Test withmysql --ssl-mode=REQUIREDfirst and checkSELECT * FROM performance_schema.session_status WHERE variable_name='Ssl_cipher';from the app.
How to fix it
Check what the server can do today
Read the current state before you change anything. require_secure_transport reading OFF means plaintext TCP connections are still accepted, and TLS being available is not the same as TLS being required: most client libraries take plaintext whenever the server allows it. On MySQL 8.4 and later have_ssl and have_openssl no longer exist, so read tls_version instead to tell whether TLS is available at all. The commands are in Verify the fix below.
Configure the certificates
Point the server at a certificate, key and CA under the [mysqld] section, and pin the protocol versions while you are there. The key must be mode 0600 and owned by the mysql user, or the server will not load it.
[mysqld]
ssl_ca = /etc/mysql/ssl/ca.pem
ssl_cert = /etc/mysql/ssl/server-cert.pem
ssl_key = /etc/mysql/ssl/server-key.pem
tls_version = TLSv1.2,TLSv1.3Turn on enforcement
Add the enforcement switch to the same section:
[mysqld]
require_secure_transport = ONOr at runtime: SET PERSIST require_secure_transport=ON; (MySQL 8), which applies to the running server and survives a restart, so no restart is needed to make it take effect. Per-account: ALTER USER 'app'@'10.0.0.5' REQUIRE SSL;.
Update the clients before you restart, not after
From the moment enforcement is on, every TCP client that does not negotiate TLS is refused with ERROR 3159. Clients on the Unix socket are unaffected. Change the application connection settings first, test each client with --ssl-mode=REQUIRED, confirm the CA bundle those clients use is right, and only then flip enforcement on.
Verify the fix
# Modern, reliable substitutes for the removed have_ssl variable:
sudo mysql -N -e "SHOW VARIABLES LIKE 'tls_version'" # non-empty means TLS is available
sudo mysql -N -e "SELECT @@ssl_cert, @@require_secure_transport"
# Prove an unencrypted connection is actually refused:
mysql --ssl-mode=DISABLED -u app -p -h 127.0.0.1 -e 'SELECT 1'
# expected: ERROR 3159 (Connections using insecure transport are prohibited)
# And that an encrypted one works, showing the negotiated cipher:
mysql --ssl-mode=REQUIRED -u app -p -h 127.0.0.1 -e "SHOW STATUS LIKE 'Ssl_cipher'"
# expected: a cipher name, not an empty value
# Per-account requirements, which this check does not look at:
sudo mysql -N -e "SELECT user, host, ssl_type FROM mysql.user WHERE ssl_type <> ''"Debugging
Almost certainly the known false positive above. Confirm with SHOW VARIABLES LIKE 'tls_version'; if it returns a list of protocols, TLS is available and the finding is wrong. Record it as a known script issue; there is no configuration change that clears it.
MySQL 8 auto-generates self-signed certificates at first start, so have_ssl=DISABLED usually means the auto-generated files were removed or ssl=0 was set. Check @@ssl_cert, @@ssl_key, and the error log at startup.
MYOK=0; see the Authentication debugging notes for socket and sudo causes.
The check tests the global switch only. Per-account REQUIRE SSL (visible in the ssl_type query above) is a valid alternative control; the clean fix that also clears the finding is require_secure_transport=ON.
Clients connecting over the Unix socket are unaffected, but any TCP client without TLS support or with a certificate-verification failure now fails. Test each client with --ssl-mode=REQUIRED first, and check the CA bundle those clients use.
Sources
- MySQL 8.4: Using Encrypted Connections
- MySQL 8.4: require_secure_transport
- MySQL 8.4: Configuring MySQL to Use Encrypted Connections (tls_version, ssl_* vars)
- MySQL 8.4: Variables removed in 8.4 (
have_ssl,have_openssl) - MySQL 8.4 release notes 8.4.0 (removal note)
- MariaDB KB: Securing Connections for Client and Server
- MariaDB KB: require_secure_transport
- OWASP Transport Layer Security Cheat Sheet
How the script reads this
Next
Re-run the Transport Encryption audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 5 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.
All 5 Transport Encryption fixes