TLS 인증서 자동화: cert-manager·Let’s Encrypt 운영
TLS 인증서를 수동으로 관리하던 시절의 악몽은 대개 하나로 요약됩니다. “인증서가 만료됐는데 아무도 몰랐다.” 갱신 담당자가 퇴사했거나, 캘린더 알림을 놓쳤거나, 갱신은 했는데 배포를 잊은 채 서비스 전체가 브라우저 경고와 함께 멈춰버리는 상황이죠. 이 위험을 근본적으로 없애는 방법은 인증서 발급과 갱신을 완전 자동화하는 것입니다.
이 글에서는 Kubernetes 환경에서 cert-manager와 Let’s Encrypt를 조합해 인증서 수명주기를 자동화하는 운영 방법을 다룹니다. HTTP-01과 DNS-01 챌린지의 차이, 와일드카드 인증서 발급, 그리고 자동화 이후에도 반드시 갖춰야 할 모니터링과 레이트 리밋 회피까지 실전 관점에서 정리합니다.
ACME와 cert-manager의 동작 원리
Let’s Encrypt는 ACME 프로토콜로 인증서를 발급합니다. 핵심은 “당신이 정말 그 도메인을 통제하는가”를 자동으로 검증하는 챌린지입니다. cert-manager는 이 ACME 흐름 전체(계정 등록, 챌린지 응답, 인증서 저장, 만료 전 자동 갱신)를 Kubernetes 리소스로 추상화합니다.
핵심 리소스는 세 가지입니다.
- Issuer/ClusterIssuer: 어떤 CA에서, 어떤 챌린지로 발급받을지 정의
- Certificate: 원하는 인증서의 명세(도메인, 저장할 Secret 이름 등)
- Secret: 발급된 실제 인증서·개인키가 저장되는 곳
# cert-manager 설치 (CRD 포함)
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=true
# 설치 확인
kubectl get pods -n cert-manager
kubectl get crd | grep cert-manager.io
HTTP-01 챌린지로 시작하기
가장 단순한 시작점은 HTTP-01 챌린지입니다. cert-manager가 임시 경로 /.well-known/acme-challenge/에 토큰을 띄우고, Let’s Encrypt가 HTTP로 접근해 도메인 통제를 확인합니다. 인그레스가 이미 있고 도메인이 공인 IP로 연결돼 있다면 가장 빠릅니다.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
인그레스에 애너테이션만 붙이면 cert-manager가 나머지를 처리합니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts: [app.example.com]
secretName: app-example-com-tls # 여기 인증서가 자동 저장됨
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port: { number: 80 }
HTTP-01의 한계는 와일드카드를 발급할 수 없다는 것과, 검증 시점에 해당 도메인이 인터넷에서 HTTP로 접근 가능해야 한다는 점입니다. 내부 서비스나 다수 서브도메인을 다룬다면 DNS-01이 필요합니다.
DNS-01과 와일드카드 인증서
DNS-01 챌린지는 도메인의 TXT 레코드를 심어 통제를 증명합니다. 이 방식의 결정적 장점은 와일드카드(*.example.com) 발급이 가능하고, 대상 서비스가 인터넷에 노출되지 않아도 된다는 점입니다. 대신 DNS 공급자의 API 자격 증명이 필요합니다.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-dns-account-key
solvers:
- dns01:
route53:
region: ap-northeast-2
# IRSA/워크로드 아이덴티티로 키 없이 권한 부여 권장
selector:
dnsZones: ["example.com"]
# 와일드카드 인증서 요청
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example
namespace: default
spec:
secretName: wildcard-example-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- "example.com"
- "*.example.com"
DNS-01에서 자주 겪는 함정은 DNS 전파 지연입니다. TXT 레코드가 퍼지기 전에 검증을 시도하면 실패합니다. cert-manager는 재시도하지만, 권한 위임(delegated subdomain)이나 CNAME 위임을 쓰면 더 안정적으로 만들 수 있습니다. DNS API 자격 증명은 최소 권한(해당 존의 TXT 레코드만 수정)으로 좁히는 것이 보안상 중요합니다.
스테이징으로 먼저 검증하고 레이트 리밋 피하기
Let’s Encrypt 프로덕션 엔드포인트에는 레이트 리밋이 있습니다. 설정 실수로 발급을 반복하면 몇 시간~일주일간 차단될 수 있습니다. 그래서 새로운 Issuer나 도메인 설정은 반드시 스테이징 엔드포인트로 먼저 검증합니다. 스테이징은 신뢰되지 않는 CA를 쓰지만 레이트 리밋이 훨씬 관대합니다.
# 스테이징 ClusterIssuer (검증용)
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
# ... 나머지 동일
# 발급 상태와 실패 원인 추적
kubectl describe certificate app-example-tls
kubectl get certificaterequest,order,challenge -A
kubectl describe challenge -n default # 챌린지가 왜 pending인지 확인
디버깅 흐름은 Certificate → CertificateRequest → Order → Challenge 순으로 파고드는 것이 정석입니다. 대개 문제는 챌린지 단계에서 드러납니다. HTTP-01이면 인그레스 라우팅, DNS-01이면 TXT 전파나 자격 증명 권한이 원인인 경우가 많습니다.
자동화 이후에도 필요한 모니터링
자동 갱신을 걸어뒀다고 해서 손을 떼면 안 됩니다. 갱신이 조용히 실패하는 경우(자격 증명 만료, DNS 권한 변경, 레이트 리밋)가 있기 때문입니다. cert-manager는 만료 지표를 노출하므로, 만료가 임박했는데 갱신이 안 된 인증서를 알림으로 잡아야 합니다.
# 만료 임박 인증서 경보 (Prometheus alert 예시)
- alert: CertExpiringSoon
expr: certmanager_certificate_expiration_timestamp_seconds - time() < 7 * 24 * 3600
for: 1h
labels: { severity: warning }
annotations:
summary: "인증서 {{ $labels.name }} 만료 7일 이내, 갱신 상태 확인 필요"
# 외부에서 실제 제공되는 인증서 만료일 교차 검증
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null \
| openssl x509 -noout -enddate
운영 체크리스트로 정리하면 다음과 같습니다.
- DNS/클라우드 API 자격 증명의 만료·회전 일정 추적
- 인증서 만료 임박 이중 경보(cert-manager 지표 + 외부 실측)
- 새 도메인·Issuer는 스테이징 선검증 후 프로덕션 전환
- 키 회전 시 재발급이 서비스 무중단으로 이어지는지 점검
마무리
TLS 인증서 자동화의 목표는 단순히 손을 덜 대는 것이 아니라, “만료로 인한 장애”라는 특정 실패 유형을 구조적으로 제거하는 것입니다. cert-manager와 Let’s Encrypt는 발급과 갱신을 Kubernetes 리소스로 선언화해 이 목표를 달성합니다. HTTP-01로 빠르게 시작하고, 와일드카드나 비공개 서비스에는 DNS-01을 쓰며, 스테이징으로 먼저 검증해 레이트 리밋을 피하는 것이 기본기입니다. 그리고 자동화 이후에도 조용한 갱신 실패를 잡아내는 모니터링을 갖춰야 비로소 인증서 만료 알림이 새벽에 울리는 일이 사라집니다.