티스토리 뷰
배경
Kubernetes에서 다음과 같은 도메인을 HTTPS로 서비스하려고 한다.
apps.example.com
*.apps.example.com
와일드카드 인증서를 Let’s Encrypt로 발급하려면 DNS-01 challenge가 필요하다.
일반적인 방식은 cert-manager가 DNS Provider API를 호출해서 _acme-challenge TXT 레코드를 자동으로 생성하는 것이다.
cert-manager → DNS Provider API → _acme-challenge TXT 생성
하지만 이 방식은 Kubernetes 클러스터 안에 DNS Provider API 권한을 넣어야 한다.
도메인을 개인이 관리하든, 회사가 관리하든, 외부 DNS 관리자가 관리하든 관계없이 다음과 같은 상황이 있을 수 있다.
- Kubernetes에 DNS Provider API 권한을 넣고 싶지 않다.
- 특정 DNS 벤더에 종속된 설정을 피하고 싶다.
- DNS Zone에는 최소한의 레코드만 직접 등록하고 싶다.
- 인증서 발급과 갱신은 Kubernetes 안에서 자동화하고 싶다.
이럴 때 acme-dns를 사용할 수 있다.
핵심 아이디어
_acme-challenge 레코드만 acme-dns로 CNAME 위임한다.
_acme-challenge.apps.example.com
→ xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.auth.acme-dns.io
그러면 cert-manager는 실제 도메인 DNS Zone을 수정하지 않는다.
대신 acme-dns API를 통해 위임된 도메인의 TXT 레코드만 갱신한다.
cert-manager → acme-dns API → TXT challenge 갱신
즉, Kubernetes에는 example.com DNS Zone에 대한 API 권한이 필요 없다.
전체 구조
flowchart LR
Client[Client Browser] --> DNS1[apps.example.com / *.apps.example.com]
DNS1 --> K8SIP[Kubernetes Ingress IP]
K8SIP --> Ingress[Ingress Controller]
Ingress --> Service[Kubernetes Service]
CM[cert-manager] --> LE[Let's Encrypt]
LE --> DNS2[_acme-challenge.apps.example.com TXT 조회]
DNS2 --> CNAME[CNAME 위임]
CNAME --> ACME[acme-dns]
CM --> ACME
1. acme-dns 계정 생성
먼저 acme-dns 계정을 생성한다.
공개 acme-dns 서버를 사용할 수도 있고, 직접 acme-dns 서버를 운영할 수도 있다.
예시는 공개 서버 기준이다.
curl -X POST https://auth.acme-dns.io/register
응답 예시는 다음과 같다.
{
"username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"fulldomain": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.auth.acme-dns.io",
"subdomain": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"allowfrom": []
}
각 값의 의미는 다음과 같다.
username : acme-dns API 인증 사용자
password : acme-dns API 인증 비밀번호
fulldomain : _acme-challenge CNAME 대상으로 사용할 도메인
subdomain : acme-dns 내부 subdomain
allowfrom : acme-dns 업데이트를 허용할 IP 목록
중요한 점은 fulldomain만 DNS에 등록하고, username/password는 Kubernetes Secret으로 보관한다는 것이다.
2. DNS 레코드 설정
DNS Zone에서 다음 레코드를 직접 설정한다.
예시 도메인:
apps.example.com
*.apps.example.com
Kubernetes Ingress 외부 IP:
203.0.113.10
acme-dns에서 발급받은 fulldomain:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.auth.acme-dns.io
DNS 레코드는 다음과 같이 설정한다.
apps.example.com. A 203.0.113.10
*.apps.example.com. A 203.0.113.10
_acme-challenge.apps.example.com. CNAME xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.auth.acme-dns.io.
Ingress가 IP가 아니라 LoadBalancer DNS 이름으로 노출되는 환경이라면 A 대신 CNAME을 사용할 수 있다.
apps.example.com. CNAME k8s-ingress.example.net.
*.apps.example.com. CNAME k8s-ingress.example.net.
_acme-challenge.apps.example.com. CNAME xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.auth.acme-dns.io.
주의할 점은 와일드카드 인증서의 DNS-01 challenge 위치다.
*.apps.example.com 인증서를 발급하더라도 _acme-challenge 레코드는 다음 위치에 만든다.
_acme-challenge.apps.example.com.
아래처럼 만들면 안 된다.
_acme-challenge.*.apps.example.com.
3. DNS 설정 확인
DNS 레코드가 제대로 반영되었는지 확인한다.
dig A apps.example.com +short
dig A test.apps.example.com +short
dig CNAME _acme-challenge.apps.example.com +short
기대 결과:
203.0.113.10
203.0.113.10
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.auth.acme-dns.io.
LoadBalancer DNS 이름을 CNAME으로 사용했다면 다음처럼 확인한다.
dig CNAME apps.example.com +short
dig CNAME test.apps.example.com +short
dig CNAME _acme-challenge.apps.example.com +short
4. acmedns.json 작성
cert-manager가 acme-dns API를 호출할 수 있도록 credentials 파일을 만든다.
파일명:
acmedns.json
내용:
{
"apps.example.com": {
"username": "<ACME_DNS_USERNAME>",
"password": "<ACME_DNS_PASSWORD>",
"fulldomain": "<ACME_DNS_FULLDOMAIN>",
"subdomain": "<ACME_DNS_SUBDOMAIN>",
"allowfrom": []
}
}
예시:
{
"apps.example.com": {
"username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"fulldomain": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.auth.acme-dns.io",
"subdomain": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"allowfrom": []
}
}
여기서 최상위 key는 인증서 발급 기준 도메인인 apps.example.com으로 둔다.
이번 인증서는 다음 두 도메인을 포함한다.
apps.example.com
*.apps.example.com
5. 민감 파일 Git 제외
acmedns.json에는 인증 정보가 들어 있으므로 Git에 커밋하면 안 된다.
.gitignore에 추가한다.
acmedns.json
acmedns-register.json
6. Kubernetes Secret 생성
cert-manager namespace에 Secret을 생성한다.
kubectl create secret generic acme-dns \
-n cert-manager \
--from-file=acmedns.json=./acmedns.json \
--dry-run=client -o yaml | kubectl apply -f -
확인:
kubectl get secret acme-dns -n cert-manager
Secret 내용을 확인해야 한다면 다음 명령을 사용할 수 있다.
kubectl get secret acme-dns -n cert-manager \
-o jsonpath='{.data.acmedns\.json}' | base64 -d
단, 이 명령은 민감정보를 출력하므로 운영 환경에서는 주의한다.
7. cert-manager ClusterIssuer 생성
DNS Provider 전용 solver를 사용하지 않고 acmeDNS solver를 사용한다.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-acme-dns
spec:
acme:
email: admin@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-acme-dns-account-key
solvers:
- dns01:
acmeDNS:
host: https://auth.acme-dns.io
accountSecretRef:
name: acme-dns
key: acmedns.json
직접 운영하는 acme-dns 서버를 사용한다면 host 값을 바꾼다.
host: https://acme-dns.example.net
적용:
kubectl apply -f clusterissuer-letsencrypt-prod-acme-dns.yaml
확인:
kubectl get clusterissuer
kubectl describe clusterissuer letsencrypt-prod-acme-dns
8. Certificate 생성
apps.example.com과 *.apps.example.com을 모두 포함하는 인증서를 생성한다.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: apps-example-com-tls
namespace: ingress-nginx
spec:
secretName: apps-example-com-tls
issuerRef:
name: letsencrypt-prod-acme-dns
kind: ClusterIssuer
dnsNames:
- apps.example.com
- "*.apps.example.com"
적용:
kubectl apply -f certificate-apps-example-com.yaml
확인:
kubectl get certificate -n ingress-nginx
kubectl describe certificate apps-example-com-tls -n ingress-nginx
발급 과정에서 다음 리소스들이 생성된다.
kubectl get certificaterequest -A
kubectl get order -A
kubectl get challenge -A
문제가 생기면 challenge를 상세 조회한다.
kubectl describe challenge -A
9. Ingress에서 인증서 사용
예시는 NGINX Ingress 기준이다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: sample-app
namespace: ingress-nginx
spec:
ingressClassName: nginx
tls:
- hosts:
- apps.example.com
- "*.apps.example.com"
secretName: apps-example-com-tls
rules:
- host: apps.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: sample-app
port:
number: 80
- host: "*.apps.example.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: sample-app
port:
number: 80
주의할 점은 TLS Secret은 namespace-scoped 리소스라는 것이다.
따라서 Ingress가 있는 namespace와 Certificate가 Secret을 생성하는 namespace가 같아야 한다.
10. 인증서 확인
인증서 Secret이 생성되었는지 확인한다.
kubectl get secret apps-example-com-tls -n ingress-nginx
HTTPS 접속 확인:
curl -v https://apps.example.com
curl -v https://test.apps.example.com
인증서 SAN 확인:
echo | openssl s_client \
-connect test.apps.example.com:443 \
-servername test.apps.example.com 2>/dev/null \
| openssl x509 -noout -text \
| grep -A1 "Subject Alternative Name"
기대 결과에는 다음이 포함되어야 한다.
DNS:apps.example.com
DNS:*.apps.example.com
운영 시 주의사항
1. acme-dns 계정 정보는 반드시 보관한다
acme-dns에서 register를 호출하면 다음 정보가 발급된다.
username
password
fulldomain
subdomain
이 중 fulldomain은 DNS CNAME 대상으로 사용한다.
username/password는 cert-manager가 acme-dns API를 호출할 때 사용하는 인증 정보다.
만약 기존 fulldomain에 대응하는 username/password를 잃어버리면 cert-manager가 TXT 레코드를 갱신할 수 없다.
이 경우 새로 register해서 새 fulldomain을 발급받고, DNS CNAME을 새 값으로 변경해야 한다.
2. 노출된 acme-dns 계정은 사용하지 않는다
username/password가 외부에 노출되었다면 운영용으로 사용하지 않는 것이 안전하다.
새로 register해서 새 계정을 만들고, 새 fulldomain으로 CNAME을 변경한다.
curl -X POST https://auth.acme-dns.io/register > acmedns-register.json
3. 하나의 acme-dns 계정에 너무 많은 도메인을 묶지 않는다
이번 구성은 다음 두 개 도메인만 하나의 인증서에 포함한다.
apps.example.com
*.apps.example.com
이 정도는 일반적인 와일드카드 인증서 구성이다.
하지만 다음과 같이 많은 도메인을 하나의 인증서와 하나의 acme-dns 계정에 계속 추가하는 것은 피하는 것이 좋다.
api.apps.example.com
*.api.apps.example.com
dev.apps.example.com
*.dev.apps.example.com
이런 경우에는 인증서를 분리하거나 acme-dns 계정을 분리하는 편이 낫다.
4. 와일드카드는 한 단계 하위 도메인만 커버한다
다음 인증서는:
*.apps.example.com
아래 도메인을 커버한다.
test.apps.example.com
api.apps.example.com
demo.apps.example.com
하지만 아래 도메인은 커버하지 않는다.
v1.api.apps.example.com
dev.test.apps.example.com
즉, *.*.apps.example.com 같은 다단계 와일드카드는 일반적인 TLS 인증서에서 지원되지 않는다.
다단계 도메인이 필요하다면 다음 중 하나를 선택해야 한다.
1. 도메인 구조를 평탄화한다.
예: v1-api.apps.example.com
2. 하위 도메인별로 별도 와일드카드 인증서를 발급한다.
예: *.api.apps.example.com
3. DNS 위임 구조를 다시 설계한다.
트러블슈팅
CNAME이 잘못된 경우
확인:
dig CNAME _acme-challenge.apps.example.com +short
정상:
<ACME_DNS_FULLDOMAIN>.
비정상:
응답 없음
다른 도메인으로 연결됨
오타 있음
acmedns.json의 key가 잘못된 경우
acmedns.json의 최상위 key는 인증서 발급 기준 도메인과 맞아야 한다.
권장:
{
"apps.example.com": {
...
}
}
Secret namespace가 잘못된 경우
ClusterIssuer에서 참조하는 acme-dns Secret은 cert-manager가 접근 가능한 namespace에 있어야 한다.
일반적으로 다음 위치에 둔다.
namespace: cert-manager
secret: acme-dns
key: acmedns.json
확인:
kubectl get secret acme-dns -n cert-manager
challenge 상태 확인
kubectl get challenge -A
kubectl describe challenge -A
cert-manager 로그 확인:
kubectl logs -n cert-manager deploy/cert-manager
정리
이 구성의 핵심은 다음이다.
서비스 DNS는 일반적인 DNS 레코드로 직접 설정한다.
Kubernetes에는 DNS Provider API 권한을 넣지 않는다.
_acme-challenge만 acme-dns로 CNAME 위임한다.
cert-manager는 acme-dns API를 통해 TXT challenge를 갱신한다.
Let’s Encrypt는 DNS-01 검증 후 와일드카드 인증서를 발급한다.
장점은 다음과 같다.
- 특정 DNS Provider API에 종속되지 않는다.
- Kubernetes에 DNS Zone 수정 권한을 제공하지 않아도 된다.
- DNS에는 A 또는 CNAME 레코드와 _acme-challenge CNAME만 설정하면 된다.
- 인증서 발급과 갱신은 cert-manager가 자동화한다.
가장 중요한 운영 포인트는 acme-dns register 결과를 안전하게 보관하는 것이다.
username
password
fulldomain
subdomain
이 네 가지를 잃어버리면 기존 CNAME을 재사용하기 어렵고, 새 계정을 발급받아 DNS CNAME을 다시 변경해야 한다.
'AI 지식' 카테고리의 다른 글
| Claude Code 업데이트 노트 — 2.1.128 → 2.1.132 (0) | 2026.05.12 |
|---|---|
| Claude Code 업데이트 노트 — 2.1.120 → 2.1.126 (3) | 2026.05.04 |
| Claude Code 완벽 가이드: Skills, Hooks, Subagents로 AI 코딩 에이전트 200% 활용하기 (0) | 2026.02.27 |
- Total
- Today
- Yesterday
- Next.js
- api gateway
- Developer Productivity
- 개발 도구
- Tailwind CSS
- LangChain
- LLM
- security
- PYTHON
- Claude
- Go
- Developer Tools
- ai 개발 도구
- claude code
- SHACL
- authorization
- architecture
- authentication
- frontend
- Ontology
- AI Development
- react
- AI
- troubleshooting
- AI agent
- Rag
- Kubernetes
- workflow
- backend
- knowledge graph
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
