Published on

Cloudflare Zero Trust로 OAuth 앱 보호 실전

Authors

서드파티 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/프록시 모드와 애플리케이션 전제

  1. 도메인이 Cloudflare에 있고, 해당 레코드가 Proxied(주황 구름) 이어야 합니다.

  2. 애플리케이션은 아래 헤더를 신뢰할 준비가 필요합니다.

  • 원본 IP가 필요하면 CF-Connecting-IP
  • 원본 프로토콜/호스트는 X-Forwarded-Proto, X-Forwarded-Host
  1. OAuth 리다이렉트 URL은 https 를 기준으로 맞추는 게 안전합니다. 프록시 뒤에서 http 로 인식하면 콜백 URL mismatch가 자주 발생합니다.

Cloudflare Zero Trust에서 Access 앱 만들기

관리 콘솔에서 Zero Trust 로 이동 후 아래 순서로 구성합니다.

1) Access 애플리케이션 생성

  • 메뉴: AccessApplicationsAdd an application
  • 유형: 보통 Self-hosted
  • 도메인 예: app.example.com
  • 경로 제한이 필요하면 Path 를 분리해서 여러 앱으로 만드는 전략이 운영에 편합니다.

예를 들어:

  • app.example.com/* : 일반 사용자
  • app.example.com/admin/* : 관리자만

2) IdP(Identity Provider) 연결

SettingsAuthentication 에서 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.protocolhttp 로 잡히고, 결과적으로 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 발급

  • AccessService AuthCreate Service Token
  • Client ID, Client Secret 을 안전한 시크릿 저장소에 보관

2) 호출 측에서 헤더로 전달

Cloudflare Access는 보통 아래 헤더를 사용합니다.

  • CF-Access-Client-Id
  • CF-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 앱 운영의 “현실적인 안전벨트” 로 꽤 강력하게 작동합니다.