본문으로 건너뛰기
wooncloud

Cloudflare Zero Trust Tunnel로 집 서버 안전하게 공개하기

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

·21 min read· views·

Cloudflare Zero Trust Tunnel로 집 서버 안전하게 공개하기

들어가며: 집 서버를 인터넷에 노출한다는 것

남는 미니 PC나 라즈베리파이에 웹 서비스를 올려두면, 다음 단계는 항상 같은 질문으로 이어진다. "이걸 어떻게 외부에서 접속하게 만들까?"

전통적인 답은 정해져 있다. 그리고 그 답은 생각보다 손이 많이 간다.

전통적 방식의 체크리스트

집에서 돌아가는 서버를 인터넷에 공개하려면 보통 이런 단계를 거친다.

  1. 공인 IP 확보 — ISP가 주는 IP가 공인 IP여야 한다. CGNAT(공유 공인 IP) 환경이면 포트포워딩 자체가 불가능하거나, ISP에 별도 요청을 해야 한다.
  2. 포트포워딩 — 라우터 관리 페이지에 들어가 "외부 443 → 내부 192.168.x.x:443" 식으로 매핑을 건다. 라우터 펌웨어마다 UI가 제각각이고, 공유기를 바꾸면 다시 설정해야 한다.
  3. 동적 DNS(DDNS) — 가정용 회선은 IP가 주기적으로 바뀐다. home.example.com이 항상 현재 IP를 가리키게 하려면 DDNS 클라이언트를 돌려야 한다.
  4. TLS 인증서 — HTTPS를 쓰려면 Let's Encrypt 등으로 인증서를 발급·갱신해야 한다. 보통 certbot이나 리버스 프록시(Caddy/Nginx)에 위임한다.
  5. 방화벽 규칙 — 열어둔 포트로 들어오는 트래픽을 필터링해야 한다.

각 단계가 동작은 한다. 문제는 이 조합이 만들어내는 공격 표면이다.

무엇이 위험한가

전통적 방식의 핵심 문제는 서버의 진짜 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 [ 내 앱 ]

요청이 들어오는 흐름은 이렇다.

  1. 방문자가 app.example.com에 접속한다. DNS는 Cloudflare 엣지를 가리킨다 (서버 IP가 아니다).
  2. Cloudflare 엣지가 요청을 받는다. 여기서 TLS 종료, WAF, DDoS 필터링이 일어난다.
  3. 엣지는 이미 맺어져 있는 터널을 통해 요청을 집 서버의 cloudflared로 전달한다.
  4. cloudflared가 ingress 규칙에 따라 요청을 로컬 서비스(http://localhost:3000)로 프록시한다.
  5. 응답이 같은 경로를 역방향으로 돌아 나간다.

이 구조가 바꾸는 것

  • 라우터에 인바운드 포트를 단 하나도 열지 않는다. 포트포워딩 설정이 통째로 사라진다.
  • 서버의 공인 IP가 DNS에 나타나지 않는다. 외부에서 보이는 건 Cloudflare의 IP뿐이다. 포트 스캔을 해도 우리 집 회선이 아니라 Cloudflare를 두드리게 된다.
  • CGNAT 뒤에서도 동작한다. 아웃바운드만 필요하므로 공인 IP가 없어도 된다. 이건 전통적 포트포워딩으로는 불가능했던 시나리오다.
  • 방화벽 정책이 단순해진다. 인바운드 규칙을 다 닫아도 된다. 나가는 연결만 허용하면 끝이다.

공격자 입장에서 보면, 두드릴 문이 사라진 것이다. 서비스는 여전히 공개되어 있지만, 그 진입점은 전적으로 Cloudflare 엣지이고 우리 서버는 그 뒤에 숨는다.

설정 단계 개요

실제 구성은 다음 순서로 진행된다. 콘솔(대시보드) 방식과 설정 파일 방식 두 가지가 있는데, 여기서는 둘 다 짚는다.

  1. 터널 생성 — Cloudflare 계정에 터널 객체를 만든다. 터널은 고유 ID와 자격증명을 가진다.
  2. 자격증명/토큰 확보 — 그 터널에 연결할 수 있는 토큰(또는 자격증명 JSON 파일)을 받는다. cloudflared가 이걸로 자기 신원을 증명한다.
  3. ingress 규칙 작성hostname → http://localhost:포트 매핑을 정의한다. 어떤 도메인으로 들어온 요청을 어느 로컬 서비스로 보낼지 결정한다.
  4. DNS 연결app.example.com이 터널을 가리키는 CNAME 레코드를 만든다. 대시보드 기반 터널이면 이 단계는 자동이다.
  5. 상시 실행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>

토큰은 명령줄에 직접 박기보다 환경변수로 주입하는 편이 안전하다. cloudflaredTUNNEL_TOKEN 환경변수를 자동으로 읽는다.

docker run -d \
  --name cloudflared \
  --restart unless-stopped \
  -e TUNNEL_TOKEN="<TUNNEL_TOKEN>" \
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run

docker-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

여기서 주목할 두 가지.

  • appports가 아니라 expose를 쓴다. ports는 호스트 포트를 여는 것이고, expose는 같은 도커 네트워크 내부에만 포트를 보이게 한다. 외부로 직접 노출되는 포트가 0이 된다.
  • ingress의 대상은 localhost가 아니라 서비스 이름이다. 같은 compose 네트워크에서는 cloudflaredhttp://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:404

ingress 규칙은 위에서 아래로 순서대로 매칭되며, 맨 마지막의 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]인 사용자만 허용" 같은 규칙을 만든다.

이렇게 하면 흐름은 이렇게 바뀐다.

  1. 방문자가 app.example.com/admin에 접근한다.
  2. Cloudflare 엣지가 앱에 도달하기 전에 가로채서 로그인 화면을 띄운다.
  3. 방문자가 IdP로 로그인하고, 정책의 허용 조건을 만족해야만 통과한다.
  4. 통과하면 그제서야 요청이 터널을 타고 집 서버에 닿는다.

핵심은 인증이 우리 애플리케이션 코드 바깥, Cloudflare 엣지에서 끝난다는 점이다. 우리 서버는 인증 통과한 요청만 받는다. 미인증 트래픽은 집 회선에 도달조차 하지 못한다.

다층 방어로 쓰기

실전에서는 Access를 1차 게이트로 쓰고, 애플리케이션 자체 인증을 2차로 두는 이중 구조가 견고하다. Access가 엣지에서 신원을 한 번 거른 뒤, 앱이 세션/권한을 다시 검증한다. 한쪽 설정에 실수가 있어도 다른 쪽이 막아주는 구조다.

정리: 이점과 주의점

이점

항목전통적 방식Cloudflare 터널
인바운드 개방 포트1개 이상 (예: 443)0개
서버 공인 IPDNS에 노출은닉 (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를 노출하는 것보다, 이 트레이드오프가 훨씬 합리적인 선택인 경우가 많다.