Cloudflare Zero Trust Tunnel로 집 서버 안전하게 공개하기
Cloudflare Zero Trust Tunnel을 활용하면 라우터 포트포워딩 없이도 집 서버를 안전하게 외부로 공개할 수 있습니다. 서버가 직접 아웃바운드 연결을 맺어 공인 IP 노출을 막고, Cloudflare 엣지 네트워크를 통해 TLS 인증과 DDoS 방어까지 한 번에 해결하는 효율적인 보안 구성 방법을 소개합니다

들어가며: 집 서버를 인터넷에 노출한다는 것
남는 미니 PC나 라즈베리파이에 웹 서비스를 올려두면, 다음 단계는 항상 같은 질문으로 이어진다. "이걸 어떻게 외부에서 접속하게 만들까?"
전통적인 답은 정해져 있다. 그리고 그 답은 생각보다 손이 많이 간다.
전통적 방식의 체크리스트
집에서 돌아가는 서버를 인터넷에 공개하려면 보통 이런 단계를 거친다.
- 공인 IP 확보 — ISP가 주는 IP가 공인 IP여야 한다. CGNAT(공유 공인 IP) 환경이면 포트포워딩 자체가 불가능하거나, ISP에 별도 요청을 해야 한다.
- 포트포워딩 — 라우터 관리 페이지에 들어가 "외부 443 → 내부 192.168.x.x:443" 식으로 매핑을 건다. 라우터 펌웨어마다 UI가 제각각이고, 공유기를 바꾸면 다시 설정해야 한다.
- 동적 DNS(DDNS) — 가정용 회선은 IP가 주기적으로 바뀐다.
home.example.com이 항상 현재 IP를 가리키게 하려면 DDNS 클라이언트를 돌려야 한다. - TLS 인증서 — HTTPS를 쓰려면 Let's Encrypt 등으로 인증서를 발급·갱신해야 한다. 보통 certbot이나 리버스 프록시(Caddy/Nginx)에 위임한다.
- 방화벽 규칙 — 열어둔 포트로 들어오는 트래픽을 필터링해야 한다.
각 단계가 동작은 한다. 문제는 이 조합이 만들어내는 공격 표면이다.
무엇이 위험한가
전통적 방식의 핵심 문제는 서버의 진짜 IP가 인터넷에 그대로 드러난다는 점이다.
- IP 직접 노출: DNS에 가정용 회선의 공인 IP가 박힌다. 그 IP는 곧 우리 집 회선이고, 같은 회선에 있는 다른 기기까지 같은 IP를 공유한다.
- 포트 스캔의 표적: 인터넷에 열린 포트는 24시간 스캔당한다.
443,22,8080같은 포트를 열어두면 봇이 자동으로 SSH 브루트포스, 알려진 CVE 익스플로잇, 취약점 스캔을 시도한다. 로그를 켜보면 분 단위로 낯선 IP가 두드리는 걸 볼 수 있다. - 패치 레이스: 노출된 서비스에 취약점이 발견되면, 공격자가 자동화 스캔으로 찾아내기 전에 내가 패치해야 한다. 이 레이스에서 항상 이길 수는 없다.
- DDoS 직격: 공인 IP가 알려지면 애플리케이션 계층뿐 아니라 회선 자체를 향한 볼류메트릭 공격에도 무방비다.
요약하면, 전통적 방식은 "문을 여러 개 열고, 각 문 앞에 경비를 세우는" 구조다. 문이 많을수록 관리할 게 많고, 문이 거기 있다는 사실 자체가 위험이다.
이 글에서 다룰 Cloudflare Zero Trust Tunnel은 접근법을 뒤집는다. 문을 하나도 열지 않는다.
cloudflared 터널의 원리
핵심 아이디어는 한 문장으로 요약된다.
서버가 Cloudflare로 아웃바운드 연결만 맺고, 외부 요청은 그 연결을 역방향으로 타고 들어온다.
인바운드 0, 아웃바운드 1
집 서버에 cloudflared라는 경량 데몬을 띄운다. 이 데몬은 시작하자마자 Cloudflare의 엣지 네트워크로 나가는 연결(보통 QUIC/HTTP2 over 443/7844)을 맺고 그 연결을 계속 유지한다.
방향이 중요하다. 이건 우리가 평소에 웹 브라우저로 사이트에 접속하는 것과 같은 아웃바운드 방향이다. 라우터/방화벽은 내부에서 밖으로 나가는 연결을 기본적으로 허용하기 때문에, 여기에는 아무 설정도 필요 없다.
[ 방문자 ] → HTTPS → [ Cloudflare 엣지 ] ⇄ (서버가 먼저 맺어둔 터널) ⇄ [ cloudflared @ 집 서버 ] → http://localhost:3000 [ 내 앱 ]요청이 들어오는 흐름은 이렇다.
- 방문자가
app.example.com에 접속한다. DNS는 Cloudflare 엣지를 가리킨다 (서버 IP가 아니다). - Cloudflare 엣지가 요청을 받는다. 여기서 TLS 종료, WAF, DDoS 필터링이 일어난다.
- 엣지는 이미 맺어져 있는 터널을 통해 요청을 집 서버의
cloudflared로 전달한다. cloudflared가 ingress 규칙에 따라 요청을 로컬 서비스(http://localhost:3000)로 프록시한다.- 응답이 같은 경로를 역방향으로 돌아 나간다.
이 구조가 바꾸는 것
- 라우터에 인바운드 포트를 단 하나도 열지 않는다. 포트포워딩 설정이 통째로 사라진다.
- 서버의 공인 IP가 DNS에 나타나지 않는다. 외부에서 보이는 건 Cloudflare의 IP뿐이다. 포트 스캔을 해도 우리 집 회선이 아니라 Cloudflare를 두드리게 된다.
- CGNAT 뒤에서도 동작한다. 아웃바운드만 필요하므로 공인 IP가 없어도 된다. 이건 전통적 포트포워딩으로는 불가능했던 시나리오다.
- 방화벽 정책이 단순해진다. 인바운드 규칙을 다 닫아도 된다. 나가는 연결만 허용하면 끝이다.
공격자 입장에서 보면, 두드릴 문이 사라진 것이다. 서비스는 여전히 공개되어 있지만, 그 진입점은 전적으로 Cloudflare 엣지이고 우리 서버는 그 뒤에 숨는다.
설정 단계 개요
실제 구성은 다음 순서로 진행된다. 콘솔(대시보드) 방식과 설정 파일 방식 두 가지가 있는데, 여기서는 둘 다 짚는다.
- 터널 생성 — Cloudflare 계정에 터널 객체를 만든다. 터널은 고유 ID와 자격증명을 가진다.
- 자격증명/토큰 확보 — 그 터널에 연결할 수 있는 토큰(또는 자격증명 JSON 파일)을 받는다.
cloudflared가 이걸로 자기 신원을 증명한다. - ingress 규칙 작성 —
hostname → http://localhost:포트매핑을 정의한다. 어떤 도메인으로 들어온 요청을 어느 로컬 서비스로 보낼지 결정한다. - DNS 연결 —
app.example.com이 터널을 가리키는 CNAME 레코드를 만든다. 대시보드 기반 터널이면 이 단계는 자동이다. - 상시 실행 —
cloudflared를 데몬이나 도커 컨테이너로 띄워 항상 살아있게 한다.
두 가지 운영 모드
Cloudflare 터널에는 두 가지 운영 방식이 있고, 코드 구성이 달라진다.
- 원격 관리(remote-managed) 터널 — ingress 규칙과 DNS를 Cloudflare 대시보드에서 관리한다. 서버의
cloudflared는 토큰 하나만 들고 시작하면, 설정을 엣지에서 받아온다. DNS CNAME도 대시보드가 자동으로 만들어준다. 구성이 가장 단순해서 도커 환경에 잘 맞는다. - 로컬 설정(locally-managed) 터널 — ingress 규칙을 서버의
config.yml에 직접 쓴다. 자격증명 JSON 파일과 함께cloudflared tunnel run으로 실행한다. 설정이 Git에 남아 버전 관리되는 게 장점이다.
아래에서 두 방식의 코드를 모두 보여준다.
코드: 도커로 cloudflared 실행하기
도커 환경에서는 토큰 기반(원격 관리) 터널이 가장 깔끔하다. 대시보드에서 터널을 만들면 다음과 같은 실행 명령을 토큰과 함께 제공한다.
단독 docker 실행
docker run -d \
--name cloudflared \
--restart unless-stopped \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate run --token <TUNNEL_TOKEN>토큰은 명령줄에 직접 박기보다 환경변수로 주입하는 편이 안전하다. cloudflared는 TUNNEL_TOKEN 환경변수를 자동으로 읽는다.
docker run -d \
--name cloudflared \
--restart unless-stopped \
-e TUNNEL_TOKEN="<TUNNEL_TOKEN>" \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate rundocker-compose 서비스 골격
실제 셀프호스팅에서는 앱과 터널을 같은 compose 파일에 묶는 게 보통이다. 토큰은 .env로 빼고, compose 파일에는 값을 직접 쓰지 않는다.
services:
app:
image: my-app:latest
restart: unless-stopped
# 호스트에 포트를 열지 않는다. 같은 도커 네트워크 안에서만 노출.
expose:
- "3000"
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
depends_on:
- app.env 파일은 다음과 같이 둔다 (Git에 절대 커밋하지 않는다).
TUNNEL_TOKEN=eyJhIjoiABC...replace-with-your-token...XYZ여기서 주목할 두 가지.
app은ports가 아니라expose를 쓴다.ports는 호스트 포트를 여는 것이고,expose는 같은 도커 네트워크 내부에만 포트를 보이게 한다. 외부로 직접 노출되는 포트가 0이 된다.- ingress의 대상은
localhost가 아니라 서비스 이름이다. 같은 compose 네트워크에서는cloudflared가http://app:3000으로 앱에 닿는다. 대시보드의 터널 설정에서 서비스 URL을http://app:3000으로 지정하면 된다. (cloudflared를 호스트에 직접 띄운 경우라면http://localhost:3000.)
로컬 설정 방식: config.yml과 자격증명
설정을 파일로 관리하고 싶다면 config.yml에 ingress 규칙을 직접 쓴다. 이 방식은 자격증명 JSON 파일(cloudflared tunnel create로 생성됨)과 함께 동작한다.
# config.yml
tunnel: my-home-tunnel
credentials-file: /etc/cloudflared/<TUNNEL_ID>.json
ingress:
# 메인 앱
- hostname: app.example.com
service: http://localhost:3000
# 다른 서비스 (예: 별도 포트의 대시보드)
- hostname: grafana.example.com
service: http://localhost:3001
# 경로 단위 라우팅도 가능
- hostname: api.example.com
path: /v1/.*
service: http://localhost:8080
# 어떤 규칙에도 안 맞으면 404 (catch-all — 필수)
- service: http_status:404ingress 규칙은 위에서 아래로 순서대로 매칭되며, 맨 마지막의 catch-all 규칙은 반드시 있어야 한다. 없으면 cloudflared가 설정을 거부한다.
이 방식의 docker-compose는 config.yml과 자격증명을 볼륨으로 마운트한다.
services:
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel run my-home-tunnel
volumes:
- ./cloudflared:/etc/cloudflared:ro./cloudflared 디렉토리 안에 config.yml과 <TUNNEL_ID>.json을 둔다. 자격증명 파일은 비밀이므로 Git에서 제외한다.
DNS 연결
로컬 설정 방식에서는 DNS CNAME을 한 번 직접 걸어줘야 한다.
cloudflared tunnel route dns my-home-tunnel app.example.com이 명령은 app.example.com을 <TUNNEL_ID>.cfargotunnel.com으로 가리키는 CNAME을 만든다. 원격 관리 터널이면 대시보드에서 hostname을 추가할 때 이 CNAME이 자동 생성되므로 이 단계를 건너뛴다.
Zero Trust Access: 민감한 경로 게이팅
터널은 서비스를 "공개"한다. 하지만 /admin이나 내부 대시보드처럼 아무나 접근하면 안 되는 경로가 있다. 여기서 Cloudflare Zero Trust Access가 한 겹을 더 얹는다.
동작 방식
Access는 터널 앞단(엣지)에서 동작하는 인증 게이트다. 특정 hostname 또는 경로에 도달하기 전에, 방문자가 지정한 IdP로 로그인하도록 강제한다.
- 애플리케이션 정의: "
app.example.com/admin이하 경로"를 보호 대상 애플리케이션으로 등록한다. - IdP 연결: Google, GitHub, 또는 일회용 PIN 이메일 등을 신원 공급자로 연결한다.
- 정책 작성: "이메일이
[email protected]인 사용자만 허용" 같은 규칙을 만든다.
이렇게 하면 흐름은 이렇게 바뀐다.
- 방문자가
app.example.com/admin에 접근한다. - Cloudflare 엣지가 앱에 도달하기 전에 가로채서 로그인 화면을 띄운다.
- 방문자가 IdP로 로그인하고, 정책의 허용 조건을 만족해야만 통과한다.
- 통과하면 그제서야 요청이 터널을 타고 집 서버에 닿는다.
핵심은 인증이 우리 애플리케이션 코드 바깥, Cloudflare 엣지에서 끝난다는 점이다. 우리 서버는 인증 통과한 요청만 받는다. 미인증 트래픽은 집 회선에 도달조차 하지 못한다.
다층 방어로 쓰기
실전에서는 Access를 1차 게이트로 쓰고, 애플리케이션 자체 인증을 2차로 두는 이중 구조가 견고하다. Access가 엣지에서 신원을 한 번 거른 뒤, 앱이 세션/권한을 다시 검증한다. 한쪽 설정에 실수가 있어도 다른 쪽이 막아주는 구조다.
정리: 이점과 주의점
이점
| 항목 | 전통적 방식 | Cloudflare 터널 |
|---|---|---|
| 인바운드 개방 포트 | 1개 이상 (예: 443) | 0개 |
| 서버 공인 IP | DNS에 노출 | 은닉 (Cloudflare IP만 보임) |
| TLS 인증서 | 직접 발급·갱신 | 엣지에서 자동 |
| DDoS/WAF | 직접 구성 | Cloudflare가 흡수 |
| CGNAT 환경 | 사실상 불가 | 동작함 |
| 포트포워딩/DDNS | 필요 | 불필요 |
핵심 이점을 다시 정리하면.
- 서버 IP 은닉: 외부에 보이는 진입점은 Cloudflare 엣지뿐이다. 포트 스캔과 IP 기반 직접 공격이 원천 차단된다.
- 자동 TLS: 인증서 발급·갱신을 엣지에 위임한다. certbot 크론을 더 이상 돌리지 않아도 된다.
- WAF/DDoS 보호: 악성 트래픽이 집 회선에 닿기 전에 Cloudflare가 흡수·필터링한다.
- 개방 포트 0: 라우터/방화벽 인바운드 규칙을 전부 닫아도 된다. 공격 표면이 극적으로 줄어든다.
주의점과 한계
장점이 분명한 만큼, 트레이드오프도 명확히 알고 들어가야 한다.
- 벤더 락인: 진입 경로 전체가 Cloudflare에 의존한다. Cloudflare에 장애가 나거나 정책이 바뀌면 우리 서비스 접근도 영향을 받는다. 진입점을 한 회사에 위임하는 구조라는 걸 받아들여야 한다.
- 무료 한도와 약관: 터널과 Access의 기본 기능은 무료 플랜으로 충분하지만, 이용 약관을 확인할 필요가 있다. 특히 비디오 스트리밍 등 대용량 비-HTML 콘텐츠를 프록시하는 용도는 표준 약관에서 제약을 받을 수 있다. 트래픽 성격이 일반적인 웹 앱을 벗어난다면 약관을 먼저 읽자.
- 프로세스 단일 장애점:
cloudflared프로세스가 죽으면 그 즉시 외부 접속이 끊긴다. 포트포워딩과 달리, 이 데몬이 살아있는 것 자체가 가용성의 전제다. 반드시restart: unless-stopped(또는 시스템 데몬이면Restart=always)로 자동 재시작을 걸어두고, 가능하면 헬스체크/모니터링을 붙인다. - 추가 홉으로 인한 지연: 모든 트래픽이 Cloudflare 엣지를 경유하므로, 직결 대비 약간의 레이턴시가 추가된다. 대부분의 셀프호스팅 용도에서는 무시할 수준이지만, 지연에 민감한 워크로드라면 인지하고 있어야 한다.
- 관측 가능성: 진입 트래픽이 엣지를 지나므로, 로컬에서만 로그를 보면 그림이 반쪽이다. Cloudflare 대시보드의 터널/Access 로그를 함께 봐야 전체 흐름이 보인다.
마치며
Cloudflare Zero Trust Tunnel의 본질은 "공개 방식을 인바운드에서 아웃바운드로 뒤집는 것"이다. 문을 여러 개 열고 지키는 대신, 문을 전부 닫고 서버가 직접 밖으로 손을 뻗어 연결을 맺는다. 그 결과 서버 IP는 숨고, 포트는 0개가 되고, TLS·WAF·DDoS 방어는 엣지가 가져간다.
대가는 Cloudflare에 대한 의존이다. 하지만 집 서버를 인터넷에 공개하는 대부분의 시나리오에서, 포트포워딩과 DDNS와 인증서 갱신을 직접 관리하며 진짜 IP를 노출하는 것보다, 이 트레이드오프가 훨씬 합리적인 선택인 경우가 많다.