gofillus 기술·운영 저널

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

새 네임스페이스를 추가할 때도 순서는 같다.

  1. 해당 네임스페이스에 Certificate를 만든다.
  2. cert-manager가 Secret을 만들 때까지 기다린다.
  3. Ingress의 tls.secretName에 그 Secret을 적는다.
  4. 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 응답까지 확인한다.

← 목록으로