Cloudflare Tunnel로 홈서버 안전하게 외부 공개하기
포트포워딩 없이 outbound-only 연결로 로컬 서비스를 안전하게 인터넷에 노출하는 방법
왜 Cloudflare Tunnel인가
홈서버나 로컬 개발 환경을 외부에 공개하려면 보통 공유기 포트포워딩을 설정해야 한다. 하지만 이 방식에는 치명적인 단점이 있다.
| 방식 | 포트포워딩 | Cloudflare Tunnel |
|---|---|---|
| 연결 방향 | 외부 → 내부 (inbound) | 내부 → Cloudflare (outbound-only) |
| 방화벽 개방 | 필요 (포트 노출) | 불필요 |
| DDoS 방어 | 없음 | Cloudflare Edge에서 자동 방어 |
| SSL 인증서 | 직접 발급/관리 | Cloudflare가 자동 발급 |
| 고정 IP | 필요 (또는 DDNS) | 불필요 |
| 추가 인증 | 직접 구현 | Zero Trust Access Policy 제공 |
Cloudflare Tunnel은 로컬 머신에서 Cloudflare Edge로 outbound 연결만 생성한다. 외부에서 로컬 네트워크로 직접 접근하는 경로가 존재하지 않기 때문에, 포트포워딩 방식 대비 공격 표면이 크게 줄어든다.
동작 원리
cloudflared 데몬이 로컬에서 Cloudflare Edge 서버로 아웃바운드 커넥션을 생성하고, 외부 트래픽은 이 터널을 통해서만 로컬 서비스에 도달한다. 공유기나 방화벽에 어떤 포트도 열 필요가 없다.
cloudflared 설치
macOS (Homebrew)
brew install cloudflared
# 버전 확인
cloudflared --versionHomebrew가 가장 간편한 설치 방법이다. Apple Silicon과 Intel Mac 모두 지원한다.
수동 설치 (Apple Silicon)
Homebrew를 사용하지 않는 환경이라면 바이너리를 직접 다운로드한다.
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz | tar xz
sudo mv cloudflared /usr/local/bin/Linux (Debian/Ubuntu)
# 공식 패키지 저장소 추가
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install cloudflared인증 및 터널 생성
Cloudflare 계정 인증
먼저 cloudflared를 Cloudflare 계정에 연결한다.
cloudflared tunnel login실행하면 브라우저가 열리고 Cloudflare 대시보드에서 도메인을 선택하는 화면이 나온다. 승인하면 ~/.cloudflared/cert.pem 인증서가 생성된다.
터널 생성
cloudflared tunnel create personaltrader실행 결과:
Tunnel credentials written to /Users/jk/.cloudflared/<TUNNEL-UUID>.json
Created tunnel personaltrader with id <TUNNEL-UUID>터널 자격 증명 파일(<TUNNEL-UUID>.json)이 ~/.cloudflared/ 디렉토리에 저장된다. 이 파일은 터널을 실행할 때 인증에 사용되므로 안전하게 보관해야 한다.
# 터널 목록 확인
cloudflared tunnel listDNS 라우팅 설정
터널을 도메인에 연결하려면 DNS 레코드를 설정해야 한다. cloudflared가 CNAME 레코드를 자동으로 생성해 준다.
# 메인 도메인 → 터널로 라우팅
cloudflared tunnel route dns personaltrader jongkwan.dev
# API 서브도메인 추가
cloudflared tunnel route dns personaltrader api.jongkwan.devCloudflare DNS에 <TUNNEL-UUID>.cfargotunnel.com을 가리키는 CNAME이 자동 생성된다. 직접 대시보드에서 CNAME을 추가해도 동일한 결과를 얻을 수 있다.
config.yml로 멀티서비스 라우팅
설정 파일 위치
~/.cloudflared/config.yml인그레스 규칙 작성
하나의 터널로 여러 서비스를 라우팅하는 것이 Cloudflare Tunnel의 핵심 기능이다. ingress 규칙은 위에서 아래로 순서대로 매칭되며, 첫 번째 매칭 규칙이 적용된다.
tunnel: <TUNNEL-UUID>
credentials-file: /Users/jk/.cloudflared/<TUNNEL-UUID>.json
# 인그레스 규칙 (위에서 아래로 순서대로 매칭)
ingress:
# API 서브도메인 → FastAPI (백엔드)
- hostname: api.jongkwan.dev
service: http://localhost:8000
originRequest:
connectTimeout: 10s
noTLSVerify: false
# WebSocket 경로 → FastAPI WebSocket
- hostname: jongkwan.dev
path: /ws/*
service: http://localhost:8000
originRequest:
connectTimeout: 30s
# 메인 도메인 → Next.js (프론트엔드)
- hostname: jongkwan.dev
service: http://localhost:3000
# 폴백 (필수 — 매칭되지 않는 요청 거부)
- service: http_status:404마지막 catch-all 규칙(
service: http_status:404)은 필수다. 이 규칙이 없으면cloudflared가 설정 검증에서 실패한다.
인그레스 규칙 요약
| 규칙 | 호스트명 | 대상 서비스 | 포트 |
|---|---|---|---|
| API | api.jongkwan.dev | FastAPI | 8000 |
| WebSocket | jongkwan.dev/ws/* | FastAPI WS | 8000 |
| Frontend | jongkwan.dev | Next.js | 3000 |
| Fallback | catch-all | 404 응답 | - |
설정 검증
설정 파일을 작성한 뒤, 반드시 검증 단계를 거치자.
# 설정 파일 문법 검증
cloudflared tunnel ingress validate정상이면 OK가 출력된다.
# 특정 URL이 어떤 규칙에 매칭되는지 테스트
cloudflared tunnel ingress rule https://jongkwan.dev
# → Using rule 3 (hostname: jongkwan.dev → http://localhost:3000)
cloudflared tunnel ingress rule https://api.jongkwan.dev/v1/health
# → Using rule 1 (hostname: api.jongkwan.dev → http://localhost:8000)
cloudflared tunnel ingress rule https://jongkwan.dev/ws/stream
# → Using rule 2 (hostname: jongkwan.dev, path: /ws/* → http://localhost:8000)터널 수동 실행 (테스트)
cloudflared tunnel run personaltrader정상적으로 연결되면 터미널에 다음과 유사한 로그가 출력된다.
INF Starting tunnel
INF Registered tunnel connection connIndex=0
INF Registered tunnel connection connIndex=1자동 HTTPS / TLS
Cloudflare Tunnel을 사용하면 SSL/TLS 인증서를 직접 관리할 필요가 없다.
트래픽 흐름
사용자 ──(HTTPS)──▶ Cloudflare Edge ──(cloudflared 터널)──▶ localhost:3000/8000각 구간별 암호화 상태:
| 구간 | 암호화 | 관리 주체 |
|---|---|---|
| 사용자 → Cloudflare | HTTPS (SSL 인증서 자동 발급) | Cloudflare |
| Cloudflare → cloudflared | 터널 내부 암호화 | cloudflared |
| cloudflared → 로컬 서비스 | HTTP (localhost) | 불필요 |
로컬 서비스는 http://localhost로 운영해도 된다. 외부 사용자는 항상 HTTPS로 접속하게 되며, 인증서 발급과 갱신은 Cloudflare가 자동으로 처리한다.
Cloudflare 대시보드 SSL 설정
최적의 보안을 위해 다음 설정을 권장한다.
SSL/TLS → Overview → Full (strict)
SSL/TLS → Edge Certificates → Always Use HTTPS: ON
SSL/TLS → Edge Certificates → Automatic HTTPS Rewrites: ONTunnel 환경에서는
Full (strict)모드가 권장되지만,Flexible로 설정해도 사용자와 Cloudflare 사이의 HTTPS는 보장된다. 핵심은 터널 자체가 암호화된 채널이라는 점이다.
macOS launchd 서비스 등록
터미널에서 cloudflared tunnel run을 직접 실행하면, 터미널을 닫을 때 터널도 종료된다. macOS의 launchd를 활용하면 로그인 시 자동 시작, 비정상 종료 시 자동 재시작이 가능하다.
자동 서비스 설치
# 사용자 레벨 (로그인 시 시작) — 권장
cloudflared service install
# 시스템 레벨 (부팅 시 시작)
sudo cloudflared service install수동 plist 작성
더 세밀한 제어가 필요하면 plist를 직접 작성한다.
파일 위치: ~/Library/LaunchAgents/com.cloudflare.cloudflared.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.cloudflare.cloudflared</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/cloudflared</string>
<string>tunnel</string>
<string>--config</string>
<string>/Users/jk/.cloudflared/config.yml</string>
<string>run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>NetworkState</key>
<true/>
</dict>
<key>StandardOutPath</key>
<string>/Users/jk/.cloudflared/cloudflared.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/jk/.cloudflared/cloudflared.err.log</string>
<key>ThrottleInterval</key>
<integer>10</integer>
</dict>
</plist>각 주요 키의 역할:
| 키 | 값 | 설명 |
|---|---|---|
RunAtLoad | true | 로그인 시 자동 실행 |
KeepAlive.NetworkState | true | 네트워크 연결 시 자동 재시작 |
ThrottleInterval | 10 | 비정상 종료 후 10초 대기 뒤 재시작 |
서비스 관리 명령어
# 서비스 로드 (등록)
launchctl load ~/Library/LaunchAgents/com.cloudflare.cloudflared.plist
# 서비스 시작
launchctl start com.cloudflare.cloudflared
# 서비스 정지
launchctl stop com.cloudflare.cloudflared
# 서비스 언로드 (해제)
launchctl unload ~/Library/LaunchAgents/com.cloudflare.cloudflared.plist
# 상태 확인
launchctl list | grep cloudflare로그 확인
# cloudflared service install로 설치한 경우
tail -f /Library/Logs/com.cloudflare.cloudflared.out.log
tail -f /Library/Logs/com.cloudflare.cloudflared.err.log
# 커스텀 plist로 설치한 경우
tail -f ~/.cloudflared/cloudflared.out.log
tail -f ~/.cloudflared/cloudflared.err.logZero Trust Access Policy
Cloudflare Tunnel만으로도 포트 노출을 방지할 수 있지만, Zero Trust Access Policy를 추가하면 허가된 사용자만 서비스에 접근할 수 있다. 이메일 OTP, IP 제한 등을 조합하여 인증 레이어를 구성한다.
Access Application 생성
Cloudflare Zero Trust 대시보드에서 애플리케이션을 등록한다.
Zero Trust → Access → Applications → Add an application설정 값:
| 항목 | 값 |
|---|---|
| Application type | Self-hosted |
| Application name | PersonalTrader |
| Application domain | jongkwan.dev |
| Session Duration | 24 hours |
Access Policy 설정
이메일 기반 인증 (One-Time PIN)
가장 간편한 방식이다. Cloudflare가 지정된 이메일로 OTP를 전송하고, 사용자가 이를 입력하면 접속을 허용한다.
policy_name: "Owner Only"
decision: Allow
include:
- email:
- "owner@example.com"
identity_providers:
- One-time PIN # Cloudflare가 이메일로 OTP 전송별도의 IdP(Identity Provider) 연동 없이도 사용할 수 있어, 개인 프로젝트에 적합하다.
IP 범위 제한 (선택)
특정 IP 대역에서만 접근을 허용할 수도 있다.
include:
- ip_ranges:
- "203.0.113.0/24" # 집/사무실 IP이메일 OTP와 IP 제한을 함께 사용하면 이중 인증 효과를 얻을 수 있다.
API 엔드포인트 보호
API 서브도메인에는 별도의 Access Application을 설정하거나, Service Token 방식을 사용한다. Service Token은 자동화된 API 호출(CI/CD, 크론 작업 등)에 적합하다.
Application domain: api.jongkwan.dev
Policy: Service Auth
- Service Token: <자동 생성 토큰>클라이언트는 요청 시 CF-Access-Client-Id와 CF-Access-Client-Secret 헤더를 포함해야 한다.
Bypass 경로 설정
헬스 체크 등 인증이 불필요한 경로는 Bypass 규칙으로 열어둔다.
path: /api/health
decision: BypassAccess Policy 구성도
운영 관리
터널 상태 확인
# CLI로 확인
cloudflared tunnel info personaltraderCloudflare 대시보드에서도 확인할 수 있다.
Zero Trust → Networks → Tunnels → personaltrader대시보드에서 확인 가능한 메트릭:
- Active connections: 현재 활성 연결 수
- Requests per second: 초당 요청 수
- Error rate: 에러율
설정 변경 후 재시작
config.yml을 수정하면 서비스를 재시작해야 변경사항이 반영된다.
launchctl stop com.cloudflare.cloudflared
launchctl start com.cloudflare.cloudflared터널 삭제
cloudflared tunnel delete personaltrader터널 삭제 후 DNS 레코드는 자동으로 삭제되지 않으므로, Cloudflare 대시보드에서 수동으로 CNAME 레코드를 제거해야 한다.
트러블슈팅
자주 발생하는 문제
| 증상 | 원인 | 해결 방법 |
|---|---|---|
ERR Connection refused | 로컬 서비스가 실행되지 않음 | FastAPI/Next.js를 먼저 시작한 뒤 터널 실행 |
ERR Too many connections | 터널이 중복 실행됨 | ps aux | grep cloudflared로 중복 프로세스 확인 후 종료 |
| DNS 미동작 | CNAME 레코드 미생성 | cloudflared tunnel route dns 재실행 |
| WebSocket 끊김 | connectTimeout 값 부족 | config.yml에서 timeout 값을 30s 이상으로 증가 |
cert.pem 만료 | 인증서 갱신 필요 | cloudflared tunnel login 재실행 |
| 502 Bad Gateway | 로컬 서비스 응답 지연 | originRequest의 connectTimeout, tlsTimeout 증가 |
디버깅 명령어
# 터널 상태 상세 확인
cloudflared tunnel info personaltrader
# 터널 실행 로그 실시간 확인 (디버그 레벨)
cloudflared tunnel --loglevel debug run personaltrader
# 인그레스 규칙 테스트
cloudflared tunnel ingress rule https://jongkwan.dev/ws/stream로컬 서비스 실행 순서
터널이 정상적으로 동작하려면 로컬 서비스가 먼저 실행되어야 한다. 권장 순서:
# 1. FastAPI 백엔드 시작
uvicorn app.main:app --host 0.0.0.0 --port 8000
# 2. Next.js 프론트엔드 시작
pnpm dev # localhost:3000
# 3. Cloudflare Tunnel 시작 (또는 launchd로 자동 시작)
cloudflared tunnel run personaltrader전체 아키텍처 요약
핵심 정리
- 보안: 포트포워딩 없이 outbound-only 연결로 공격 표면 최소화
- HTTPS: Cloudflare가 SSL 인증서 자동 발급/관리
- 멀티서비스: 하나의 터널로 프론트엔드, 백엔드, WebSocket 동시 라우팅
- 자동 복구: launchd의
KeepAlive로 네트워크 복구 시 자동 재연결 - 접근 제어: Zero Trust Access Policy로 이메일 OTP, IP 제한 등 추가 인증
Cloudflare Tunnel은 홈서버를 외부에 공개할 때 가장 안전하고 간편한 방법이다. 포트포워딩의 보안 위험 없이, Cloudflare의 글로벌 네트워크와 보안 기능을 무료로 활용할 수 있다.