Rotate TLS certificates without restarting Kong Gateway

TL;DR

Reference the cert-manager Secret from a Gateway listener. Kong Ingress Controller converts the Secret into Certificate and SNI entities and pushes them to Kong Gateway over the Admin API on every renewal, so the new certificate is served without a reload or a pod restart. Certificates configured in kong.conf, such as ssl_cert, behave differently: they’re read once at startup and need a restart.

Prerequisites

If you don’t have a Konnect account, you can get started quickly with our onboarding wizard.

  1. The following Konnect items are required to complete this tutorial:
    • Personal access token (PAT): Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.
  2. Set the personal access token as an environment variable:

    export KONNECT_TOKEN='YOUR KONNECT TOKEN'
  1. Install the Gateway API CRDs before installing Kong Ingress Controller.

    kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml
  2. Create a Gateway and GatewayClass instance to use.

    echo "
    apiVersion: v1
    kind: Namespace
    metadata:
      name: kong
    ---
    apiVersion: gateway.networking.k8s.io/v1
    kind: GatewayClass
    metadata:
      name: kong
      annotations:
        konghq.com/gatewayclass-unmanaged: 'true'
    spec:
      controllerName: konghq.com/kic-gateway-controller
    ---
    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
      name: kong
    spec:
      gatewayClassName: kong
      listeners:
      - name: proxy
        port: 80
        protocol: HTTP
        allowedRoutes:
          namespaces:
            from: All
    " | kubectl apply -n kong -f -

Use the Konnect API to create a new CLUSTER_TYPE_K8S_INGRESS_CONTROLLER Control Plane:

CONTROL_PLANE_DETAILS=$(curl -X POST "https://us.api.konghq.com/v2/control-planes" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" \
     --json '{
       "name": "My KIC CP",
       "cluster_type": "CLUSTER_TYPE_K8S_INGRESS_CONTROLLER"
     }'
)

We’ll need the id and telemetry_endpoint for the values.yaml file later. Save them as environment variables:

CONTROL_PLANE_ID=$(echo $CONTROL_PLANE_DETAILS | jq -r .id)
CONTROL_PLANE_TELEMETRY=$(echo $CONTROL_PLANE_DETAILS | jq -r '.config.telemetry_endpoint | sub("https://";"")')

Create mTLS certificates

Kong Ingress Controller talks to Konnect over a connected secured with TLS certificates.

Generate a new certificate using openssl:

openssl req -new -x509 -nodes -newkey rsa:2048 -subj "/CN=kongdp/C=US" -keyout ./tls.key -out ./tls.crt

The certificate needs to be a single line string to send it to the Konnect API with curl. Use awk to format the certificate:

export CERT=$(awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' tls.crt);

Next, upload the certificate to Konnect:

curl -X POST "https://us.api.konghq.com/v2/control-planes/$CONTROL_PLANE_ID/dp-client-certificates" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" \
     --json '{
       "cert": "'$CERT'"
     }'

Finally, store the certificate in a Kubernetes secret so that Kong Ingress Controller can read it:

kubectl create namespace kong -o yaml --dry-run=client | kubectl apply -f -
kubectl create secret tls konnect-client-tls -n kong --cert=./tls.crt --key=./tls.key
  1. Add the Kong Helm charts:

    helm repo add kong https://charts.konghq.com
    helm repo update
  2. Create a values.yaml file:

    cat <<EOF > values.yaml
    controller:
      ingressController:
        image:
          tag: "3.5"
        env:
          feature_gates: "FillIDs=true"
        konnect:
          license:
            enabled: true
          enabled: true
          controlPlaneID: "$CONTROL_PLANE_ID"
          tlsClientCertSecretName: konnect-client-tls
          apiHostname: "us.kic.api.konghq.com"
    gateway:
      image:
        repository: kong
        tag: "3.9.3"
      env:
        konnect_mode: 'on'
        vitals: "off"
        cluster_mtls: pki
        cluster_telemetry_endpoint: "$CONTROL_PLANE_TELEMETRY:443"
        cluster_telemetry_server_name: "$CONTROL_PLANE_TELEMETRY"
        cluster_cert: /etc/secrets/konnect-client-tls/tls.crt
        cluster_cert_key: /etc/secrets/konnect-client-tls/tls.key
        lua_ssl_trusted_certificate: system
        proxy_access_log: "off"
        dns_stale_ttl: "3600"
      secretVolumes:
         - konnect-client-tls
    EOF
  3. Install Kong Ingress Controller using Helm:

    helm install kong kong/ingress -n kong --create-namespace --values ./values.yaml --wait 
  4. Set $PROXY_IP as an environment variable for future commands:

    export PROXY_IP=$(kubectl get svc --namespace kong kong-gateway-proxy -o jsonpath='{range .status.loadBalancer.ingress[0]}{@.ip}{@.hostname}{end}')
    echo $PROXY_IP
  1. Add the Kong Helm charts:

    helm repo add kong https://charts.konghq.com
    helm repo update
  2. Install Kong Ingress Controller using Helm:

    helm install kong kong/ingress -n kong --create-namespace --wait 
  3. Set $PROXY_IP as an environment variable for future commands:

    export PROXY_IP=$(kubectl get svc --namespace kong kong-gateway-proxy -o jsonpath='{range .status.loadBalancer.ingress[0]}{@.ip}{@.hostname}{end}')
    echo $PROXY_IP

This how-to requires some Kubernetes services to be available in your cluster. These services will be used by the resources created in this how-to.

kubectl apply -f https://developer.konghq.com/manifests/kic/echo-service.yaml -n kong

Wait until the pods for the services are ready:

kubectl wait --for=condition=Ready pod --all -n kong --timeout=300s

Install cert-manager in your cluster to issue and rotate certificates automatically:

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm upgrade --install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true
kubectl wait -n cert-manager --for=condition=ready pod --all --timeout=90s

cert-manager renews certificates and writes the new key pair to a Kubernetes Secret. Whether Kong Gateway starts serving that new certificate depends on how the certificate reached the proxy in the first place.

In this guide, we’ll issue a certificate with cert-manager, serve it from a Gateway listener, force a renewal, and confirm that Kong Gateway picks up the new certificate without restarting.

Referencing a Secret from a Gateway listener is what makes rotation automatic: Kong Ingress Controller watches the Secret and converts it into Certificate and SNI entities, which Kong Gateway re-reads on every configuration update. Certificates set through kong.conf behave differently. For more information, see When certificates are loaded and reloaded.

Create a cert-manager Issuer

The Issuer resource represents the certificate authority that signs your certificates. Create a self-signed issuer in the kong namespace:

echo '
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: selfsigned-issuer
  namespace: kong
spec:
  selfSigned: {}' | kubectl apply -f -

A self-signed issuer keeps this guide self-contained. In production, use an ACME issuer such as Let’s Encrypt, or a CA issuer. For all issuer types, see the cert-manager configuration documentation.

Issue a certificate

Create a Certificate for the demo.example.com hostname. cert-manager writes the key pair to a Secret named demo.example.com and renews it automatically before it expires:

echo '
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: demo.example.com
  namespace: kong
spec:
  secretName: demo.example.com
  duration: 24h
  renewBefore: 12h
  issuerRef:
    name: selfsigned-issuer
    kind: Issuer
  dnsNames:
    - demo.example.com' | kubectl apply -f -

Wait for cert-manager to issue the certificate and confirm that the Secret exists:

kubectl wait --for=condition=Ready certificate/demo.example.com -n kong --timeout=90s
kubectl get secret demo.example.com -n kong

You should see the Secret listed with three keys:

NAME               TYPE                DATA   AGE
demo.example.com   kubernetes.io/tls   3      5s

The three keys are tls.crt, tls.key, and ca.crt.

Add an HTTPS listener that references the Secret

Add a TLS listener to the Gateway and point certificateRefs at the cert-manager Secret:

kubectl patch -n kong --type=json gateway kong -p='[
    {
        "op":"add",
        "path":"/spec/listeners/-",
        "value":{
            "name": "https",
            "port": 443,
            "protocol": "HTTPS",
            "hostname": "demo.example.com",
            "allowedRoutes": {
              "namespaces": {
                "from": "All"
              }
            },
            "tls": {
              "mode": "Terminate",
              "certificateRefs":[{
                "group": "",
                "kind": "Secret",
                "name": "demo.example.com"
              }]
            }
        }
    }
]'

Kong Ingress Controller now watches demo.example.com and re-syncs the Certificate entity every time cert-manager writes to it.

Route traffic through the listener

Create an HTTPRoute that attaches to the https listener and sends requests to the echo Service we created as a prerequisite:

Wait for the httproute to be True.

kubectl wait -n kong --timeout=30s httproute/echo \
  --for='jsonpath={.status.parents[?(@.parentRef.name=="kong")].conditions[?(@.type=="Programmed")].status }=True'

Record the certificate currently being served

  1. Check the certificate that Kong Gateway presents for demo.example.com, and note the serial number and validity dates:

    echo "" | openssl s_client -connect $PROXY_IP:443 -servername demo.example.com 2>/dev/null \
      | openssl x509 -noout -serial -dates -ext subjectAltName

    The output should look like this:

    serial=5B2C9A1E7F04D8B3A6E1C0F29D3847BA
    notBefore=Aug  7 10:12:04 2026 GMT
    notAfter=Aug  8 10:12:04 2026 GMT
    X509v3 Subject Alternative Name: critical
        DNS:demo.example.com
  2. Note how long the Kong Gateway pods have been running, so we can check later that they didn’t restart:

    kubectl get pods -n kong

Rotate the certificate

Force cert-manager to issue a new certificate ahead of schedule by deleting the Secret. cert-manager detects that the Certificate no longer has a valid Secret and immediately reissues it:

kubectl delete secret demo.example.com -n kong
kubectl wait --for=condition=Ready certificate/demo.example.com -n kong --timeout=90s

Validate the rotation

  1. Query the proxy again:

    echo "" | openssl s_client -connect $PROXY_IP:443 -servername demo.example.com 2>/dev/null \
      | openssl x509 -noout -serial -dates -ext subjectAltName

    The serial number and the validity dates should have changed:

    serial=91F4D07C3A5B8E26D14903BC7E85A2FD
    notBefore=Aug  7 10:19:41 2026 GMT
    notAfter=Aug  8 10:19:41 2026 GMT
    X509v3 Subject Alternative Name: critical
        DNS:demo.example.com
  2. Confirm that no pod restarted:

    kubectl get pods -n kong

    The value of the RESTARTS column should still be 0.

  3. Confirm that traffic was never interrupted:

    curl -sk --resolve demo.example.com:443:$PROXY_IP https://demo.example.com/echo -o /dev/null -w "%{http_code}\n"

    The response should be 200.

Cleanup

kubectl delete -n kong -f https://developer.konghq.com/manifests/kic/echo-service.yaml
kubectl delete httproute echo -n kong
kubectl delete certificate demo.example.com -n kong
kubectl delete issuer selfsigned-issuer -n kong
kubectl delete secret demo.example.com -n kong --ignore-not-found
helm uninstall kong -n kong
helm uninstall cert-manager -n cert-manager
kubectl delete namespace cert-manager

FAQs

Certificates set through kong.conf are read once at startup, so they don’t follow the rotation described here. Check whether your Kong Gateway pods set any of them:

kubectl get pods -n kong -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range .spec.containers[*].env[*]}{"  "}{.name}{"="}{.value}{"\n"}{end}{end}' | grep -i "ssl_cert\|cluster_cert"

If the command returns nothing, every certificate in your deployment rotates automatically. If it returns a proxy certificate, move it out of ssl_cert and onto a Gateway listener as shown in this guide. For the rest, rotation requires the pods to be replaced: see Restart Kong Gateway on Kubernetes, or Restart Kong Gateway when a mounted certificate changes to automate it.

For the full list of parameters that behave this way and why, see When certificates are loaded and reloaded.

Help us make these docs great!

Kong Developer docs are open source. If you find these useful and want to make them better, contribute today!