Skip to content
LinkState
Go back

Root CA rotation partitioned one tenant after a seemingly clean rollout

Incident Overview

Incident Summary

At 02:14 UTC on 2025‑08‑12, the tenant‑isolated micro‑service group svc‑frontend‑a began returning TLS handshake failures (SSL_alert_handshake_failure) to external clients, while all other service groups in the same tenant continued to serve traffic successfully. Dashboards for the automated root‑CA rotation pipeline showed a green “rotation complete” status at 01:58 UTC, and the new root CA (root‑ca‑2025‑08) was present in the central trust‑anchor repository. Investigation revealed that the sidecar proxies attached to svc‑frontend‑a were still using the stale root CA (root‑ca‑2024‑11) because the sidecar’s trust‑anchor file had not been reloaded after rotation, creating a trust‑anchor skew that isolated only that service group.

Incident Timeline

Time (UTC)Event
01:45Automated CA rotation job starts: generates new root CA, signs intermediate CA, pushes new trust bundle to ConfigMap trust-anchors.
01:50Job updates Kubernetes Secret sidecar‑tls‑secrets that sidecars mount as a volume for their root‑CA bundle.
01:58Job reports success; monitoring dashboards display “Root CA rotation completed – 0 errors”.
02:00Sidecar agent (istio-proxy) for svc‑frontend‑a receives SIGHUP from the pod’s init container but fails to reload the mounted volume due to a stale inotify watch (see Amplifying Factors).
02:02External clients attempting mutual TLS with svc‑frontend‑a receive certificate_unknown errors; sidecar logs show failed to verify peer certificate: unable to get local issuer certificate.
02:05Service‑level SLO alerts fire for svc‑frontend‑a (latency ↑, error rate 12%).
02:14On‑call engineer receives PagerDuty alert; begins investigation.
02:30Engineer checks sidecar container logs, discovers trust‑anchor mismatch.
02:45Engineer forces a sidecar restart (kubectl rollout restart deployment/svc-frontend-a) – sidecars load new trust bundle, TLS handshakes succeed.
03:00Service returns to normal; SLOs recover.
03:15Post‑mortem meeting scheduled.

Affected Services

Root Cause Analysis

Initial Assessment

The initial hypothesis was a silent failure of the CA rotation job because the dashboard showed success while the service exhibited TLS failures. Verification of the central ConfigMap and Secret confirmed the presence of the new root CA (root‑ca‑2025‑08). The discrepancy pointed to a failure in the consumption of the updated trust anchor by the sidecar proxies.

Deep Dive Analysis

Root CA Rotation Process

  1. Generation – HashiCorp Vault PKI role creates a new self‑signed root CA (root‑ca‑2025‑08) with a 10‑year validity.
  2. Signing – The new root signs the existing intermediate CA (intermediate‑ca‑2023‑04) to preserve existing leaf certificates.
  3. Distribution – Concatenated PEM bundle (root‑ca‑2025‑08.pem + intermediate‑ca‑2023‑04.pem) is written to:
    • ConfigMap trust-anchors (used by admission controllers for validating webhook TLS).
    • Secret sidecar‑tls‑secrets (mounted at /etc/istio/certs/root-ca.pem in each sidecar container).
  4. Notification – The job emits a Prometheus metric ca_rotation_success{status="1"} and writes a completion entry to an audit log.

All steps completed without error; the metric showed 1 at 01:58 UTC.

Sidecar Configuration

Trust Anchor Management

Thus, the root cause is the sidecar’s inability to reload its trust‑anchor volume due to a blocked inotify watch, causing a trust‑anchor skew that affected only the subset of pods that had not been restarted since the seccomp profile was applied.

Amplifying Factors

Inadequate Monitoring

Insufficient Logging

Incomplete Automation

Detection Gaps

Limitations of Current Tooling

Inadequate Alerting

Lack of Anomaly Detection

Troubleshooting Steps

Initial Troubleshooting

  1. Verified the CA rotation job’s output logs – showed success.
  2. Checked the ConfigMap trust-anchors and Secret sidecar‑tls‑secrets – both contained the new root CA PEM.
  3. Executed openssl x509 -in /etc/istio/certs/root-ca.pem -text -noout inside a sidecar container – returned the old root CA (root‑ca‑2024‑11).
  4. Reviewed Istio agent logs (istiod) – confirmed that a secret update event was published and SIGHUP sent to the pod at 02:02 UTC.

In‑Depth Troubleshooting

CLI Commands Used

# 1. Verify secret contents
kubectl -n acme-corp-tenant-prod get secret sidecar-tls-secrets -o jsonpath='{.data.root-ca\.pem}' | base64 -d | openssl x509 -noout -subject -dates

# 2. Check sidecar volume mount
kubectl -n acme-corp-tenant-prod exec deploy/svc-frontend-a -c istio-proxy -- ls -l /etc/istio/certs/root-ca.pem

# 3. Examine inotify watch status (requires strace)
kubectl -n acme-corp-tenant-prod exec deploy/svc-frontend-a -c istio-proxy -- strace -e trace=inotify_add_watch,inotify_rm_watch -p $(pgrep istio-proxy) 2>&1 | head -20

# 4. Review seccomp profile applied to the pod
kubectl -n acme-corp-tenant-prod get pod <pod-name> -o jsonpath='{.spec.securityContext.seccompProfile}'
# Output showed:
#   {
#     "type": "Localhost",
#     "localhostProfile": "profiles/seccomp/strict.json"
#   }
# The strict profile blocked `inotify_add_watch` for non‑root UIDs.

# 5. Force sidecar reload via restart
kubectl -n acme-corp-tenant-prod rollout restart deployment/svc-frontend-a

Code Snippets Analyzed

Istio agent secret watcher (simplified):

func (s *secretWatcher) Run(stopCh <-chan struct{}) {
    informer := s.kube.InformerFactory.Core().V1().Secrets().Informer()
    informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
        UpdateFunc: func(old, new interface{}) {
            if !reflect.DeepEqual(old.(*v1.Secret).Data, new.(*v1.Secret).Data) {
                s.podLister.Pods(s.namespace).List(labels.Everything(), func(p *v1.Pod) error {
                    if podHasSidecar(p) {
                        s.signalPod(p) // sends SIGHUP
                    }
                    return nil
                })
            }
        }
    })
    informer.Run(stopCh)
}

The watcher correctly detected the secret change and signaled the pod. The failure point was not in this code.

Sidecar SIGHUP handler (from proxy source):

static void
reload_cert_chain(int sig) {
    if (sig == SIGHUP) {
        TRACE("reload: SIGHUP received");
        if (load_root_certs("/etc/istio/certs/root-ca.pem") != 0) {
            ERROR("failed to reload root certs");
        }
    }
}

The function is called, but load_root_certs() reads from a memory‑mapped copy of the file that was never unmapped because the file descriptor watch never triggered a munmap/mmap cycle. The seccomp block prevented the inotify_add_watch syscall that would have notified the sidecar’s runtime library (libevent) to invalidate the mapping.

Code and CLI Examples

Relevant Configuration Files

values.yaml (Helm) – seccomp profile:

podSecurityContext:
  seccompProfile:
    type: Localhost
    localhostProfile: profiles/seccomp/strict.json

profiles/seccomp/strict.json (excerpt):

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    { "name": "inotify_add_watch", "action": "SCMP_ACT_ERRNO" },
    { "name": "inotify_rm_watch",  "action": "SCMP_ACT_ERRNO" }
  ]
}

Istio sidecar deployment snippet:

containers:
- name: istio-proxy
  image: docker.io/istio/proxyv2:1.18.0
  volumeMounts:
  - name: tls-secrets
    mountPath: /etc/istio/certs
    readOnly: true
volumes:
- name: tls-secrets
  secret:
    secretName: sidecar-tls-secrets

CLI Commands for Root CA Rotation

# Example: generate new root CA and push to cluster (simplified)
vault pki issue -domain="internal.acme-corp" -ttl=8760h > new-ca.pem
kubectl -n acme-corp-tenant-prod create configmap trust-anchors --from-file=root-ca.pem=new-ca.pem --dry-run=client -o yaml | kubectl apply -f -
kubectl -n acme-corp-tenant-prod create secret generic sidecar-tls-secrets --from-file=root-ca.pem=new-ca.pem --dry-run=client -o yaml | kubectl apply -f -

Share this post on:

Previous Post
Quorum design for multi-region network controllers
Next Post
Staged segmentation changes with permit shadowing