Skip to content
Published on

Ingress TLS 자동화 — cert-manager와 ACME로 인증서 완전 자동화

Authors

들어가며

운영 클러스터에서 TLS 인증서를 수동으로 관리하던 시절을 떠올려 보면, 90일짜리 Let’s Encrypt 인증서를 매번 갱신하느라 캘린더에 알림을 걸어두고, 만료 직전에 부랴부랴 갱신 스크립트를 돌리던 기억이 납니다. 인증서 하나만 깜빡해도 새벽에 "사이트가 안 열린다"는 호출을 받기 일쑤였습니다.

cert-manager는 이 문제를 근본적으로 해결합니다. Kubernetes 네이티브 컨트롤러로서 인증서의 발급, 갱신, 폐기 라이프사이클 전체를 선언적으로 관리합니다. Ingress에 어노테이션 한 줄만 붙이면, 나머지는 cert-manager가 알아서 ACME 프로토콜로 Let’s Encrypt와 통신해 인증서를 받아오고 만료 전에 자동으로 갱신합니다.

2026년 현재의 맥락도 짚고 넘어가겠습니다. Ingress API는 사실상 frozen 상태로, 더 이상 새로운 기능이 추가되지 않습니다. 후계 표준은 Gateway API이며, cert-manager 역시 Gateway API의 TLS 자동화를 지원합니다. ingress-nginx는 유지보수 모드로 전환되며 보안 패치 위주로만 관리되는 흐름이라, 신규 구축이라면 Gateway API를 함께 검토하는 것이 좋습니다. 이 글에서는 Ingress 중심으로 설명하되, Gateway API와의 연계도 마지막에 다루겠습니다.

이 글의 목표는 다음과 같습니다.

  • cert-manager의 CRD 구조와 컨트롤 흐름을 정확히 이해하기
  • HTTP-01과 DNS-01 챌린지의 차이와 선택 기준 정립하기
  • 와일드카드 인증서를 안전하게 발급하기
  • Ingress 어노테이션 기반 통합(ingress-shim) 동작 원리 파악하기
  • 갱신과 만료를 Prometheus로 모니터링하기
  • 사내 CA 및 HashiCorp Vault Issuer 연동하기
  • 챌린지 실패를 체계적으로 진단하기

cert-manager 아키텍처

cert-manager는 여러 개의 CRD(Custom Resource Definition)와 그것을 조정하는 컨트롤러들로 구성됩니다. 핵심 리소스의 관계를 먼저 그림으로 이해하면 전체 흐름이 명확해집니다.

┌──────────────────────────────────────────────────────────┐
│  Issuer / ClusterIssuer   (인증서 발급 주체 정의)         │
│  - ACME(Let's Encrypt), CA, Vault, SelfSigned ...         │
└───────────────────────────┬──────────────────────────────┘
                            │ 참조
┌──────────────────────────────────────────────────────────┐
│  Certificate              (원하는 인증서의 선언적 명세)    │
│  - dnsNames, secretName, issuerRef, duration ...          │
└───────────────────────────┬──────────────────────────────┘
                            │ 발급 요청 생성
┌──────────────────────────────────────────────────────────┐
│  CertificateRequest       (단일 발급 시도, CSR 포함)      │
└───────────────────────────┬──────────────────────────────┘
                            │ ACME일 경우
┌──────────────────────────────────────────────────────────┐
│  Order                    (ACME 주문 = 하나의 발급 트랜잭션)│
└───────────────────────────┬──────────────────────────────┘
                            │ 도메인 소유 검증
┌──────────────────────────────────────────────────────────┐
│  Challenge                (HTTP-01 또는 DNS-01 검증 단위)  │
└───────────────────────────┬──────────────────────────────┘
                            │ 검증 성공 시
┌──────────────────────────────────────────────────────────┐
│  Secret (kubernetes.io/tls)   tls.crt + tls.key 저장      │
└──────────────────────────────────────────────────────────┘

각 리소스의 역할을 정리하면 다음과 같습니다.

리소스범위역할
Issuer네임스페이스해당 네임스페이스 안에서만 인증서 발급
ClusterIssuer클러스터 전역모든 네임스페이스에서 공유하는 발급 주체
Certificate네임스페이스원하는 인증서의 최종 상태를 선언
CertificateRequest네임스페이스한 번의 CSR 발급 시도를 표현
Order네임스페이스ACME 발급 트랜잭션(여러 도메인 묶음)
Challenge네임스페이스도메인 소유 증명 단위(HTTP-01/DNS-01)

여기서 중요한 멘탈 모델은 "Certificate는 원하는 상태(desired state)이고, 나머지 하위 리소스들은 그 상태를 달성하기 위해 cert-manager가 자동으로 생성하고 소멸시키는 중간 산출물"이라는 점입니다. 운영자는 보통 Issuer/ClusterIssuer와 Certificate(혹은 Ingress 어노테이션)만 직접 다루고, CertificateRequest/Order/Challenge는 디버깅할 때만 들여다보게 됩니다.

설치

cert-manager는 Helm 또는 정적 매니페스트로 설치합니다. 프로덕션에서는 CRD를 명시적으로 관리하는 것을 권장합니다.

# Helm 저장소 등록
helm repo add jetstack https://charts.jetstack.io
helm repo update

# CRD 포함 설치
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.17.0 \
  --set crds.enabled=true

# 설치 확인
kubectl get pods -n cert-manager
kubectl get crd | grep cert-manager.io

설치가 완료되면 세 개의 핵심 파드가 뜹니다. cert-manager(컨트롤러), cert-manager-webhook(검증/변환 웹훅), cert-manager-cainjector(CA 번들 주입기)입니다. 웹훅이 정상 동작하지 않으면 CRD 적용 자체가 거부되므로, 설치 직후 웹훅 파드의 상태를 반드시 확인합니다.

ACME 챌린지: 도메인 소유 증명

Let’s Encrypt 같은 ACME CA가 인증서를 발급하려면, 요청자가 해당 도메인을 실제로 통제하고 있음을 증명해야 합니다. ACME 프로토콜(RFC 8555)은 이를 챌린지(challenge)로 정의하며, cert-manager는 HTTP-01과 DNS-01 두 가지를 지원합니다.

HTTP-01 챌린지 흐름

사용자 도메인: app.example.com

1. cert-manager → ACME 서버: app.example.com 인증서 주문
2. ACME 서버 → cert-manager: 토큰 발급 + 검증 경로 안내
3. cert-manager: 임시 파드/서비스/Ingress 생성
   경로 /.well-known/acme-challenge/<token> 응답 준비
4. ACME 서버 → http://app.example.com/.well-known/acme-challenge/<token>
   (80 포트로 HTTP 요청)
5. 응답이 기대한 키 인증 값과 일치하면 검증 성공
6. ACME 서버 → cert-manager: 인증서 발급
7. cert-manager: 임시 리소스 정리 + Secret 저장

HTTP-01은 80 포트로 들어오는 외부 인터넷 트래픽이 클러스터의 Ingress까지 도달해야 합니다. 즉, 도메인의 A 레코드가 Ingress의 공인 IP를 가리키고 있어야 하고, 방화벽에서 80 포트가 열려 있어야 합니다.

DNS-01 챌린지 흐름

사용자 도메인: app.example.com (또는 *.example.com)

1. cert-manager → ACME 서버: 인증서 주문
2. ACME 서버 → cert-manager: 토큰 발급
3. cert-manager → DNS 공급자 API:
   _acme-challenge.example.com TXT 레코드에 검증 값 등록
4. ACME 서버 → DNS 조회: _acme-challenge.example.com TXT 확인
5. TXT 값이 일치하면 검증 성공
6. ACME 서버 → cert-manager: 인증서 발급
7. cert-manager: TXT 레코드 정리 + Secret 저장

DNS-01은 인바운드 트래픽이 전혀 필요 없습니다. 대신 cert-manager가 DNS 공급자(Route53, Cloud DNS, Cloudflare 등)의 API 자격증명을 가지고 TXT 레코드를 동적으로 생성/삭제할 수 있어야 합니다.

HTTP-01 vs DNS-01 비교

항목HTTP-01DNS-01
검증 방식80 포트 HTTP 응답DNS TXT 레코드
인바운드 트래픽 필요필요(공인 IP + 80 포트)불필요
와일드카드 인증서불가가능(필수)
DNS API 자격증명불필요필요
내부망/프라이빗 클러스터어려움적합
검증 지연빠름(초 단위)DNS 전파 대기 필요
멀티 도메인 SAN도메인별 HTTP 검증TXT 묶음 검증
주요 실패 원인라우팅/방화벽/리다이렉트DNS 권한/전파 지연

선택 기준을 단순화하면 다음과 같습니다. 공개된 단일 도메인이고 인바운드 80 포트를 열 수 있다면 HTTP-01이 가장 간단합니다. 와일드카드 인증서가 필요하거나, 인바운드를 열 수 없는 프라이빗 클러스터이거나, 다수 도메인을 한 번에 처리하고 싶다면 DNS-01을 선택합니다.

ClusterIssuer 구성하기

대부분의 환경에서는 클러스터 전역에서 공유 가능한 ClusterIssuer를 사용합니다. 먼저 Let’s Encrypt 스테이징과 프로덕션 두 개를 만드는 것을 권장합니다. 스테이징은 레이트 리밋이 관대해서 테스트에 적합하고, 프로덕션은 신뢰되는 인증서를 발급하지만 발급 횟수 제한이 엄격합니다.

HTTP-01 방식 ClusterIssuer

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: platform@example.com
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: platform@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

privateKeySecretRef는 ACME 계정 키를 저장할 Secret 이름입니다. 처음 ClusterIssuer를 적용하면 cert-manager가 자동으로 ACME 계정을 등록하고 이 Secret에 계정 키를 보관합니다. 이 Secret을 삭제하면 계정 재등록이 일어나므로 주의합니다.

DNS-01 방식 ClusterIssuer (Route53 예시)

DNS-01을 쓰려면 DNS 공급자 API 자격증명이 필요합니다. AWS Route53을 예로 들면, IAM 자격증명 또는 IRSA(IAM Roles for Service Accounts)를 사용합니다.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: platform@example.com
    privateKeySecretRef:
      name: letsencrypt-dns-account-key
    solvers:
      - dns01:
          route53:
            region: ap-northeast-2
            hostedZoneID: Z0123456789ABCDEFGHIJ
            # IRSA를 쓰면 accessKeyID/secretAccessKey 생략 가능
        selector:
          dnsZones:
            - example.com

여러 솔버를 selector로 분기할 수도 있습니다. 예컨대 *.example.com은 DNS-01, 그 외 공개 도메인은 HTTP-01로 처리하도록 한 ClusterIssuer에 두 솔버를 정의할 수 있습니다.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-mixed
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: platform@example.com
    privateKeySecretRef:
      name: letsencrypt-mixed-account-key
    solvers:
      - dns01:
          route53:
            region: ap-northeast-2
            hostedZoneID: Z0123456789ABCDEFGHIJ
        selector:
          dnsZones:
            - example.com
      - http01:
          ingress:
            ingressClassName: nginx

cert-manager는 요청된 도메인이 selector의 dnsZones와 가장 구체적으로 매칭되는 솔버를 우선 선택합니다.

Certificate 리소스 직접 작성하기

Ingress 어노테이션을 쓰면 Certificate가 자동 생성되지만, 명시적으로 Certificate를 작성하면 발급 정책을 더 세밀하게 통제할 수 있습니다.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app-tls
  namespace: production
spec:
  secretName: app-tls
  duration: 2160h        # 90일
  renewBefore: 720h      # 만료 30일 전 갱신
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always
  dnsNames:
    - app.example.com
    - www.app.example.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
    group: cert-manager.io

주요 필드를 설명하겠습니다.

  • secretName: 발급된 인증서가 저장될 Secret. 타입은 kubernetes.io/tls이며 tls.crt, tls.key 키를 가집니다.
  • renewBefore: 만료 며칠 전에 갱신할지 지정합니다. 기본값은 인증서 수명의 3분의 1 시점입니다.
  • privateKey.rotationPolicy: Always는 갱신 시마다 새 키를 생성합니다. 보안상 권장됩니다.
  • issuerRef: 어떤 Issuer/ClusterIssuer를 사용할지 지정합니다.

와일드카드 인증서

와일드카드 인증서(*.example.com)는 반드시 DNS-01 챌린지로만 발급할 수 있습니다. HTTP-01은 와일드카드를 지원하지 않습니다. ACME 사양상 와일드카드 도메인의 소유 증명은 DNS 레코드로만 가능하기 때문입니다.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example-tls
  namespace: production
spec:
  secretName: wildcard-example-tls
  dnsNames:
    - "*.example.com"
    - "example.com"
  issuerRef:
    name: letsencrypt-dns
    kind: ClusterIssuer
    group: cert-manager.io

위 예시처럼 와일드카드와 apex 도메인을 함께 SAN으로 넣는 것이 일반적입니다. 와일드카드 *.example.comexample.com 자체를 커버하지 않으므로 둘 다 필요합니다.

Ingress 통합 (ingress-shim)

cert-manager의 가장 편리한 기능은 Ingress 어노테이션 기반 자동화입니다. 이를 담당하는 컴포넌트가 ingress-shim입니다. Ingress 리소스에 특정 어노테이션을 붙이면, ingress-shim이 그 Ingress의 TLS 명세를 읽어 Certificate 리소스를 자동으로 생성하고 관리합니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-svc
                port:
                  number: 8080

이 매니페스트를 적용하면 다음 순서로 동작합니다.

1. Ingress 생성 (cert-manager.io/cluster-issuer 어노테이션 포함)
2. ingress-shim이 tls[].secretName과 hosts를 읽음
3. ingress-shim이 Certificate 리소스를 자동 생성
   - dnsNames = tls[].hosts
   - secretName = tls[].secretName
   - issuerRef = 어노테이션의 cluster-issuer
4. cert-manager 본 컨트롤러가 Certificate를 처리
   → CertificateRequest → Order → Challenge → Secret 발급
5. Ingress Controller가 발급된 Secret을 TLS 종단에 사용

핵심 어노테이션은 다음과 같습니다.

어노테이션의미
cert-manager.io/cluster-issuer사용할 ClusterIssuer 이름
cert-manager.io/issuer사용할 네임스페이스 Issuer 이름
cert-manager.io/common-name인증서 CN 명시
cert-manager.io/duration인증서 수명
cert-manager.io/renew-before갱신 시점

주의할 점은 ingress-shim이 자동 생성한 Certificate를 직접 수정하면 안 된다는 것입니다. 다음 조정 주기에 ingress-shim이 Ingress의 명세를 기준으로 다시 덮어쓰기 때문입니다. 세밀한 제어가 필요하면 어노테이션을 제거하고 Certificate를 직접 관리하는 편이 낫습니다.

운영: 갱신과 만료 모니터링

cert-manager는 갱신을 자동으로 처리하지만, "자동이니까 신경 안 써도 된다"는 태도는 위험합니다. DNS API 자격증명 만료, 레이트 리밋 도달, 솔버 설정 오류 등으로 갱신이 조용히 실패하면 결국 인증서가 만료됩니다. 따라서 모니터링은 필수입니다.

인증서 상태 확인

# 모든 Certificate 상태 한눈에 보기
kubectl get certificate -A

# 특정 인증서 상세 (Ready 조건, 만료일 확인)
kubectl describe certificate app-tls -n production

# cert-manager가 추적하는 발급 진행 상황
kubectl get certificaterequest,order,challenge -A

kubectl get certificate의 출력에서 READY 컬럼이 True여야 정상입니다. False라면 describe로 Events와 Conditions를 확인합니다.

Prometheus 메트릭

cert-manager는 기본적으로 9402 포트로 Prometheus 메트릭을 노출합니다. 가장 중요한 메트릭은 인증서 만료 시각입니다.

# 인증서 만료 시각(Unix epoch 초)
certmanager_certificate_expiration_timestamp_seconds

# 인증서 갱신 시각
certmanager_certificate_renewal_timestamp_seconds

# Ready 상태 게이지(1=Ready, 0=NotReady)
certmanager_certificate_ready_status

# ACME 클라이언트 요청 수(상태 코드별)
certmanager_http_acme_client_request_count

# 컨트롤러 큐 처리 지연
certmanager_controller_sync_call_count

만료 임박 알림은 PromQL로 다음처럼 구성합니다. 표현식 자체는 코드 블록 안에 두어야 안전합니다.

# 7일(604800초) 이내 만료 예정 인증서 경보
certmanager_certificate_expiration_timestamp_seconds - time() < 604800

# Ready 상태가 아닌 인증서 경보
certmanager_certificate_ready_status == 0

Alertmanager 규칙 예시는 다음과 같습니다.

groups:
  - name: cert-manager
    rules:
      - alert: CertificateExpiringSoon
        expr: certmanager_certificate_expiration_timestamp_seconds - time() < 604800
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "인증서가 7일 이내 만료 예정"
          description: "namespace/name 라벨을 확인하세요"
      - alert: CertificateNotReady
        expr: certmanager_certificate_ready_status == 0
        for: 30m
        labels:
          severity: critical
        annotations:
          summary: "인증서가 Ready 상태가 아님"

ServiceMonitor를 통해 Prometheus Operator가 cert-manager 메트릭을 수집하도록 설정합니다.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: cert-manager
  namespace: cert-manager
  labels:
    release: kube-prometheus-stack
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: cert-manager
  endpoints:
    - port: http-metrics
      interval: 60s

레이트 리밋 관리

Let’s Encrypt 프로덕션 엔드포인트는 엄격한 레이트 리밋을 적용합니다. 대표적으로 등록 도메인당 일주일에 50개 인증서 제한이 있습니다. 테스트는 반드시 스테이징 엔드포인트에서 진행하고, 프로덕션 발급은 실제 필요한 경우에만 합니다. CI 파이프라인에서 클러스터를 반복 생성/삭제하면 빠르게 리밋에 도달하므로 주의합니다.

사내 CA 및 Vault Issuer

모든 인증서가 공개 CA에서 와야 하는 것은 아닙니다. 내부 서비스 간 mTLS나 사내 도메인은 사내 CA로 발급하는 것이 일반적입니다.

자체 CA Issuer

먼저 루트 또는 중간 CA 키 쌍을 Secret으로 보관한 뒤, CA Issuer를 만듭니다.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-ca-key-pair
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-svc-tls
  namespace: backend
spec:
  secretName: internal-svc-tls
  dnsNames:
    - payments.svc.cluster.local
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer

이 방식은 ACME 챌린지가 필요 없어 인바운드/DNS 요구사항 없이 즉시 발급됩니다. 단, 클라이언트가 사내 CA를 신뢰하도록 CA 번들을 배포해야 합니다.

HashiCorp Vault Issuer

Vault의 PKI 시크릿 엔진을 백엔드로 사용하면, 중앙 집중식 인증서 발급 정책과 감사 로그를 활용할 수 있습니다.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: vault-issuer
spec:
  vault:
    server: https://vault.internal.example.com:8200
    path: pki_int/sign/example-dot-com
    auth:
      kubernetes:
        role: cert-manager
        mountPath: /v1/auth/kubernetes
        serviceAccountRef:
          name: cert-manager-vault

Vault Issuer는 Vault의 Kubernetes 인증 방식을 통해 ServiceAccount 토큰으로 인증합니다. path는 Vault PKI 엔진의 서명 경로이며, Vault 측에서 해당 role에 허용 도메인과 수명 정책을 정의합니다. 이 구조는 인증서 발급 권한을 Vault 정책으로 일원화할 수 있어 규제 환경에서 유리합니다.

Gateway API와의 관계

앞서 언급했듯이 Ingress API는 frozen 상태이고 Gateway API가 후계 표준입니다. cert-manager는 Gateway API에 대해서도 자동 인증서 발급을 지원합니다. ingress-shim이 Ingress의 어노테이션을 보고 Certificate를 만들듯, Gateway 리소스의 어노테이션을 보고 Certificate를 만드는 방식입니다.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: app-gateway
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  gatewayClassName: nginx
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: app.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - name: app-tls
            kind: Secret

이 기능은 과거 실험적(experimental) 기능 게이트를 통해 활성화해야 했던 시기가 있었으나, 최신 버전에서는 Gateway API 지원이 정식 기능으로 자리잡는 흐름입니다. cert-manager 컨트롤러 플래그로 Gateway API 지원을 켤 수 있으며, listener의 hostname을 dnsNames로, certificateRefs의 첫 Secret을 secretName으로 사용해 Certificate를 자동 생성합니다. 신규 클러스터를 구축한다면 Ingress 대신 Gateway API 기반 TLS 자동화를 처음부터 채택하는 것을 권장합니다.

트러블슈팅: 챌린지 실패 진단

챌린지 실패는 cert-manager 운영에서 가장 흔한 문제입니다. 다음 진단 플로우를 따라가면 대부분 원인을 좁힐 수 있습니다.

        인증서가 Ready=False
   kubectl describe certificate 확인
        Events에 무슨 메시지?
      ┌─────────┴──────────┐
      │                    │
 Order 생성됨          Order 없음/에러
      │                    │
      ▼                    ▼
 kubectl describe       issuerRef 오타?
   order ...            ClusterIssuer Ready?
      │                 ACME 계정 등록 실패?
 Challenge 상태 확인
 kubectl describe challenge ...
 ┌────┴─────┐
 │          │
HTTP-01    DNS-01
 │          │
 ▼          ▼
1. DNS A 레코드가     1. DNS API 자격증명 유효?
   Ingress IP?       2. TXT 레코드 실제 생성됨?
2. 80 포트 외부       3. DNS 전파 완료?
   접근 가능?         4. CNAME 위임 설정?
3. 챌린지 경로        5. zone selector 매칭?
   응답 200?
4. HTTPS 강제
   리다이렉트가
   80 검증을 방해?

HTTP-01 실패의 흔한 원인

가장 빈번한 실패는 80 포트로 들어온 ACME 검증 요청이 강제 HTTPS 리다이렉트에 막히는 경우입니다. ingress-nginx의 ssl-redirect가 켜져 있으면 검증 경로마저 HTTPS로 돌려보내 검증이 깨질 수 있습니다. cert-manager가 생성하는 임시 검증 Ingress는 이를 우회하도록 설계되어 있지만, 글로벌 설정이 강하게 걸려 있으면 문제가 됩니다.

다음 명령으로 검증 경로가 실제로 응답하는지 확인합니다. URL의 토큰 부분은 실제 challenge 리소스에서 확인합니다.

# 진행 중인 challenge 확인
kubectl get challenge -A

# challenge 상세에서 토큰과 URL 확인
kubectl describe challenge <challenge-name> -n production

# 외부에서 검증 경로 직접 호출 (TOKEN은 위에서 확인한 값)
curl -v http://app.example.com/.well-known/acme-challenge/TOKEN

응답이 200이 아니거나, 301/302로 HTTPS 리다이렉트된다면 라우팅과 리다이렉트 설정을 점검합니다.

DNS-01 실패의 흔한 원인

DNS-01에서는 TXT 레코드가 실제로 생성되었는지, 그리고 전파되었는지를 직접 확인하는 것이 핵심입니다.

# cert-manager가 등록한 TXT 레코드 직접 조회
dig +short TXT _acme-challenge.example.com

# 권한 있는 네임서버에 직접 질의
dig @ns-1.example-dns.com TXT _acme-challenge.example.com

# challenge 이벤트에서 DNS 공급자 에러 확인
kubectl describe challenge <challenge-name> -n production

DNS API 자격증명이 만료되었거나 권한이 부족하면 TXT 레코드 생성 자체가 실패합니다. 또한 CNAME 위임을 쓰는 경우 위임 대상 존에 레코드가 만들어졌는지 확인해야 합니다. DNS 전파가 느린 공급자에서는 propagation 타임아웃을 늘려야 할 수도 있습니다.

cmctl로 빠르게 진단하기

cert-manager는 cmctl이라는 진단 CLI를 제공합니다.

# 인증서 상태 요약
cmctl status certificate app-tls -n production

# 강제 갱신 트리거
cmctl renew app-tls -n production

# 발급 진행 상황 추적
cmctl inspect secret app-tls -n production

운영 체크리스트

프로덕션 도입 전후로 다음 항목을 점검하면 사고를 크게 줄일 수 있습니다.

  • 스테이징 ClusterIssuer로 먼저 검증한 뒤 프로덕션으로 전환했는가
  • ClusterIssuer가 Ready 상태이고 ACME 계정이 정상 등록되었는가
  • 와일드카드 인증서는 DNS-01 솔버를 사용하는가
  • DNS-01 API 자격증명에 만료/회전 정책이 있는가
  • certmanager_certificate_expiration_timestamp_seconds 기반 만료 알림이 있는가
  • certmanager_certificate_ready_status == 0 경보가 걸려 있는가
  • renewBefore 값이 운영 대응 시간을 고려해 충분히 여유 있는가
  • 인증서 Secret을 백업하거나 GitOps로 재현 가능한가
  • cert-manager 웹훅 파드의 고가용성(복제본 2개 이상)을 확보했는가
  • Let’s Encrypt 레이트 리밋을 CI 환경에서 소진하지 않도록 격리했는가
  • 사내 CA를 쓰는 경우 클라이언트에 CA 번들이 배포되었는가
  • 신규 구축이라면 Gateway API 기반 TLS 자동화를 검토했는가

마치며

cert-manager는 TLS 인증서 관리를 "정기적으로 챙겨야 하는 운영 부담"에서 "선언하면 알아서 돌아가는 인프라"로 바꿔 줍니다. 핵심은 CRD 구조를 정확히 이해하고, 환경에 맞는 챌린지 방식을 선택하며, 자동 갱신을 맹신하지 않고 모니터링을 함께 구축하는 것입니다.

HTTP-01은 공개 단일 도메인에 간단하고, DNS-01은 와일드카드와 프라이빗 환경에 필수입니다. Ingress 어노테이션 기반 ingress-shim은 대부분의 사용 사례를 한 줄로 해결하지만, 세밀한 제어가 필요하면 Certificate를 직접 다루면 됩니다. 사내 CA와 Vault Issuer로 내부 인증서까지 일원화하면, 클러스터 전체의 TLS를 하나의 일관된 흐름으로 관리할 수 있습니다.

마지막으로 2026년의 방향성을 다시 강조하면, Ingress API는 frozen이고 Gateway API가 후계입니다. 지금 새로 구축한다면 Gateway API 기반의 TLS 자동화를 처음부터 고려하는 것이 미래를 위한 선택입니다.

참고 자료