Cloudflare Tunnel을 기본 경로로, NPM을 예비 경로로 둔 이유
Cloudflare Tunnel을 기본 외부 경로로, NGINX Proxy Manager를 공인 NAT용 예비 경로로 분리하고 DNS-01 Wildcard 인증서를 준비한 이유를 정리했다.
프로젝트 기간: 2026-08-18 ~ 2026-09-02
글 범위: Cloudflare Tunnel 기본 경로와 NPM 예비 경로
MeetBack의 외부 진입 경로를 설계할 때 두 가지 현실적인 조건이 있었다. public IP와 port forwarding을 항상 사용할 수 없었고, application은 Docker Swarm의 여러 replica로 배포될 예정이었다.
나는 기본 경로를 Cloudflare Tunnel로 정하고 NGINX Proxy Manager는 public NAT가 가능할 때 사용하는 예비 경로로 분리했다.
기본 경로
Client
→ Cloudflare Edge
→ Cloudflare Tunnel
→ cloudflared connectors
→ Swarm Service DNS/VIP
→ Application replica
예비 경로
Client
→ Cloudflare Edge
→ Public IP / NAT
→ NGINX Proxy Manager
→ Swarm Service DNS/VIP
→ Application replica
왜 Tunnel이 NPM을 거치지 않게 했는가
처음에는 Cloudflare Tunnel의 origin을 NPM으로 두고 NPM이 application VIP를 가리키는 구조도 고민했다. 중앙에서 proxy rule을 관리하기는 편하지만 NPM 장애가 기본 Tunnel 경로까지 막는다. hop과 certificate 경계도 하나 늘어난다.
Tunnel은 Swarm service name/VIP를 직접 origin으로 볼 수 있다. 기본 경로에 NPM이 꼭 필요한 요구가 없으므로 중간에서 제거했다. NPM은 public inbound가 가능할 때만 켜는 독립 예비 경로로 두었다.
cloudflared를 Global Service로 배포했다
Swarm node 5대에 cloudflared connector가 하나씩 실행되도록 global service를 사용했다. 한 node가 내려가도 다른 connector가 Cloudflare와 tunnel connection을 유지할 수 있게 하기 위해서다.
Tunnel token은 stack file에 평문으로 넣지 않고 Docker Secret으로 전달했다. connector는 outbound QUIC connection을 만들기 때문에 router에서 application용 inbound port를 항상 열 필요가 없다.
검증은 manager node에서 진행했다.
docker service ls
docker service ps <cloudflared-service>
docker service logs <cloudflared-service>
worker에서 같은 service 관리 명령을 실행했을 때 “This node is not a swarm manager” 오류가 발생했다. connector 실패가 아니라 cluster 전체 state 조회는 manager 권한이라는 뜻이었다. worker에서는 local container만 확인하고 service 상태는 manager로 이동해 확인했다.
최종적으로 다섯 connector task의 연결과 overlay network 접근을 확인했다. 실제 application service가 아직 없으므로 최종 hostname의 user flow까지 완료됐다고 하지는 않는다.
NPM은 상태가 있는 단일 Service로 뒀다
NPM은 certificate, proxy host와 user setting을 persistent data에 저장한다. 현재는 특정 manager에 single replica로 배치하고 local persistent directory를 mount했다.
이 구성은 NPM 고가용성이 아니다. task를 임의 node로 옮기면 같은 local data가 없을 수 있기 때문에 placement를 고정했다. 예비 경로의 단순성을 우선한 선택이다. 향후 NPM 자체 HA가 필요하면 shared database, shared certificate storage와 active/standby 전략을 별도로 설계해야 한다.
Public port 없이 DNS-01 인증서를 발급했다
HTTP-01 challenge는 public DNS와 80 port를 통해 validation request가 origin에 도달해야 한다. 현재 환경에서는 public NAT를 사용할 수 없었다.
대신 Cloudflare DNS-01 challenge를 사용했다. Let's Encrypt가 요구하는 TXT record를 Cloudflare API로 생성해 domain control을 증명하므로 inbound port가 없어도 certificate를 발급할 수 있다.
certificate에는 apex domain과 wildcard domain을 함께 넣었다.
example.com
*.example.com
Cloudflare API token은 전체 account 권한이 아니라 대상 zone의 DNS edit/read 범위로 제한했다. 실제 token은 공개 문서와 source에 넣지 않았다.
NPM의 certificate 화면에서 상태가 Not Used로 표시됐는데 이는 실패가 아니었다. 아직 proxy host에 certificate를 연결하지 않았기 때문이다. 발급 log에서 fullchain과 private key 저장, 만료 시각을 확인했다.
왜 NPM 외부 E2E는 보류했는가
certificate 발급 성공과 외부 HTTPS 경로 성공은 별개다. NPM의 예비 경로를 실제로 검증하려면 다음이 필요하다.
Public IP
+ TCP 80/443 port forwarding
+ Cloudflare DNS record 전환
+ NPM proxy host
+ Swarm application origin
현재 public IP와 port forwarding을 사용할 수 없어 이 전체 경로는 보류했다. 나중에 한시적으로 사용할 수 있을 때 application 전용 hostname으로 시험하고, 끝난 뒤 port forwarding과 DNS record를 원복할 계획이다.
공인 경로를 검증하지 않았는데 certificate만 보고 “NPM 예비 경로 구축 완료”라고 쓰지 않는 이유다.
자체 DNS를 Cloudflare 복제본으로 두지 않은 이유
Cloudflare zone을 local DNS server에 복제해 secondary처럼 운영하는 방안도 검토했다. 하지만 Cloudflare managed DNS를 임의 authoritative server와 병행하려면 zone transfer 지원, delegation, DNSSEC, record 동기화와 failover 판단이 필요하다.
단순히 BIND를 설치해 record를 복사한다고 Cloudflare DNS의 이중화가 되는 것은 아니다. 오히려 stale record와 서로 다른 authority가 생길 수 있다. 이번 프로젝트에서는 Cloudflare DNS를 단일 managed authority로 유지하고 자체 DNS 이중화는 제외했다.
WAF는 Edge에서 적용하는 방향
외부 application이 공개되면 Cloudflare WAF와 rate limiting을 먼저 적용할 수 있다. origin이 Tunnel 뒤에 있으면 직접 노출을 줄이고 edge rule로 기본적인 공격과 비정상 request를 걸러낼 수 있다.
다만 WAF가 application authorization과 input validation을 대신하지 않는다. login, meeting access, file upload 제한은 application에서 계속 검증해야 한다. NPM 예비 경로를 켤 때도 Cloudflare proxy를 유지해 같은 edge policy를 받도록 설계할 예정이다.
현재 구조의 정확한 상태
완료한 것은 다음이다.
- Cloudflare Tunnel과 Swarm global connector 구성
- Tunnel token의 Docker Secret 분리
- connector connection과 overlay 접근 확인
- NPM service와 persistent data 구성
- Cloudflare DNS-01 wildcard certificate 발급
남은 것은 실제 application origin 연결, public NAT 기반 NPM E2E와 firewall 정책이다.
외부 진입 구조를 만들며 가장 중요했던 판단은 기본 경로와 예비 경로를 억지로 직렬 연결하지 않는 것이었다. Tunnel은 Tunnel답게 직접 service discovery를 사용하고, NPM은 public path가 필요할 때만 독립적으로 사용한다. 각 경로의 성공 조건과 아직 검증하지 못한 조건도 따로 기록했다.