- Published on
Cloudflare Zero Trust로 OAuth 앱 보호 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 OAuth 로그인(Google, GitHub, Azure AD 등)을 붙인 웹앱/백엔드는 기본적으로 public internet 에 노출됩니다. 설령 애플리케이션 레벨에서 OAuth 2.0을 잘 구현했더라도, 운영 단계에서는 다음 같은 문제가 자주 남습니다.
- 관리자 페이지나 내부용 콘솔이 같은 도메인에 섞여 노출됨
- OAuth 콜백 URL이 공격 표면이 됨(스캐닝, 리다이렉트 오용, 세션 고정 시도)
- API 엔드포인트가 토큰 검증 이전에 과도한 트래픽을 받아 비용/장애 유발
staging/preview환경이 실수로 외부에 열림
Cloudflare Zero Trust의 Access 는 앱 앞단에서 “한 번 더” 인증과 정책을 적용하는 게 핵심입니다. 이 글은 OAuth 앱을 Cloudflare 뒤에 두고, Access 정책 + 세션 + 헤더/토큰 전달 + 예외 경로 까지 실제로 운영 가능한 형태로 정리합니다.
참고로 OAuth 자체 트러블슈팅(특히 PKCE) 쪽은 아래 글이 같이 도움이 됩니다.
목표 아키텍처: OAuth는 그대로, 앞단에 Access를 둔다
권장 구성은 “앱의 OAuth 로그인” 과 “Cloudflare Access 인증” 을 분리하는 것입니다.
- Cloudflare Access:
누가 이 앱 URL에 접근 가능한가를 먼저 판단 - 애플리케이션 OAuth: 앱 내부에서
사용자 계정 식별/권한/프로필 연동을 수행
즉, Access는 Reverse Proxy 앞단 게이트 로써 동작하고 앱은 기존 OAuth 플로우를 유지합니다.
어떤 케이스에 특히 효과적인가
- 사내 직원만 접근해야 하는 OAuth 앱(예: 내부 대시보드)
- 고객용 서비스지만 특정 경로는 제한해야 하는 경우(예:
/admin,/ops) - 프리뷰 환경을 안전하게 공유해야 하는 경우(이메일 도메인 제한, 일회성)
사전 준비: DNS/프록시 모드와 애플리케이션 전제
도메인이 Cloudflare에 있고, 해당 레코드가
Proxied(주황 구름) 이어야 합니다.애플리케이션은 아래 헤더를 신뢰할 준비가 필요합니다.
- 원본 IP가 필요하면
CF-Connecting-IP - 원본 프로토콜/호스트는
X-Forwarded-Proto,X-Forwarded-Host
- OAuth 리다이렉트 URL은
https를 기준으로 맞추는 게 안전합니다. 프록시 뒤에서http로 인식하면 콜백 URL mismatch가 자주 발생합니다.
Cloudflare Zero Trust에서 Access 앱 만들기
관리 콘솔에서 Zero Trust 로 이동 후 아래 순서로 구성합니다.
1) Access 애플리케이션 생성
- 메뉴:
Access→Applications→Add an application - 유형: 보통
Self-hosted - 도메인 예:
app.example.com - 경로 제한이 필요하면
Path를 분리해서 여러 앱으로 만드는 전략이 운영에 편합니다.
예를 들어:
app.example.com/*: 일반 사용자app.example.com/admin/*: 관리자만
2) IdP(Identity Provider) 연결
Settings → Authentication 에서 IdP를 연결합니다.
- Google Workspace, GitHub, Azure AD/Entra ID, Okta 등
- 중요한 점: 여기서의 IdP는 “Cloudflare Access 로그인” 용입니다. 앱 내부 OAuth와 동일할 수도, 다를 수도 있습니다.
3) 정책(Policy)으로 접근 제어
가장 흔한 정책 예시는 이메일 도메인 기반입니다.
- Allow:
Emails ending in→@yourcompany.com - Deny:
Everyone(기본 차단)
추가로 운영에서 자주 쓰는 조건:
- 특정 GitHub Org/Team만 허용
- 국가/ASN 기반 차단(보안 요구가 있을 때)
- MFA 강제(가능한 IdP에서)
OAuth 앱과 Access가 충돌하는 지점 3가지
OAuth 앱 보호에서 “잘 되다가 특정 상황에서만 깨지는” 포인트는 대체로 아래 3곳입니다.
1) OAuth 콜백 경로가 Access에 의해 막힘
앱이 /oauth/callback 같은 경로로 IdP에서 돌아오는데, Access가 그 요청을 “아직 인증 안 됨” 으로 보고 차단/리다이렉트할 수 있습니다.
해결 전략은 두 가지입니다.
전략 A: 콜백도 Access 인증을 통과시키기
- 권장. 콜백도 결국 사용자 브라우저가 접근하는 경로이므로, Access 세션이 유지되면 정상 동작합니다.
- 단, Access 세션 만료/서드파티 쿠키 정책/사파리 ITP 등에 의해 예외가 생길 수 있어 테스트가 필요합니다.
전략 B: 콜백 경로만 예외 처리(Bypass)
- 특정 환경에서만 필요. 예외는 공격 표면이 되므로 최소화합니다.
Cloudflare Access 정책에서 Include/Exclude 또는 별도 앱으로 분리해 /oauth/callback 만 정책을 다르게 가져가는 방식이 운영상 깔끔합니다.
2) 프록시 뒤에서 redirect_uri 가 불일치
앱이 redirect_uri 를 만들 때 http:// 로 생성하거나, 호스트를 내부 도메인으로 만들면 IdP가 redirect_uri mismatch 로 거절합니다.
앱에서 신뢰 프록시 설정을 제대로 해 두세요.
Express 예시
import express from 'express';
const app = express();
// Cloudflare 같은 리버스 프록시 뒤에서는 필수
app.set('trust proxy', 1);
app.get('/debug', (req, res) => {
res.json({
protocol: req.protocol,
host: req.get('host'),
xfp: req.get('x-forwarded-proto'),
});
});
Spring Boot 예시
server:
forward-headers-strategy: framework
이 설정이 없으면 req.protocol 이 http 로 잡히고, 결과적으로 OAuth 콜백 URL이 틀어집니다.
3) API 호출은 “브라우저 세션” 이 아니라 “서비스 토큰” 이다
Access는 기본적으로 브라우저 기반 세션에 강합니다. 하지만 백엔드 간 호출이나 CI에서 호출하는 API는 브라우저 쿠키가 없습니다.
이때 선택지는 다음입니다.
- Cloudflare Access의
Service Auth(서비스 토큰) 사용 - 혹은 앱 자체의 API 키/JWT 인증을 유지하고, Access는 네트워크 레벨에서만 제한
운영에서 흔한 패턴은 관리자 UI는 Access 세션 으로, 머신 투 머신 API는 Service Token 으로 분리하는 것입니다.
실전 구성: 관리자 경로만 Access로 강하게 잠그기
가장 현실적인 시나리오로 app.example.com 은 공개 서비스(OAuth 로그인은 앱에서 처리)이고, /admin 만 사내 계정으로 제한해 봅니다.
1) Access 앱을 2개로 나눈다
- App A:
app.example.com/*(정책을 느슨하게 또는 아예 만들지 않음) - App B:
app.example.com/admin/*(사내 이메일만 허용)
이렇게 하면 고객 트래픽과 내부 운영 트래픽을 분리할 수 있고, 장애/정책 변경의 영향 범위가 줄어듭니다.
2) 앱 레벨에서도 최소한의 방어는 유지
Access가 있다고 해서 앱의 권한 체크를 빼면 안 됩니다.
- Access는 “문 앞 경비”
- 앱은 “건물 내부의 출입 통제”
예를 들어 /admin 은 앱에서도 role=admin 을 확인하세요.
Service Token으로 서버 간 API 보호하기
배치/크론/내부 서비스가 https://app.example.com/internal/jobs/run 같은 엔드포인트를 호출한다고 가정합니다.
1) Cloudflare에서 Service Token 발급
Access→Service Auth→Create Service TokenClient ID,Client Secret을 안전한 시크릿 저장소에 보관
2) 호출 측에서 헤더로 전달
Cloudflare Access는 보통 아래 헤더를 사용합니다.
CF-Access-Client-IdCF-Access-Client-Secret
curl 예시:
curl -sS https://app.example.com/internal/jobs/run \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"
서버에서 별도 구현 없이도 Access 정책이 검증해 주는 점이 장점입니다.
Access JWT를 앱에서 활용하기(선택)
Access를 통과한 요청에는 사용자 식별 정보가 담긴 JWT가 헤더로 전달될 수 있습니다. 이를 앱에서 받아 추가 권한 체크 나 감사 로그 에 활용하면 좋습니다.
일반적으로는 CF-Access-Jwt-Assertion 같은 헤더로 제공됩니다(테넌트/설정에 따라 다를 수 있으니 실제 대시보드 문서를 확인하세요).
Node에서 JWT를 파싱하는 예시(검증은 생략, 운영에서는 반드시 서명 검증 필요):
import jwt from 'jsonwebtoken';
export function readAccessJwt(req) {
const token = req.header('CF-Access-Jwt-Assertion');
if (!token) return null;
// 운영에서는 공개키(JWKS)로 서명 검증을 수행해야 함
const decoded = jwt.decode(token);
return decoded;
}
이 값을 기반으로:
email이 사내 도메인인지 재확인- 특정 그룹 클레임이 있는지 확인
- 감사 로그에
who did what을 남기기
운영 체크리스트: OAuth 보호에서 자주 놓치는 것들
세션/쿠키
- Access 세션 만료 시간과 앱 세션 만료 시간을 의도적으로 정렬하세요.
- 사파리/모바일에서 콜백이 반복 리다이렉트되는지 테스트하세요.
레이트 리밋과 장애 대응
Access는 인증 게이트이지만, 앱이 토큰 교환을 하는 /token 단계에서 외부 IdP 레이트 리밋이 터질 수 있습니다. 특히 재시도/백오프가 없으면 사용자 경험이 급격히 나빠집니다.
위 글의 백오프 패턴은 OAuth 토큰 교환 실패(일시적 5xx, 네트워크 오류)에도 그대로 응용할 수 있습니다.
프록시 환경에서 스트리밍/SSE가 있다면
OAuth 앱이 로그인 후 실시간 스트리밍(SSE, 웹소켓)을 제공한다면, 프록시/버퍼링/타임아웃 문제로 “인증은 됐는데 연결이 끊기는” 케이스가 생깁니다. Cloudflare를 포함한 다중 프록시 체인에서 특히 빈번합니다.
트러블슈팅: 로그인 루프/403/콜백 실패 빠른 진단
증상 1: Access 로그인 화면으로 계속 돌아간다
- Access 세션 쿠키가 저장되지 않는 환경인지 확인(브라우저 정책, 서드파티 쿠키)
- 앱 도메인과 Access 앱 도메인이 정확히 일치하는지 확인
- 콜백 경로가 다른 Access 앱 정책에 걸려 “서로 다른 정책이 번갈아 적용” 되는지 확인
증상 2: OAuth 콜백에서 invalid_grant
- 서버 시간이 틀어진 경우(NTP)
- PKCE
code_verifier저장이 깨진 경우(분산 환경에서 세션 스토리지) redirect_uri가 1바이트라도 다른 경우
자세한 원인 분해는 아래 글을 참고하세요.
증상 3: API는 되는데 관리자 페이지만 403
/admin이 별도 Access 앱으로 분리되어 있다면 정책을 확인- 캐시/리다이렉트 설정으로
/admin이 다른 호스트로 튀지 않는지 확인 - 앱 내부 권한 체크(예:
role=admin) 가 정상인지 확인
정리: “OAuth만”으로 부족한 운영 보안을 Access로 메운다
Cloudflare Zero Trust(Access)를 OAuth 앱 앞단에 두면, 애플리케이션 코드를 크게 바꾸지 않고도 다음을 얻습니다.
- 경로 단위로 접근 제어(
/admin같은 민감 구간) - 사내 계정/조직 기반의 강제 인증
- 서비스 토큰 기반의 머신 투 머신 보호
- 앱이 토큰 검증을 하기 전에 트래픽을 줄여 비용과 장애 가능성 감소
핵심은 콜백 경로, 프록시 뒤 redirect_uri, 브라우저 세션이 아닌 API 호출 이 세 지점에서 설계를 분리하는 것입니다. 이 원칙만 지키면 Access는 OAuth 앱 운영의 “현실적인 안전벨트” 로 꽤 강력하게 작동합니다.