오라클 클라우드(OCI)에서 포트 없이 HTTPS 서비스 공개하기 — Cloudflare Tunnel

오라클 클라우드(OCI)에서 포트 없이 HTTPS 서비스 공개하기 — Cloudflare Tunnel
Photo by Jeremy Kwok / Unsplash

한 줄 요약

OCI의 이중 방화벽(콘솔 Security List + 인스턴스 iptables)을 건드리지 않고, 인바운드 포트를 하나도 열지 않은 채 Docker 서비스를 HTTPS로 공개하는 방법. Cloudflare Tunnel이 아웃바운드 연결만으로 이걸 가능하게 한다.


1. 문제 — OCI에서 포트 열기가 왜 번거로운가

OCI 인스턴스를 외부에 노출하려면 방화벽이 2겹이라는 걸 알아야 한다.

  1. 콘솔 Security List / NSG — VCN 레벨 방화벽. 여기서 Ingress Rule로 80/443을 열어준다.
  2. 인스턴스 내부 iptables — Ubuntu 이미지에 기본 룰이 들어있어 대부분의 인바운드를 막는다.

"Security List에서 80 열었는데 왜 접속이 안 되지?" 의 90%는 iptables를 안 건드려서다. 두 곳을 다 열어야 하고, 그렇게 열고 나면 이번엔 origin 공인 IP가 그대로 노출되어 포트 스캔·DDoS의 표적이 된다.

아래는 마이그레이션 전, 22/80/443이 0.0.0.0/0(전체)에 열려 있는 "before" 상태다.

22/80/443이 0.0.0.0/0(전체)에 열려 있는상태

OCI 콘솔 → VCN → 보안 목록(Security List) → 수신 규칙(Ingress Rules). 마이그레이션 후 80/443은 닫을 것이다.


2. 개념 — Cloudflare Tunnel은 이걸 어떻게 푸는가

2-1. 주황 구름 vs 회색 구름 (Proxied / DNS only)

Cloudflare는 DNS 레코드마다 토글이 있다.

구름 흐름 효과
🟠 Proxied 방문자 → Cloudflare → origin DDoS·WAF·캐시·IP 은닉·자동 인증서
⚪ DNS only 방문자 → origin 직접 CF 기능 없음 (일반 DNS)

웹 서비스는 주황(Proxied) 으로 둬야 CF의 보호를 받는다.

2-2. TLS가 2구간으로 분리된다

방문자 ──[구간1: CF Universal SSL·자동]──▶ Cloudflare ══[Tunnel·암호화]══▶ cloudflared ──▶ 서비스
            (CF가 발급·자동갱신, 손댈 일 없음)                              (origin 인증서 불필요)
  • 구간1 (방문자↔CF): Cloudflare가 인증서를 자동 발급·영구 자동 갱신. certbot·갱신 스크립트가 통째로 사라진다.
  • Tunnel 구간: cloudflared아웃바운드로 CF에 연결을 건다. 즉 인바운드 포트가 0개. OCI 방화벽을 통과시킬 ingress 규칙이 아예 필요 없으므로 2겹 방화벽을 우회한다.
핵심: 방화벽은 "들어오는 연결"을 막는다. Tunnel은 안에서 나가는 연결만 쓰므로 방화벽과 무관하게 동작한다.

3. 사전 준비

가정하는 구성

  • OCI ARM 인스턴스 1대, Docker로 서비스 운영 중
  • 앞단에 리버스 프록시 Nginx Proxy Manager(NPM) — 경로 기반 라우팅·다중 서비스용 (없어도 됨)
  • 도메인 1개 (이 글은 lazycorn.net 사용)

3-1. 도메인을 Cloudflare에 올리고 zone Active 확인

도메인을 Cloudflare Registrar에서 사거나, 기존 도메인의 네임서버를 Cloudflare로 위임한다. zone이 Active 가 되면 준비 끝.

CF 대시보드 → 도메인 Overview. "Your domain is now protected by Cloudflare" 가 뜨면 Active.


4. 실전 — Cloudflare Tunnel 만들고 서비스 연결

이 글은 대시보드(토큰) 방식으로 진행한다. config.yml을 직접 관리하는 로컬 방식보다 단순하고 Docker에 잘 맞는다.

4-1. Zero Trust 플랜 — Free로 충분

터널은 Cloudflare Zero Trust(= Cloudflare One) 안에서 만든다. 첫 사용 시 "Choose a plan" 화면이 뜨는데, 맨 왼쪽 Zero Trust Free ($0/seat/month, "home labs" 용) 를 고르면 개인·홈랩엔 차고 넘친다 (50 seat, cloudflared 터널 무제한).

Zero Trust Free는

가입 단계에서 결제수단(카드) 등록을 요구하는 경우가 있다. $0이라 실제 과금은 안 되지만 모르면 당황한다. 카드 등록이 싫으면 토큰 방식 대신 cloudflared tunnel login(cert) 방식으로 우회할 수 있다.

4-2. 터널 생성

Networks → Connectors → Add a tunnel

Cloudflared 선택 (Recommended):

Cloudflared 선택

터널 이름 입력 (여기선 lazycorn):

4-3. cloudflared 설치 (Docker)

터널을 저장하면 OS별 설치 명령이 나온다. Docker를 고르면 아래 형태의 명령이 주어진다:

docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token <YOUR-TUNNEL-TOKEN>

토큰은 시크릿이다

이 명령의 --token 값은 누구나 이 토큰만 있으면 당신의 터널을 띄울 수 있는 자격증명이다. 스크린샷·블로그·깃 커밋에 절대 넣지 말 것. (그래서 이 단계는 캡처가 아니라 코드블록으로 둔다.)

운영은 docker run보다 compose + .env 로 두는 게 깔끔하다. ~/infra/cloudflared/ 에:

# docker-compose.yml
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
    networks:
      - npm_proxy            # NPM과 같은 docker 망 → http://npm 으로 도달
networks:
  npm_proxy:
    external: true
    name: npm_default        # 기존 NPM 컨테이너가 속한 네트워크 이름

토큰은 .env 에 넣는다 (이 파일은 .gitignore):

# 토큰을 셸 히스토리·화면에 남기지 않고 넣기
read -rsp 'CF tunnel token: ' T && printf 'TUNNEL_TOKEN=%s\n' "$T" > ~/infra/cloudflared/.env && unset T
cd ~/infra/cloudflared && docker compose up -d

4-4. 연결 확인 (HEALTHY)

docker logs cloudflaredRegistered tunnel connection 이 4개쯤 뜨면 성공. 대시보드에서도 HEALTHY 로 보인다:

Networks → Connectors → Cloudflare Tunnels 목록. Status가 HEALTHY면 cloudflared가 CF 엣지에 붙은 것.

4-5. Public Hostname 라우트 — 도메인 → 서비스 연결

터널을 클릭 → Published application routes(구 Public Hostnames) → Add a published application route.

  • Subdomain: cairn / Domain: lazycorn.net → Full hostname cairn.lazycorn.net
  • Service Type: HTTP / URL: npm:80 (cloudflared가 같은 docker 망의 NPM 컨테이너로 보냄)

저장하면 라우트가 등록되고 DNS 레코드(주황 구름)는 자동 생성된다:

리버스 프록시(NPM)로 보내는 이유

cloudflared의 라우트는 호스트명 단위다. /api/*·/oauth2/* 같은 경로 기반 라우팅이 필요하면 cloudflared → NPM으로 보내고 세부 라우팅은 NPM이 처리하게 한다. 그러면 기존 NPM 설정을 그대로 재사용할 수 있다.

4-6. 검증 — 포트 0개로 외부 접속 확인

$ curl -sI https://cairn.lazycorn.net
HTTP/2 200
server: cloudflare

방문자 → CF 엣지 → 터널 → cloudflared → npm:80 경로가 인바운드 포트 없이 닿았다. (이 시점엔 NPM 기본 응답 — 실제 앱 서빙은 다음 단계.)

주소창 자물쇠 → 인증서를 보면, 내가 발급한 적 없는데 Cloudflare가 자동으로 발급한 인증서가 붙어 있다 (Issued By: Google Trust Services = CF 엣지 CA, 유효기간 ~90일·자동 갱신). 2-2의 "구간1 자동 인증서"가 이것.

4-7. NPM 프록시호스트 추가

cloudflared가 npm:80으로 보낸 트래픽을 NPM이 호스트명(cairn.lazycorn.net)으로 받아 컨테이너로 라우팅한다. NPM에서 프록시호스트 추가:

  • Domain Names: cairn.lazycorn.net
  • Scheme: http / Forward Hostname: cairn-frontend / Forward Port: 8080
  • SSL: 인증서 불필요 (CF가 엣지 TLS, 터널이 CF↔NPM 암호화 → NPM은 HTTP만)

OAuth 앱이면 경로 라우팅 + X-Forwarded-Proto 주의

/api·/oauth2·/login/oauth2·/logout 같은 경로를 backend로 보내는 Custom Location을 추가하고, 각 location에 proxy_set_header X-Forwarded-Proto https; 를 넣어야 OAuth redirect_uri가 https로 생성된다 (자세한 이유는 §6 트러블슈팅 참고).