The 3 AM Kubernetes Outage That Turned Out to Be a Certificate
Translated from the original Korean post. 한국어 원문 보기 →
The 3 A.M. Nightmare
A coworker told me this one a few days ago. Three in the morning, PagerDuty going off nonstop, and the Kubernetes API server was flat-out unreachable. He SSH'd in and found Pods failing one after another, nodes dropping to NotReady one by one.
The logs had a one-line answer.
x509: certificate has expired
One forgotten certificate took down an entire cluster. GitHub had a similar incident in 2025. Microsoft Azure in 2019. Different scale, same root cause: nobody was tracking an expiration date.
This isn't a subtle bug. It isn't an unpreventable failure. It's just unmanaged time, accumulating until it blows. Certificates live in operations, not in code — but once you issue one, everybody forgets it like it's code. What gets missed is that issuing a cert starts a timer.
Having handled certificates in financial services, this pattern is familiar. Certificate renewal always sits in "somebody must be on top of this," right up until that somebody takes vacation or quits. The responsibility isn't written into any system.

The Certificates in Kubernetes
Start with what's actually running inside the cluster.
| 인증서 유형 | 용도 | 만료 시 영향 |
|---|---|---|
| API 서버 인증서 | 제어 평면과의 보안 통신 | 클러스터 전체 접근 불가 |
| Kubelet 인증서 | 노드의 API 서버 인증 | 노드 NotReady, Pod 스케줄링 실패 |
| Ingress 인증서 | 외부 HTTPS 트래픽 | 서비스 접근 불가 |
| etcd 인증서 | etcd 멤버 간 통신 | 데이터 저장소 장애 |
Each has an expiration date, and when it hits, that communication path dies. The structure is simpler than you'd expect. The problem is that these certs don't live in one place. Some sit in Secrets, some on node filesystems, some between etcd members. Scattered means easy to lose sight of, and what you can't see doesn't get managed.
Mistake 1: Not Knowing the Expiration Dates
Most incidents start with "we didn't know when it expired." You can't fix what you can't see. The first value of monitoring isn't the alert — it's the visibility.
Fix: Build Automated Monitoring
Install x509-certificate-exporter first. It exposes every certificate in the cluster as Prometheus metrics.
helm repo add enix https://charts.enix.io
helm repo update
helm upgrade --install x509-certificate-exporter enix/x509-certificate-exporter \
--set prometheusServiceMonitor.enabled=true \
--set prometheusServiceMonitor.labels.release=prometheus \
--set service.port=9793
Then turn on cert-manager metrics.
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
--version v1.19.4 \
--namespace cert-manager \
--create-namespace \
--set prometheus.enabled=true \
--set prometheus.servicemonitor.enabled=true
What's left is the Prometheus alerting rules. Warn at 30 days and again at 7. Two thresholds because they mean different things: 30 days is room to plan, 7 days is "you actually have to touch this now."
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-cert-rules
namespace: monitoring
data:
cert-expiry.rules: |
groups:
- name: certificate_expiry
interval: 1h
rules:
- alert: CertManagerCertExpiring30Days
expr: (certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 30
for: 1h
labels:
severity: warning
annotations:
summary: "인증서 {{ $labels.name }}가 30일 내 만료됩니다"
- alert: ControlPlaneCertExpiring7Days
expr: (x509_cert_not_after - time()) / 86400 < 7
for: 1h
labels:
severity: critical
annotations:
summary: "긴급: 제어플레인 인증서가 7일 내 만료됩니다"
You can always check by hand too. These commands are your last resort when monitoring itself is broken.
# kubelet 인증서 확인
openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -noout -enddate
# API 서버 인증서 확인
openssl s_client -connect localhost:6443 -showcerts 2>/dev/null | \
openssl x509 -noout -enddate
Mistake 2: Managing Certificates by Hand
Manual management is slow, error-prone, and doesn't scale. But the truly fatal part is something else. You forget. Any operation that depends on human memory will fail at least once. It just happens to be at 3 a.m.
Fix: Automate with cert-manager
Start with a ClusterIssuer.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
Attach it to your Ingress and renewal happens on its own.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
cert-manager.io/renew-before: "720h" # 30일 전 갱신
spec:
tls:
- hosts:
- example.com
secretName: example-com-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
cert-manager issues the certificate and renews it 30 days before expiry. Nothing for you to touch. Moving renewal from a person's job to a system's job is the point — automation is really about relocating responsibility.
Mistake 3: Not Enabling Kubelet Certificate Rotation
When the kubelet certificate expires, the node can't talk to the API server. It goes NotReady immediately. Ingress certificates are visible enough that people stay on top of them; kubelet certificates aren't visible day to day, so a surprising number of teams miss them.
Fix: Turn On Kubelet Auto-Rotation
# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
rotateCertificates: true
serverTLSBootstrap: true
For a kubeadm-built cluster, set it like this.
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
RotateKubeletServerCertificate: true
rotateCertificates: true
serverTLSBootstrap: true
Restart the kubelet after applying and rotation kicks in.
Mistake 4: Never Testing That Rotation Works
People wire up the automation and never verify it actually runs. Then rotation fails when it counts. Backups, automation, whatever — a safety mechanism you've never tested isn't a safety mechanism.
Fix: Rehearse Rotation Regularly
Issue a test certificate with a short TTL and watch it rotate with your own eyes.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: test-rotation-cert
spec:
secretName: test-rotation-tls
duration: 2h # 2시간만 유효
renewBefore: 1h # 1시간 전 갱신
issuerRef:
name: letsencrypt-staging # 반드시 staging 사용!
kind: ClusterIssuer
dnsNames:
- test.example.com
Always use the Let's Encrypt staging environment for tests. Production has rate limits, and hammering them in a test can block you from issuing real certificates. You'd be validating your renewal automation and end up unable to renew. Not funny in the moment.

Mistake 5: Not Backing Up Certificates
When a certificate gets deleted by accident, or rotation goes sideways, recovery without a backup is grim. Automation exists, and automation also breaks — the backup is what you fall back on when it does.
Fix: Automated Backups
apiVersion: batch/v1
kind: CronJob
metadata:
name: cert-backup
namespace: kube-system
spec:
schedule: "0 2 * * *" # 매일 새벽 2시
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: bitnami/kubectl:latest
command:
- /bin/sh
- -c
- |
kubectl get secret --all-namespaces -o yaml > /backup/certs-$(date +%Y%m%d).yaml
volumeMounts:
- name: backup-volume
mountPath: /backup
volumes:
- name: backup-volume
persistentVolumeClaim:
claimName: cert-backup-pvc
restartPolicy: OnFailure
One thing to flag: a Secret dump contains private keys in the clear. Access control and encryption on that backup volume are a separate job you need to do. A backup that becomes a new leak path is a bad trade.
Mistake 6: Self-Signed Certificates in Production
Self-signed certificates throw browser warnings, make auto-rotation awkward, and complicate troubleshooting. The self-signed cert someone used to stand up a PoC quickly follows the service all the way into production more often than you'd think.
Fix: Use a Trusted CA
For external services, use Let's Encrypt. Free, automated, trusted everywhere. For internal services, pair cert-manager with a private CA.
Here's an internal CA setup.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca
spec:
ca:
secretName: internal-ca-key-pair
Even internally, a clear trust chain lets you run rotation and validation through the same pipeline you use for external services. Not splitting certificates into two different operational models pays off over time.
Mistake 7: Operating Without an Incident Plan
When an unexpected expiration hits and there's no procedure, recovery time multiplies. Recalling commands from memory at 3 a.m. is nothing like having a doc open and typing what it says.
Fix: A Certificate Incident Playbook
First five minutes: assess. Which certificate is involved, and when did it expire. That's it.
# 영향받은 인증서 식별
kubectl get certificate --all-namespaces
# 만료일 확인
openssl x509 -in /path/to/cert.pem -noout -enddate
Next ten minutes: mitigate. For Ingress, delete the Certificate and reapply. For kubelet and the API server, renew with kubeadm.
# Ingress 인증서 강제 갱신
kubectl delete certificate <cert-name> -n <namespace>
kubectl apply -f ingress-with-cert.yaml
# Kubelet 인증서 갱신
sudo kubeadm certs renew all
sudo systemctl restart kubelet
# API 서버 인증서 갱신
sudo kubeadm certs renew apiserver
The last five minutes: verify.
# 새 인증서 확인
kubectl get nodes
kubectl get pods --all-namespaces
curl -k https://your-api-endpoint/healthz
The 5-10-5 time budget isn't decoration. When your sense of time collapses in the middle of an incident, per-step targets give you a basis for deciding "do I keep digging, or do I work around it."

What I've Taken From This
Certificate management is simpler than people assume. There's no clever logic involved — just know in advance, renew automatically, and check that it works.
Three things. Get the alert 30 days out so you have room to respond. Automate with cert-manager so there's no point where a human forgets. Test regularly so you know the automation actually runs.
Certificate outages aren't a technical problem, they're an operational one. From the code seat it looks like a one-line expiration error. From the ops seat it was a timer nobody owned. The real purpose of automation is moving that timer out of human memory and into a system.
Nobody wants to be up at 3 a.m. This is the kind of thing you set up once and stop worrying about. It's also the item where procrastinating costs you the most.
Was this post helpful?
One click helps me write the next one