Tailscale + Caddy + DNS Challenge로 VPN 환경에서 SSL 인증서 발급하기
2025.07.15
w0nder

## 개요
VPN 환경에서 SSL 인증서 발급이 얼마나 까다로운지 직접 겪어봤다. Public IP가 없고 방화벽 뒤에 있는 서버에서는 일반적인 Let's Encrypt 인증서 발급이 불가능하다.
Tailscale을 사용하고 있는 환경에서 Redash에 접근하려고 할 때 이런 문제에 부딪혔다. 결국 Cloudflare DNS Challenge를 활용해서 해결했는데, 생각보다 간단했다. 이 과정을 정리해봤다.
## 사전 요구사항
- ✅ 리눅스 서버에 Tailscale 설치 완료 (IP: 100.199.199.199)
- ✅ 방화벽으로 인한 포트 제한 (Tailscale 통해서만 접근 가능)
- ✅ Redash 서비스 실행 중 (포트 5000)
- ✅ Cloudflare DNS에 Tailscale IP 등록 완료
- ✅ `redash.infra.com` 도메인 사용 예정 (예시)
## DNS Challenge 이해하기
### 왜 DNS Challenge가 필요한가?
처음에는 일반적인 Let's Encrypt 인증서 발급을 시도했다. 하지만 실패했다. Public IP가 없고 80/443 포트에 외부 접근이 불가능한 환경에서는 당연한 결과였다.
그때 DNS Challenge라는 방식을 알게 됐다. DNS 시스템을 통해 도메인 소유권을 확인하는 방식인데, 외부에서 직접 접근할 수 없는 환경에서도 SSL 인증서를 발급받을 수 있다.
### HTTP Challenge vs DNS Challenge
**HTTP Challenge**
- **필요 조건**: Public IP + 80/443 포트
- **동작 방식**: Let's Encrypt가 서버에 직접 접근
- **VPN 환경**: ❌ 불가능
- **방화벽 뒤**: ❌ 불가능
- **자동화**: 부분적
**DNS Challenge**
- **필요 조건**: DNS API 접근 권한
- **동작 방식**: DNS 레코드를 통한 소유권 확인
- **VPN 환경**: ✅ 가능
- **방화벽 뒤**: ✅ 가능
- **자동화**: ✅ 완전 자동화
### DNS Challenge 동작 원리
DNS Challenge는 다음과 같은 단계로 진행된다:
```
1. 인증서 요청
Caddy → Let's Encrypt: "redash.infra.com 인증서 발급 요청"
2. DNS Challenge 요청
Let's Encrypt → Caddy: "TXT 레코드 생성 요청"
TXT: _acme-challenge.redash.infra.com = "abc123..."
3. DNS 레코드 생성
Caddy → Cloudflare API: "TXT 레코드 생성"
Cloudflare → DNS: 레코드 생성 완료
4. 소유권 확인
Let's Encrypt → DNS: "TXT 레코드 확인"
DNS → Let's Encrypt: "확인 성공"
5. 인증서 발급
Let's Encrypt → Caddy: "인증서 발급 완료"
6. 정리 작업
Caddy → Cloudflare API: "임시 TXT 레코드 삭제"
```
이 과정에서 임시로 생성된 TXT 레코드는 인증서 발급 후 자동으로 삭제되어 DNS를 깔끔하게 유지한다.
## 설정 단계
### 1단계: Tailscale 상태 확인
```bash
# Tailscale 연결 상태 확인
tailscale status
# Tailscale IP 주소 확인
tailscale ip -4
```
### 2단계: Cloudflare DNS 설정
#### 2.1 DNS A 레코드 추가
Cloudflare 대시보드에서 다음 레코드를 추가한다:
- **Type**: A
- **Name**: redash
- **Content**: 100.199.199.199 (Tailscale IP)
- **Proxy**: DNS only (회색 구름)
- **TTL**: Auto
#### 2.2 API 토큰 생성
Cloudflare API 토큰이 필요하다. Caddy가 DNS 레코드를 자동으로 관리할 수 있게 해주는 역할을 한다.
1. **Cloudflare 대시보드** → **My Profile** → **API Tokens**
2. **"Create Token"** 클릭
3. **"Custom token"** 선택
4. **권한 설정**:
- Zone:Zone:Edit
- Zone:DNS:Edit
5. **Zone Resources**: `infra.com` 도메인 선택
6. **토큰 생성** 후 안전하게 보관
API 토큰은 민감한 정보니까 환경 변수로 관리하고, 절대 코드에 직접 포함하지 마라.
### 3단계: Docker 설정
#### 3.2 Caddyfile 생성
Caddy 설정 파일을 만든다. DNS Challenge와 리버스 프록시를 설정하는 부분이다.
```caddyfile
# caddy/Caddyfile
# HTTP를 HTTPS로 자동 리디렉션
:80 {
redir https://{host}{uri} permanent
}
# 메인 도메인 설정
redash.infra.com {
# DNS Challenge를 통한 SSL 인증서 발급
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
# Redash 서비스로 프록시
reverse_proxy 100.199.199.199:5000
}
```
이 설정으로 HTTP 요청은 자동으로 HTTPS로 리디렉션되고, DNS Challenge를 통해 SSL 인증서가 자동으로 발급된다.
#### 3.3 Dockerfile 생성
Cloudflare DNS 플러그인이 포함된 Caddy 이미지를 빌드한다.
```dockerfile
# caddy/Dockerfile
# Cloudflare DNS 플러그인이 포함된 Caddy 빌드
FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare
# 최종 이미지 생성
FROM caddy:2-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
```
공식 Caddy 이미지에 Cloudflare DNS 플러그인을 추가해서 DNS Challenge 기능을 사용할 수 있게 만든다.
#### 3.4 docker-compose.yml 생성
Docker Compose로 Caddy 서비스를 관리한다.
```yaml
# docker-compose.yml
services:
caddy:
build:
context: ./caddy
dockerfile: Dockerfile
container_name: caddy-redash
restart: unless-stopped
ports:
- '80:80'
- '443:443'
env_file:
- .env
volumes:
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
```
#### 3.5 환경 변수 설정
Cloudflare API 토큰을 환경 변수로 설정한다.
```bash
# .env 파일 생성
cat > .env << EOF
CLOUDFLARE_API_TOKEN=your_cloudflare_api_token_here
EOF
# 보안을 위한 파일 권한 설정
chmod 600 .env
```
실제 API 토큰으로 `your_cloudflare_api_token_here` 부분을 교체하면 된다.
### 4단계: 서비스 실행
모든 설정이 끝나면 서비스를 시작한다.
```bash
# Docker Compose로 서비스 시작
docker-compose up -d
```
서비스가 정상적으로 시작되면 Caddy가 자동으로 SSL 인증서를 발급받기 시작한다. 처음 실행할 때는 DNS 전파 시간 때문에 몇 분 정도 걸릴 수 있다.
## 검증 및 테스트
### 1. 브라우저 접근 테스트
브라우저로 실제 접근을 테스트한다.
1. 브라우저에서 `https://redash.infra.com` 접속
2. SSL 인증서가 정상적으로 표시되는지 확인
3. Redash 로그인 페이지가 정상적으로 로드되는지 확인
브라우저 주소창에 자물쇠 아이콘이 표시되고 "안전함" 메시지가 나타나면 성공이다.
### 2. 자동 갱신
Let's Encrypt 인증서는 90일마다 자동으로 갱신된다.
Caddy는 인증서 만료 30일 전부터 자동으로 갱신을 시도하니까, 별도의 관리 없이도 인증서가 항상 유효한 상태를 유지한다.