Skip to content
ssldays

Guide

MySQL SSL certificate expired

MySQL clients fail VERIFY_IDENTITY when the server TLS cert is past notAfter. Probe mysqld on 3306 externally, renew server.pem, and reload without confusing it with user client certs.

Updated May 9, 2026 851 words

ERROR 2026 (HY000): SSL connection error: SSL certificate verify failed often means mysqld is still presenting an expired server certificate—not that your app forgot --ssl-ca.

DBAs and application owners see mysql ssl certificate expired when JDBC, PHP PDO, or the mysql CLI enforces VERIFY_IDENTITY or ssl_mode=VERIFY_CA while the leaf handed out during the handshake is already past notAfter. The failure surfaces in connection pools and migration jobs long before anyone opens the marketing site in a browser.

MySQL terminates TLS on the database port itself in many deployments, so a renewed certificate sitting in /etc/letsencrypt for nginx does nothing until the same material is wired into ssl_cert / ssl_cert_file and mysqld reloads. Prove expiry on the live listener before touching application code.

First response

Capture the exact client error (ssl_rsa_certificate_expired, OpenSSL verify messages, or ORM stack traces), the hostname the DSN uses (not always the Kubernetes service name), and whether TLS is mandatory via require_secure_transport. From a bastion or laptop outside the VPC, open a TLS session to the same host and port the application uses.

Record serial numbers and notAfter from the failing handshake. Note if traffic passes through ProxySQL, MaxScale, or a cloud RDS endpoint—those layers have their own certificates. Escalations should attach openssl s_client output and SHOW VARIABLES LIKE '%ssl%'; excerpts rather than screenshots of a successful login from an admin account with verification disabled.

See also curl: SSL certificate problem: certificate has expired and How to fix an expired SSL certificate.

What this error means

When require_secure_transport is on, every TCP connection upgrades to TLS. The server sends its X.509 leaf during the handshake; clients that verify trust and dates reject the chain the moment notAfter is in the past. MySQL user SSL client certificates are separate: an expired server cert breaks every client even when mutual TLS user certs are still valid.

Cloud RDS and Aurora rotate Amazon-issued server identities automatically; self-managed mysqld on VMs or in Kubernetes does not. Operators sometimes enable ssl_mode=DISABLED temporarily, which masks expiry until a compliance scan or a stricter driver version turns verification back on.

In PKIX terms, verification happens before authentication: expired material never reaches username negotiation. Name mismatches between the DSN host and certificate SAN produce different errors—confirm expiry first with dated proof from the wire.

Common causes

Renewal completed on a bastion host while production mysqld still references the previous server-cert.pem path. Orchestration updated a Kubernetes Secret but the StatefulSet was not rolled, so the pod kept the old mount. Managed certificates on an ingress were refreshed while applications still connect directly to db.internal:3306 on mysqld’s own cert.

A fullchain file missing intermediates can look like “untrusted” in some clients and “expired” in others if cross-signed roots were retired—still, many incidents are plain past notAfter. Certificate files owned by mysql with mode 0644 were replaced by automation running as root without chown mysql:mysql, so mysqld never read the new bytes and continued serving the in-memory or previous file.

Galera or replication does not replicate TLS files; each node needs the renewed server cert installed and a reload (mysqladmin reload or restart) on every member.

Operator confusion often mixes Aurora global endpoints with regional reader endpoints—each DNS name may present different Amazon-issued validity windows, but self-managed RDS replicas you administer yourself still follow file-based renewal rules. Vitess, Citus routers, and Vitess gateways can terminate SQL while speaking another TLS profile to backends; a green check on the gate does not prove mysqld’s own port is fresh. Compliance scans that only inspect https:// admin panels miss 3306 entirely until JDBC enforces verify mode in production.

Inventory hygiene matters: the same organisation may run MySQL 5.7 with have_openssl and MySQL 8.4 with mysql_native_password disabled, yet share one forgotten server-cert.pem on a shared NFS mount. When that file expires, every consumer breaks simultaneously. Document which team owns rotation (DBA vs platform vs security PKI) and which automation account writes into datadir.

Confirm from the outside

Probe the same address your application DSN resolves to, including read replicas and connection-router ports.

openssl s_client -connect db.example.com:3306 -starttls mysql </dev/null 2>/dev/null | openssl x509 -noout -dates -subject -issuer

If mysqld only listens on a private IP, run the same command from a subnet peer or through your approved jump host—corporate VPN split tunnels are a common false negative.

mysql -h db.example.com -u monitor -p --ssl-mode=VERIFY_IDENTITY -e "SELECT 1"

On the server, confirm configured paths match deployed files:

SHOW VARIABLES LIKE 'have_ssl';
SHOW VARIABLES LIKE 'ssl_%';

Compare openssl x509 -in /var/lib/mysql/server-cert.pem -noout -dates with what clients receive; drift means the wrong file is bound or reload did not occur.

Java applications surface com.mysql.cj.exceptions.SSLParamsException or unable to find valid certification path—grep CI logs for those strings and map them to the same host/port you openssl. PHP PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT and Ruby mysql2 with sslca behave similarly; one expired leaf fails every language runtime using strict mode.

When Cloud SQL Auth Proxy or IAM authentication is in play, clients may connect through a local proxy port while the cloud control plane manages upstream TLS. Still validate what the proxy negotiates when VERIFY_IDENTITY is enabled against the cloud hostname documented for your project.

How to read the output

Past notAfter= on the leaf guarantees failure for verifying clients regardless of issuer brand. subject= and SAN entries must include the hostname in the JDBC URL when verification is strict. Note whether the issuer is a public CA, an internal PKI, or Amazon RDS—renewal workflow differs for each.

Fix checklist

  1. Identify every endpoint apps use (primary, replicas, ProxySQL, tunnel ports).
  2. OpenSSL each endpoint with -starttls mysql and archive expired serials.
  3. Issue or download a renewed server certificate covering required DNS names.
  4. Install leaf plus chain where ssl_cert and ssl_ca point; fix ownership and permissions.
  5. Run mysqladmin reload or restart mysqld on each cluster member.
  6. Re-run the failing client command with VERIFY_IDENTITY.
  7. Re-enable require_secure_transport if it was relaxed during triage.
  8. Add live TLS monitoring on db.example.com:3306 (not only HTTPS front doors).

Procurement should list every DNS name embedded in JDBC URLs, Helm secrets, and bastion SSH tunnels. Internal CAs need renewed server cert plus distributing updated ca.pem to Terraform-managed clients before cutover. For Let’s Encrypt, confirm DNS-01 or HTTP-01 challenges use names clients resolve, not admin-only mysql-admin.corp labels absent from SANs.

After installation, compare serial numbers from external openssl s_client with SELECT 1 through ORM integration tests in CI. Fail the pipeline when dates slip inside your policy window. Only then re-enable any relaxed ssl_mode used during the bridge. Post-mortems should note whether connection poolers masked expiry for apps using non-verifying modes while analytics jobs with stricter settings failed first.

When to use automation

Database uptime checks that only run SELECT 1 with TLS verification disabled lie about certificate health. Automate live TLS handshakes against the hostname and port in production DSNs, including replicas. Pair certificate notAfter with RDAP on any registrar-facing names used in validation workflows.

Run a one-shot proof on /check, then schedule alerts at 30, 14, 7, and 1 day thresholds before expiry so on-call has time for failover, change windows, and replication-aware reloads. Email alerts are available on all plans; Slack and Discord webhooks require Starter or Pro.

Free tier monitoring covers three hostnames with daily checks—enough for primary plus two replicas if you choose carefully. Starter hourly checks help busy OLTP clusters where weekend expiry would page before Monday business hours. Pair database TLS watches with check SSL expiry from PowerShell or OpenSSL check certificate expiry date runbooks your DBAs already use, so alerts and manual playbooks cite the same hostnames.

Registrar RDAP monitoring catches domain lapses that block future OV/EV MySQL-facing hostname revalidation even while today’s mysqld cert is still valid—especially when the cert SAN matches a customer-facing API domain that shares branding with the database CNAME.

Frequently asked questions

We use AWS RDS—can mysqld certs expire?

RDS manages its server certificate; you rotate via the AWS console or API. Custom domains, self-hosted replicas, or LDAP-style sidecars are separate—probe each endpoint.

Is disabling `VERIFY_IDENTITY` a permanent fix?

It removes date checks and often hostname checks, which violates common compliance baselines. Use only as a short incident bridge while renewing on the server.

Do client certs expire separately?

Yes. User/client certificates for mutual TLS have their own notAfter. Server expiry breaks all connections; client expiry breaks only accounts bound to that cert.

ProxySQL terminates TLS—who renews what?

Clients may see ProxySQL’s cert while mysqld uses another. Test both hops with openssl to the port each layer exposes.

See what the server presents right now

Free live certificate check — no signup.