새벽 3시에 터지는 Kubernetes 클러스터, 인증서 때문이었다
새벽 3시의 악몽
며칠 전 동료로부터 들은 이야기입니다. 새벽 3시, PagerDuty 알림이 끊임없이 울리더니 Kubernetes API 서버에 아예 접근이 안 되더라는 거예요. SSH로 급히 들어가보니 Pod들이 줄줄이 실패하고, 노드들이 하나씩 NotReady 상태가 되어가고 있었다고 합니다.
로그를 확인해보니 답이 나왔습니다:
x509: certificate has expired
잊혀진 인증서 하나가 전체 클러스터를 마비시킨 것이죠. GitHub(2025년), Microsoft Azure(2019년)에서도 비슷한 사고가 있었는데, 모두 단순한 이유였습니다. 아무도 인증서 만료일을 챙기지 못했던 것이죠.
복잡한 버그도 아니고, 예방할 수 없는 장애도 아닙니다. 그냥 관리가 안 됐을 뿐이에요.

Kubernetes의 인증서들
먼저 Kubernetes에서 사용되는 인증서들을 정리해보겠습니다:
| 인증서 유형 | 용도 | 만료 시 영향 |
|---|---|---|
| API 서버 인증서 | 제어 평면과의 보안 통신 | 클러스터 전체 접근 불가 |
| Kubelet 인증서 | 노드의 API 서버 인증 | 노드 NotReady, Pod 스케줄링 실패 |
| Ingress 인증서 | 외부 HTTPS 트래픽 | 서비스 접근 불가 |
| etcd 인증서 | etcd 멤버 간 통신 | 데이터 저장소 장애 |
각각 만료일이 있고, 만료되면 해당 통신이 실패합니다. 예상 외로 단순한 구조죠.
실수 1: 만료일을 모르고 있기
대부분의 장애는 "언제 만료되는지 몰랐다"에서 시작됩니다. 볼 수 없는 건 고칠 수도 없으니까요.
해결책: 자동 모니터링 구축
1단계: x509-certificate-exporter 설치
Prometheus 메트릭으로 모든 인증서를 감시할 수 있게 해줍니다:
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
2단계: cert-manager 메트릭 활성화
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
3단계: Prometheus 알림 규칙 설정
30일, 7일 전에 미리 알려주는 규칙을 만듭니다:
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일 내 만료됩니다"
수동으로도 확인할 수 있습니다:
# 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
실수 2: 수동으로 인증서 관리하기
수동 관리는 시간도 오래 걸리고, 실수하기 쉽고, 스케일되지도 않습니다. 더 중요한 건 깜빡하기 쉽다는 거예요.
해결책: cert-manager로 자동화
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
자동 갱신되는 Ingress 인증서
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가 알아서 인증서를 발급하고, 만료 30일 전에 자동으로 갱신합니다. 손댈 필요가 없어요.
실수 3: Kubelet 인증서 회전 설정 안 하기
Kubelet 인증서가 만료되면 노드가 API 서버와 통신할 수 없어서 NotReady 상태가 됩니다. 의외로 많은 분들이 놓치는 부분이에요.
해결책: Kubelet 자동 회전 활성화
# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
rotateCertificates: true
serverTLSBootstrap: true
kubeadm으로 구성된 클러스터라면:
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
RotateKubeletServerCertificate: true
rotateCertificates: true
serverTLSBootstrap: true
설정 후 kubelet을 재시작하면 자동 회전이 시작됩니다.
실수 4: 회전이 잘 되는지 테스트 안 하기
자동화를 구현했지만 실제로 작동하는지 확인하지 않는 경우가 많습니다. 그러다 실제 상황에서 실패하죠.
해결책: 정기적인 회전 훈련
짧은 TTL로 테스트 인증서를 만들어 회전을 확인해보세요:
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
⚠️ 중요: 테스트에는 반드시 Let's Encrypt staging 환경을 사용하세요. production 환경은 요청 제한이 있어서 테스트로 사용하면 실제 인증서 발급이 막힐 수 있습니다.

실수 5: 인증서 백업 안 하기
인증서가 실수로 삭제되거나 회전 중 문제가 생겼을 때, 백업이 없으면 복구가 어려워집니다.
해결책: 자동 백업 시스템
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
실수 6: 프로덕션에서 자체 서명 인증서 사용
자체 서명 인증서는 브라우저 경고를 발생시키고, 자동 회전이 까다로우며, 문제 해결을 복잡하게 만듭니다.
해결책: 신뢰할 수 있는 CA 사용
- 외부 서비스: Let's Encrypt (무료, 자동화, 신뢰됨)
- 내부 서비스: cert-manager와 private CA 조합
내부 CA 설정 예시:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca
spec:
ca:
secretName: internal-ca-key-pair
실수 7: 사고 대응 계획 없이 운영하기
예상치 못한 인증서 만료가 발생했을 때 명확한 절차가 없으면 복구 시간이 몇 배로 늘어납니다.
해결책: 인증서 사고 대응 플레이북
1단계: 즉시 평가 (5분)
# 영향받은 인증서 식별
kubectl get certificate --all-namespaces
# 만료일 확인
openssl x509 -in /path/to/cert.pem -noout -enddate
2단계: 즉각적인 완화 (10분)
# 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
3단계: 검증 (5분)
# 새 인증서 확인
kubectl get nodes
kubectl get pods --all-namespaces
curl -k https://your-api-endpoint/healthz

경험에서 배운 것들
인증서 관리는 생각보다 단순합니다. 복잡한 로직이 필요한 게 아니라, 그냥 미리 알고, 자동으로 갱신하고, 잘 되는지 확인하는 것뿐이에요.
중요한 건 이 세 가지입니다:
- 30일 전에 알림 - 여유롭게 대응할 시간 확보
- cert-manager로 자동화 - 사람이 깜빡하는 실수 방지
- 정기적인 테스트 - 자동화가 실제로 작동하는지 확인
새벽 3시에 깨는 일은 정말 피하고 싶잖아요. 미리미리 준비해두면 충분히 예방할 수 있는 문제라서, 한 번 설정해두고 나면 마음이 편해집니다.