오라클 클라우드(OCI)에서 포트 없이 HTTPS 서비스 공개하기 — Cloudflare Tunnel
한 줄 요약
OCI의 이중 방화벽(콘솔 Security List + 인스턴스 iptables)을 건드리지 않고, 인바운드 포트를 하나도 열지 않은 채 Docker 서비스를 HTTPS로 공개하는 방법. Cloudflare Tunnel이 아웃바운드 연결만으로 이걸 가능하게 한다.
1. 문제 — OCI에서 포트 열기가 왜 번거로운가
OCI 인스턴스를 외부에 노출하려면 방화벽이 2겹이라는 걸 알아야 한다.
- 콘솔 Security List / NSG — VCN 레벨 방화벽. 여기서 Ingress Rule로 80/443을 열어준다.
- 인스턴스 내부 iptables — Ubuntu 이미지에 기본 룰이 들어있어 대부분의 인바운드를 막는다.
"Security List에서 80 열었는데 왜 접속이 안 되지?" 의 90%는 iptables를 안 건드려서다. 두 곳을 다 열어야 하고, 그렇게 열고 나면 이번엔 origin 공인 IP가 그대로 노출되어 포트 스캔·DDoS의 표적이 된다.
아래는 마이그레이션 전, 22/80/443이 0.0.0.0/0(전체)에 열려 있는 "before" 상태다.

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

터널 이름 입력 (여기선 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 cloudflared 에 Registered 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 hostnamecairn.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 트러블슈팅 참고).