Post

[Kubernetes] cert-manager 완전 정리: TLS 인증서 자동 발급과 갱신

cert-manager

1. 개요

한 줄 요약: Kubernetes에서 TLS 인증서를 자동으로 발급, 갱신, 관리하는 인증서 관리 시스템

언제 사용하나:

  • Ingress에 HTTPS/TLS 적용 시 인증서 자동 발급
  • Let’s Encrypt로 무료 SSL 인증서 자동 갱신
  • 내부 Private CA로 자체 서명 인증서 발급
  • 인증서 만료 전 자동 갱신으로 장애 방지

인증서 발급 전체 흐름

1
2
3
4
5
6
Certificate 리소스 생성
  → Issuer/ClusterIssuer 참조
  → ACME Challenge 수행
  → 인증서 발급
  → Secret 저장 (tls.crt, tls.key)
  → Ingress에서 Secret 참조하여 HTTPS 적용

2. 핵심 개념

2.1. 주요 구성 요소

리소스역할
Certificate인증서 요청 리소스 — 어떤 도메인의 인증서가 필요한지 정의
Issuer네임스페이스 범위의 인증서 발급자
ClusterIssuer클러스터 전체 범위의 인증서 발급자
Secret발급된 인증서 저장 (tls.crt, tls.key)
CertificateRequestCertificate → Issuer 간 인증서 요청 중간 리소스
ChallengeACME 프로토콜의 도메인 소유 증명 방식

2.2. Issuer vs ClusterIssuer

항목IssuerClusterIssuer
범위특정 네임스페이스클러스터 전체
사용 시기네임스페이스별 독립 관리여러 네임스페이스에서 공통 사용
예시개발팀별 Let’s Encrypt 계정회사 공통 CA 인증서

2.3. ACME Challenge 방식 비교

방식설명장점단점사용 시기
HTTP-01http://<domain>/.well-known/acme-challenge/ 경로에 파일 생성간단, DNS 설정 불필요80 포트 필요, 와일드카드 불가단일 도메인
DNS-01DNS TXT 레코드 추가로 도메인 소유 증명와일드카드 가능, 80/443 불필요DNS 제공자 API 필요와일드카드 인증서

2.4. 주요 특징

  • 자동 갱신: 인증서 만료 30일 전 자동 갱신 시도, 실패 시 매일 재시도
  • 다양한 Issuer 지원: Let’s Encrypt, Vault, Venafi, Self-signed
  • 선언적 관리: YAML로 인증서 요청 정의

3. 기본 명령어

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# cert-manager Pod 3개 실행 확인
# (cert-manager, cert-manager-webhook, cert-manager-cainjector)
kubectl get pods -n cert-manager

# ClusterIssuer / Issuer 확인
kubectl get clusterissuer
kubectl get issuer -n <namespace>

# Certificate 확인 (READY 상태 확인)
kubectl get certificate -n <namespace>

# Certificate 상세 정보 (Events에서 에러 확인)
kubectl describe certificate <cert-name> -n <namespace>

# 인증서 발급 진행 상태 확인
kubectl get certificaterequest -n <namespace>

# ACME Challenge 확인 (발급 중일 때)
kubectl get challenge -n <namespace>

# 발급된 인증서 Secret 확인
kubectl get secret <tls-secret-name> -n <namespace>

# 인증서 내용 확인 (만료일 등)
kubectl get secret <tls-secret-name> -n <namespace> \
  -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -text -noout

# cert-manager 로그 확인
kubectl logs -n cert-manager deploy/cert-manager -f

Certificate 상태 출력 예시:

1
2
3
4
5
6
7
8
9
10
11
NAME                    READY   SECRET           AGE
app-example-com-tls     True    app-tls-secret   5d

# describe 출력
Status:
  Conditions:
    Status:      True
    Type:        Ready
  Not After:     2024-03-06T12:00:00Z   ← 만료일
  Not Before:    2024-12-06T12:00:00Z
  Renewal Time:  2024-02-05T12:00:00Z   ← 갱신 시작 시간

4. 실무 패턴

4.1. cert-manager 설치

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Helm으로 설치 (권장)
helm repo add jetstack https://charts.jetstack.io
helm repo update

# CRD 먼저 설치
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.crds.yaml

# cert-manager 설치
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.13.0

# 설치 확인
kubectl get pods -n cert-manager

4.2. Let’s Encrypt ClusterIssuer

프로덕션용:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: traefik  # HyperCloud 환경

테스트용 Staging (Rate Limit 없음, 브라우저 신뢰 X):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
    - http01:
        ingress:
          class: traefik

Staging 먼저 테스트 필수: Production은 주당 50개/도메인 Rate Limit이 있습니다.

4.3. Ingress에 자동 인증서 발급 (annotation 방식)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: default
  annotations:
    kubernetes.io/ingress.class: traefik
    cert-manager.io/cluster-issuer: "letsencrypt-prod"  # 자동 Certificate 생성
spec:
  tls:
  - hosts:
    - app.example.com
    secretName: app-tls-secret   # 인증서가 저장될 Secret 이름
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app-service
            port:
              number: 8080

동작 과정:

1
2
3
4
5
6
1. Ingress 생성 → cert-manager가 annotation 감지
2. Certificate 리소스 자동 생성
3. Let's Encrypt에 인증서 요청
4. HTTP-01 Challenge 수행 (임시 Ingress 생성)
5. 인증서 발급 → app-tls-secret에 저장
6. Ingress가 Secret 참조하여 HTTPS 적용

4.4. Certificate 리소스 직접 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app-certificate
  namespace: default
spec:
  secretName: app-tls-secret       # 인증서가 저장될 Secret 이름
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - app.example.com
  - www.app.example.com
  duration: 2160h                  # 90일 (Let's Encrypt 기본)
  renewBefore: 720h                # 만료 30일 전부터 갱신 시도
1
2
3
4
5
# 적용 후 확인
kubectl get certificate app-certificate -n default
# READY: True → 발급 완료

kubectl get secret app-tls-secret -n default

4.5. 와일드카드 인증서 (DNS-01 Challenge)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-dns
    solvers:
    - dns01:
        route53:
          region: ap-northeast-2
          accessKeyID: AKIAIOSFODNN7EXAMPLE
          secretAccessKeySecretRef:
            name: route53-credentials
            key: secret-access-key
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-certificate
  namespace: default
spec:
  secretName: wildcard-tls-secret
  issuerRef:
    name: letsencrypt-dns
    kind: ClusterIssuer
  dnsNames:
  - "*.example.com"   # 와일드카드
  - "example.com"     # 루트 도메인
1
2
3
4
# Route53 자격 증명 Secret 생성
kubectl create secret generic route53-credentials \
  --from-literal=secret-access-key=<AWS_SECRET_ACCESS_KEY> \
  -n cert-manager

4.6. Self-signed 인증서 (개발/테스트)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: dev-certificate
  namespace: default
spec:
  secretName: dev-tls-secret
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
  dnsNames:
  - dev.example.local
  - "*.dev.example.local"

외부 CA 없이 즉시 발급되지만, 브라우저에서 “신뢰할 수 없는 인증서” 경고가 발생하므로 개발 환경에서만 사용합니다.

4.7. 내부 Private CA (금융권 폐쇄망 환경)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 1. Self-signed Root CA 생성
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-ca
  namespace: cert-manager
spec:
  secretName: my-ca-secret
  isCA: true                  # CA 인증서로 설정
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer
  commonName: "My Internal CA"
  dnsNames:
  - "My Internal CA"
---
# 2. Private CA를 Issuer로 등록
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: my-ca-issuer
spec:
  ca:
    secretName: my-ca-secret
---
# 3. Private CA로 내부 서비스 인증서 발급
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-app-cert
  namespace: default
spec:
  secretName: internal-app-tls
  issuerRef:
    name: my-ca-issuer
    kind: ClusterIssuer
  dnsNames:
  - internal-app.company.local

금융권 폐쇄망 보안 정책:

  • Let’s Encrypt 사용 불가 시 내부 CA 구축
  • 인증서 만료 알림 설정 (Prometheus Alert)
  • 인증서 Secret 접근 권한 최소화 (RBAC)

5. 트러블슈팅

5.1. 실무 디버깅 순서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Step 1: Certificate 상태 확인
kubectl get certificate app-certificate -n default
# READY: False → 문제 있음

# Step 2: Events 확인 (에러 메시지 확인)
kubectl describe certificate app-certificate -n default
# Events:
#   Warning  Failed  Issuer not found

# Step 3: ClusterIssuer 상태 확인
kubectl get clusterissuer letsencrypt-prod
# READY: True 확인

# Step 4: CertificateRequest 확인
kubectl get certificaterequest -n default
kubectl describe certificaterequest <n> -n default

# Step 5: Challenge 확인 (발급 진행 중)
kubectl get challenge -n default
# State: pending → 진행 중
# State: valid   → 성공

# Step 6: 임시 Ingress 확인 (HTTP-01)
kubectl get ingress -n default
# cm-acme-http-solver-xxx 임시 Ingress 생성 여부 확인

# Step 7: 도메인 접근 테스트
curl -v http://app.example.com/.well-known/acme-challenge/

# Step 8: cert-manager 로그
kubectl logs -n cert-manager deploy/cert-manager -f

5.2. 추가 디버깅 명령어

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ACME Challenge 수동 테스트 (HTTP-01)
curl -v http://app.example.com/.well-known/acme-challenge/test
# 200 OK 응답 확인

# 인증서 재발급 강제 실행
kubectl delete certificaterequest <request-name> -n <namespace>
kubectl delete secret <tls-secret-name> -n <namespace>
# Certificate가 자동으로 재요청

# Let's Encrypt Rate Limit 확인
# https://crt.sh/?q=example.com 에서 최근 발급 내역 조회

# DNS-01 Challenge TXT 레코드 확인
dig app.example.com TXT
# _acme-challenge.app.example.com TXT 레코드 확인

# cert-manager webhook 확인
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations
# cert-manager-webhook이 있어야 함

5.3. 흔한 실수 7가지

1. ClusterIssuer 생성 전 Certificate 생성

1
2
3
4
5
❌ ClusterIssuer: letsencrypt-prod 없음
   Certificate에서 참조
→ Error: issuer not found

✅ ClusterIssuer 먼저 생성 후 Certificate 생성

2. HTTP-01 Challenge 실패 (80 포트 접근 불가)

1
2
3
4
5
6
❌ Ingress Controller가 80 포트 리스닝 안 함
   방화벽에서 80 포트 차단
→ Challenge 실패, 인증서 발급 안 됨

✅ 80 포트 외부 접근 가능하도록 설정
   또는 DNS-01 Challenge로 변경

3. Let’s Encrypt Rate Limit 초과

1
2
3
4
5
❌ 동일 도메인으로 1시간에 5회 이상 요청
→ Error: too many failed authorizations recently

✅ Staging 환경에서 먼저 테스트
   letsencrypt-staging 사용 후 prod로 전환

4. Ingress annotation 오타

1
2
3
4
❌ cert-manager.io/issuer: "letsencrypt-prod"
   (ClusterIssuer인데 cluster-issuer 대신 issuer 사용)

✅ cert-manager.io/cluster-issuer: "letsencrypt-prod"

5. 도메인 DNS가 Ingress를 가리키지 않음

1
2
3
4
5
6
❌ app.example.com → DNS 미설정 또는 잘못된 IP
→ HTTP-01 Challenge 실패

✅ 도메인 DNS A 레코드 확인
   nslookup app.example.com
   # Ingress Controller IP와 일치해야 함

6. 인증서 갱신 실패

1
2
3
4
5
6
❌ 갱신 시도 중 Challenge 실패
   cert-manager Pod 장애
→ HTTPS 접속 불가

✅ renewBefore: 720h (30일) 권장
   cert-manager 로그 모니터링

7. 여러 Ingress가 동일 도메인 사용

1
2
3
4
5
❌ Ingress A: app.example.com
   Ingress B: app.example.com
→ 인증서 충돌, Challenge 실패

✅ 하나의 Ingress로 통합 또는 다른 도메인 사용

6. 운영 참고사항

자동 갱신 타이밍

  • Let’s Encrypt: 90일 유효, 30일 전부터 갱신 시도
  • renewBefore: 720h (30일) 권장
  • 갱신 실패 시 매일 재시도

Staging vs Production

항목StagingProduction
용도테스트실서비스
Rate Limit없음주당 50개/도메인
브라우저 신뢰XO

와일드카드 제약

  • HTTP-01 Challenge는 와일드카드 불가
  • DNS-01 Challenge만 와일드카드 지원 (DNS 제공자 API 키 필요)

인증서 만료 모니터링 (Prometheus Alert)

# 7일 이내 만료 예정 인증서 알림
certmanager_certificate_expiration_timestamp_seconds - time() < 604800

인증서 백업

1
kubectl get secret app-tls-secret -n default -o yaml > backup-$(date +%Y%m%d).yaml

cert-manager 업그레이드 시

CRD를 먼저 업그레이드해야 하며, 기존 Certificate 리소스는 자동 마이그레이션되어 유지됩니다.

HyperCloud 환경 참고

  • cert-manager는 HyperCloud 기본 포함 패키지가 아니므로 별도 설치 필요
  • K8s 1.21 환경: cert-manager v1.5+ 권장
  • API 버전: cert-manager.io/v1 사용 (v1.0+ 기준)

참고 자료

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.