IT/DevSecOps

cert-manager로 Wildcard 인증서 자동화하기 (운영하며 만난 함정들)

gfrog 2026. 5. 24. 06:15
반응형

작년 말에 우리 팀은 사내 서비스용 도메인의 TLS 인증서를 ACM에서 cert-manager로 옮겼다. EKS 클러스터가 늘어나면서 ALB마다 ACM 인증서 attach하고 갱신 알람 처리하는 게 점점 귀찮아진 게 직접적인 이유였고, 비용보다는 운영 부담 쪽이 컸다. 6개월쯤 굴려보니 마이그레이션 직후엔 안 보이던 문제들이 슬슬 보이기 시작해서, 정리해 두면 누군가는 덜 헤맬 것 같아 글로 남긴다.

먼저 짚고 가야 할 거 하나. Wildcard 인증서(*.internal.example.com)는 HTTP01 challenge로 못 받는다. DNS01만 된다. 이건 Let's Encrypt 정책이라 우회할 방법이 없다. 그래서 cert-manager + Route53(우리 환경 기준) 조합이 사실상 표준 답안이 된다.

기본 셋업과 staging 분리

IAM access key를 Secret에 박아두는 방식 말고, IRSA(또는 EKS Pod Identity)로 권한을 주는 편을 권한다. 처음에 키 방식으로 시작했다가 키 로테이션이 귀찮아져서 결국 IRSA로 갈아탔다.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - selector:
          dnsZones:
            - "internal.example.com"
        dns01:
          route53:
            region: ap-northeast-2
            hostedZoneID: Z0123456789ABC
            role: arn:aws:iam::111111111111:role/cert-manager-route53

hostedZoneID는 명시하는 편을 추천한다. 빼면 cert-manager가 ListHostedZones 권한이 필요해서 IAM policy가 더 넓어진다.

그리고 더 중요한 건 staging Issuer를 분리해 두는 것이다. Let's Encrypt는 동일 도메인에 주당 50건 발급 제한이 있고, 실패한 ACME order도 카운트에 잡힌다. 처음 셋업하면서 ClusterIssuer 설정 잘못해서 5분마다 retry가 도는 걸 한 시간쯤 방치한 적이 있었는데, prod 발급이 막혀서 새벽에 staging issuer로 갈아끼웠다. 그날 이후 규칙으로 정했다. 새 도메인 추가나 issuer 변경은 무조건 staging부터.

spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory

staging 인증서는 브라우저에서 신뢰 안 하지만, ACME flow가 끝까지 도는지 확인하기엔 충분하다.

DNS propagation timeout — split-horizon에서 자주 터진다

Waiting for DNS-01 challenge propagation 상태에서 멈춰 있는 경우가 종종 있다. cert-manager 기본 동작은 도메인의 authoritative name server를 직접 쿼리해서 TXT 레코드가 보이면 ACME에 알리는 식이다. 그런데 사내 split-horizon DNS가 끼면 일부 NS가 늦게 동기화돼서 propagation 체크가 실패한다. 우리도 사내 Route53 inbound resolver 때문에 한참 헤맸다.

해결책은 dns01-recursive-nameservers 옵션으로 public resolver만 쓰게 강제하는 것:

extraArgs:
  - --dns01-recursive-nameservers-only
  - --dns01-recursive-nameservers=8.8.8.8:53,1.1.1.1:53

이렇게 하면 cert-manager가 authoritative를 안 보고 8.8.8.8/1.1.1.1을 통해 propagation을 확인한다. split-horizon 환경에서는 거의 필수다.

Cross-account Route53 — trust policy 함정

DNS zone은 networking 계정에 있고, EKS는 workload 계정에 있는 구조. 회사가 어느 정도 규모가 되면 흔한 패턴인데, 이 경우 cert-manager가 networking 계정의 role을 assume해야 한다.

ClusterIssuer의 solvers.dns01.route53.role에 networking 계정 role ARN을 적고, 그 role의 trust policy에 workload 계정의 cert-manager IRSA role을 sts:AssumeRole 가능하게 등록한다. 한 번 잘못 설정하면 에러 메시지가 그리 친절하지 않아서 한참 헤맨다. cert-manager Pod 로그에서 AccessDenied가 보이면 십중팔구 trust policy 문제다. networking 쪽 인프라 팀에 IAM 변경 PR을 보내는 흐름이라 PR review에 하루 걸리는 것도 미리 생각해 두자.

갱신은 자동, 모니터링은 직접

cert-manager는 만료 30일 전쯤 자동 renewal을 시도한다. 잘 동작하면 좋은데, 갱신이 조용히 실패하는 케이스가 있다. 대표적으로 IAM role 변경, ClusterIssuer 변경, Let's Encrypt rate limit hit. 갱신 실패는 인증서가 살아있는 동안엔 알람이 안 울려서 만료 직전에 발견되곤 한다.

우리는 결국 Prometheus rule을 직접 만들었다. cert-manager가 노출하는 certmanager_certificate_expiration_timestamp_seconds를 보고 14일 이내 만료될 인증서가 있으면 알람을 띄운다.

(certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 14

이걸 슬랙으로 보내게 했더니 그 뒤로 인증서 만료로 새벽에 깨는 일은 없어졌다. 추가로 certmanager_certificate_ready_status{condition="False"} 도 같이 보면 갱신 실패 자체를 미리 잡을 수 있다.

마무리

cert-manager는 깔아두면 잊고 살 수 있는 도구처럼 보이지만, 실제로는 위 함정들 때문에 한 번씩은 새벽에 깨게 된다. staging 분리, recursive nameserver 설정, cross-account trust policy 점검, 갱신 모니터링. 이 네 가지만 셋업 단계에서 미리 챙겨두면 운영 중 사고 확률이 크게 줄어든다.

그리고 올해 초에 Let's Encrypt가 발표한 DNS-PERSIST-01도 한 번 봐둘 만하다. 한 번 검증한 도메인에 대해 일정 기간 검증 상태를 유지하는 방식인데, staging은 Q1 2026 말, 프로덕션은 Q2 2026 중 예정이다. cert-manager 쪽에도 이슈가 열려 있고 1.20쯤에 들어올 가능성이 있다. 들어오면 DNS propagation 관련 함정 자체가 줄어들지 않을까 기대 중이다.

혹시 다른 함정 만나신 분 있으면 댓글로 공유 부탁드린다. 다음에는 cert-manager를 multi-cluster에서 어떻게 배포하는지(centralized vs per-cluster) 정리해보려고 한다.

반응형