NYO_O

Cloudflare Tunnel을 활용한 단일 VM Zero Trust 아키텍처 전환기 본문

프로젝트/똑똣(TokTot)

Cloudflare Tunnel을 활용한 단일 VM Zero Trust 아키텍처 전환기

NYO_O 2026. 5. 24. 10:57
반응형

방화벽 인바운드 개방의 맹점과 역방향 터널링의 필요성

일반적으로 웹 서비스를 배포할 때 가장 먼저 하는 작업은 클라우드 제공자(GCP, AWS 등)의 방화벽 설정에서 80(HTTP)과 443(HTTPS) 포트를 개방하는 것입니다. 웹 트래픽을 받아야 하니 인바운드(Inbound)를 열어두는 것이 당연하다고 생각하기 쉽습니다. 하지만 여기에는 치명적인 보안의 맹점이 존재합니다.

도메인에 WAF(웹 방화벽)나 DDoS 보호 서비스를 연동해 두더라도, 서버의 공인 IP(Public IP) 자체가 열려 있다면 공격자는 도메인을 거치지 않고 공인 IP로 직접 접근하여 보안망을 쉽게 우회할 수 있습니다.

이를 해결하기 위해 이번 배포에서는 전통적인 리버스 프록시(Nginx)를 제거하고 Cloudflare Tunnel을 도입했습니다. Cloudflare Tunnel의 핵심은 방화벽의 인바운드를 완전히 차단하고, 내부 서버에서 Cloudflare의 엣지망으로 직접 아웃바운드(Outbound) 연결을 맺는 '역방향 터널링'입니다. 즉, 외부에서는 서버로 들어올 수 있는 문 자체가 존재하지 않으며, 서버가 스스로 뚫어놓은 터널을 통해서만 트래픽이 교환되는 완벽한 Zero Trust 아키텍처를 구현했습니다.

Docker Compose 기반 네트워크 격리 설계

이러한 터널링 아키텍처를 구현하기 위해 서버 내부의 구조를 개편했습니다. 애플리케이션(Spring Boot)과 cloudflared 커넥터를 동일한 Docker 네트워크 대역에 배치하여, 호스트(VM) 외부로는 어떠한 포트도 노출되지 않도록 설계했습니다.

실제 프로덕션 환경에 적용된 docker-compose.prod.yml의 핵심 구성은 다음과 같습니다.

services:
  app:
    image: asia-northeast3-docker.pkg.dev/${PROJECT_ID}/toktot-images/toktot-server:${IMAGE_TAG}
    container_name: toktot-prod-app
    depends_on:
      cloud-sql-proxy:
        condition: service_started
      redis:
        condition: service_healthy
    environment:
      SPRING_PROFILES_ACTIVE: prod
      TZ: Asia/Seoul
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      timeout: 10s
      interval: 30s
      retries: 3
      start_period: 1m30s
    restart: unless-stopped

  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: toktot-prod-cloudflared
    command: tunnel --no-autoupdate run ${CLOUDFLARE_TUNNEL_TOKEN}
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped

위 설정에서 가장 주목해야 할 엔지니어링 포인트는 다음과 같습니다.

  • 포트 바인딩(ports)의 완전한 제거: app 서비스에 ports: ["8080:8080"] 설정이 없습니다. 호스트의 네트워크와 컨테이너를 단절시켰으며, 외부에서 VM의 8080 포트로 접근하는 것은 원천적으로 불가능합니다.
  • 보안 토큰 기반의 터널 연결: cloudflared 서비스는 CLOUDFLARE_TUNNEL_TOKEN 환경 변수를 사용하여 Cloudflare와 연결됩니다. 이 토큰 하나만으로 DNS 라우팅과 터널링이 모두 인증됩니다.
  • 정교한 컨테이너 의존성(depends_on) 제어: cloudflared는 app 컨테이너가 service_healthy 상태가 되기 전까지 실행되지 않습니다. Spring Boot가 완전히 부팅되고 /actuator/health 엔드포인트가 정상 응답을 반환해야만 터널이 외부와 연결됩니다. 이를 통해 배포 중 발생할 수 있는 502 Bad Gateway 에러 노출 시간을 원천 차단했습니다.

구간별 프로토콜 최적화: 왜 HTTPS가 아닌 HTTP 통신인가?

보안을 강화했다고 하면 서버 내부에서도 HTTPS를 사용해야 한다고 생각하기 쉽습니다. 하지만 Cloudflare Tunnel을 설정할 때, cloudflared 컨테이너와 Spring Boot 컨테이너 사이의 통신 타입은 HTTPS가 아닌 HTTP로 지정했습니다.

통신 구간을 세분화하여 분석해 보면 그 이유가 명확해집니다.

  1. 사용자 ↔ Cloudflare 엣지망: Cloudflare가 자동으로 관리하는 무료 범용 인증서를 통해 완벽한 HTTPS 암호화 통신이 이루어집니다.
  2. Cloudflare 엣지망 ↔ cloudflared 커넥터: Cloudflare 자체 프로토콜을 사용한 고도의 암호화된 터널로 트래픽이 전달됩니다.
  3. cloudflared 커넥터 ↔ Spring Boot (app): 동일한 가상 내부망(Docker Network) 안에서의 통신입니다.

이미 외부로부터의 모든 트래픽이 암호화되어 안전하게 내부망까지 도달했기 때문에, 마지막 구간인 3번 단계에서는 추가적인 TLS/SSL 암호화가 불필요합니다. 만약 여기서 HTTPS를 강제한다면, Spring Boot에 인증서를 마운트해야 하고 트래픽 복호화 과정에서 불필요한 CPU 오버헤드만 발생합니다.

따라서 Nginx를 거쳐 인증서를 처리하던 기존 방식을 버리고, 클라우드플레어 엣지에서 TLS 암복호화를 전담(Offloading)하게 만들어 Spring Boot는 오직 비즈니스 로직 처리에만 시스템 리소스를 온전히 집중할 수 있도록 최적화했습니다.

반응형