IT/Kubernets

cert-manager DNS01 챌린지가 두 시간을 넘겨서야 통과했다

gfrog 2026. 9. 6. 12:11

지난 목요일이었다. 스테이징 클러스터에 새 와일드카드 인증서 하나를 붙이는 단순한 작업이었는데, Certificate 리소스가 Issuing 상태에서 안 벗어났다. 처음에는 그냥 Route53 반영 시간이 좀 걸리는 거겠거니 하고 커피 마시고 왔다. 돌아와서 봐도 그대로였다. 그때부터 두 시간 반 동안 삽질했다.

상황

  • cert-manager 1.16.2, EKS 1.30, ExternalDNS 병행
  • ClusterIssuer는 Let's Encrypt production, DNS01 solver는 Route53
  • 발급하려던 건 *.staging.example.internal 와일드카드
  • 기존에도 같은 방식으로 다른 도메인 3개는 잘 굴러가고 있었다

kubectl describe challenge 찍어보면 계속 이 메시지였다.

Waiting for DNS-01 challenge propagation:
DNS record for "_acme-challenge.staging.example.internal"
not yet propagated

Route53 콘솔에서 TXT 레코드는 이미 5분 전부터 박혀 있는 게 보였다. 그런데 왜 propagated가 아니라는 걸까.

첫 번째 헛짚음: 그냥 기다림

솔직히 처음 30분은 "DNS 전파 원래 오래 걸리잖아" 하면서 그냥 방치했다. TTL 60초로 박혀 있으니 최대 몇 분이면 될 텐데. 이게 첫 번째 실수였다. cert-manager는 자기가 정한 nameserver 리스트로 직접 물어봐서 확인한다. 로컬 dig가 잘 돌아온다고 cert-manager가 통과시키는 게 아니다.

두 번째 헛짚음: DNS 서버 지정

cert-manager 문서 뒤져보다가 --dns01-recursive-nameservers 옵션을 알게 됐다. 기본값은 1.1.1.1:53,8.8.8.8:53이다. Helm values에 명시적으로 넣어보고 파드 재시작.

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

여전히 안 됐다. 근데 이때부터 힌트가 나오기 시작했다. cert-manager 파드 로그에 이런 게 찍혔다.

DNS record ... has 2 records for "_acme-challenge...",
expected 1

레코드가 2개? Route53 콘솔에서는 하나만 보였는데.

진짜 원인: 사설망 split-horizon DNS

우리 스테이징은 staging.example.internal 존을 두 군데서 관리하고 있었다. 하나는 Route53 프라이빗 존, 하나는 온프렘에서 넘어온 유산 시스템의 BIND. 워크로드는 대부분 Route53 쪽만 보게 세팅돼 있었는데, cert-manager 파드가 사용하는 kube-dns의 upstream이 하필 그 BIND를 거치도록 라우팅되고 있었다.

cert-manager가 DNS01 검증을 할 때, 옵션에 관계없이 도메인의 SOA 레코드를 먼저 찾아서 authoritative nameserver를 자동으로 판단하는 로직이 있다. _acme-challenge.staging.example.internal의 SOA를 물었더니 사내 BIND가 응답했고, BIND에는 오래된 delegation이 남아 있어서 엉뚱한 nameserver를 authoritative라고 알려줬다. 그 nameserver로 TXT를 물어보니 옛날 인증서 시도 때 남은 잔재 레코드 하나가 더 있었던 거다.

정리하면 이런 흐름이었다.

  1. cert-manager가 SOA 찾기 → 사내 BIND 응답 → 잘못된 authoritative NS 반환
  2. 그 NS에 TXT 조회 → 신규 레코드 + 잔재 레코드 총 2개
  3. expected 1 실패 → 재시도 → 무한 루프

해결

우선 급한 불부터. Route53 프라이빗 존에 딱 필요한 인증 도메인만 별도의 authoritative 응답을 강제하도록 delegation을 정리했다. 그리고 cert-manager의 DNS 리졸버 우선순위를 아예 우회하도록 값 하나를 더 넣었다.

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

핵심은 두 번째 줄인 --dns01-recursive-nameservers-only다. 이걸 붙이면 authoritative 자동 판단을 건너뛰고 지정한 recursive 리졸버만 쓴다. 우리 케이스에서는 사내 BIND를 아예 회피할 수 있었다.

붙이고 파드 재시작 5분 뒤에 챌린지가 통과됐다. Issuer가 order를 finalize하고 Secret에 tls.crt가 박히는 걸 보니 눈물이 좀 났다.

남는 질문들

--dns01-recursive-nameservers-only를 붙이면 프라이빗 존 도메인 인증은 어떻게 될까? 우리 케이스는 Let's Encrypt public이라 어차피 외부에서 검증하니 문제 없었다. 근데 내부 CA로 사설 도메인 인증서 발급하는 팀도 있어서, 그 케이스는 별도 issuer로 분리해두는 게 나을 것 같다. 이건 아직 팀 내부에서 논의 중이다.

그리고 잔재 TXT 레코드 문제. 매번 챌린지 성공/실패 후 cleanup이 안 되는 케이스가 어떤 조건에서 발생하는지는 계속 관찰 중이다. cert-manager 이슈 트래커에 비슷한 사례가 몇 개 올라와 있는데 명확한 재현 조건이 아직 안 잡혔다.

교훈이라면

  • DNS01 실패 시 로그의 expected N 숫자를 무조건 먼저 볼 것
  • Route53 콘솔이 진실이 아니다. cert-manager 파드가 무슨 nameserver를 물어보는지가 진실이다
  • split-horizon DNS 환경에서는 --dns01-recursive-nameservers-only 기본으로 켜두는 걸 고려하자

혹시 비슷한 상황 겪으신 분 있으면 어떻게 해결하셨는지 궁금하다. 우리 팀 세팅이 좀 특수한 건지, 아니면 다들 한 번씩은 겪는 건지.