Thursday, 29 October 2026

Why Our Gateway Pods Failed 60 Days After a Deploy, and How We Fixed It

We have discussed ngix/gateway deployment on aks in the below posts.

Then we fixed an issue with cert renewal at deployment as explained in the below post.

The nginx gateway setup explained in above posts has two parts that talk to each other over a secure connection:

  • The control part (Fabric) sends configuration.
  • The traffic part (nginx pods) receives it and routes user traffic.

To trust each other, both use certificates. We use cert-manager to create and renew them. There are three certificates:

  1. A CA certificate. This is the "boss" certificate. It signs the other two.
  2. A server certificate for the control part.
  3. A client certificate for the nginx pods.

The server and client certificates must be signed by the same CA. If they are not, the two parts stop trusting each other. The problem explained in the Handle Nginx Gateway Certificate Refresh While In Place Upgrade in AKS is partly soved in that post but, if a deployment does not happen for a long time problem occurs again. We used the cert-manager defaults for all three certificates:

  • Each certificate lives 90 days.
  • Each one renews at day 60.

All three were created at the same moment as we explained in Handle Nginx Gateway Certificate Refresh While In Place Upgrade in AKS . So all three also renewed at the same moment, at day 60.

The problem

When the CA renews, it gets a new key. The two other certificates must be signed by this new CA. But all three renewed at the same second, so it was a race. Either of these could happen:

  • One certificate was signed by the old CA and the other by the new CA. The two parts no longer trusted each other. New control plane and data plane connections failed from day 60.
  • Both were signed by the old CA. This looks fine at first. But the old CA dies at day 90, while the new certificates live until day 150. New control plane and data plane connections failed from day 90.
Running pods kept serving users, because the certificate is only checked when a connection starts. The failure showed up only when a new pod started or a connection had to be created again.

Why did we not see it earlier? Our deploy pipeline deletes and recreates the certificates on every deploy. So most of the time everything was fresh. The problem only appeared when no deploy happened for 60 days or more.

We had below in the file pipelines\aks_manifests\nginx_gateway\nginx_gateway_fabric_prerequisites.yaml as explained in the post Deploy Nginx-Gateway-Fabric. So, we used the cert-manager defaults for all three certificates:


    apiVersion: cert-manager.io/v1
    kind: Issuer
    metadata:
      name: selfsigned-issuer
      namespace: nginx-gateway
    spec:
      selfSigned: {}

    ---
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: nginx-gateway-ca
      namespace: nginx-gateway
    spec:
      isCA: true
      commonName: nginx-gateway
      secretName: nginx-gateway-ca
      privateKey:
        algorithm: RSA
        size: 2048
      issuerRef:
        name: selfsigned-issuer
        kind: Issuer
        group: cert-manager.io

    ---
    apiVersion: cert-manager.io/v1
    kind: Issuer
    metadata:
      name: nginx-gateway-issuer
      namespace: nginx-gateway
    spec:
      ca:
        secretName: nginx-gateway-ca

    ---
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: nginx-gateway
      namespace: nginx-gateway
    spec:
      secretName: server-tls
      usages:
      - digital signature
      - key encipherment
      dnsNames:
      - ngf-nginx-gateway-fabric.nginx-gateway.svc
      issuerRef:
        name: nginx-gateway-issuer

    ---
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: nginx
      namespace: nginx-gateway
    spec:
      secretName: agent-tls
      usages:
      - "digital signature"
      - "key encipherment"
      dnsNames:
      - "*.cluster.local"
      issuerRef:
        name: nginx-gateway-issuer

    The fix

    We stopped letting everything renew together. We set our own timings:


    Three simple rules sit behind these numbers:

    1. A certificate must never live longer than the CA that signed it. The CA renews 90 days before it ends. That is more than the 60 days a certificate lives. So the CA always outlives the certificates it signed.
    2. The CA and the other certificates should not renew at the same time. The CA renews every 365 days. The others renew every 30 days. 365 is not a multiple of 30, so they do not line up. The first time they would meet again is after about 6 years.
    3. Both certificates always get the same CA. The CA changes rarely, so when the two certificates renew, they are signed by the same one.

    We also kept the deploy pipeline step that recreates the certificates. It does not affect live traffic, and every deploy restarts all the timers. In practice, the 6-year meeting point is never reached.


    apiVersion: cert-manager.io/v1
    kind: Issuer
    metadata:
      name: selfsigned-issuer
      namespace: nginx-gateway
    spec:
      selfSigned: {}

    ---
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: nginx-gateway-ca
      namespace: nginx-gateway
    spec:
      isCA: true
      commonName: nginx-gateway
      secretName: nginx-gateway-ca
      # Renews every 365 days; renewBefore (90d) must exceed leaf duration (60d)
    # and keep CA renewal off the 30-day leaf renewal days.
      duration: 10920h # 455 days
      renewBefore: 2160h # 90 days
      privateKey:
        algorithm: RSA
        size: 2048
        rotationPolicy: Always
      issuerRef:
        name: selfsigned-issuer
        kind: Issuer
        group: cert-manager.io

    ---
    apiVersion: cert-manager.io/v1
    kind: Issuer
    metadata:
      name: nginx-gateway-issuer
      namespace: nginx-gateway
    spec:
      ca:
        secretName: nginx-gateway-ca

    ---
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: nginx-gateway
      namespace: nginx-gateway
    spec:
      secretName: server-tls
      duration: 1440h # 60 days
      renewBefore: 720h # 30 days, renews every 30 days
      usages:
      - digital signature
      - key encipherment
      dnsNames:
      - ngf-nginx-gateway-fabric.nginx-gateway.svc
      issuerRef:
        name: nginx-gateway-issuer

    ---
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: nginx
      namespace: nginx-gateway
    spec:
      secretName: agent-tls
      duration: 1440h # 60 days
      renewBefore: 720h # 30 days, renews every 30 days
      usages:
      - "digital signature"
      - "key encipherment"
      dnsNames:
      - "*.cluster.local"
      issuerRef:
        name: nginx-gateway-issuer

    How we tested it

    We tested on a test cluster with very short times, so we could see months of life in minutes: Below timeline is set.

    • Certificates renewed every 5 minutes, and the CA every 8 minutes.
    • We watched 3 CA renewals and 5 certificate renewals.
    • Both certificates always had the same CA.
    • A certificate never lived longer than its CA.
    • No pod restarted.

    Each certificate renewal caused about 2 minutes of connection resets between the control part and the nginx pods, then everything recovered. This connection only carries configuration, not user traffic, so routing was not interrupted.

    The expectation was that, with the above settings, 40 minutes would proportionally represent six years of collision expectations under production conditions.

    To monitor we used below script on wsl connected to the AKS where nginx is deployed.

    CTX=ch-demo-dev-euw-002-aks-green-admin
    NS=nginx-gateway

    cert_info() { # $1 = secret, $2 = label
      local s=$1 l=$2
      kubectl --context $CTX -n $NS get secret $s -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/leaf.crt 2>/dev/null
      kubectl --context $CTX -n $NS get secret $s -o jsonpath='{.data.ca\.crt}'  | base64 -d > /tmp/ca.crt   2>/dev/null
      printf "%-10s leaf serial=%s notAfter=%s | ca.crt serial=%s key=%s notAfter=%s\n" "$l" \
        "$(openssl x509 -in /tmp/leaf.crt -noout -serial | cut -d= -f2 | cut -c1-8)" \
        "$(openssl x509 -in /tmp/leaf.crt -noout -enddate | cut -d= -f2)" \
        "$(openssl x509 -in /tmp/ca.crt -noout -serial | cut -d= -f2 | cut -c1-8)" \
        "$(openssl x509 -in /tmp/ca.crt -noout -pubkey | sha256sum | cut -c1-8)" \
        "$(openssl x509 -in /tmp/ca.crt -noout -enddate | cut -d= -f2)"
    }

    while true; do
      echo "===== $(date -u +%H:%M:%S) UTC ====="
      kubectl --context $CTX -n $NS get secret nginx-gateway-ca -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/cacur.crt
      printf "%-10s serial=%s key=%s notAfter=%s\n" "CA" \
        "$(openssl x509 -in /tmp/cacur.crt -noout -serial | cut -d= -f2 | cut -c1-8)" \
        "$(openssl x509 -in /tmp/cacur.crt -noout -pubkey | sha256sum | cut -c1-8)" \
        "$(openssl x509 -in /tmp/cacur.crt -noout -enddate | cut -d= -f2)"
      cert_info server-tls "server-tls"
      cert_info agent-tls  "agent-tls"
      cert_info pocrmq-apps-gateway-nginx-agent-tls "agent-copy"
      echo "fabric TLS errors (2m): $(kubectl --context $CTX -n $NS logs -l app.kubernetes.io/name=nginx-gateway-fabric --since=2m --tail=-1 --prefix 2>/dev/null | grep -ciE 'tls|certificate|handshake')"
      echo "nginx agent resets (2m): $(kubectl --context $CTX -n $NS logs -l gateway.networking.k8s.io/gateway-name=pocrmq-apps-gateway -c nginx --since=2m --tail=-1 2>/dev/null | grep -ciE 'credential|reset')"
      echo "restarts: $(kubectl --context $CTX -n $NS get pods --no-headers | awk '{s+=$4} END{print s}')"
      sleep 10
    done 2>&1 | tee ~/ca-leaf-rotation.log

    Below is some of monitoring resutls.

    What we learned

    • Do not renew everything at the same time. Spread the timings out.
    • A certificate must not outlive the CA that signed it.
    • A problem that only shows up after 60 days is easy to miss, because deploys hide it.
    • Test with short times. You can see the whole renewal process in minutes.

    No comments:

    Popular Posts