Published on

Cloudflare 뒤 OAuth 콜백 521·리다이렉트 루프 해결

Authors

서드파티 OAuth 로그인(구글, 깃허브, 카카오 등)을 붙인 서비스가 Cloudflare 프록시 뒤로 들어가면, 평소엔 멀쩡하다가도 redirect_uri 콜백 구간에서만 521이 뜨거나 무한 리다이렉트 루프가 발생하는 경우가 많습니다. 특히 Next.js, Spring Boot, Nginx Ingress, ALB 같은 조합에서는 TLS 종료 지점이 여러 군데로 나뉘고, X-Forwarded-* 헤더 해석이 엇갈리면서 “클라이언트는 https로 왔다고 믿는데 백엔드는 http로 왔다고 판단”하는 순간 루프가 시작됩니다.

이 글은 증상을 두 갈래로 나눠(521 / 루프) 각각의 전형적인 원인과, Cloudflare 설정부터 애플리케이션 레벨까지 “어디를 어떻게 고치면 끝나는지”를 순서대로 정리합니다.

문제를 두 가지로 분해하기

1) Cloudflare 521이 의미하는 것

Cloudflare의 521은 대체로 “Cloudflare가 오리진에 TCP 연결을 못 했다”에 가깝습니다. 즉 OAuth 자체 문제라기보다, 콜백을 처리하는 오리진이 해당 요청에서만 연결/응답을 못 하고 있다는 신호입니다.

자주 겪는 케이스는 다음과 같습니다.

  • 오리진 방화벽이 Cloudflare IP 대역을 막음
  • 오리진이 특정 경로(예: /oauth/callback)에서만 리다이렉트 폭주로 커넥션이 고갈됨
  • 오리진이 https로만 열려 있는데 Cloudflare가 http로 붙으려 함(SSL 모드/포트 불일치)
  • WAF/봇 방어가 콜백을 차단해 오리진이 정상 응답을 못 주는 것처럼 보임

2) 리다이렉트 루프의 전형적인 구조

리다이렉트 루프는 대부분 아래 패턴입니다.

  • 브라우저 https://app.example.com/login 접속
  • Cloudflare가 오리진에 전달(대개 http)
  • 오리진이 “http로 왔네”라고 판단하고 https로 리다이렉트
  • Cloudflare가 다시 오리진에 http로 전달
  • 반복

OAuth에서는 여기에 redirect_uri가 얹히면서 더 복잡해집니다.

  • IdP가 redirect_uri=https://app.example.com/oauth/callback 로 리다이렉트
  • 콜백에서 세션 쿠키/CSRF state 검증 실패
  • 앱이 “로그인 안 됨”으로 판단하고 /login으로 리다이렉트
  • /login이 다시 IdP로… 반복

1단계: Cloudflare에서 가장 먼저 볼 것

SSL/TLS 모드 점검: Full(Strict)로 고정

Cloudflare 대시보드에서 SSL/TLS 모드가 Flexible이면 루프의 확률이 급상승합니다. Flexible은 클라이언트와 Cloudflare만 TLS이고, Cloudflare와 오리진은 http가 될 수 있어 백엔드가 https 강제를 걸면 바로 루프가 납니다.

권장:

  • 오리진에 유효한 인증서(퍼블릭 또는 Cloudflare Origin Certificate)를 설치
  • Cloudflare SSL/TLS encryption modeFull (strict)로 설정

Always Use HTTPS / Redirect Rules 확인

Cloudflare의 Always Use HTTPS 또는 Redirect Rule이 앱/인그레스의 https 리다이렉트와 중복되면, 콜백에서만 루프가 나타날 수 있습니다.

원칙:

  • https 강제는 “한 군데”에서만
  • Cloudflare에서 강제할 거면, 오리진(앱/인그레스)에서는 강제를 끄거나 조건을 맞추기

캐시/봇/WAF가 콜백을 건드리는지 확인

OAuth 콜백은 대부분 GET /oauth/callback?code=...&state=... 형태입니다. 이 URL은 절대 캐시되면 안 됩니다.

  • Cache Rules에서 콜백 경로는 Bypass cache
  • Bot Fight Mode, Super Bot Fight Mode, WAF Managed Rules가 콜백을 막지 않는지 이벤트 로그 확인

추가로, Cloudflare Access를 쓰는 경우 콜백 경로가 Access 정책에 걸리면 “IdP 콜백이 다시 Access 로그인으로” 튕기며 루프가 날 수 있습니다. 콜백 경로는 예외로 빼는 설계가 흔합니다.

2단계: 오리진 연결(521)부터 확실히 잡기

오리진 방화벽에서 Cloudflare IP 허용

521이 진짜 네트워크 차단이라면, 가장 흔한 원인은 오리진 방화벽이 Cloudflare IP 대역을 허용하지 않은 것입니다.

  • 오리진이 보안 그룹/iptables/UFW를 사용한다면 Cloudflare IP 대역을 allowlist
  • Cloudflare 공식 IP 목록을 주기적으로 동기화(수동이면 누락이 잦음)

포트/프로토콜 불일치 확인

Cloudflare는 기본적으로 오리진에 80 또는 443으로 연결합니다. 오리진이 8443 같은 포트를 쓰면 프록시 연결이 실패할 수 있습니다.

  • 오리진 서비스 포트를 443으로 맞추거나
  • Cloudflare Spectrum/터널/별도 구성을 사용

“루프가 521로 보이는” 상황

리다이렉트가 과도하게 발생하면 오리진이 순간적으로 커넥션/스레드를 고갈시키고, Cloudflare 관점에서는 오리진이 죽은 것처럼 보이며 521이 뜰 수 있습니다. 즉 521을 네트워크로만 단정하면 삽질합니다.

오리진에서 아래를 같이 확인하세요.

  • 인그레스/앱 로그에 동일 경로 리다이렉트 반복 흔적
  • 429 또는 타임아웃 증가
  • 워커/스레드/커넥션 풀 포화

이런 “로그가 애매한” 상황에서는 인프라 레벨에서부터 차근차근 확인하는 접근이 유효합니다. 비슷한 방식의 진단 흐름은 Kubernetes CrashLoopBackOff, 로그 없이 진단하는 법에서도 다뤘습니다.

3단계: 리다이렉트 루프의 핵심은 X-Forwarded-Proto

백엔드가 실제 스킴을 https로 인식하게 만들기

Cloudflare는 오리진으로 요청을 전달할 때 보통 다음 헤더를 붙입니다.

  • X-Forwarded-Proto: https
  • CF-Visitor: {"scheme":"https"}
  • CF-Connecting-IP: ...

문제는 앱/프레임워크/리버스프록시가 이 헤더를 “신뢰하지 않거나”, 중간 프록시가 덮어쓰는 경우입니다.

Nginx(리버스 프록시) 예시

오리진 앞단 Nginx가 있다면 아래처럼 명시합니다.

location / {
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

  proxy_pass http://app_upstream;
}

여기서 핵심은 $scheme이 오리진에서 관측되는 스킴이라는 점입니다. Cloudflare가 오리진으로 http로 붙으면 $schemehttp가 됩니다. 즉 이 설정만으로는 Cloudflare의 https 정보를 잃을 수 있습니다.

Cloudflare에서 넘어온 헤더를 유지하려면, 조건적으로 헤더를 보존하거나 Cloudflare 전용 헤더를 사용합니다.

map $http_x_forwarded_proto $real_xfp {
  default $http_x_forwarded_proto;
  ""      $scheme;
}

server {
  location / {
    proxy_set_header X-Forwarded-Proto $real_xfp;
    proxy_set_header Host $host;
    proxy_pass http://app_upstream;
  }
}

Kubernetes Nginx Ingress 예시

Nginx Ingress에서는 아래 설정이 자주 필요합니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
  annotations:
    nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "false"
spec:
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app
                port:
                  number: 8080
  • use-forwarded-headersX-Forwarded-Proto 등을 신뢰
  • https 강제가 Cloudflare에 있다면 인그레스의 강제 리다이렉트는 끄는 편이 단순합니다

Spring Boot 예시

Spring Boot는 프록시 환경에서 Forwarded/X-Forwarded-* 처리를 명시해야 합니다.

application.yml:

server:
  forward-headers-strategy: framework

또는 톰캣/프록시 설정에 따라 native가 필요할 수 있습니다. 목표는 “요청의 scheme/host/port를 프록시 헤더 기준으로 재구성”하는 것입니다.

Next.js(특히 App Router)에서 콜백 URL 생성 주의

서버에서 절대 URL을 만들 때 req.headers.host와 스킴을 잘못 조합하면 http 콜백 URL이 생성되어 IdP 설정과 불일치가 납니다.

Node/Next 서버에서 스킴을 잡을 때는 다음 우선순위가 안전합니다.

export function getExternalBaseUrl(req: Request) {
  const host = req.headers.get("x-forwarded-host") ?? req.headers.get("host");
  const proto = req.headers.get("x-forwarded-proto") ?? "https";
  return `${proto}://${host}`;
}

주의: 본문에 부등호 문자가 노출되면 MDX에서 문제가 될 수 있으니, 위처럼 코드 블록 안에서만 사용하세요.

4단계: OAuth 콜백에서만 루프가 나는 주요 원인 4가지

1) redirect_uri 스킴/호스트 불일치

IdP 콘솔에 등록된 redirect_uri는 보통 완전 일치가 필요합니다.

  • 등록: https://app.example.com/oauth/callback
  • 실제 요청: http://app.example.com/oauth/callback 또는 https://www.app.example.com/...

Cloudflare, www 리다이렉트, 국가별 도메인, 멀티 테넌트 서브도메인에서 자주 터집니다.

해결:

  • 외부에 노출되는 canonical 도메인을 하나로 고정
  • 앱이 콜백 URL을 만들 때 X-Forwarded-Proto/X-Forwarded-Host를 기준으로 생성

2) SameSite 쿠키 정책으로 state 세션이 사라짐

OAuth는 state 검증을 위해 로그인 시작 시점의 세션/쿠키가 콜백까지 유지돼야 합니다. 그런데 크로스 사이트 리다이렉트가 끼면 브라우저가 쿠키를 안 보내는 경우가 있습니다.

체크 포인트:

  • 쿠키에 SameSite=Lax 또는 None 필요 여부
  • SameSite=None이면 Secure 필수(https에서만)

예: Express 세션 쿠키 설정

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: true,
      sameSite: "lax",
    },
  })
);

프록시 뒤에서 secure: true가 먹으려면 앱이 “현재 요청이 https”임을 알아야 하므로, 결국 X-Forwarded-Proto 처리가 다시 핵심이 됩니다.

3) 콜백 경로가 인증 미들웨어에 걸려 재로그인 루프

보호된 라우트 미들웨어가 /oauth/callback에도 적용되면,

  • 콜백 처리 전에 인증이 없다고 판단
  • /login으로 리다이렉트
  • IdP로 다시 리다이렉트

이 루프가 생깁니다.

해결:

  • 콜백 엔드포인트는 인증 예외 처리
  • 또는 콜백에서만 허용되는 최소 검증(예: state 검증 후 세션 발급)으로 구성

4) Cloudflare에서의 URL 리라이트/정규화

Cloudflare Transform Rules로 쿼리 스트링을 제거하거나, trailing slash를 강제로 붙이는 규칙이 있으면 code/state가 사라져 콜백이 실패합니다.

  • 콜백 경로에서는 쿼리 보존
  • Normalize 규칙이 있다면 콜백 경로 예외

5단계: 재현과 관측을 위한 최소 커맨드

curl로 헤더와 리다이렉트 체인 확인

로컬에서 루프를 빠르게 확인하려면 다음이 가장 효율적입니다.

curl -I -L https://app.example.com/oauth/callback?code=dummy\&state=dummy
  • Location 헤더가 http로 떨어지는지
  • 동일 URL로 반복되는지
  • 중간에 cf-ray가 찍히는지(Cloudflare 경유 확인)

오리진에 직접 붙어 비교(가능할 때)

오리진이 퍼블릭이면 Cloudflare를 우회해 붙어 차이를 봅니다.

curl -I https://ORIGIN_IP \
  -H 'Host: app.example.com'

여기서도 리다이렉트가 발생하면 앱/인그레스 문제일 확률이 큽니다.

권장 해결 조합(가장 많이 성공하는 레시피)

  1. Cloudflare SSL/TLSFull (strict)로 설정
  2. https 강제는 Cloudflare Redirect Rule 하나로 통일(오리진 강제는 끄거나 조건 정리)
  3. 오리진(인그레스/앱)이 X-Forwarded-Proto를 신뢰하도록 설정
  4. 콜백 경로는 캐시/봇/WAF/Access 정책에서 예외 또는 완화
  5. 쿠키 SameSite/Secure를 콜백 플로우에 맞게 조정

이 5가지만 정리해도 521과 루프의 대부분이 해결됩니다.

체크리스트: “원인별로 어디를 고치나” 요약

  • 521이 바로 뜸: 방화벽 allowlist, 오리진 포트/프로토콜, 오리진 다운 여부
  • 루프(https 강제 반복): SSL 모드(Flexible 금지), 중복 리다이렉트 제거, X-Forwarded-Proto 신뢰
  • 콜백에서만 루프: redirect_uri 불일치, 콜백 경로 인증 미들웨어 예외, 쿼리 스트링 보존, SameSite 쿠키

부록: 인증 서버(Keycloak 등) 사용 시 추가 함정

Keycloak 같은 IdP를 붙이면, 콜백 루프가 “토큰 검증 실패”로도 이어질 수 있습니다. 예를 들어 JWKS 캐시/키 회전 타이밍 이슈로 401이 나고, 앱이 이를 로그인 미완료로 처리해 다시 로그인으로 보내면 루프처럼 보이기도 합니다. 이 경우는 네트워크/리다이렉트와 별개로 토큰 검증 체인을 함께 점검해야 합니다.

마무리

Cloudflare 뒤 OAuth 콜백 문제는 “OAuth가 어렵다”기보다, TLS 종료 지점과 프록시 헤더 신뢰, 그리고 리다이렉트/쿠키 정책이 서로 어긋나서 생기는 인프라-애플리케이션 경계 이슈입니다. 521이면 먼저 오리진 연결성을 확정하고, 루프면 X-Forwarded-Proto와 https 강제 정책을 하나로 정리하세요. 콜백에서만 터지면 redirect_uri 정합성과 SameSite 쿠키, 콜백 경로 예외 처리가 마지막 퍼즐인 경우가 많습니다.