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"
}'
)Rotate TLS certificates without restarting Kong Gateway
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
Kong Konnect
If you don’t have a Konnect account, you can get started quickly with our onboarding wizard.
- 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.
-
Set the personal access token as an environment variable:
export KONNECT_TOKEN='YOUR KONNECT TOKEN'
Enable the Gateway API
-
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 -
Create a
GatewayandGatewayClassinstance 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 -
Create a KIC Control Plane
Use the Konnect API to create a new CLUSTER_TYPE_K8S_INGRESS_CONTROLLER Control Plane:
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.crtThe 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.keyKong Ingress Controller running (attached to Konnect)
-
Add the Kong Helm charts:
helm repo add kong https://charts.konghq.com helm repo update -
Create a
values.yamlfile: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 -
Install Kong Ingress Controller using Helm:
helm install kong kong/ingress -n kong --create-namespace --values ./values.yaml --wait -
Set
$PROXY_IPas 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
Kong Ingress Controller running
-
Add the Kong Helm charts:
helm repo add kong https://charts.konghq.com helm repo update -
Install Kong Ingress Controller using Helm:
helm install kong kong/ingress -n kong --create-namespace --wait -
Set
$PROXY_IPas 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
Required Kubernetes resources
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 kongWait until the pods for the services are ready:
kubectl wait --for=condition=Ready pod --all -n kong --timeout=300sInstall cert-manager
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=90scert-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 kongYou should see the Secret listed with three keys:
NAME TYPE DATA AGE
demo.example.com kubernetes.io/tls 3 5sThe 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
-
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 subjectAltNameThe 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 -
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=90sValidate the rotation
-
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 subjectAltNameThe 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 -
Confirm that no pod restarted:
kubectl get pods -n kongThe value of the
RESTARTScolumn should still be0. -
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
Delete created Kubernetes resources
kubectl delete -n kong -f https://developer.konghq.com/manifests/kic/echo-service.yamlDelete the certificate resources
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-foundUninstall Kong Ingress Controller from your cluster
helm uninstall kong -n kongUninstall cert-manager
helm uninstall cert-manager -n cert-manager
kubectl delete namespace cert-managerFAQs
How do I check whether my deployment uses certificates that don’t rotate this way?
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.