Skip to content

Rate this page
☆ ☆ ☆ ☆ ☆
Thanks for your feedback
Thank you! The feedback has been submitted.

Get free database assistance or contact our experts for personalized support.

Update certificates

How your TLS certificates are updated depends on how they were created:

  • Certificates generated by the Operator are long-term. If you need to rotate them, you must do it manually.

  • Certificates issued by cert-manager are short-term. They are valid for 3 months. cert-manager reissues the server certificate (the leaf) on schedule. Starting with version 1.3.0, MySQL Pods pick up that new leaf without a restart.

  • Certificates you generated yourself are not renewed automatically. It is your responsibility to timely update them. Use the steps in this document for how to do it.

Before you start

Export the namespace, cluster name, and TLS Secret name. Replace the placeholders with your values. If you set spec.sslSecretName, use that name for SSL_SECRET_NAME:

export NAMESPACE=<namespace>
export CLUSTER_NAME=<cluster-name>
export SSL_SECRET_NAME=${CLUSTER_NAME}-ssl

Check your certificates for expiration

If you use cert-manager:

  1. Check the certificate and Secret names (ps-cluster1-ssl and ps-cluster1-ca-cert by default):

    kubectl get certificate -n $NAMESPACE
    
    Sample output
    ps-cluster1-ca-cert   True    ps-cluster1-ca-cert   45m
    ps-cluster1-ssl       True    ps-cluster1-ssl       43m
    
  2. Optionally you can also check that the certificates issuer is up and running:

    kubectl get issuer -n $NAMESPACE
    

    The response should be as follows:

    NAME                              READY   AGE
    ps-cluster1-ps-ca-issuer   True    40m
    ps-cluster1-ps-issuer      True    38m
    

    Note

    If you don’t use cert-manager, list your secrets:

    kubectl get secrets -n $NAMESPACE
    

    Then either use the default ones or the one you created.

  3. Use the following command to find out the certificates validity dates, substituting Secret names if necessary:

    {
    kubectl get secret/${CLUSTER_NAME}-ca-cert -n $NAMESPACE -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -noout -dates
    kubectl get secret/${SSL_SECRET_NAME} -n $NAMESPACE -o jsonpath='{.data.ca\.crt}' | base64 --decode | openssl x509 -noout -dates
    }
    
    Sample output
    notBefore=Nov  7 10:54:00 2025 GMT
    notAfter=Nov  7 10:54:00 2026 GMT
    

Rotate the server certificate

Use this procedure when the new server certificate is signed by the same CA. This is the usual case: cert-manager renewals, and most manual rotations.

Let cert-manager renew the certificate

If you use cert-manager, you do not need to do anything for a scheduled leaf renewal. cert-manager writes a new tls.crt and tls.key into the TLS Secret. The Operator reloads that leaf into mysqld. HAProxy, MySQL Router, and Orchestrator Pods restart.

To force a renewal before expiry, refer to the cert-manager renewal documentation .

Apply a new certificate yourself

If you created the certificates yourself, replace only the leaf in the existing Secret. Keep ca.crt unchanged.

You need the current CA certificate and the CA private key to sign a new leaf. The TLS Secret stores the CA certificate as ca.crt. It does not store the CA private key. Use the ca-key.pem file you saved when you generated the CA.

  1. Extract the current CA certificate from the Secret. You put this same file back in the Secret later so the CA does not change:

    kubectl get secret/${SSL_SECRET_NAME} -n $NAMESPACE -o jsonpath='{.data.ca\.crt}' | base64 --decode > ca.pem
    
  2. Copy your saved ca-key.pem into the same directory as ca.pem. Confirm both files are present:

    ls ca.pem ca-key.pem
    
  3. Generate a new server certificate (server.pem) and key (server-key.pem) signed by that CA. Do not create a new CA. Use the server certificate command in Generate certificates manually.

  4. Apply the new leaf to the TLS Secret:

    kubectl create secret generic ${SSL_SECRET_NAME} \
    --from-file=tls.crt=server.pem \
    --from-file=tls.key=server-key.pem \
    --from-file=ca.crt=ca.pem \
    --type=kubernetes.io/tls -n $NAMESPACE -o yaml --dry-run=client | kubectl apply -f -
    

Starting with version 1.3.0, MySQL Pods stay running. HAProxy, MySQL Router, and Orchestrator Pods go through a rolling restart.

To confirm the reload:

  1. List MySQL Pod unique identifiers (UIDs) before you apply the Secret and again after. The names and UIDs stay the same:

    kubectl get pods -n $NAMESPACE \
      -l app.kubernetes.io/instance=${CLUSTER_NAME},app.kubernetes.io/name=mysql \
      -o custom-columns=NAME:.metadata.name,UID:.metadata.uid --sort-by=.metadata.name
    
  2. Check the certificate start date that mysqld currently serves. Repeat for each MySQL Pod. The date changes after the reload:

    kubectl exec -n $NAMESPACE ${CLUSTER_NAME}-mysql-0 -c mysql -- \
      bash -c 'mysql -uroot -p"$(cat /etc/mysql/mysql-users-secret/root)" -NB -e "SHOW GLOBAL STATUS LIKE \"Ssl_server_not_before\""'
    

Rotate the CA certificate

Use this procedure when you replace the CA, not only the server certificate.

If the current certificates are still valid, follow these steps. For already expired certificates, see Replace expired certificates.

  1. Generate a new CA certificate (ca.pem), a new TLS certificate (server.pem), and a key for it (server-key.pem). See Generate certificates manually.

  2. Get the current CA (ca.pem.old) and TLS (tls.pem.old) certificates and the TLS certificate key (tls.key.old):

    kubectl get secret/${SSL_SECRET_NAME} -n $NAMESPACE -o jsonpath='{.data.ca\.crt}' | base64 --decode > ca.pem.old
    kubectl get secret/${SSL_SECRET_NAME} -n $NAMESPACE -o jsonpath='{.data.tls\.crt}' | base64 --decode > tls.pem.old
    kubectl get secret/${SSL_SECRET_NAME} -n $NAMESPACE -o jsonpath='{.data.tls\.key}' | base64 --decode > tls.key.old
    
  3. Combine the new and current CA certificates into a ca.pem.combined file:

    cat ca.pem ca.pem.old > ca.pem.combined
    
  4. Create a new Secrets object with the old TLS certificate (tls.pem.old) and key (tls.key.old), but a new combined CA (ca.pem.combined):

    kubectl create secret generic ${SSL_SECRET_NAME} \
    --from-file=tls.crt=tls.pem.old \
    --from-file=tls.key=tls.key.old \
    --from-file=ca.crt=ca.pem.combined \
    --type=kubernetes.io/tls -n $NAMESPACE -o yaml --dry-run=client | kubectl apply -f -
    

    ca.crt changed, so MySQL, HAProxy, MySQL Router, and Orchestrator Pods go through a rolling restart. Every node still presents the old leaf and now trusts both CAs.

  5. Create a new Secrets object again. This time use a new TLS certificate (server.pem) and a new TLS key (server-key.pem), and again the combined CA certificate (ca.pem.combined):

    kubectl create secret generic ${SSL_SECRET_NAME} \
    --from-file=tls.crt=server.pem \
    --from-file=tls.key=server-key.pem \
    --from-file=ca.crt=ca.pem.combined \
    --type=kubernetes.io/tls -n $NAMESPACE -o yaml --dry-run=client | kubectl apply -f -
    

    The CA is unchanged in this step, so starting with version 1.3.0 MySQL Pods stay running and reload the new leaf. HAProxy, MySQL Router, and Orchestrator Pods restart. Every node already trusts the new CA, so a joiner with the new leaf can join. Joiners also have the combined CA, so they can still verify nodes that present the old leaf.

  6. Create a final Secrets object: use the new TLS certificate (server.pem) and its key (server-key.pem), and only the new CA certificate (ca.pem):

    kubectl create secret generic ${SSL_SECRET_NAME} \
    --from-file=tls.crt=server.pem \
    --from-file=tls.key=server-key.pem \
    --from-file=ca.crt=ca.pem \
    --type=kubernetes.io/tls -n $NAMESPACE -o yaml --dry-run=client | kubectl apply -f -
    

    ca.crt changed again, so Pods go through a rolling restart. The old CA is gone, and every node already uses the new leaf, so no node still depends on the old CA.

Replace expired certificates

If the certificates have already expired, nodes cannot verify each other. Do not use the CA rotation procedure. Pause the cluster, replace the Secret, then unpause.

  1. Pause the cluster and wait until it reaches the paused state.

  2. If you use cert-manager, delete the issuer and certificates so they can be recreated:

    kubectl delete issuer/${CLUSTER_NAME}-ps-ca-issuer issuer/${CLUSTER_NAME}-ps-issuer -n $NAMESPACE
    kubectl delete certificate/${CLUSTER_NAME}-ssl certificate/${CLUSTER_NAME}-ca-cert -n $NAMESPACE
    
  3. Delete the TLS Secrets to force reconciliation:

    kubectl delete secret/${SSL_SECRET_NAME} secret/${CLUSTER_NAME}-ca-cert -n $NAMESPACE
    

    If you manage certificates yourself, apply a new Secret with a valid CA, server certificate, and key instead of deleting:

    kubectl create secret generic "${SSL_SECRET_NAME}" \
      --from-file=tls.crt=server.pem \
      --from-file=tls.key=server-key.pem \
      --from-file=ca.crt=ca.pem \
      --type=kubernetes.io/tls -n "${NAMESPACE}" -o yaml --dry-run=client | kubectl apply -f -
    
  4. Check that new certificates exist and are valid. See Check your certificates for expiration.

  5. Unpause the cluster and wait until it reaches the ready state. MySQL Pods start with the new CA and leaf.


Last update: October 6, 2026
Created: October 6, 2026