사무실 없이 운영하는 팀의 백오피스 보안: Cloudflare Access + Tunnel 도입기
사무실 없이 운영하는 팀의 백오피스를 Cloudflare Access와 Tunnel로 보호한 도입기. EC2 포트를 인터넷에 노출하지 않으면서 팀원 이메일 인증으로 접근을 통제한 구성과 시행착오를 정리한다.

이번에 findit 백오피스를 구현하면서 Cloudflare Access와 Tunnel을 도입했습니다. 도입 배경부터 실제 설정 과정, 그리고 진행하면서 마주친 여러 문제와 해결 방법을 기록으로 남겨둡니다.
어쩌다 Cloudflare를 쓰게 됐나
findit 백오피스는 중개사, 손님 회원 정보와 관리자 계정을 다루는 내부 관리 도구입니다. 외부에 노출되면 안 되는데, 우리 팀은 MVP 출시를 앞둔 단계에 사무실 없이 작업하는 환경이라 흔히 쓰는 “사무실 IP만 허용” 방식이 불가능했습니다.
대안을 한참 고민했습니다.
- 퍼블릭 공개 + ID/PW만으로 보호? 무차별 대입 공격에 노출됩니다.
- IP 화이트리스트? 팀원들이 집·카페·이동 중에 작업하는데 고정 IP가 없습니다.
- WireGuard 같은 VPN? 팀원 5명 각자에게 클라이언트 깔게 하고 관리하는 게 너무 무겁습니다.
- Cloudflare Access? 무료, 설정 간편, VPN 불필요. 이메일 인증 게이트로 통과한 사람만 백오피스에 접근 가능.
마지막 옵션이 압도적으로 매력적이었습니다. 50명 이하 팀은 Free 플랜으로 충분하고, 팀원들은 그냥 브라우저에서 Google 계정으로 한 번 더 인증만 거치면 되기 때문입니다.
Cloudflare란 무엇인가
Cloudflare는 전 세계 300개 이상의 데이터센터를 운영하는 인터넷 인프라 회사입니다. 크게 세 가지 서비스를 합니다.
첫째, DNS 서버. 도메인의 네임서버를 Cloudflare로 바꾸면, findit.im에 대한 DNS 질의를 Cloudflare가 응답합니다.
둘째, 리버스 프록시. 사용자와 실제 서버 사이에 Cloudflare가 끼어서 DDoS 차단, 캐싱, HTTPS 처리를 대신해줍니다. 사용자는 Cloudflare IP로 접속하니까 실제 서버 IP가 노출되지 않습니다.
셋째, Zero Trust / Access. “누가 이 서비스에 접근할 수 있는가”를 제어하는 보안 플랫폼입니다. 이번에 사용한 게 바로 이 서비스입니다. 기업 VPN을 대체하는 개념입니다.
백오피스에서는 어떻게 활용했나
Cloudflare를 쓴 목적은 두 가지였습니다.
하나는 도메인 DNS 관리입니다. findit.im 도메인을 hosting.kr에서 구매했고, 기존엔 AWS Route53이 DNS를 담당하고 있었습니다. 네임서버를 Cloudflare로 이전해서 이후 DNS 관리를 한 곳에서 처리하도록 일원화했습니다.
다른 하나는 백오피스 접근 제어입니다. admin.findit.im으로 들어오는 요청을 Cloudflare Access가 먼저 받아서 팀원 인증을 검사합니다. 인증을 통과한 요청만 Cloudflare Tunnel을 통해 EC2 서버로 전달됩니다.
여기서 Tunnel의 핵심을 짚고 가야 하는데, 보통 서버에 외부에서 접속하려면 80/443 포트를 인터넷에 열어둬야 합니다. 그런데 Cloudflare Tunnel은 정반대 방향으로 동작합니다. EC2가 먼저 Cloudflare 쪽으로 연결을 맺어두고, Cloudflare가 그 터널을 통해 요청을 보내주는 구조입니다. 그래서 EC2 보안그룹에서 SSH(22) 외 모든 인바운드를 차단해도 백오피스 접속이 됩니다.
전체 흐름은 이렇게 됩니다.
팀원 브라우저
│
▼ HTTPS
Cloudflare Access ← 이메일/Google 인증 게이트
│ 통과 시에만
▼
Cloudflare Tunnel ← EC2가 먼저 맺어둔 터널
│
▼ localhost:3001
EC2 백오피스 서버 ← 인터넷에 포트 노출 없음
설정 과정
실제로 어떻게 설정했는지 정리합니다. 사전 준비물은 Cloudflare 무료 계정, 도메인, AWS EC2(Ubuntu 24.04), 백오피스 서버가 EC2에서 돌고 있는 상태입니다.
1. Zero Trust 활성화
Cloudflare 대시보드에서 좌측 사이드바의 Zero Trust를 클릭하면 처음에 팀 이름(slug)을 정하라고 합니다. 우리는 findit-dev로 정했는데, 한 번 정하면 못 바꿉니다. 신중하게 정해야 합니다. 플랜은 Free로 선택하고, 결제 정보는 등록하지만 50명 이하면 실제 과금은 없습니다.
2. Tunnel 생성

Zero Trust 대시보드에서 Networks → Connectors → Create a tunnel로 이동합니다. Connector 타입은 Cloudflared를 고르고 적당한 이름을 입력합니다. 저장하면 설치 명령어 화면이 나오는데, Linux 탭에서 토큰이 포함된 명령어를 복사해서 EC2에서 실행하면 됩니다. 명령어는 대충 이런 모양입니다.
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
sudo cloudflared service install <YOUR_TUNNEL_TOKEN>
설치가 끝나면 Cloudflare 화면에서 Connector 상태가 Connected로 바뀝니다.
3. Public Hostname 연결

한 EC2에서 dev와 live 두 환경을 동시에 운영하고 있어서, 같은 터널에 hostname을 두 개 등록했습니다. 터널의 Configure → Public Hostname 탭에서 위 두 개를 각각 HTTP / localhost:포트로 추가하면 끝납니다. 이 시점에선 인증 게이트가 없어서 누구나 접속 가능한 상태가 됩니다.
4. Access Application으로 인증 게이트 추가 및 정책으로 팀원만 허용
Zero Trust → Access → Applications → Add an application에서 Self-hosted를 선택합니다. live와 dev 각각 따로 생성해야 했습니다. 그리고 Application 만들면서 정책을 같이 설정합니다. Action은 Allow, Selector는 Emails, Value에 팀원 이메일 5개를 줄바꿈으로 입력했습니다. 이메일은 나중에 언제든 추가/제거할 수 있어서 부담 없이 진행하면 됩니다.
5. 로그인 방식: OTP + Google OAuth
기본은 이메일로 일회용 PIN이 오는 OTP 방식이 활성화되어 있습니다. 다만 OTP는 메일 도착이 늦거나 안 올 때가 있어서, Google 로그인도 같이 등록해뒀습니다.
여기서 좀 헤맸는데, 가이드 문서들이 보통 “Settings → Authentication → Login methods”라고 적혀있는데 현재 Cloudflare UI는 다릅니다. 좌측 사이드바 맨 아래 Integrations → Identity providers가 정확한 경로입니다.
Google OAuth 등록은 두 단계로 나눠서 진행했습니다.
먼저 Google Cloud Console에서 OAuth 클라이언트를 만듭니다.
- https://console.cloud.google.com 에서 프로젝트 생성
- API 및 서비스 → OAuth 동의 화면 시작, 앱 이름 입력하고 대상은 외부로
- API 및 서비스 → 사용자 인증 정보 → 사용자 인증 정보 만들기 → OAuth 클라이언트 ID
- 애플리케이션 유형은 웹 애플리케이션, 승인된 리디렉션 URI에
https://<team-domain>.cloudflareaccess.com/cdn-cgi/access/callback입력 - 생성된 클라이언트 ID와 보안 비밀 복사
dev/live 모두 같은 팀 도메인을 통과하기 때문에 redirect URI는 하나만 등록하면 됩니다.
그 다음 Cloudflare에 등록합니다. Integrations → Identity providers → Add new → Google에서 위에서 받은 Client ID/Secret을 붙여넣고 저장. Test 버튼으로 정상 작동까지 확인했습니다. 한 가지 오해하기 쉬운 부분이 있는데, Google 로그인을 추가했다고 보안이 약해지는 게 아닙니다. Cloudflare Access의 인증은 두 단계로 나뉘어 있습니다. Identity Provider(Google이든 OTP든)는 “이 사람이 이 이메일 주인이 맞나”를 검증할 뿐이고, 실제 접근 허용 여부는 Access Policy의 화이트리스트가 결정합니다. 정책에 등록된 5개 이메일이 아니면 어떤 인증을 거쳐도 통과 못 합니다.
6. EC2 보안그룹 정리
Cloudflare Tunnel 덕분에 EC2 인바운드 80/443을 열 필요가 없어졌습니다. SSH(22)만 본인 IP에 한해서 열어두고 나머지는 모두 차단했습니다.
진행하면서 마주친 문제들
여기까지 글로 적으니까 수월해 보이지만, 실제로는 사이사이 막히는 지점이 꽤 많았습니다. 마주친 문제들을 기록해둡니다.
realtor 서비스 로그인이 안 되는 문제
DNS 이전 직후, 백오피스랑 무관한 realtor 서비스(dev-realtor.findit.im:8088)에서 로그인 요청이 ERR_CONNECTION_TIMED_OUT으로 막히기 시작했습니다. 원인은 Route53의 *.findit.im 와일드카드 A 레코드가 Cloudflare로 이전되면서 Proxied(주황 구름) 상태로 들어왔기 때문입니다. Cloudflare 프록시는 표준 포트(80/443/2052/2083 등)만 지원하는데, 8088은 거기 없었습니다. 결과적으로 모든 *.findit.im 트래픽이 8088에서 막혀버린 것이었습니다. 해결은 Cloudflare DNS에서 와일드카드 * A 레코드를 DNS only(회색 구름)로 바꾸는 것이었습니다. 비표준 포트를 쓰는 백엔드는 프록시를 안 타게 해야 한다는 교훈이었습니다.
프로필 이미지가 404로 뜨는 문제
realtor 프로필 페이지에서 이미지가 안 뜨길래 봤더니, https://dev-cdn.findit.im/... 응답이 CDN이 아닌 엉뚱한 서버 IP에서 오고 있었습니다. Route53에는 dev-cdn.findit.im CNAME → CloudFront 레코드가 있었는데 Cloudflare 자동 마이그레이션에서 누락됐던 것입니다. 와일드카드 *.findit.im로 fallback되어 ALB로 잘못 라우팅되고 있었습니다. Cloudflare DNS에 dev-cdn CNAME → dXXXXXXXXXXXXX.cloudfront.net(실제 CloudFront 배포 도메인)을 DNS only로 직접 추가해서 해결했습니다.
realtor가 또 간헐적으로 안 되는 문제
이게 가장 골치 아팠던 문제입니다. DNS 이전 후 며칠 동안 dev-realtor.findit.im:8088이 어떨 땐 잘 되고 어떨 땐 timeout이 나는 증상이었습니다. dig로 추적해본 결과, Cloudflare는 두 개의 IP(편의상 IP-A, IP-B)를 응답하는데, 실제 ALB는 IP-A와 새로운 IP-C로 바뀌어 있었습니다. 즉 트래픽 50%가 더 이상 ALB가 안 쓰는 stale IP(IP-B)로 가고 있었던 것입니다.
원인을 알고 보니, Route53에서 *.findit.im은 ALB alias였습니다. ALB IP가 바뀌어도 자동으로 추적하는 동적 레코드였습니다. 그런데 Cloudflare 자동 마이그레이션은 이걸 그 시점의 IP로 고정한 static A 레코드 두 개로 변환해버린 것입니다.
해결은 와일드카드를 CNAME으로 교체하는 것이었습니다. ALB DNS 이름(dualstack.<your-alb>.<region>.elb.amazonaws.com 형태)을 가리키도록 바꾸면 ALB IP 변경을 자동 추적하게 됩니다.
이건 다음에 또 같은 작업을 할 때 꼭 미리 챙겨야 하는 부분입니다. Route53 alias → Cloudflare 이전 시 alias의 동적 추적 기능이 사라진다는 건 마이그레이션의 유명한 함정입니다. ALB, CloudFront, ELB 같은 AWS 리소스를 가리키는 레코드는 이전 후 반드시 CNAME으로 수동 교체해야 합니다.
EC2를 다른 VPC에 만들어버린 문제
EC2를 새로 만들어서 백오피스를 띄웠더니 로그인 시 500 에러가 나고, PM2 로그에 connect ETIMEDOUT이 찍혔습니다. RDS 연결이 안 되는 거였는데, 로컬에선 RDS 접속이 잘 됐어서 한참 헤맸습니다.
알고 보니 RDS는 커스텀 VPC에 있는데 새로 만든 EC2는 기본 VPC에 들어가 있었습니다. EC2 생성할 때 VPC 설정을 신경 안 쓰면 기본값으로 들어갑니다. VPC가 다르면 보안그룹 ID로 직접 참조도 안 되고(You have specified two resources that belong to different networks 에러), 통신도 자연스럽게 안 됩니다.
처음엔 EC2 퍼블릭 IP를 RDS 인바운드에 화이트리스트로 넣어볼까 했는데, 어차피 나중에 VPC 정리는 해야 할 일이고 지금이 가장 쉬울 때라고 판단했습니다. EC2를 RDS와 같은 VPC, 같은 가용영역(2c)에 다시 만들었습니다.
EC2 재생성하면서 깨달은 건, 어제 한 셋업을 그대로 다시 해야 한다는 점이었습니다. Node.js, PM2, git, mysql-client, redis, cloudflared 설치하고, GitLab Deploy Key 새로 만들어 등록하고, 레포 clone 하고, .env 다시 작성하고, PM2로 두 환경 띄우고… 어제 머릿속에 있을 때 다시 하니 1~2시간이면 끝났습니다. 만약 한 달 뒤에 했다면 디테일을 까먹어서 훨씬 오래 걸렸을 것이고, 운영 중에 했다면 다운타임도 협의해야 했을 것입니다.
옛 EC2 정리할 때 한 가지 함정이 있었습니다. cloudflared 서비스를 systemctl stop 했는데도 Cloudflare 대시보드 connector 목록에서 옛 hostname이 안 사라지는 증상이 있었습니다. 보니까 어제 수동으로 실행한 cloudflared tunnel run --token ... 프로세스가 systemctl 서비스랑 별개로 살아있었습니다. sudo pkill -f "cloudflared tunnel run"까지 해야 완전 종료됐습니다.
Cloudflare Access OTP 메일이 안 오는 문제
정책에 등록된 이메일로 OTP 입력했는데 메일이 안 오는 증상이 있었습니다. 스팸함도 비어 있었습니다. 한 시간 가까이 설정만 다시 들여다보다가, 혹시나 해서 Cloudflare Status 페이지를 확인했더니 Access: Degraded Performance가 떠 있었습니다. 우리 쪽 문제가 아니라 Cloudflare 자체 일시 장애였습니다.
이게 나중에 Google OAuth를 추가하기로 결정한 계기가 됐습니다. OTP 단일 의존성을 두지 않고 Google까지 확보해두면 한쪽이 안 될 때 우회로가 됩니다.
live에서 이미지가 dev CDN으로 요청되는 문제
live 백오피스에서 사업자등록증/중개업등록증 이미지가 안 보였습니다. 개발자도구를 열어보니 이미지 URL이 https://dev-cdn.findit.im/...로 요청되고 있었습니다. live인데 dev CDN을 보고 있었던 것입니다.
원인은 두 가지가 겹쳤습니다.
첫째, 코드의 fallback. src/lib/storage.ts에 CDN_HOST = process.env.CDN_URL || "dev-cdn.findit.im"이라는 줄이 있어서, live .env에 CDN_URL이 비어 있으면 dev CDN으로 떨어지게 되어 있었습니다.
둘째, DNS 누락. 정작 live용 cdn.findit.im도 마이그레이션에서 누락된 상태였습니다. (앞에 dev-cdn.findit.im 누락이랑 똑같은 케이스입니다.) CloudFront 배포는 살아있었는데 도메인이 안 풀려서 직접 접속도 안 되는 상태였습니다.
해결은 두 단계였습니다. Cloudflare DNS에 cdn CNAME을 live CloudFront 배포 도메인으로 가리키게 추가하고, live .env에 CDN_URL=cdn.findit.im을 명시한 다음 PM2를 재시작했습니다.
여기서 보너스로 발견한 게 있는데, prod S3 버킷 정책에 Principal: "*"로 누구나 GetObject 할 수 있는 PublicReadGetObject 규칙이 들어 있었습니다. dev엔 없는 규칙이었습니다. 이게 있으면 사업자/중개사 라이선스 같은 민감 파일이 S3 URL만 알면 인증 없이 다운로드 가능합니다. CloudFront OAC만 있어도 백오피스 이미지 로드는 잘 되니까 그 규칙은 바로 제거하고, “모든 퍼블릭 액세스 차단”도 활성화했습니다.
정리하면서 느낀 점
작업 끝내고 돌아보니 몇 가지 분명한 패턴이 보였습니다.
Cloudflare 자동 마이그레이션은 만능이 아닙니다. Route53에서 Cloudflare로 도메인 옮길 때 자동으로 레코드가 옮겨가긴 하지만, CNAME이 종종 누락되고, alias는 static A로 변환되면서 동적 추적 기능을 잃습니다. 마이그레이션 직후 Route53 레코드 목록과 1:1 비교하는 검증 단계를 꼭 거쳤어야 했습니다.
환경별 설정은 명시적으로 하는 게 안전합니다. 코드의 fallback은 안전망이지만 그게 환경별 .env에 명시되지 않은 핑계가 되면 안 됩니다. live가 dev CDN을 보고 있던 사고가 그 예시입니다.
MVP 단계에서도 인프라 기본은 잡아둬야 합니다. EC2를 기본 VPC에 잘못 만든 걸 발견했을 때, “MVP니까 나중에”가 아니라 그 자리에서 옮긴 게 정답이었습니다. 어제 작업한 셋업이 머리에 있을 때, 운영 시작 전에, 시간이 가장 적게 드는 시점에 정리하는 게 최선입니다.
보안은 한 줄로 끝내지 않습니다. Cloudflare Access(외부 게이트) + 앱 admin 로그인(super-admin/admin 권한 분리) + 감사 로그(누가 뭘 했는지), 세 층이 각각 다른 역할을 합니다. 어느 하나도 다른 걸 대체하지 않습니다.
결과
지금 백오피스는 이렇게 운영되고 있습니다.
EC2 (ap-northeast-2c, t3.micro, RDS와 같은 VPC)
├── /home/ubuntu/backoffice-live → PORT=3001, .env, DB: prod_db
└── /home/ubuntu/backoffice-dev → PORT=3000, .env, DB: dev_db
Cloudflare Tunnel (cloudflared)
├── admin.findit.im → localhost:3001
└── admin-dev.findit.im → localhost:3000
https://admin.findit.im이나 https://admin-dev.findit.im으로 접속하면 Cloudflare Access 인증 페이지가 먼저 뜹니다. 정책에 등록된 팀원 이메일이 아니면 거기서 막히고, 통과해도 그 안에서 앱 자체 admin 로그인을 한 번 더 거쳐야 합니다. EC2의 80/443은 인터넷에 노출되지 않고, VPN 클라이언트도 없이 어디서든 접속 가능합니다.
비용은 Cloudflare Access(50명 이하)와 Tunnel이 모두 무료, EC2 t3.micro가 월 10달러 정도입니다. 도메인은 기존 거 재사용해서 추가 비용은 없었습니다.
처음 도입을 고민할 때만 해도 “사무실도 없이 작업하는데 백오피스 보안을 어떻게 하지” 막막했지만, 막상 끝내고 나니 무료에 가까운 비용으로 꽤 단단한 게이트가 만들어졌습니다.