서브도메인이 늘어날 때마다 SSL 인증서를 하나씩 발급하고 갱신하느라 식은땀을 흘려본 적 있으신가요?
도메인이 하나씩 늘 때마다 인증서 만료 알림이 따라오는 그 고통, 저도 압니다. Let's Encrypt 와일드카드 인증서를 FQDN 기반으로 제대로 구축하면, 단 하나의 인증서로 모든 서브도메인을 커버하고 90일마다 자동 갱신까지 손 놓고 기다릴 수 있습니다. 이 글에서는 DNS-01 챌린지 원리부터 Cloudflare API 연동을 통한 완전 자동화, FQDN 기반 인증서 경로 관리까지 실무 기준으로 낱낱이 풀어드립니다.
📌 이 글의 핵심 요약
1. 와일드카드 인증서(*.example.com)는 반드시 DNS-01 챌린지 방식으로만 발급 가능 — HTTP-01로는 절대 안 됨
2. Cloudflare, Route53 등 DNS API를 연동하면 수동 TXT 레코드 작업 없이 완전 자동 갱신 구현 가능
3. certbot의 --manual 방식으로 발급한 인증서는 자동 갱신이 불가 — 처음부터 DNS 플러그인 방식으로 구축해야 함
목차
- 1. 왜 와일드카드 인증서인가 - 단일 도메인 인증서와 비교
- 2. FQDN과 와일드카드 인증서의 관계
- 3. DNS-01 챌린지 원리 - 왜 이것만 와일드카드가 되는가
- 4. 수동 발급 방법 (개념 파악용)
- 5. Cloudflare API 연동 완전 자동화 구축
- 6. 자동 갱신 설정 - crontab vs systemd timer
- 7. 초보자가 반드시 겪는 실수 TOP 4
- 마무리 요약 & 독자 질문
1. 왜 와일드카드 인증서인가 - 단일 도메인 인증서와 비교
서브도메인 3개만 넘어가면 와일드카드가 답이다
Let's Encrypt에서 무료로 발급받을 수 있는 인증서는 크게 두 가지입니다. 하나는 특정 도메인에만 유효한 단일/SAN 인증서이고, 다른 하나는 모든 서브도메인을 한 번에 커버하는 와일드카드 인증서(*.example.com)입니다. 서브도메인이 2개 이하라면 SAN 인증서로도 충분하지만, 마이크로서비스 구조나 멀티 테넌트 환경처럼 서브도메인이 동적으로 생성되는 환경에서는 와일드카드 인증서가 유일한 현실적 선택입니다.
| 항목 | 단일/SAN 인증서 | 와일드카드 인증서 |
|---|---|---|
| 적용 범위 | 명시한 도메인만 (예: [api.example.com](http://api.example.com/)) | 모든 서브도메인 (*.example.com) |
| 발급 방법 | HTTP-01 또는 DNS-01 | DNS-01 전용 |
| 자동화 난이도 | 낮음 (webroot, nginx 플러그인) | 보통 (DNS API 연동 필요) |
| 2단계 이상 서브도메인 | SAN에 개별 추가 필요 | 불가 (*.sub.example.com 별도 발급) |
| 갱신 시 서비스 중단 | standalone 방식 시 순간 중단 가능 | DNS 방식이므로 중단 없음 |
| Let's Encrypt 비용 | 무료 | 무료 |
전문가 비하인드: 보통 "도메인 하나에 인증서 하나"라고 생각하는 분들이 많습니다. 그런데 실무에서는 와일드카드 하나로 루트 도메인([example.com](http://example.com/))과 모든 서브도메인을 커버하는 것이 훨씬 관리하기 편합니다. 단, 와일드카드가 커버하지 못하는 경우가 하나 있는데,
2단계 이상 서브도메인([dev.api.example.com](http://dev.api.example.com/))은 별도 와일드카드(*.api.example.com)를 발급해야 합니다. 이 점을 미리 알고 아키텍처를 설계하는 것이 나중에 헤매지 않는 핵심입니다.
그렇다면 여기서 한 가지 의문이 생깁니다. FQDN과 와일드카드 인증서는 어떻게 연결되는 걸까요?
2. FQDN과 와일드카드 인증서의 관계
FQDN이란 - 끝에 점(.)이 핵심이다
FQDN(Fully Qualified Domain Name)은 DNS 계층의 루트(.)부터 호스트까지 완전하게 표기한 도메인 이름입니다.

예를 들어 mail.example.com. 처럼 마지막에 점(.)이 붙은 형태가 진짜 FQDN입니다. 브라우저나 일반 도구는 이 점을 생략해도 처리해주지만, BIND 같은 DNS 서버 zone 파일이나 SSL 인증서 구성에서는 FQDN을 정확히 인식하는 것이 중요합니다.
인증서 발급 시 FQDN을 신경써야 하는 이유
certbot으로 와일드카드 인증서를 발급할 때 -d 옵션에 넣는 도메인 값이 바로 FQDN 기준으로 처리됩니다. Let's Encrypt ACME 서버는 이 도메인의 DNS 소유권을 확인하기 위해 _acme-challenge.example.com이라는 정확한 FQDN에 TXT 레코드가 등록되어 있는지 조회합니다. 이 레코드가 없거나, 잘못된 서브도메인에 등록되어 있으면 인증이 실패합니다. 특히 _acme-challenge 앞에 이미 서브도메인이 있는 경우(예: api.example.com의 와일드카드를 발급하는 경우) 레코드 위치가 헷갈리기 쉬운데, 항상 최상위 도메인의 _acme-challenge에 등록해야 한다는 점을 기억해두세요.

3. DNS-01 챌린지 원리 - 왜 이것만 와일드카드가 되는가
HTTP-01이 와일드카드를 지원하지 않는 이유
가장 흔히 쓰이는 HTTP-01 챌린지는 Let's Encrypt가 발급 서버의 특정 URL([http://도메인/.well-known/acme-challenge/토큰](http://xn--hq1bm8jm9l/.well-known/acme-challenge/%ED%86%A0%ED%81%B0))에 접속해서 파일을 확인하는 방식입니다. 그런데 *.example.com은 어디로 접속해야 할지 알 수 없습니다. 와일드카드는 실제 A 레코드가 없는 개념적 표현이기 때문에, HTTP 접근으로 소유권을 증명할 수 없습니다. 이것이 Let's Encrypt 공식 문서에서도 명시하는 DNS-01 필수 사용 이유입니다.
DNS-01 챌린지 동작 흐름
- certbot이 Let's Encrypt ACME 서버에 인증서 발급 요청
- ACME 서버가 무작위 토큰 값을 발급
- certbot이 해당 토큰을 _acme-challenge.example.com TXT 레코드에 등록하도록 요청
- TXT 레코드 등록 (수동이면 직접, 자동이면 DNS API가 처리)
- DNS 전파 대기 (평균 30초~수 분, 경우에 따라 그 이상)
- Let's Encrypt가 전 세계 여러 지점에서 해당 TXT 레코드 조회 및 검증
- 검증 성공 시 인증서 발급, /etc/letsencrypt/live/example.com/ 경로에 저장
전문가 비하인드: 여기서 많은 분들이 실수하는 것이 바로 DNS 전파 시간입니다. TXT 레코드를 등록한 직후 Enter를 눌러버리면 아직 전파가 안 된 상태에서 Let's Encrypt가 조회를 시도해 실패합니다. 특히 Cloudflare의 경우 전파가 빠르지만, 그래도 최소 30초 이상 기다린 후 dig TXT _acme-challenge.example.com +short 명령으로 실제 TXT가 반영됐는지 확인하고 진행하는 것이 안전합니다.

4. 수동 발급 방법 (개념 파악용)
certbot 설치 (Ubuntu/Debian 기준)
certbot은 snap 방식으로 설치하는 것을 공식 권장합니다. apt로 설치된 구버전 certbot과 snap 버전이 혼재하면 심볼릭 링크 문제로 자동 갱신이 구버전으로 돌아가는 트러블이 생길 수 있으니 주의하세요.
sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
수동 발급 명령어 (--manual 방식)
아래 명령어는 개념을 이해하기 위한 수동 방식입니다. 와일드카드와 루트 도메인을 함께 발급하려면 -d 옵션을 두 번 쓰는 것이 포인트입니다.
sudo certbot certonly \
--manual \
--preferred-challenges dns-01 \
--server https://acme-v02.api.letsencrypt.org/directory \
--agree-tos \
-m [your-email@example.com](mailto:your-email@example.com) \
-d [example.com](http://example.com/) \
-d "*.example.com"
명령을 실행하면 아래와 같은 메시지가 나타납니다.
Please deploy a DNS TXT record under the name:
_acme-challenge.example.com
with the following value:
aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890-example
Before continuing, verify the record is deployed.
이 값을 DNS 관리 패널에서 _acme-challenge 서브도메인의 TXT 레코드로 등록하고, 아래 명령으로 전파를 확인한 뒤 Enter를 눌러야 합니다.
# TXT 전파 확인
dig TXT _acme-challenge.example.com +short
# 또는
nslookup -type=TXT _acme-challenge.example.com
발급 성공 시 인증서는 /etc/letsencrypt/live/example.com/ 경로에 저장됩니다. 파일 구성은 아래와 같습니다.
/etc/letsencrypt/live/example.com/
├── cert.pem # 도메인 인증서
├── chain.pem # 중간 인증서 (CA 체인)
├── fullchain.pem # cert.pem + chain.pem (Nginx/Apache에서 이 파일 사용)
├── privkey.pem # 개인키 (절대 외부에 노출 금지)
└── README
⚠️ 중요: --manual 방식으로 발급한 인증서는 certbot이 자동 갱신 시 다시 사람이 TXT 레코드를 수동으로 입력해야 합니다. 즉, 자동 갱신이 사실상 불가능합니다. 이것이 다음 섹션에서 DNS 플러그인 자동화가 필요한 이유인 것이죠.
그렇다면 여기서 한 가지 의문이 생깁니다. 자동화를 하려면 도대체 어떻게 DNS 레코드를 스크립트가 알아서 등록하게 만들 수 있을까요?
5. Cloudflare API 연동 완전 자동화 구축
왜 Cloudflare인가
DNS-01 챌린지 자동화의 핵심은 DNS API입니다. certbot이 TXT 레코드를 자동으로 등록/삭제할 수 있어야 사람 개입 없이 자동 갱신이 가능합니다. Cloudflare는 DNS API가 가장 잘 정비되어 있고, certbot 공식 플러그인인 certbot-dns-cloudflare가 있어서 설정이 간단합니다. AWS Route53, GCP Cloud DNS 등도 플러그인이 있으나, 이 가이드에서는 Cloudflare 기준으로 설명합니다.
Step 1 - Cloudflare API 토큰 발급
- Cloudflare 대시보드 → API 토큰 접속
- "토큰 만들기" 클릭 → "DNS 영역 편집" 템플릿 선택
- 영역 리소스를 "특정 영역"으로 설정하고 본인 도메인 선택
- 토큰 생성 후 반드시 복사 (이후 다시 볼 수 없음)
Step 2 - certbot DNS Cloudflare 플러그인 설치
# snap 방식 (권장)
sudo snap install certbot-dns-cloudflare
# apt 방식 (certbot을 apt로 설치한 경우)
sudo apt install python3-certbot-dns-cloudflare
Step 3 - API 토큰 자격증명 파일 생성
sudo mkdir -p /etc/letsencrypt
sudo nano /etc/letsencrypt/cloudflare.ini
파일 내용:
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN_HERE
파일 권한을 반드시 600으로 설정해야 합니다. 이 파일에는 DNS 편집 권한이 있는 토큰이 들어있으므로 root 외에는 읽지 못하게 해야 합니다.
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
Step 4 - 와일드카드 인증서 자동 발급
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
--dns-cloudflare-propagation-seconds 60 \
-d [example.com](http://example.com/) \
-d "*.example.com" \
--agree-tos \
--email [your-email@example.com](mailto:your-email@example.com)
이 명령을 실행하면 사람 개입 없이 자동으로 TXT 레코드가 등록되고, 검증이 완료되면 인증서가 발급됩니다. --dns-cloudflare-propagation-seconds 60 옵션은 TXT 레코드 등록 후 60초 대기 후 검증을 시도하게 하는 것입니다. 기본값인 10초는 너무 짧아 실패할 수 있으니 60초~120초로 설정하는 것을 권장합니다.
Step 5 - Nginx에 인증서 적용
server {
listen 443 ssl;
server_name [example.com](http://example.com/) *.example.com;
```
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
```
}
6. 자동 갱신 설정 - crontab vs systemd timer
Let's Encrypt 인증서 유효기간과 갱신 타이밍
Let's Encrypt 인증서 유효기간은 90일이며, 만료 30일 전부터 갱신이 가능합니다. snap으로 certbot을 설치한 경우 systemd timer가 자동으로 설정되어 있어 별도 설정이 필요 없을 수 있습니다. 아래 명령으로 확인하세요.
# systemd timer 확인
sudo systemctl list-timers | grep certbot
# 기존 cron 설정 확인
cat /etc/cron.d/certbot
systemd timer 방식 (현대적 방식, 권장)
# systemd timer 활성화 및 시작
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
# 갱신 후 Nginx 리로드를 post-hook으로 설정
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh 파일 생성
#!/bin/bash
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
crontab 방식 (전통적 방식)
# crontab 편집
sudo crontab -e
# 매일 새벽 3시에 갱신 체크 (만료 30일 이내인 경우에만 실제 갱신)
0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
certbot 공식 문서에 따르면, 하루 2회(0시, 12시) 실행에 무작위 sleep을 섞어 Let's Encrypt 서버 부하를 분산시키는 방식도 권장됩니다.
SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}');
echo "0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q --post-hook 'systemctl reload nginx'" \
| sudo tee -a /etc/crontab > /dev/null
갱신 테스트 - 실제 발급 없이 시뮬레이션
# dry-run으로 갱신 시뮬레이션 (실제 발급 없이 전체 흐름 테스트)
sudo certbot renew --dry-run
# 특정 인증서만 테스트
sudo certbot renew --cert-name [example.com](http://example.com/) --dry-run
전문가 비하인드: dry-run을 습관적으로 돌려보는 것이 중요합니다. 실제 갱신 실패는 인증서가 만료될 때까지 모를 수 있습니다. 월 1회 dry-run을 cron에 추가해두고 성공/실패 로그를 이메일로 받아보는 것이 프로덕션 환경에서 안전한 운영 방식입니다.
7. 초보자가 반드시 겪는 실수 TOP 4
실수 1 - --manual 방식으로 발급했더니 자동 갱신이 안 된다
가장 흔한 실수입니다. --manual 방식으로 와일드카드를 발급하면 90일 후 갱신할 때 또 사람이 TXT 레코드를 직접 입력해야 합니다. 처음부터 DNS 플러그인(--dns-cloudflare 등)을 써서 발급해야 합니다. 이미 --manual로 발급한 인증서가 있다면, DNS 플러그인 방식으로 재발급(certbot certonly --dns-cloudflare ...)해서 갱신 설정 파일을 덮어써야 합니다.
실수 2 - TXT 레코드 전파 전에 Enter를 눌러버린다
수동 발급 시 TXT 레코드를 등록한 직후 Enter를 누르면 Let's Encrypt가 아직 전파되지 않은 레코드를 조회해 실패합니다. 한 시간에 5회라는 빡빡한 도메인 검증 실패 횟수 제한이 있어, 5번 연속 실패하면 1시간 동안 발급이 막힙니다. 반드시 dig나 nslookup으로 전파를 확인한 후 Enter를 누르세요.
실수 3 - cloudflare.ini 권한을 600으로 안 했다
certbot-dns-cloudflare 플러그인은 실행 시 cloudflare.ini 파일의 권한을 체크합니다. 600 미만이면 보안 경고와 함께 실행을 거부합니다. chmod 600 /etc/letsencrypt/cloudflare.ini를 반드시 먼저 실행하세요.
실수 4 - 루트 도메인을 -d에 빠뜨린다
*.example.com 와일드카드는 서브도메인만 커버하고, 루트 도메인([example.com](http://example.com/)) 자체는 커버하지 않습니다. 따라서 -d example.com과 -d "*.example.com"을 항상 함께 넣어야 example.com과 *.example.com 모두 유효한 인증서가 만들어집니다. 한쪽만 넣으면 루트나 서브 중 하나에서 인증서 오류가 납니다.
Rate Limit 주의사항
| 제한 항목 | 한도 | 비고 |
|---|---|---|
| 동일 도메인 인증서 발급 | 주당 50개 | 테스트 포함 |
| 도메인 검증 실패 | 시간당 5회 | 가장 주의해야 할 제한 |
| 계정 등록 | IP당 10개/3시간 | 일반적으로 해당 없음 |
| Staging 서버 제한 | 훨씬 느슨함 | 테스트는 반드시 Staging에서 |
테스트는 항상 Staging 서버에서 먼저 하세요. --staging 플래그를 추가하면 실제 발급 없이 전체 흐름을 테스트할 수 있습니다.
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d [example.com](http://example.com/) \
-d "*.example.com" \
--staging
마무리 요약 & 독자 질문
핵심 Aha-Moment 5가지
- 와일드카드 인증서는 DNS-01 챌린지 전용이다. HTTP-01로는 발급 자체가 불가능하다
- --manual 방식은 자동 갱신이 안 된다. 처음부터 DNS 플러그인(certbot-dns-cloudflare 등)으로 발급해야 한다
- Cloudflare API 토큰은 Zone:DNS:Edit 권한만 부여하고, cloudflare.ini는 chmod 600으로 보호해야 한다
- *.example.com은 루트 도메인([example.com](http://example.com/))을 커버하지 않는다. -d example.com을 항상 함께 명시할 것
- dry-run 테스트를 월 1회 습관적으로 돌려야 갱신 실패를 사전에 잡을 수 있다
💬 여러분에게 질문드립니다!
Q1. 지금 운영 중인 서버의 SSL 인증서 갱신을 수동으로 하고 계신가요? 아니면 이미 자동화가 되어 있나요? 어떤 방식을 쓰고 계신지 댓글로 공유해 주세요!
Q2. Cloudflare 외에 Route53, GCP Cloud DNS 등 다른 DNS 공급자를 쓰는 분들은 플러그인 연동하면서 어떤 이슈를 겪으셨나요? 특이한 케이스가 있으시면 공유 부탁드립니다!
다음으로 읽으면 좋은 글
- Nginx HTTPS 설정 완전 가이드 - ssl_protocols, HSTS, OCSP Stapling까지
- FQDN vs 일반 도메인 완벽 정리 - DNS zone 파일에서 헷갈리지 않는 법