- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. TLS/SSL 인증서 기본 개념
- 2. Let's Encrypt란
- 3. ACME 프로토콜 동작 원리
- 4. Certbot 설치
- 5. 인증서 발급 방법
- 6. 발급된 파일 구조
- 7. 자동 갱신 설정
- 8. Rate Limits 주의사항
- 9. 대안 도구
- 10. 트러블슈팅
- 11. 보안 모범 사례
- 12. 결론
1. TLS/SSL 인증서 기본 개념
1.1 왜 TLS 인증서가 필요한가
TLS(Transport Layer Security) 인증서는 웹 통신의 세 가지 핵심 보안 요소를 제공한다.
- 암호화(Encryption): 클라이언트와 서버 간 데이터를 암호화하여 도청 방지
- 인증(Authentication): 서버가 진짜 해당 도메인의 소유자임을 증명
- 무결성(Integrity): 데이터가 전송 중 변조되지 않았음을 보장
1.2 인증서 체인 구조
┌───────────────────────┐
│ Root CA Certificate │ ← 브라우저/OS에 내장된 신뢰 앵커
│ (Self-Signed) │
└───────────┬───────────┘
│ 서명
┌───────────▼───────────┐
│ Intermediate CA Cert │ ← 중간 CA 인증서
│ (Signed by Root CA) │
└───────────┬───────────┘
│ 서명
┌───────────▼───────────┐
│ Server Certificate │ ← 도메인 인증서 (Leaf)
│ (Signed by Inter. CA) │
└───────────────────────┘
1.3 공개키/개인키 쌍
┌──────────┐ CSR 생성 ┌──────────────┐
│ 개인키 │ ──────────────> │ CSR │
│(Private │ │(인증서 서명 │
│ Key) │ │ 요청) │
└──────────┘ └──────┬───────┘
│ │ CA에 제출
│ ┌──────▼───────┐
│ │ CA │
│ │ (서명 발급) │
│ └──────┬───────┘
│ │
│ ┌──────▼───────┐
└───────── 함께 사용 ─────>│ 인증서 │
│ (Certificate) │
└──────────────┘
- 개인키(Private Key): 서버만 보유, 절대 외부 유출 금지
- CSR(Certificate Signing Request): 공개키 + 도메인 정보, CA에 제출
- 인증서(Certificate): CA가 서명한 공개키 + 도메인 정보
2. Let's Encrypt란
2.1 개요
Let's Encrypt는 ISRG(Internet Security Research Group) 가 운영하는 무료, 자동화된, 개방형 인증 기관(CA)이다.
핵심 특징:
- 무료: 도메인 검증(DV) 인증서를 무료로 발급
- 자동화: ACME 프로토콜로 인증서 발급/갱신 완전 자동화
- 개방: 오픈소스 프로토콜과 도구 사용
- 유효기간: 90일 (짧은 주기로 보안 강화)
- 신뢰: 모든 주요 브라우저와 OS에서 신뢰
2.2 인증서 유형 비교
| 유형 | 검증 수준 | 발급 시간 | 비용 | Let's Encrypt |
|---|---|---|---|---|
| DV (Domain Validation) | 도메인 소유 확인 | 수 분 | 무료~저가 | 지원 |
| OV (Organization Validation) | 조직 확인 | 수 일 | 유료 | 미지원 |
| EV (Extended Validation) | 확장 검증 | 수 주 | 고가 | 미지원 |
3. ACME 프로토콜 동작 원리
ACME(Automatic Certificate Management Environment)는 인증서 발급을 자동화하는 프로토콜이다.
3.1 전체 흐름
┌────────┐ ┌──────────────┐
│ Certbot│ │ Let's Encrypt│
│(Client)│ │ (CA/ACME) │
└───┬────┘ └──────┬───────┘
│ 1. Account Registration │
│──────────────────────────────────────>│
│ 2. Account Created │
│<──────────────────────────────────────│
│ │
│ 3. Order (domain list) │
│──────────────────────────────────────>│
│ 4. Authorizations + Challenges │
│<──────────────────────────────────────│
│ │
│ 5. Respond to Challenge │
│ (HTTP-01 / DNS-01 / TLS-ALPN-01) │
│──────────────────────────────────────>│
│ │
│ 6. Challenge Validated │
│<──────────────────────────────────────│
│ │
│ 7. Finalize (send CSR) │
│──────────────────────────────────────>│
│ 8. Certificate issued │
│<──────────────────────────────────────│
3.2 HTTP-01 Challenge
가장 일반적인 챌린지 방식이다. 포트 80을 통해 특정 파일에 접근할 수 있는지 검증한다.
Let's Encrypt → http://yourdomain.com/.well-known/acme-challenge/TOKEN_VALUE
검증 과정:
1. Certbot이 토큰 파일을 웹 서버에 배치
2. Let's Encrypt가 HTTP로 해당 파일에 접근
3. 파일 내용이 기대값과 일치하면 도메인 소유 확인
장점:
- 가장 간단하고 일반적
- 추가 DNS 설정 불필요
- 대부분의 웹 서버에서 쉽게 설정
제한:
- 포트 80이 외부에서 접근 가능해야 함
- 와일드카드 인증서 발급 불가
- 로드밸런서 뒤에 있을 때 설정이 복잡할 수 있음
3.3 DNS-01 Challenge
DNS TXT 레코드를 생성하여 도메인 소유를 증명한다. 와일드카드 인증서 발급에 필수다.
Let's Encrypt → DNS 조회: _acme-challenge.yourdomain.com TXT
검증 과정:
1. Certbot이 토큰을 계산
2. _acme-challenge.yourdomain.com TXT 레코드에 토큰 값 설정
3. Let's Encrypt가 DNS를 조회하여 값 확인
4. 일치하면 도메인 소유 확인
장점:
- 와일드카드 인증서 발급 가능
- 웹 서버가 없어도 가능
- 포트 80이 열려있지 않아도 됨
- 여러 서버에서 하나의 인증서 사용 가능
제한:
- DNS Provider API가 필요 (자동화 시)
- DNS 전파 지연 가능성
- 수동 설정 시 번거로움
3.4 TLS-ALPN-01 Challenge
포트 443에서 TLS 연결 시 ALPN(Application-Layer Protocol Negotiation) 확장을 통해 검증한다.
검증 과정:
1. Let's Encrypt가 포트 443으로 TLS 연결 시도
2. ALPN으로 "acme-tls/1" 프로토콜 협상
3. 서버가 자체 서명 인증서로 응답 (특정 확장 포함)
4. 인증서의 acmeIdentifier 확장값으로 검증
장점:
- 포트 80이 불필요
- 포트 443만으로 검증 가능
제한:
- 대부분의 웹 서버에서 기본 지원하지 않음
- 별도 소프트웨어 필요 (예: Caddy)
3.5 챌린지 비교 표
| 기능 | HTTP-01 | DNS-01 | TLS-ALPN-01 |
|---|---|---|---|
| 포트 | 80 | 없음 (DNS) | 443 |
| 와일드카드 | X | O | X |
| 웹 서버 필요 | O | X | O |
| 자동화 난이도 | 낮음 | 중간 (DNS API 필요) | 높음 |
| DNS 설정 필요 | X | O | X |
| 주요 용도 | 일반 웹 서버 | 와일드카드, 내부 서버 | 특수 환경 |
4. Certbot 설치
4.1 Ubuntu / Debian
# snap으로 설치 (권장)
sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
4.2 CentOS / RHEL / Rocky Linux
# EPEL 저장소 활성화
sudo dnf install epel-release
sudo dnf install certbot
# Nginx 플러그인
sudo dnf install python3-certbot-nginx
4.3 macOS
brew install certbot
4.4 Docker
docker run -it --rm \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
certbot/certbot certonly --help
5. 인증서 발급 방법
5.1 Standalone 모드
Certbot이 자체 웹 서버를 띄워서 HTTP-01 챌린지를 처리한다. 기존 웹 서버를 중지해야 한다.
# 포트 80을 사용하므로 기존 웹 서버 중지 필요
sudo systemctl stop nginx
sudo certbot certonly --standalone \
-d example.com \
-d www.example.com \
--agree-tos \
--email admin@example.com \
--non-interactive
# 웹 서버 재시작
sudo systemctl start nginx
5.2 Webroot 모드
기존 웹 서버를 중지하지 않고 인증서를 발급받는다. 웹 서버가 특정 디렉토리를 서빙하도록 설정해야 한다.
Nginx 설정 추가:
server {
listen 80;
server_name example.com www.example.com;
# ACME 챌린지용 디렉토리
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# 나머지 요청은 HTTPS로 리다이렉트
location / {
return 301 https://$host$request_uri;
}
}
# 디렉토리 생성
sudo mkdir -p /var/www/certbot
# Webroot 모드로 인증서 발급
sudo certbot certonly --webroot \
-w /var/www/certbot \
-d example.com \
-d www.example.com \
--agree-tos \
--email admin@example.com
5.3 Nginx 플러그인
Certbot이 Nginx 설정을 자동으로 수정해 준다.
sudo certbot --nginx \
-d example.com \
-d www.example.com \
--agree-tos \
--email admin@example.com
Certbot이 자동으로 수행하는 작업:
- HTTP-01 챌린지 처리
- Nginx 설정에 SSL 관련 지시자 추가
- HTTP를 HTTPS로 리다이렉트하는 설정 추가
5.4 Apache 플러그인
sudo certbot --apache \
-d example.com \
-d www.example.com \
--agree-tos \
--email admin@example.com
5.5 DNS 플러그인 (와일드카드 인증서)
와일드카드 인증서는 DNS-01 챌린지가 필수다.
Cloudflare DNS 플러그인:
# 플러그인 설치
sudo snap install certbot-dns-cloudflare
# API 토큰 파일 생성
sudo mkdir -p /etc/letsencrypt
cat > /etc/letsencrypt/cloudflare.ini << 'CFEOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
CFEOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
# 와일드카드 인증서 발급
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "example.com" \
-d "*.example.com" \
--agree-tos \
--email admin@example.com
AWS Route 53 DNS 플러그인:
sudo snap install certbot-dns-route53
# AWS 자격 증명 설정 (IAM Role 또는 환경변수)
sudo certbot certonly \
--dns-route53 \
-d "example.com" \
-d "*.example.com" \
--agree-tos \
--email admin@example.com
수동 DNS 챌린지:
sudo certbot certonly --manual \
--preferred-challenges dns \
-d "example.com" \
-d "*.example.com" \
--agree-tos \
--email admin@example.com
# Certbot이 TXT 레코드 값을 안내하면
# DNS 관리 페이지에서 수동으로 TXT 레코드 추가:
# _acme-challenge.example.com TXT "제시된_토큰_값"
6. 발급된 파일 구조
# 인증서 파일 경로
ls -la /etc/letsencrypt/live/example.com/
# 파일 구조
/etc/letsencrypt/live/example.com/
cert.pem # 서버 인증서 (도메인 인증서만)
chain.pem # 중간 CA 인증서 체인
fullchain.pem # cert.pem + chain.pem (서버에서 사용)
privkey.pem # 개인키
# 실제 파일은 archive 디렉토리에 있고 live는 심볼릭 링크
/etc/letsencrypt/archive/example.com/
cert1.pem
chain1.pem
fullchain1.pem
privkey1.pem
| 파일 | 내용 | 용도 |
|---|---|---|
| cert.pem | 서버 인증서 | 단독 사용 (잘 사용하지 않음) |
| chain.pem | 중간 CA 인증서 | OCSP Stapling용 |
| fullchain.pem | 서버 인증서 + 중간 CA | Nginx ssl_certificate에 사용 |
| privkey.pem | 개인키 | Nginx ssl_certificate_key에 사용 |
Nginx에 적용:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}
7. 자동 갱신 설정
7.1 갱신 테스트
# 실제 갱신 없이 테스트만 수행
sudo certbot renew --dry-run
7.2 systemd Timer (권장)
Certbot snap 설치 시 자동으로 systemd timer가 생성된다.
# Timer 상태 확인
sudo systemctl status snap.certbot.renew.timer
# Timer 목록
sudo systemctl list-timers | grep certbot
직접 생성하는 경우:
# /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Certbot renewal timer
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/certbot-renew.service
[Unit]
Description=Certbot renewal service
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renew.timer
7.3 Crontab
# crontab -e
# 매일 새벽 2시, 오후 2시에 갱신 시도
0 2,14 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"
7.4 갱신 훅(Hook) 설정
# 갱신 전 훅
sudo mkdir -p /etc/letsencrypt/renewal-hooks/pre
cat > /etc/letsencrypt/renewal-hooks/pre/stop-service.sh << 'HOOKEOF'
#!/bin/bash
# Standalone 모드용: 웹 서버 중지
systemctl stop nginx
HOOKEOF
chmod +x /etc/letsencrypt/renewal-hooks/pre/stop-service.sh
# 갱신 후 훅
sudo mkdir -p /etc/letsencrypt/renewal-hooks/post
cat > /etc/letsencrypt/renewal-hooks/post/start-service.sh << 'HOOKEOF'
#!/bin/bash
# 웹 서버 재시작
systemctl start nginx
HOOKEOF
chmod +x /etc/letsencrypt/renewal-hooks/post/start-service.sh
# 배포 훅 (성공적으로 갱신된 경우에만 실행)
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh << 'HOOKEOF'
#!/bin/bash
systemctl reload nginx
echo "Certificate renewed and nginx reloaded at $(date)" >> /var/log/certbot-deploy.log
HOOKEOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
8. Rate Limits 주의사항
Let's Encrypt는 남용 방지를 위해 다음 Rate Limits를 적용한다.
| 제한 | 값 | 설명 |
|---|---|---|
| Certificates per Registered Domain | 50 / 주 | 같은 도메인에 대해 주당 50개 |
| Duplicate Certificate | 5 / 주 | 동일한 도메인 세트에 대해 5개 |
| Failed Validations | 5 / 시간 / 계정 / 호스트명 | 검증 실패 제한 |
| New Orders | 300 / 3시간 | 새 주문 제한 |
| Accounts per IP | 10 / 3시간 | IP당 계정 생성 제한 |
Staging 환경 활용:
# 테스트 시에는 Staging 서버 사용 (Rate Limit 훨씬 관대)
sudo certbot certonly --standalone \
--staging \
-d example.com \
--agree-tos \
--email admin@example.com
9. 대안 도구
9.1 acme.sh
순수 쉘 스크립트로 작성된 ACME 클라이언트다.
# 설치
curl https://get.acme.sh | sh
# 인증서 발급
acme.sh --issue -d example.com -w /var/www/html
# 와일드카드 (DNS API 사용)
acme.sh --issue \
-d example.com \
-d "*.example.com" \
--dns dns_cf \
--dnssleep 120
# Nginx에 설치
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/example.com.key \
--fullchain-file /etc/nginx/ssl/example.com.fullchain.pem \
--reloadcmd "systemctl reload nginx"
9.2 Caddy (자동 HTTPS)
Caddy는 웹 서버 자체에서 HTTPS를 자동으로 처리한다.
# Caddyfile
example.com {
root * /var/www/html
file_server
}
# 이것만으로 Let's Encrypt 인증서 자동 발급 및 갱신!
9.3 lego
Go로 작성된 ACME 클라이언트다.
# 설치
go install github.com/go-acme/lego/v4/cmd/lego@latest
# HTTP 챌린지
lego --email admin@example.com \
--domains example.com \
--http \
run
# DNS 챌린지 (Cloudflare)
CLOUDFLARE_DNS_API_TOKEN=xxx \
lego --email admin@example.com \
--domains "*.example.com" \
--dns cloudflare \
run
9.4 cert-manager (Kubernetes)
Kubernetes 환경에서 인증서를 자동 관리한다.
# 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
# Ingress에서 인증서 요청
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- app.example.com
secretName: app-tls-secret
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
# Certificate 리소스 직접 생성
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-certificate
namespace: default
spec:
secretName: app-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- app.example.com
- api.example.com
renewBefore: 720h # 30일 전에 갱신
10. 트러블슈팅
10.1 일반적인 오류와 해결법
Challenge 실패:
# HTTP-01 챌린지 파일 접근 확인
curl -v http://yourdomain.com/.well-known/acme-challenge/test
# 방화벽 확인 (포트 80)
sudo ufw status
sudo iptables -L -n | grep 80
# DNS 확인
dig +short yourdomain.com
nslookup yourdomain.com
인증서 갱신 실패:
# 갱신 상태 확인
sudo certbot certificates
# 상세 로그 확인
sudo certbot renew --dry-run -v
# 로그 파일 확인
sudo cat /var/log/letsencrypt/letsencrypt.log
인증서 정보 확인:
# 인증서 내용 확인
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -text -noout
# 만료일 확인
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -enddate -noout
# 원격 서버 인증서 확인
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
openssl x509 -text -noout
# 인증서 체인 검증
openssl verify -CAfile /etc/letsencrypt/live/example.com/chain.pem \
/etc/letsencrypt/live/example.com/cert.pem
10.2 인증서 폐기(Revoke)
# 인증서 폐기
sudo certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem
# 인증서 삭제 (파일 정리)
sudo certbot delete --cert-name example.com
11. 보안 모범 사례
- 개인키 보호: privkey.pem 파일의 권한을 600으로 설정, root만 읽기 가능
- 자동 갱신 모니터링: 갱신 실패 시 알림 설정 (이메일, Slack 등)
- Staging 먼저: 새로운 설정은 항상 Staging 환경에서 테스트
- HSTS 적용: 인증서 발급 후 HSTS 헤더 설정
- 인증서 투명성 모니터링: crt.sh 등에서 도메인 인증서 발급 내역 모니터링
- 백업: /etc/letsencrypt 디렉토리 정기 백업
# 개인키 권한 확인
ls -la /etc/letsencrypt/live/example.com/privkey.pem
# -rw------- 1 root root ... privkey.pem
# 인증서 투명성 로그 조회
# https://crt.sh/?q=example.com
12. 결론
Let's Encrypt와 Certbot은 무료 TLS 인증서의 표준이 되었다. 핵심 정리:
- HTTP-01 챌린지는 가장 간단하지만 와일드카드는 DNS-01 필수
- Certbot의 Webroot 또는 Nginx 플러그인이 가장 실용적
- 자동 갱신은 systemd timer 또는 crontab으로 설정
- Kubernetes 환경에서는 cert-manager가 표준
- Rate Limits를 이해하고 테스트 시 Staging 서버를 활용
- 인증서 발급 후 Nginx TLS 보안 설정을 반드시 최적화해야 한다