Nginx Proxy Manager 2026.09.09 6 min read

Cloudflare Tunnel과 NPM으로 외부 경로를 이중화한 과정

Cloudflare Tunnel 기본 경로와 NPM 예비 경로를 각각 연결하고 TLS·readiness·WebSocket을 시험했다. 리디렉션과 쿠키에 남은 문제도 함께 정리했다.

MeetBack은 외부 인바운드 포트를 직접 열지 않아도 되는 Cloudflare Tunnel로 공개했다. 이후 공인 IP와 포트 포워딩을 사용할 수 있게 되면서 NGINX Proxy Manager를 거치는 경로도 외부에서 시험했다.

Tunnel은 기본 경로로, NPM은 별도의 예비 경로로 구성했다. 연결 흐름은 다음과 같다.

기본 경로
Client → Cloudflare Edge → Tunnel → cloudflared → Swarm VIP → App

예비 경로
Client → Cloudflare DNS → Public NAT → NPM → Swarm VIP → App

기본 Tunnel 경로에서 NPM을 뺐다

Tunnel의 원본 서버를 NPM으로 지정하면 프록시 규칙을 한곳에서 관리할 수 있다. 다만 NPM이 멈추면 기본 Tunnel 경로까지 끊기고, 요청이 거치는 단계와 TLS 확인 지점도 늘어난다.

cloudflared는 오버레이 네트워크에서 App 서비스 이름과 VIP에 직접 접근할 수 있었다. 그래서 Tunnel을 App으로 바로 연결했다. NPM은 외부 인바운드 경로가 필요할 때 사용할 수 있도록 따로 뒀다.

cloudflared는 5개 node에 global로 배포했다

Swarm Manager 3대와 Worker 2대에 커넥터가 하나씩 실행되도록 global 서비스로 배포했다. 한 노드가 내려가도 나머지 커넥터가 Tunnel 연결을 유지할 수 있게 한 구성이다.

Tunnel 토큰은 Docker Secret으로 전달했다. 최종 점검에서 cloudflared 태스크 5/5가 Running이었고, 로그와 지표에서 최근 연결 오류가 있는지 확인했다.

로그에는 QUIC 버퍼 경고도 보였다. 경고 문구만으로 연결 실패라고 판단할 수는 없어 커넥터 상태와 실제 요청 결과를 함께 확인했다.

NPM은 public 80/443만 열고 관리 포트는 숨겼다

라우터의 외부 80/443을 NPM이 실행되는 노드로 포트 포워딩했다. 관리자 포트는 Tailscale에서만 접속할 수 있도록 iptables와 Docker DOCKER-USER 체인에 접근 규칙을 적용했다.

NPM은 설정과 인증서를 저장하는 단일 복제본이다. 이 NPM이 멈추면 예비 경로도 중단된다. 현재 구성에서는 NPM을 복구하는 절차로 대응하도록 정했다.

DNS-01로 wildcard 인증서를 발급했다

외부 인바운드 접속이 준비되기 전에도 인증서를 발급받기 위해 Cloudflare DNS-01을 사용했다. 토큰 권한은 대상 Zone의 DNS 수정·조회 범위로 제한했다.

와일드카드와 루트 도메인을 인증서에 함께 넣었다. 외부에서 openssl s_client로 subject, issuer, SAN, 유효기간을 확인했다. NPM의 Not Used 표시는 아직 프록시 호스트에 인증서를 연결하지 않았다는 의미였다.

두 경로를 같은 방법으로 검증했다

기본 호스트 이름과 예비 호스트 이름에서 같은 readiness 엔드포인트를 호출했다.

Cloudflare Tunnel 경로 → HTTP 200, TLS verify 0, {"status":"UP"}
Public IP / NPM 경로 → HTTP 200, TLS verify 0, {"status":"UP"}

WebSocket /ws도 두 경로에서 모두 HTTP 101 Switching Protocols를 반환했다. 이 결과만 보면 Presence 지연을 프록시의 WebSocket 차단으로 설명하기는 어려웠다.

HTTPS인데 HTTP redirect가 생성된 문제

외부에서 HTTPS로 요청했지만 루트 경로의 응답에는 http://.../home Location이 들어 있었다. NPM이 다시 HTTPS로 전환해 화면은 열렸지만, HTTP 주소로 내려갔다가 다시 접속하는 불필요한 왕복이 생겼다.

Cloudflare의 SSL 모드만 볼 문제가 아니었다. 애플리케이션이 원래 요청을 어떻게 인식하는지 확인하려고 다음 항목을 나눠 점검했다.

  • Cloudflare/NPM이 보내는 X-Forwarded-ProtoX-Forwarded-Host
  • Spring Boot의 전달 헤더 처리 설정
  • Spring Security의 리디렉션 생성 위치
  • 세션 쿠키의 Secure, SameSite, 도메인 설정

최종 점검에서도 JSESSIONIDSecure가 없는 문제는 남았다. TLS 연결은 성공했지만 애플리케이션의 스킴 인식과 쿠키 설정에는 추가 수정이 필요했다.

이중 경로의 의미

기본·예비 호스트 이름으로 각각 접속할 수는 있었다. 하지만 자동 장애 전환까지 구성한 상태는 아니었다. DNS를 전환할 조건, 상태 확인 방법, 사용자 세션 처리, 단일 NPM의 장애 대응 절차가 더 필요했다.

프로젝트 종료 시점에는 다음 구성이 동작했다.

  • 기본 Tunnel 경로의 커넥터 다중화
  • 공인 NAT/NPM 예비 경로의 외부 요청 검증
  • 두 경로의 TLS와 readiness 확인
  • WebSocket 업그레이드 확인
  • 관리자 포트의 Tailscale 접근 제한

후속 작업으로는 외부 점검과 전환 절차를 연결해 장애 감지부터 경로 변경까지 걸리는 시간을 측정할 계획을 남겼다.

이 경로와 서버 상태를 확인하기 위한 Prometheus·Grafana·Loki 구성도 별도 글에 정리했다.

실제 검증 화면

  1. Manager 3대와 Worker 2대에서 cloudflared global 태스크 5개가 실행되는지 확인했다.
  2. 예비 경로에서 받은 Let's Encrypt 인증서의 subject, issuer, SAN, 만료일을 확인했다.
  3. 기본·예비 호스트 이름 모두 HTTP 200과 UP 응답을 반환했고 TLS 검증도 통과했다.
    Swarm 노드 다섯 곳에서 cloudflared 작업이 Running인 서비스 배치 조회 결과

OpenSSL로 확인한 와일드카드 TLS 인증서의 발급자, 유효 기간과 도메인 목록

Cloudflare Tunnel과 NPM 경로의 readyz 요청이 모두 HTTP 200과 상태 UP을 반환한 결과

SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 결과 이동