ACME란?
ACME (Automatic Certificate Management Environment)는 표준 인증서 자동 발급 프로토콜입니다. 2019년 IETF에서 RFC 8555로 표준화되었습니다.
ACME를 지원하는 CA (Certificate Authority)
ACME는 Let’s Encrypt만의 프로토콜이 아닙니다. 여러 CA들이 지원합니다:
| CA | 무료 | 상용 | 특징 |
|---|---|---|---|
| Let’s Encrypt | ✅ | ❌ | 가장 유명, 90일 갱신 |
| ZeroSSL | ✅ | ✅ | 90일 무료, 유료 플랜 제공 |
| Buypass Go SSL | ✅ | ❌ | 180일 갱신 (노르웨이) |
| Google Trust Services | ✅ | ❌ | 90일, Google Cloud 통합 |
| Sectigo (상용) | ❌ | ✅ | 기업용, OV/EV 인증서 |
| DigiCert (상용) | ❌ | ✅ | 기업용, 고급 검증 |
cert-manager에서 다른 CA 사용 예시:
# ZeroSSL 사용
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: zerossl
spec:
acme:
server: https://acme.zerossl.com/v2/DV90
email: admin@example.com
externalAccountBinding:
keyID: your-eab-kid
keySecretRef:
name: zerossl-eab
key: secret
privateKeySecretRef:
name: zerossl-key
solvers:
- http01:
ingress:
class: nginx
---
# Buypass 사용 (180일 인증서)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: buypass
spec:
acme:
server: https://api.buypass.com/acme/directory
email: admin@example.com
privateKeySecretRef:
name: buypass-key
solvers:
- http01:
ingress:
class: nginx왜 Let’s Encrypt가 가장 유명한가?
- 완전 무료: 상업적 제한 없음
- 자동화: ACME 프로토콜 최초 구현 및 대중화
- 커뮤니티 지원: cert-manager 등 도구들의 기본 설정
- 신뢰성: Mozilla, Chrome, EFF 등의 후원
다른 CA를 고려해야 할 때:
- Buypass: 180일 인증서 필요 시 (Let’s Encrypt는 90일)
- ZeroSSL: 상용 지원 필요 시
- Google Trust Services: GCP 환경에서 통합 필요 시
- DigiCert/Sectigo: OV/EV 인증서 필요 시 (회사 정보 검증)
참고: 대부분의 경우 Let’s Encrypt로 충분하며, 이 글의 예시도 주로 Let’s Encrypt를 사용합니다.
ACME Challenge란?
인증서를 발급받기 위해서는 도메인 소유권을 증명해야 하는데, 이 검증 과정을 Challenge라고 합니다.
Challenge 종류
| Challenge 타입 | 검증 방식 | 포트 요구사항 | Wildcard 지원 |
|---|---|---|---|
| HTTP-01 | HTTP 응답 검증 | 80 필수 | ❌ |
| DNS-01 | DNS TXT 레코드 검증 | 불필요 | ✅ |
| TLS-ALPN-01 | TLS 핸드셰이크 검증 | 443 필수 | ❌ |
HTTP-01 Challenge
동작 원리
검증 과정 상세
- 인증서 요청: cert-manager가 Let’s Encrypt에 인증서 발급 요청
- Challenge 생성: Let’s Encrypt가 랜덤 토큰 생성 (예:
abc123xyz) - HTTP 엔드포인트 노출: cert-manager가 임시 Pod를 생성하여 다음 경로에 토큰 배치
http://example.com/.well-known/acme-challenge/abc123xyz - 검증: Let’s Encrypt가 해당 URL에 접근하여 토큰 확인
- 인증서 발급: 검증 성공 시 인증서 발급
Kubernetes에서 HTTP-01 Challenge
1. ClusterIssuer 설정
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-http01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-http01-key
solvers:
- http01:
ingress:
class: nginx # 또는 traefik2. Certificate 생성
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-cert
namespace: default
spec:
secretName: example-tls
issuerRef:
name: letsencrypt-http01
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.com3. Challenge Pod 확인
# Challenge 리소스 확인
kubectl get challenges
# Challenge Pod 확인
kubectl get pods -l acme.cert-manager.io/http01-solver=true
# Challenge Ingress 확인
kubectl get ingress -A | grep cm-acme-http-solver자동 생성되는 리소스:
# cert-manager가 자동으로 생성하는 임시 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cm-acme-http-solver-xxxxx
spec:
rules:
- host: example.com
http:
paths:
- path: /.well-known/acme-challenge/abc123xyz
pathType: Exact
backend:
service:
name: cm-acme-http-solver-xxxxx
port:
number: 8089HTTP-01 장점
✅ 간단한 설정: DNS 설정 불필요, Ingress만 있으면 됨 ✅ 빠른 검증: 즉시 검증 가능 (DNS 전파 대기 불필요) ✅ 무료 DNS API 불필요: 외부 서비스 연동 불필요 ✅ 대부분의 호스팅 지원: 일반적인 웹 서버 환경에서 작동
HTTP-01 단점
❌ 80 포트 필수: 방화벽에서 80 포트 개방 필요
❌ Wildcard 인증서 불가: *.example.com 발급 불가
❌ 내부 네트워크 불가: 인터넷에서 접근 가능한 도메인만 가능
❌ 멀티 인증서 충돌: 같은 도메인에 여러 Challenge 동시 실행 시 충돌 가능
HTTP-01 사용 사례
적합한 경우:
- 일반 웹 애플리케이션 (example.com, www.example.com)
- 80 포트가 열려 있는 환경
- 단일 도메인 또는 서브도메인별 인증서
- 빠른 발급이 필요한 경우
예시:
# 블로그, E-Commerce, SaaS 애플리케이션
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: webapp-cert
spec:
secretName: webapp-tls
issuerRef:
name: letsencrypt-http01
kind: ClusterIssuer
dnsNames:
- blog.example.com
- shop.example.com
- app.example.comDNS-01 Challenge
동작 원리
검증 과정 상세
- 인증서 요청: cert-manager가 Wildcard 인증서 요청 (
*.example.com) - Challenge 생성: Let’s Encrypt가 랜덤 토큰 생성
- TXT 레코드 추가: cert-manager가 DNS Provider API를 통해 다음 레코드 생성
_acme-challenge.example.com. IN TXT "abc123xyz" - DNS 전파 대기: 전 세계 DNS 서버에 레코드 전파 (60초~10분)
- 검증: Let’s Encrypt가 Public DNS로 TXT 레코드 조회
- 인증서 발급: 검증 성공 시 인증서 발급
- 레코드 정리: cert-manager가 TXT 레코드 삭제
Kubernetes에서 DNS-01 Challenge
1. Cloudflare를 사용한 ClusterIssuer
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token
namespace: cert-manager
stringData:
api-token: "your-cloudflare-api-token"
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns01-cloudflare
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-dns01-key
solvers:
- dns01:
cloudflare:
email: admin@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token2. Route53을 사용한 ClusterIssuer
apiVersion: v1
kind: Secret
metadata:
name: route53-credentials
namespace: cert-manager
stringData:
secret-access-key: "AWS_SECRET_ACCESS_KEY"
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns01-route53
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-dns01-key
solvers:
- dns01:
route53:
region: us-east-1
accessKeyID: AKIAIOSFODNN7EXAMPLE
secretAccessKeySecretRef:
name: route53-credentials
key: secret-access-key3. Google Cloud DNS를 사용한 ClusterIssuer
apiVersion: v1
kind: Secret
metadata:
name: clouddns-service-account
namespace: cert-manager
stringData:
key.json: |
{
"type": "service_account",
"project_id": "my-project",
"private_key_id": "...",
"private_key": "...",
"client_email": "cert-manager@my-project.iam.gserviceaccount.com"
}
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns01-clouddns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-dns01-key
solvers:
- dns01:
cloudDNS:
project: my-project
serviceAccountSecretRef:
name: clouddns-service-account
key: key.json4. Wildcard 인증서 발급
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-cert
namespace: default
spec:
secretName: wildcard-tls
issuerRef:
name: letsencrypt-dns01-cloudflare
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- example.com # Apex 도메인도 포함지원되는 DNS Provider
cert-manager가 지원하는 주요 DNS Provider:
| Provider | 설정 난이도 | API 비용 | 전파 속도 |
|---|---|---|---|
| Cloudflare | 쉬움 | 무료 | 빠름 (1-2분) |
| Route53 | 보통 | 유료 ($0.50/zone) | 빠름 (1-2분) |
| Google Cloud DNS | 보통 | 유료 ($0.20/zone) | 빠름 (1-2분) |
| Azure DNS | 보통 | 유료 | 중간 (3-5분) |
| DigitalOcean | 쉬움 | 무료 | 중간 (3-5분) |
| RFC2136 | 어려움 | 무료 (BIND 등) | 빠름 |
DNS-01 장점
✅ Wildcard 인증서: *.example.com 발급 가능
✅ 포트 불필요: 80, 443 포트 개방 불필요
✅ 내부 네트워크 가능: 인터넷 접근 불필요 (DNS만 공개)
✅ 멀티 서브도메인 효율적: 하나의 인증서로 모든 서브도메인 커버
✅ 방화벽 우회: 엄격한 방화벽 환경에서도 작동
DNS-01 단점
❌ DNS Provider API 필요: API 토큰/자격증명 관리 필요 ❌ DNS 전파 시간: 검증에 수 분 소요 (HTTP-01보다 느림) ❌ 복잡한 설정: DNS Provider별 설정 방법 다름 ❌ 보안 위험: DNS API 토큰 유출 시 도메인 탈취 위험 ❌ 비용 발생 가능: 일부 Provider는 유료 (Route53, Cloud DNS 등)
DNS-01 사용 사례
적합한 경우:
- Wildcard 인증서가 필요한 경우
- 80 포트를 열 수 없는 환경 (기업 방화벽)
- 내부 네트워크 환경
- 멀티 테넌트 SaaS (테넌트별 서브도메인)
- API Gateway, CDN 등 다수의 서브도메인
예시 1: 멀티 테넌트 SaaS
# tenant1.saas.com, tenant2.saas.com, ... 모두 커버
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: saas-wildcard
spec:
secretName: saas-wildcard-tls
issuerRef:
name: letsencrypt-dns01-cloudflare
kind: ClusterIssuer
dnsNames:
- "*.saas.com"
- saas.com예시 2: 내부 Kubernetes 클러스터
# 80 포트가 막혀있는 내부 네트워크
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: internal-cert
spec:
secretName: internal-tls
issuerRef:
name: letsencrypt-dns01-route53
kind: ClusterIssuer
dnsNames:
- "*.internal.company.com"TLS-ALPN-01 Challenge (참고)
개요
TLS-ALPN-01은 TLS 핸드셰이크 과정에서 검증하는 방식입니다. HTTP-01의 대안으로 고안되었지만, 실무에서는 거의 사용되지 않습니다.
특징
- 포트: 443 포트 사용
- Wildcard: 지원 안 함
- 장점: 80 포트 불필요
- 단점: 대부분의 Ingress Controller가 미지원
왜 잘 안 쓰이나?
- NGINX Ingress, Traefik 등에서 공식 지원 제한적
- HTTP-01이 더 널리 지원되고 안정적
- DNS-01이 Wildcard를 지원하므로 대체 가능
HTTP-01 vs DNS-01 비교
기능 비교
| 항목 | HTTP-01 | DNS-01 |
|---|---|---|
| Wildcard | ❌ | ✅ |
| 80 포트 필요 | ✅ | ❌ |
| DNS API 필요 | ❌ | ✅ |
| 검증 속도 | 빠름 (10초) | 느림 (1-10분) |
| 내부 네트워크 | ❌ | ✅ |
| 설정 복잡도 | 낮음 | 높음 |
| 보안 위험 | 낮음 | 중간 (API 토큰) |
| 비용 | 무료 | Provider 따라 유료 |
시나리오별 추천
시나리오별 선택:
| 시나리오 | 추천 | 이유 |
|---|---|---|
| 블로그, 포트폴리오 | HTTP-01 | 간단, 빠름 |
| 멀티 테넌트 SaaS | DNS-01 | Wildcard 필요 |
| 기업 내부 서비스 | DNS-01 | 80 포트 제한 |
| E-Commerce | HTTP-01 | 안정적, 검증됨 |
| API Gateway | DNS-01 | 다수 서브도메인 |
| Development | HTTP-01 | 빠른 테스트 |
실무 가이드
HTTP-01 설정 예시 (NGINX Ingress)
# 1. ClusterIssuer
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-key
solvers:
- http01:
ingress:
class: nginx
---
# 2. Ingress (자동 인증서 발급)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- example.com
secretName: example-tls # cert-manager가 자동 생성
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80DNS-01 설정 예시 (Cloudflare)
# 1. Cloudflare API 토큰 Secret
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token
namespace: cert-manager
type: Opaque
stringData:
api-token: "your-cloudflare-api-token"
---
# 2. ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns-cloudflare
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-dns-key
solvers:
- dns01:
cloudflare:
email: admin@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- example.com
---
# 3. Wildcard Certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example
namespace: default
spec:
secretName: wildcard-example-tls
issuerRef:
name: letsencrypt-dns-cloudflare
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- example.comCloudflare API 토큰 생성 방법
- Cloudflare 대시보드 → My Profile → API Tokens
- Create Token → Edit zone DNS 템플릿 사용
- Permissions:
- Zone - DNS - Edit
- Zone - Zone - Read
- Zone Resources:
- Include - Specific zone - example.com
- 토큰 복사 후 Secret에 저장
AWS Route53 IAM 정책
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:GetChange",
"route53:ListHostedZones"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/HOSTED_ZONE_ID"
}
]
}트러블슈팅
HTTP-01 Challenge 실패
문제 1: “Waiting for HTTP-01 challenge propagation”에서 멈춤
원인:
- 80 포트가 열려있지 않음
- Ingress Controller가 Challenge Ingress를 인식 못함
- DNS가 잘못된 IP를 가리킴
해결:
# 1. Challenge 상태 확인
kubectl describe challenge <challenge-name>
# 2. Challenge Ingress 확인
kubectl get ingress -A | grep cm-acme-http-solver
# 3. Challenge Pod 로그 확인
kubectl logs -l acme.cert-manager.io/http01-solver=true
# 4. 외부에서 접근 테스트
curl -v http://example.com/.well-known/acme-challenge/test
# 5. DNS 확인
dig example.com
nslookup example.com해결 방법:
# Ingress Class를 명시적으로 지정
spec:
acme:
solvers:
- http01:
ingress:
class: nginx
podTemplate:
spec:
nodeSelector:
kubernetes.io/os: linux문제 2: “Connection refused” 또는 “Timeout”
원인:
- 방화벽에서 80 포트 차단
- Load Balancer가 Health Check 실패
- Ingress Controller Pod가 없음
해결:
# LoadBalancer 확인
kubectl get svc -n ingress-nginx
# Ingress Controller Pod 확인
kubectl get pods -n ingress-nginx
# 80 포트 접근 테스트
telnet <external-ip> 80DNS-01 Challenge 실패
문제 1: “DNS record not found” 또는 “DNS propagation timeout”
원인:
- DNS API 인증 실패
- TXT 레코드 생성 실패
- DNS 전파가 느림
해결:
# 1. Challenge 상태 확인
kubectl describe challenge <challenge-name>
# 2. TXT 레코드 확인
dig _acme-challenge.example.com TXT
nslookup -type=TXT _acme-challenge.example.com
# 3. cert-manager 로그 확인
kubectl logs -n cert-manager -l app=cert-manager
# 4. DNS Provider 로그 확인 (Cloudflare 예시)
# Cloudflare 대시보드 → Audit Log해결 방법:
# DNS 전파 대기 시간 늘리기
spec:
acme:
solvers:
- dns01:
cloudflare:
email: admin@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
# DNS 전파 대기 (기본 60초)
selector:
dnsZones:
- example.com문제 2: “Invalid API token” 또는 “Permission denied”
원인:
- API 토큰 만료
- 권한 부족
- Secret 오타
해결:
# Secret 확인
kubectl get secret cloudflare-api-token -n cert-manager -o yaml
# Secret 값 디코딩
kubectl get secret cloudflare-api-token -n cert-manager \
-o jsonpath='{.data.api-token}' | base64 -d
# API 토큰 테스트 (Cloudflare)
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer YOUR_API_TOKEN"문제 3: DNS Provider 별 이슈
Cloudflare:
# Zone ID 명시적 지정
dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
# Zone ID 추가 (선택사항)Route53:
# 리전 명시
dns01:
route53:
region: us-east-1 # 명시적 리전 지정
accessKeyID: AKIAIOSFODNN7EXAMPLE
secretAccessKeySecretRef:
name: route53-credentials
key: secret-access-keyGoogle Cloud DNS:
# 프로젝트 ID 확인
dns01:
cloudDNS:
project: my-project-id # 정확한 프로젝트 ID
serviceAccountSecretRef:
name: clouddns-service-account
key: key.json디버깅 커맨드 모음
# Certificate 상태 확인
kubectl get certificate
kubectl describe certificate <cert-name>
# CertificateRequest 확인
kubectl get certificaterequest
kubectl describe certificaterequest <request-name>
# Order 확인
kubectl get order
kubectl describe order <order-name>
# Challenge 확인 (상세)
kubectl get challenges
kubectl describe challenge <challenge-name>
# cert-manager 로그
kubectl logs -n cert-manager -l app=cert-manager -f
# Challenge Pod 로그 (HTTP-01)
kubectl logs -l acme.cert-manager.io/http01-solver=true
# Challenge Ingress 확인 (HTTP-01)
kubectl get ingress -A | grep cm-acme-http-solver
kubectl describe ingress <challenge-ingress>
# DNS 레코드 확인 (DNS-01)
dig _acme-challenge.example.com TXT +short
nslookup -type=TXT _acme-challenge.example.com 8.8.8.8
# Secret 확인
kubectl get secret <tls-secret> -o yaml
kubectl get secret <tls-secret> -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -text -noout
# 인증서 만료일 확인
kubectl get secret <tls-secret> -o jsonpath='{.data.tls\.crt}' | \
base64 -d | openssl x509 -noout -dates
# Challenge 수동 삭제 (재시도)
kubectl delete challenge --all
kubectl delete order --all
kubectl delete certificaterequest --all성능 및 모니터링
인증서 발급 시간 비교
| Challenge 타입 | 평균 발급 시간 | 최대 시간 |
|---|---|---|
| HTTP-01 | 10-30초 | 2분 |
| DNS-01 | 2-5분 | 10분 |
모니터링 설정
Prometheus Rule (인증서 만료 알림):
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: cert-manager-alerts
spec:
groups:
- name: certificates
rules:
- alert: CertificateExpiringSoon
expr: |
certmanager_certificate_expiration_timestamp_seconds - time() < 604800
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate {{ $labels.name }} expiring in 7 days"
description: "Certificate {{ $labels.name }} in namespace {{ $labels.namespace }} expires in less than 7 days"
- alert: CertificateRenewalFailed
expr: |
certmanager_certificate_ready_status{condition="False"} == 1
for: 1h
labels:
severity: critical
annotations:
summary: "Certificate {{ $labels.name }} renewal failed"
description: "Certificate {{ $labels.name }} in namespace {{ $labels.namespace }} failed to renew"Best Practices
보안
-
API 토큰 관리
- Secret은 cert-manager Namespace에만 저장
- RBAC으로 접근 제한
- 토큰 정기 교체 (6개월마다)
- Sealed Secrets 또는 External Secrets 사용 권장
-
최소 권한 원칙
# Cloudflare: Zone DNS Edit만 허용 # Route53: 특정 Hosted Zone만 허용 # GCP: 특정 프로젝트만 허용 -
Production vs Staging
# Staging (테스트용, Rate Limit 높음) server: https://acme-staging-v02.api.letsencrypt.org/directory # Production (실제 인증서) server: https://acme-v02.api.letsencrypt.org/directory
효율성
-
Wildcard 인증서 재사용
# 하나의 Wildcard 인증서로 모든 Ingress 커버 apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: wildcard-shared spec: secretName: wildcard-tls dnsNames: - "*.example.com" --- # 모든 Ingress에서 재사용 spec: tls: - secretName: wildcard-tls -
DNS-01 Selector 활용
# 특정 도메인만 DNS-01 사용 solvers: - dns01: cloudflare: apiTokenSecretRef: name: cloudflare-api-token key: api-token selector: dnsZones: - "*.internal.example.com" - http01: ingress: class: nginx selector: dnsNames: - "www.example.com" -
인증서 갱신 모니터링
- cert-manager는 만료 30일 전 자동 갱신 시도
- Prometheus + Alertmanager로 실패 감지
- Slack/Email 알림 설정