티스토리 뷰

배경

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을 다시 변경해야 한다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/05   »
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
글 보관함