이종관
글 목록으로

Cloudflare Tunnel로 홈서버 안전하게 외부 공개하기

포트포워딩 없이 outbound-only 연결로 로컬 서비스를 안전하게 인터넷에 노출하는 방법

2026년 2월 21일·16 min read·
infra
cloudflare
tunnel
zero-trust
networking
security
devops

왜 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)

bash
brew install cloudflared
 
# 버전 확인
cloudflared --version

Homebrew가 가장 간편한 설치 방법이다. Apple Silicon과 Intel Mac 모두 지원한다.

수동 설치 (Apple Silicon)

Homebrew를 사용하지 않는 환경이라면 바이너리를 직접 다운로드한다.

bash
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)

bash
# 공식 패키지 저장소 추가
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 계정에 연결한다.

bash
cloudflared tunnel login

실행하면 브라우저가 열리고 Cloudflare 대시보드에서 도메인을 선택하는 화면이 나온다. 승인하면 ~/.cloudflared/cert.pem 인증서가 생성된다.

터널 생성

bash
cloudflared tunnel create personaltrader

실행 결과:

plaintext
Tunnel credentials written to /Users/jk/.cloudflared/<TUNNEL-UUID>.json
Created tunnel personaltrader with id <TUNNEL-UUID>

터널 자격 증명 파일(<TUNNEL-UUID>.json)이 ~/.cloudflared/ 디렉토리에 저장된다. 이 파일은 터널을 실행할 때 인증에 사용되므로 안전하게 보관해야 한다.

bash
# 터널 목록 확인
cloudflared tunnel list

DNS 라우팅 설정

터널을 도메인에 연결하려면 DNS 레코드를 설정해야 한다. cloudflared가 CNAME 레코드를 자동으로 생성해 준다.

bash
# 메인 도메인 → 터널로 라우팅
cloudflared tunnel route dns personaltrader jongkwan.dev
 
# API 서브도메인 추가
cloudflared tunnel route dns personaltrader api.jongkwan.dev

Cloudflare DNS에 <TUNNEL-UUID>.cfargotunnel.com을 가리키는 CNAME이 자동 생성된다. 직접 대시보드에서 CNAME을 추가해도 동일한 결과를 얻을 수 있다.


config.yml로 멀티서비스 라우팅

설정 파일 위치

plaintext
~/.cloudflared/config.yml

인그레스 규칙 작성

하나의 터널로 여러 서비스를 라우팅하는 것이 Cloudflare Tunnel의 핵심 기능이다. ingress 규칙은 위에서 아래로 순서대로 매칭되며, 첫 번째 매칭 규칙이 적용된다.

yaml
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가 설정 검증에서 실패한다.

인그레스 규칙 요약

규칙호스트명대상 서비스포트
APIapi.jongkwan.devFastAPI8000
WebSocketjongkwan.dev/ws/*FastAPI WS8000
Frontendjongkwan.devNext.js3000
Fallbackcatch-all404 응답-

설정 검증

설정 파일을 작성한 뒤, 반드시 검증 단계를 거치자.

bash
# 설정 파일 문법 검증
cloudflared tunnel ingress validate

정상이면 OK가 출력된다.

bash
# 특정 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)

터널 수동 실행 (테스트)

bash
cloudflared tunnel run personaltrader

정상적으로 연결되면 터미널에 다음과 유사한 로그가 출력된다.

plaintext
INF Starting tunnel
INF Registered tunnel connection connIndex=0
INF Registered tunnel connection connIndex=1

자동 HTTPS / TLS

Cloudflare Tunnel을 사용하면 SSL/TLS 인증서를 직접 관리할 필요가 없다.

트래픽 흐름

plaintext
사용자 ──(HTTPS)──▶ Cloudflare Edge ──(cloudflared 터널)──▶ localhost:3000/8000

각 구간별 암호화 상태:

구간암호화관리 주체
사용자 → CloudflareHTTPS (SSL 인증서 자동 발급)Cloudflare
Cloudflare → cloudflared터널 내부 암호화cloudflared
cloudflared → 로컬 서비스HTTP (localhost)불필요

로컬 서비스는 http://localhost로 운영해도 된다. 외부 사용자는 항상 HTTPS로 접속하게 되며, 인증서 발급과 갱신은 Cloudflare가 자동으로 처리한다.

Cloudflare 대시보드 SSL 설정

최적의 보안을 위해 다음 설정을 권장한다.

plaintext
SSL/TLS → Overview → Full (strict)
SSL/TLS → Edge Certificates → Always Use HTTPS: ON
SSL/TLS → Edge Certificates → Automatic HTTPS Rewrites: ON

Tunnel 환경에서는 Full (strict) 모드가 권장되지만, Flexible로 설정해도 사용자와 Cloudflare 사이의 HTTPS는 보장된다. 핵심은 터널 자체가 암호화된 채널이라는 점이다.


macOS launchd 서비스 등록

터미널에서 cloudflared tunnel run을 직접 실행하면, 터미널을 닫을 때 터널도 종료된다. macOS의 launchd를 활용하면 로그인 시 자동 시작, 비정상 종료 시 자동 재시작이 가능하다.

자동 서비스 설치

bash
# 사용자 레벨 (로그인 시 시작) — 권장
cloudflared service install
 
# 시스템 레벨 (부팅 시 시작)
sudo cloudflared service install

수동 plist 작성

더 세밀한 제어가 필요하면 plist를 직접 작성한다.

파일 위치: ~/Library/LaunchAgents/com.cloudflare.cloudflared.plist

xml
<?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>

각 주요 키의 역할:

설명
RunAtLoadtrue로그인 시 자동 실행
KeepAlive.NetworkStatetrue네트워크 연결 시 자동 재시작
ThrottleInterval10비정상 종료 후 10초 대기 뒤 재시작

서비스 관리 명령어

bash
# 서비스 로드 (등록)
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

로그 확인

bash
# 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.log

Zero Trust Access Policy

Cloudflare Tunnel만으로도 포트 노출을 방지할 수 있지만, Zero Trust Access Policy를 추가하면 허가된 사용자만 서비스에 접근할 수 있다. 이메일 OTP, IP 제한 등을 조합하여 인증 레이어를 구성한다.

Access Application 생성

Cloudflare Zero Trust 대시보드에서 애플리케이션을 등록한다.

plaintext
Zero Trust → Access → Applications → Add an application

설정 값:

항목
Application typeSelf-hosted
Application namePersonalTrader
Application domainjongkwan.dev
Session Duration24 hours

Access Policy 설정

이메일 기반 인증 (One-Time PIN)

가장 간편한 방식이다. Cloudflare가 지정된 이메일로 OTP를 전송하고, 사용자가 이를 입력하면 접속을 허용한다.

yaml
policy_name: "Owner Only"
decision: Allow
include:
  - email:
    - "owner@example.com"
 
identity_providers:
  - One-time PIN  # Cloudflare가 이메일로 OTP 전송

별도의 IdP(Identity Provider) 연동 없이도 사용할 수 있어, 개인 프로젝트에 적합하다.

IP 범위 제한 (선택)

특정 IP 대역에서만 접근을 허용할 수도 있다.

yaml
include:
  - ip_ranges:
    - "203.0.113.0/24"  # 집/사무실 IP

이메일 OTP와 IP 제한을 함께 사용하면 이중 인증 효과를 얻을 수 있다.

API 엔드포인트 보호

API 서브도메인에는 별도의 Access Application을 설정하거나, Service Token 방식을 사용한다. Service Token은 자동화된 API 호출(CI/CD, 크론 작업 등)에 적합하다.

plaintext
Application domain: api.jongkwan.dev
Policy: Service Auth
  - Service Token: <자동 생성 토큰>

클라이언트는 요청 시 CF-Access-Client-IdCF-Access-Client-Secret 헤더를 포함해야 한다.

Bypass 경로 설정

헬스 체크 등 인증이 불필요한 경로는 Bypass 규칙으로 열어둔다.

yaml
path: /api/health
decision: Bypass

Access Policy 구성도


운영 관리

터널 상태 확인

bash
# CLI로 확인
cloudflared tunnel info personaltrader

Cloudflare 대시보드에서도 확인할 수 있다.

plaintext
Zero Trust → Networks → Tunnels → personaltrader

대시보드에서 확인 가능한 메트릭:

  • Active connections: 현재 활성 연결 수
  • Requests per second: 초당 요청 수
  • Error rate: 에러율

설정 변경 후 재시작

config.yml을 수정하면 서비스를 재시작해야 변경사항이 반영된다.

bash
launchctl stop com.cloudflare.cloudflared
launchctl start com.cloudflare.cloudflared

터널 삭제

bash
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 증가

디버깅 명령어

bash
# 터널 상태 상세 확인
cloudflared tunnel info personaltrader
 
# 터널 실행 로그 실시간 확인 (디버그 레벨)
cloudflared tunnel --loglevel debug run personaltrader
 
# 인그레스 규칙 테스트
cloudflared tunnel ingress rule https://jongkwan.dev/ws/stream

로컬 서비스 실행 순서

터널이 정상적으로 동작하려면 로컬 서비스가 먼저 실행되어야 한다. 권장 순서:

bash
# 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

전체 아키텍처 요약

핵심 정리

  1. 보안: 포트포워딩 없이 outbound-only 연결로 공격 표면 최소화
  2. HTTPS: Cloudflare가 SSL 인증서 자동 발급/관리
  3. 멀티서비스: 하나의 터널로 프론트엔드, 백엔드, WebSocket 동시 라우팅
  4. 자동 복구: launchd의 KeepAlive로 네트워크 복구 시 자동 재연결
  5. 접근 제어: Zero Trust Access Policy로 이메일 OTP, IP 제한 등 추가 인증

Cloudflare Tunnel은 홈서버를 외부에 공개할 때 가장 안전하고 간편한 방법이다. 포트포워딩의 보안 위험 없이, Cloudflare의 글로벌 네트워크와 보안 기능을 무료로 활용할 수 있다.