Comparing Intended Peer Identity to Observed DNS Names and Presented Certificates in Headless StatefulSet Traffic
Introduction
A headless Service (clusterIP: None) does not allocate a virtual IP; it returns DNS A records that point directly to the pod IPs backing the Service. When paired with a StatefulSet, each replica gets a stable, ordinal‑based DNS name:
<statefulset-name>-<ordinal>.<service-name>.<namespace>.svc.cluster.local
This stable naming lets applications address peers by a deterministic identity that survives pod rescheduling, even though the underlying pod IP may change.
In mutual TLS (mTLS) or certificate‑based authentication, each pod must verify that the peer it talks to is the expected replica. Verification typically involves:
- Resolving the peer’s DNS name to an IP address.
- Performing a TLS handshake and checking the presented certificate’s Subject Alternative Name (SAN) against the expected identity.
- Applying any additional peer‑validation rules (hostname verification, certificate pinning, etc.).
A mismatch at any step aborts the TLS handshake, often appearing as transient connection errors that look like “pod flaps” in logs or monitoring, even though the pod remains Running.
Understanding Peer Identity and DNS Names
Intended Peer Identity
For a StatefulSet named web with three replicas, the intended peer identities are:
web-0.web.default.svc.cluster.localweb-1.web.default.svc.cluster.localweb-2.web.default.svc.cluster.local
These names are stable across pod recreation; the ordinal never changes for a given pod unless the pod is deleted and a new pod with the same ordinal is created (which retains the name).
Observed DNS Names in Headless StatefulSets
A headless Service creates an A record for each pod:
web-0.web.default.svc.cluster.local. 30 IN A 10.244.1.5
web-1.web.default.svc.cluster.local. 30 IN A 10.244.1.6
web-2.web.default.svc.cluster.local. 30 IN A 10.244.1.7
Clients that query the headless Service directly receive the current pod IP. If a pod is rescheduled, its IP changes but the A record is updated accordingly, so the observed DNS name matches the intended name as long as the resolver queries the headless Service directly.
Sources of Divergence
Divergence can occur when:
- Applications bypass the headless Service and resolve via an external DNS or a Service with
clusterIPset, causing CNAME chaining or load‑balanced IP returns. - Network policies or DNS caching cause stale A records to be used.
- Custom CoreDNS plugins or external DNS controllers rewrite the response.
In such cases, the observed IP may correspond to a different ordinal, leading to a mismatch between the DNS name used for TLS verification and the certificate presented by the actual peer.
Presented Certificates in Headless StatefulSet Traffic
Certificate Presentation
During an mTLS handshake, the server presents its X.509 certificate. The client validates:
- The certificate chains to a trusted CA.
- The
notBefore/notAftertimestamps. - The SAN extension (or, if absent, the Common Name) contains the expected DNS name.
A common practice is to issue a certificate per ordinal, with SAN entries like:
DNS: web-0.web.default.svc.cluster.local
DNS: web-0.web
Alternatively, a wildcard SAN (*.web.default.svc.cluster.local) can be used for a single cert per StatefulSet.
Verification of Presented Certificates
Verification follows RFC 6125: the client attempts to match the hostname used in the connection (the DNS name resolved earlier) against each SAN entry. If the hostname is an IP address, the client checks the iPAddress SAN type. A mismatch aborts the TLS handshake with certificate_unknown or bad_certificate alert.
Impact of Certificate Mismatch
When the observed DNS name does not match any SAN in the presented certificate:
- The TLS handshake fails before application data is exchanged.
- The client logs an SSL error (e.g.,
x509: certificate is valid for web-1.web.default.svc.cluster.local, not web-0.web.default.svc.cluster.local). - The connection is torn down, and the application may retry, leading to a rapid connect‑fail‑retry pattern that resembles pod restarts or flapping.
If the application treats TLS failures as transient and restarts the connection loop, the pod’s readiness probe may fail repeatedly, causing the pod to be marked NotReady and triggering a restart loop in some controllers.
Ordinal Moves and Peer Identity Verification
Ordinal Index in StatefulSets
Each pod in a StatefulSet has an immutable ordinal index assigned at creation time (0‑based). The ordinal is part of the pod’s hostname (<statefulset-name>-<ordinal>) and is used to generate the stable DNS name. The ordinal does not change when the pod is rescheduled; only if the pod is deleted and a new pod with the same ordinal is created does the ordinal persist.
Effect of Ordinal Moves on Peer Identity
Although the ordinal itself is static, the perceived ordinal can change if:
- A pod is deleted and the StatefulSet controller creates a replacement with a higher ordinal because the original ordinal’s PVC is bound to a node that is now unavailable, forcing the controller to skip to the next free ordinal (this can happen with
volumeBindingMode: WaitForFirstConsumerand insufficient PV capacity). - An operator manually patches the StatefulSet’s
spec.replicasdown and then up, causing the controller to reuse ordinals from deleted pods. - A misconfigured
podManagementPolicy: Parallelcombined with rapid scaling can cause race conditions where two pods temporarily claim the same ordinal in DNS (though the controller prevents this, external DNS tools may not).
When the observed ordinal differs from the intended one, the DNS name used for TLS verification points to a different pod, whose certificate SAN set does not match, causing verification failure.
Troubleshooting Ordinal‑Related Peer Identity Issues
- Check the pod’s hostname:
kubectl exec <pod> -- hostnameshould return<statefulset-name>-<ordinal>. - Verify the StatefulSet’s ordinal assignment:
kubectl get pods -l app=<app> -o custom-columns=NAME:.metadata.name,ORDINAL:.metadata.annotations.kubernetes\.io/pod-name. - Inspect events for pod deletions/recreations:
kubectl get events --field-selector involvedObject.name=<pod>. - Ensure PVCs are correctly bound and that sufficient PVs exist for the requested storage class.
- If using external DNS, verify that it respects the headless Service’s A records and does not perform CNAME flattening that could change the ordinal portion.
Custom SAN Patterns and Peer Validation Rules
Custom Subject Alternative Name (SAN) Patterns
Operators may define SAN patterns that deviate from the default <statefulset>-<ordinal>.<service>.<namespace>.svc.cluster.local. Examples:
- Service‑mesh sidecar injection:
DNS: *.svc.cluster.localto allow any service in the cluster. - Multi‑cluster federation:
DNS: web-0.web.global.example.com. - Legacy applications:
CN: web-0(Common Name only) orDNS: web-0.local.
These patterns are specified in the certificate signing request (CSR) or via cert‑manager’s dnsNames field.
Peer Validation Rules in Headless StatefulSets
Beyond hostname matching, applications may enforce additional rules:
- Certificate pinning: Expect a specific public key hash.
- Issuer verification: Require the certificate to be issued by a particular CA (e.g.,
cfsslorvault). - Extended Key Usage (EKU): Ensure the certificate includes
TLS Web Server Authentication. - Custom SAN validation: Accept names that match a regex, e.g.,
^web-[0-9]+\.web\.default\.svc\.cluster\.local$.
These rules are typically implemented in the TLS library configuration (e.g., Go’s tls.Config.VerifyPeerCertificate, Java’s X509TrustManager, or Envoy’s validation_context).
Configuring Custom SAN Patterns and Peer Validation Rules
Example using cert‑manager to issue a certificate with custom SANs for a StatefulSet named web:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: web-tls
namespace: default
spec:
secretName: web-tls-secret
dnsNames:
- web-0.web.default.svc.cluster.local
- web-1.web.default.svc.cluster.local
- web-2.web.default.svc.cluster.local
- "*.web.default.svc.cluster.local" # optional wildcard for future scale
issuerRef:
name: ca-issuer
kind: Issuer
Application‑side verification (Go example):
tlsConfig := &tls.Config{
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return err
}
hostname := "web-0.web.default.svc.cluster.local" // resolved earlier
if err := cert.VerifyHostname(hostname); err != nil {
return fmt.Errorf("hostname mismatch: %w", err)
}
// Additional pinning check
expected := sha256.Sum256([]byte("pin-value"))
actual := sha256.Sum256(cert.Raw)
if !bytes.Equal(expected[:], actual[:]) {
return errors.New("certificate pinning failed")
}
return nil
},
}
Creating Failures that Resemble Random Pod Flaps
Simulating Random Pod Flaps with Ordinal Moves and Custom SAN Patterns
To reproduce the symptom:
- Deploy a headless StatefulSet with three replicas, each serving an mTLS endpoint.
- Issue certificates with SANs that only match the exact ordinal names (no wildcard).
- Introduce a scenario where a pod is deleted and its PVC cannot be re‑bound (e.g., insufficient storage). The StatefulSet controller will then create a new pod with the next ordinal (e.g., if pod
web-1is deleted and cannot reuse its PVC, a new podweb-3may be created, leaving a gap). - Configure clients to resolve peers via the headless Service but cache DNS responses for 30 s.
- After the ordinal gap appears, clients attempting to connect to
web-1will receive the IP ofweb-3(due to DNS caching or external DNS load‑balancing). The TLS handshake fails becauseweb-3’s certificate does not containweb-1in its SAN. - The client logs connection errors, retries, and the pod’s readiness probe fails, causing the pod to be marked
NotReady. From an external viewpoint, the pod appears to flap.
Identifying and Troubleshooting Peer Identity Verification Failures
Key indicators:
- TLS alert
bad_certificateorcertificate_unknownin sidecar logs. - DNS TTL mismatches:
dig +trace web-0.web.default.svc.cluster.localshows stale A record. - Ordinal mismatch:
kubectl get pods -l app=webshows missing ordinal while DNS still resolves to an existing pod. - Readiness probe failures correlating with TLS handshake errors.
Troubleshooting steps:
- Capture TLS handshake with
tcpdumporopenssl s_client -connect <pod-ip>:<port> -servername <expected-hostname>. - Compare the presented certificate’s SAN list with the hostname used in the
-servernameflag. - Verify DNS resolution at the moment of failure:
kubectl exec <client-pod> -- nslookup web-0.web.default.svc.cluster.local. - Check StatefulSet events for pod deletions/recreations:
kubectl describe statefulset web. - Ensure that the headless Service’s
clusterIPis indeedNone:kubectl get svc web -o yaml.
Code Examples for Reproducing and Debugging Peer Identity Issues
Reproducer (bash)
# 1. Create StorageClass with limited capacity (for demo)
cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: limited-sc
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
EOF
# 2. Create PVCs (only 2 PVs available)
for i in 0 1 2; do
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: web-pv-$i
namespace: default
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
storageClassName: limited-sc
EOF
done
# 3. Deploy headless StatefulSet (3 replicas) with TLS
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
namespace: default
spec:
serviceName: "web"
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: app
image: gcr.io/google-samples/hello-app:1.0
env:
- name: PORT
value: "8443"
volumeMounts:
- name: tls
mountPath: /etc/tls
- name: data
mountPath: /data
- name: envoy
image: envoyproxy/envoy:v1.25-latest
volumeMounts:
- name: tls
mountPath: /etc/envoy/tls
args:
- "-c"
- "/etc/envoy/envoy.yaml"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
storageClassName: limited-sc
volumeMounts:
- name: tls
mountPath: /etc/tls
- name: data
mountPath: /data
EOF
# 4. Issue certs with exact ordinal SANs (using cert-manager)
# (Assume cert-manager and issuer already installed)
for i in 0 1 2; do
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: web-tls-$i
namespace: default
spec:
secretName: web-tls-secret-$i
dnsNames:
- web-${i}.web.default.svc.cluster.local
issuerRef:
name: ca-issuer
kind: Issuer
EOF
done
Debugging Commands
# Check pod hostname
kubectl exec web-0 -- hostname
# Verify ordinal assignment
kubectl get pods -l app=web -o custom-columns=NAME:.metadata.name,ORDINAL:.metadata.annotations.kubernetes\.io/pod-name
# Look for deletion/recreation events
kubectl get events --field-selector involvedObject.name=web-0
# Test TLS handshake with expected hostname
openssl s_client -connect 10.244.1.6:8443 -servername web-1.web.default.svc.cluster.local -showcerts
# Verify DNS resolution at runtime
kubectl exec <client-pod> -- nslookup web-0.web.default.svc.cluster.local
These steps help isolate whether the failure stems from ordinal drift, DNS caching, or SAN mismatches, allowing you to correct the underlying configuration rather than treating the symptom as random pod flapping.