Kubernetes에서 Let's Encrypt 와일드카드 인증서를 쓰고 자동 갱신한 방법
cert-manager와 DNS-01을 사용해 와일드카드 TLS 인증서를 발급하고, 여러 네임스페이스의 Ingress에서 사용한 구성과 자동 갱신을 확인하는 방법을 정리한다.
Kubernetes에 서비스가 하나둘 늘면 HTTPS 인증서도 함께 늘어난다. 새 주소를 만들 때마다 HTTP-01 인증서를 발급하면 Ingress와 DNS 설정을 매번 확인해야 하고, 인증서와 Secret도 서비스 수만큼 관리해야 한다.
운영 클러스터에서는 한 단계 아래 서브도메인을 모두 덮는 Let’s Encrypt 와일드카드 인증서를 사용했다. 인증서 발급은 cert-manager가 맡고, 도메인 소유 확인은 Cloudflare DNS-01로 처리한다. 인증서가 만료되기 전에 cert-manager가 다시 발급하고, Kubernetes Secret을 갱신한다.
배경 — 주소가 늘 때마다 인증서를 만들지 않기
예를 들어 다음 주소들이 있다고 하자.
argocd.example.com
monitoring.example.com
storage.example.com
이 주소들은 모두 *.example.com에 포함된다. 따라서 다음 두 이름을 한 인증서에 넣었다.
*.example.com
example.com
apex 도메인인 example.com은 *.example.com이 대신하지 못한다. 루트 주소도 HTTPS로 열 계획이라면 두 이름을 모두 dnsNames에 넣어야 한다.
선택 — HTTP-01 대신 DNS-01
HTTP-01은 Let’s Encrypt가 서비스 주소로 HTTP 요청을 보내 소유권을 확인한다. 와일드카드 인증서는 HTTP-01로 발급할 수 없으므로 DNS-01을 사용해야 한다.
DNS-01에서는 cert-manager가 Cloudflare DNS에 _acme-challenge.example.com TXT 레코드를 잠시 만들고, Let’s Encrypt가 그 값을 조회한다. 따라서 인증서를 쓸 서비스가 외부에 공개되어 있지 않아도 된다. Ingress가 아직 만들어지지 않은 새 주소에도 인증서를 먼저 발급할 수 있다.
Cloudflare 자격은 Global API Key가 아니라 DNS 레코드 편집 권한만 가진 API Token으로 만들었다. Token은 Git 저장소에 넣지 않고 cert-manager 네임스페이스의 Secret으로 보관한다.
적용 — ClusterIssuer 만들기
ClusterIssuer는 클러스터 전체에서 사용할 Let’s Encrypt 발급 정책이다. 실제 이메일과 Secret 이름은 환경에 맞게 바꾼다.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: admin@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
ClusterIssuer는 issuerRef.kind: ClusterIssuer로 참조한다. 이 예시의 cloudflare-api-token Secret은 매니페스트에 평문으로 넣지 않는다.
적용 — 네임스페이스마다 Certificate 만들기
TLS Secret은 네임스페이스에 속한다. 그래서 common에서 만든 Secret을 monitoring의 Ingress가 바로 사용할 수 없다.
같은 와일드카드 이름을 쓰더라도 네임스페이스마다 Certificate를 하나씩 만든다.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-tls
namespace: app
spec:
secretName: tls-wildcard-example
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- "example.com"
Ingress에서는 같은 네임스페이스의 Secret을 참조한다.
spec:
tls:
- hosts:
- app.example.com
secretName: tls-wildcard-example
새 네임스페이스를 추가할 때도 순서는 같다.
- 해당 네임스페이스에
Certificate를 만든다. - cert-manager가 Secret을 만들 때까지 기다린다.
- Ingress의
tls.secretName에 그 Secret을 적는다. - HTTPS 응답과 인증서 SAN을 확인한다.
실수한 것들
와일드카드 Secret 하나를 모든 네임스페이스에서 쓰려고 했다
Kubernetes Secret은 네임스페이스에 속한다. common/tls-wildcard-example을 monitoring Ingress에서 이름만 적어 사용할 수 없다.
해결: 네임스페이스마다 같은 Certificate를 만들고, 각 네임스페이스에 같은 이름의 Secret을 발급했다. 인증서 발급 정책은 하나의 ClusterIssuer를 공유하고, 결과 Secret만 나눈다.
두 단계 서브도메인도 와일드카드가 덮는다고 생각했다
*.example.com은 app.example.com은 덮지만 dev.app.example.com은 덮지 않는다. 와일드카드는 한 단계만 일치한다.
해결: 두 단계 주소에는 별도 Certificate를 만들고 dev.app.example.com을 dnsNames에 직접 넣었다.
인증서 발급과 갱신을 수동 명령으로 처리할 뻔했다
cert-manager를 사용하면 인증서 발급 뒤에 파일을 복사하거나 Secret을 직접 갱신할 필요가 없다. Certificate 상태를 감시하다가 갱신 시점이 되면 새 ACME 주문을 만들고, DNS-01 TXT 검증을 수행한 뒤 Secret을 교체한다.
자동 갱신 확인
인증서와 Secret을 직접 수정하지 않고 cert-manager의 상태와 이벤트를 확인한다.
kubectl get certificate -A
kubectl describe certificate wildcard-example-tls -n app
kubectl get secret tls-wildcard-example -n app
kubectl get certificaterequest,order,challenge -n app
확인할 값은 다음과 같다.
Certificate의READY가True인가Not After가 미래 날짜인가- 최근
CertificateRequest가 성공했는가 - DNS-01
Challenge가valid또는 완료 상태인가 - Secret의
tls.crt와tls.key가 함께 갱신됐는가
Let’s Encrypt 인증서는 기본적으로 90일 동안 유효하다. cert-manager는 만료 직전에 자동 갱신을 시도한다. 운영에서는 만료일만 기다리지 않고 Certificate 상태와 cert-manager 이벤트를 모니터링한다.
외부에서 실제로 어떤 인증서를 내보내는지도 확인했다.
openssl s_client -connect app.example.com:443 \
-servername app.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
Ingress의 Secret이 바뀌어도 ingress-nginx가 새 인증서를 읽지 못하는 경우를 잡기 위해, 갱신 뒤 HTTPS 응답까지 확인한다.
결과
| 항목 | 결과 |
|---|---|
| 발급 기관 | Let’s Encrypt production |
| 소유권 확인 | Cloudflare DNS-01 |
| 인증서 이름 | *.example.com, example.com |
| 발급 정책 | cert-manager ClusterIssuer |
| Secret | 네임스페이스별 Certificate가 각각 생성 |
| 갱신 | cert-manager 자동 갱신 |
| Ingress 변경 | 새 주소마다 인증서 발급 작업 불필요 |
| API 자격 | Cloudflare API Token, Git 미저장 |
실제로 사용하는 도메인 이름을 바꾸지 않고도 새 서비스 주소를 추가할 수 있게 됐다. 단, 새 주소가 와일드카드의 한 단계 범위에 들어가고, 해당 네임스페이스에 인증서 Secret이 먼저 있어야 한다.
남는 것
와일드카드 인증서의 핵심은 인증서 한 장을 무조건 공유하는 데 있지 않다. ClusterIssuer로 발급 정책을 통일하고, DNS-01을 통해 발급·갱신을 자동화하며, 네임스페이스별 Secret 경계를 지키는 데 있다.
- 와일드카드는 한 단계 서브도메인만 덮는다.
- 루트 도메인은
dnsNames에 따로 넣는다. - TLS Secret은 네임스페이스마다 필요하다.
- Cloudflare API Token은 DNS 편집 권한만 주고 Git에 넣지 않는다.
- 갱신은 만료일보다
Certificate상태와 이벤트로 확인한다. - 인증서가 갱신됐어도 외부 HTTPS 응답까지 확인한다.