Tech Artifacts/K8s Security

K8s Security - 07. cert-manager

인증서 만료는 클러스터의 타이머 폭탄이다 — cert-manager가 자동화하는 것

14장에서 kubeadm 인증서가 1년 만료라고 했다. 잊고 있던 어느 날, apiserver 인증서가 만료돼 클러스터가 멈췄다 — 아무것도 연결이 안 됐다. 인증서 만료는 Kubernetes의 타이머 폭탄이다. cert-manager가 그 폭탄을 자동 해체한다 — 발급·갱신·배포를 코드로. 단지 kubeadm 시스템 인증서가 아니라, 앱의 TLS 인증서까지.

이 글이 푸는 것은: cert-manager가 인증서 생명주기를 어떻게 자동화하고, Issuer/ClusterIssuer가 무엇인가다.

인증서가 Kubernetes에서 왜 그렇게 흔한가

flowchart LR
    USE["인증서 사용처"] --> A1["apiserver/kubelet 상호 인증<br/>(kubeadm)"]
    USE --> A2["Service Mesh mTLS<br/>(k8s networking Istio)"]
    USE --> A3["Ingress/Gateway TLS<br/>(외부 HTTPS)"]
    USE --> A4["Webhook 어드미션<br/>(k8s security)"]

Kubernetes 곳곳이 인증서. 각각 만료가 있고, 만료되면 해당 구성요소가 멈춘다. 수동 관리는 불가능에 가깝다 — 몇십 장의 인증서 만료일을 사람이 추적할 수 없다. 자동화가 생존 조건.

cert-manager — 인증서를 CRD로

cert-manager는 인증서를 Kubernetes 객체로 다룬다. 세 핵심 CRD:

  • Issuer: 한 namespace의 인증서 발급자.
  • ClusterIssuer: 클러스터 전체 발급자.
  • Certificate: 발급받을 인증서 요청 + 결과.
# Kubernetes 1.36 — Let's Encrypt ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: {name: letsencrypt}
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef: {name: letsencrypt-key}
    solvers:
    - http01: {ingress: {class: nginx}}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata: {name: app-tls, namespace: app}
spec:
  secretName: app-tls-secret           # 결과 Secret
  issuerRef: {name: letsencrypt, kind: ClusterIssuer}
  dnsNames: [app.example.com]

cert-manager가 이 Certificate를 보고 Issuer에 발급 요청(ACME), 결과를 Secret(kubernetes.io/tls)에 저장. Ingress/Gateway가 이 Secret을 TLS에 쓴다.

자동 갱신 — 타이머 폭탄 해체

cert-manager의 핵심 가치: 자동 갱신. Certificate에 만료일이 다가오면(기본 2/3 지점), cert-manager가 재발급. 사람의 개입 없이. 인증서 만료로 클러스터가 멈추는 사태를 영구적으로 막는다.

flowchart LR
    CERT["Certificate 객체<br/>(만료일 추적)"] -->|"만료 임박"| RENEW["자동 재발급 요청"]
    RENEW --> ISSUER["Issuer/CA"]
    ISSUER --> SEC["Secret 갱신"]
    SEC --> USE["Ingress/Gateway/mTLS 사용"]

발급 소스의 종류 — 어디서 인증서를 받나

  • ACME(Let's Encrypt): 외부 공개 CA. HTTP-01/DNS-01 챌린지로 도메인 소유 증명. 외부 서비스용.
  • 자체 서명 CA(SelfSigned/CA): 클러스터 내부 CA. 내부 서비스/mTLS용.
  • Vault PKI: Vault를 CA로. 엔터프라이즈 내부.
  • 클라우드(AWS Private CA 등): 클라우드 관리형 CA.

Issuer 종류에 따라 발급 소스 선택. 외부 인터넷 서비스 → Let's Encrypt, 내부 메시 mTLS → 자체 CA/Vault.

인증서와의 연결 — 어디에 쓰나

발급된 인증서(Secret)는 여러 곳에서 참조:

  • Ingress/Gateway: tls.secretName. 외부 HTTPS 종단.
  • Istio mTLS: Istiod가 발급하거나 cert-manager와 통합.
  • Webhook 어드미션: webhook 서버가 TLS 필요 → cert-manager로.
  • 커스텀 컨트롤러: HTTPS를 쓰는 모든 컴포넌트.

cert-manager 자체는 Kubernetes 시스템 인증서(kubeadm이 만든 apiserver 인증서 등)를 직접 관리하지 않는다 — 그건 여전히 kubeadm의 영역(14장, kubeadm certs renew). cert-manager는 앱/부가 서비스 인증서를 자동화. 단, 일부 환경에선 시스템 인증서까지 cert-manager로 통합하기도.

직접 확인하기

# Kubernetes 1.36 — cert-manager 설치 후
kubectl get pods -n cert-manager
kubectl get clusterissuer,certificate -A

cert-manager 별도 설치(helm). kind 클러스터에서 동작. 미검증 환경은 노트.

# 인증서 상태와 갱신 추적
kubectl describe certificate app-tls -n app | grep -E 'Not After|Status'

확인할 것: 만료일(Not After)과 갱신 상태(Renewal). cert-manager가 만료 임박 시 자동 갱신.

ACME 챌린지의 두 종류 — HTTP-01과 DNS-01

Let's Encrypt(ACME)가 도메인 소유를 증명받는 방식이 두 가지:

챌린지 방식 요구 적합
HTTP-01 http://도메인/.well-known/acme-challenge/...로 응답 공인 IP, 80 포트 노출 단순, 일반적
DNS-01 _acme-challenge.도메인 TXT 레코드에 토큰 DNS provider API 접근 와일드카드(*.domain), 80 안 열어도 됨
# HTTP-01 solver (Ingress 경유)
solvers:
- http01: {ingress: {class: nginx}}
# DNS-01 solver (DNS provider API)
solvers:
- dns01: {route53: {region: ap-northeast-2, ...}}

DNS-01이 더 강력하다 — 와일드카드 인증서(*.app.example.com 한 장으로 모든 서브도메인 커버)를 발급받으려면 DNS-01 필수. 단 DNS provider API 자격증명이 필요(AWS Route 53, Cloudflare 등). HTTP-01은 단순하지만 80 포트 노출이 전제.

Certificate 객체의 생명주기 — 발급에서 갱신까지

cert-manager가 관리하는 Certificate 객체의 상태 기계:

flowchart LR
    CREATE["Certificate 생성"] --> REQ["발급 요청 (Order)"]
    REQ --> CHAL["ACME 챌린지 수행<br/>(HTTP-01/DNS-01)"]
    CHAL --> ISSUED["인증서 발급 → Secret"]
    ISSUED --> WATCH["만료일 감시"]
    WATCH -. 만료 2/3 임박 .-> RENEW["재발급"]
    RENEW --> ISSUED
  • Order: ACME 주문 객체. 챌린지 → 발급의 단위.
  • Challenge: 도메인 소유 증명 객체(HTTP-01/DNS-01).
  • Certificate: 최종 인증서(Secret에 저장). 만료일 추적.
  • 자동 갱신: 만료일의 2/3 시점에 재발급 트리거. 예: 90일 인증서면 60일째에 재발급.

이 생명주기가 "인증서 만료 타이머 폭탄"을 영구 해체 — 사람이 만료일을 추적할 필요 없이 cert-manager가 알아서.

Ingress/Gateway와 cert-manager 통합 — 한 매니페스트로

Ingress에 annotation을 붙이면 cert-manager가 자동으로 Certificate를 생성:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt    # 이 한 줄로 인증서 자동 발급
spec:
  tls:
  - hosts: [app.example.com]
    secretName: app-tls

이 annotation을 보면 cert-manager가 Certificate 객체를 자동 생성 → 발급 → app-tls Secret 채움. Ingress는 그 Secret을 TLS에. 인증서 관리가 Ingress 정의 한 곳에서 끝. Gateway API도 비슷한 통합 지원.

Vault PKI — 내부 인증 기관으로

cert-manager가 Let's Encrypt(공개 CA) 외에 Vault PKI를 CA로 쓸 수 있다. Vault가 인증 기관 역할:

apiVersion: cert-manager.io/v1
kind: Issuer
metadata: {name: vault-issuer}
spec:
  vault:
    server: https://vault.example.com
    path: pki/sign/internal        # Vault PKI 엔진 경로
    auth: {kubernetes: {mountPath: /v1/auth/kubernetes, role: cert-manager}}

이제 인증서가 Vault가 서명한 내부 CA 체인으로 발급. 엔터프라이즈 내부 서비스(mTLS, 내부 HTTPS)에 적합 — 외부 CA(Let's Encrypt)는 외부 도메인만, 내부 서비스는 Vault/자체 CA가 맞음.

자체 서명 CA로 시작 — 개발/테스트

cert-manager의 SelfSigned issuer로 자체 서명 CA를 만들고, 그 CA로 다른 인증서를 서명:

# 1. 자체 서명 CA 인증서
# 2. CA Issuer (그 CA로 서명)
# 3. 일반 인증서 (CA Issuer에서 발급)

이 3단계가 "클러스터 안 자체 CA 풀"을 만드는 패턴. 개발/테스트 환경에 적합 — 외부 의존 없이 내부 mTLS/TLS를. 단 자체 서명은 공개 신뢰가 아니므로, 외부 서비스엔 부적합(브라우저 경고 등).

인증서 갱신 트리거와 연쇄 — 인증서가 바뀌면 무엇이 갱신되나

cert-manager가 인증서를 갱신하면 Secret이 갱신. 그 Secret을 참조하는 리소스들이 자동으로 갱신되나?

  • Ingress/Gateway: 새 Secret을 읽음(TLS 인증서). 보통 자동.
  • Pod(볼륨 마운트): 10장처럼 kubelet이 수분 단위로 갱신.
  • Pod(환경 변수): 재시작까지 안 바뀜.

그래서 갱신의 효과가 참조 방식에 따라 다르다. 인증서 회전이 모든 소비자에 즉시 전파되지 않음 — 이것이 06장(secrets-management)의 회전→갱신 문제와 같은 맥락.

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"cert-manager는 kubeadm 인증서도 관리한다" 주로 앱/부가 인증서. 시스템은 kubeadm
"인증서는 한 번 발급이면 끝" 만료 있음. 자동 갱신이 핵심 가치
"Issuer와 ClusterIssuer는 같다" scope 다름. Issuer는 namespace, ClusterIssuer는 전체
"Let's Encrypt는 내부 mTLS에 쓴다" 외부 도메인용. 내부는 자체 CA/Vault
"인증서 Secret은 평문 비밀이다" TLS 개인키. Secret 보안 모델 적용 대상
"갱신은 수동 트리거해야" cert-manager가 만료 임박 시 자동

요약 — 이 글의 결론

  • 인증서가 Kubernetes 곳곳 — apiserver, mTLS, Ingress/Gateway TLS, webhook. 각각 만료, 만료되면 해당 구성요소 멈춤. 수동 관리 불가.
  • cert-manager가 인증서를 CRD로 — Issuer/ClusterIssuer(발급자) + Certificate(요청/결과). 결과는 Secret으로.
  • 자동 갱신이 핵심 가치 — 만료 임박 시 재발급. 인증서 만료 타이머 폭탄 영구 해체.
  • 발급 소스: ACME(Let's Encrypt, 외부)/자체 CA(내부 mTLS)/Vault/클라우드 CA. 용도에 따라.
  • cert-manager는 앱/부가 인증서 중심. 시스템(kubeadm) 인증서는 여전히 kubeadm 영역. 단 일부 환경은 통합.
  • 발급된 Secret을 Ingress/Gateway/mTLS/webhook이 참조. 인증서 생명주기가 클러스터 전체에 영향.

생각해 볼 문제

  1. Let's Encrypt 인증서가 90일마다 만료다. cert-manager 없이 수동 관리 시 생기는 일을 시나리오로.
  2. 내부 mTLS(메시)용과 외부 HTTPS용 발급 소스를 왜 다르게 잡나?
  3. cert-manager가 다운됐다. 이미 발급된 인증서는? 만료 임박 것은?
  4. ClusterIssuer를 쓰는 Certificate가 갱신됐다. Secret은 어떻게 갱신되나? (참조자에게 전파?)
  5. 웹hook 어드미션 서버의 TLS 인증서를 cert-manager로 관리하면 얻는 이점은?
  6. kubeadm 시스템 인증서 만료와 cert-manager 관리 인증서 만료의 차이를 영향 범위로 비교하라.

참고