- Published on
Cloudflare 뒤에서 OAuth 콜백이 http로 떨어질 때
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서드파티 OAuth(구글/깃허브/카카오 등)를 붙여두고 Cloudflare(프록시, Load Balancer, Cloudflare Tunnel) 뒤로 서비스를 옮긴 뒤, 갑자기 콜백이 https://가 아니라 http://로 “떨어지는” 현상을 자주 봅니다. 증상은 대개 아래 중 하나로 나타납니다.
- IdP 콘솔에는
https://app.example.com/oauth/callback로 등록했는데, 실제 앱이 만든redirect_uri가http://app.example.com/oauth/callback로 나가서redirect_uri_mismatch발생 - 로그인은 되는데 콜백에서 다시
http로 리다이렉트되어 무한 리다이렉트/쿠키 누락(secure cookie)로 세션이 증발 - 특정 경로(예:
/auth/callback)만 http로 계산되어 프레임워크가 절대 URL 생성에 실패
핵심 원인은 단순합니다. 클라이언트↔Cloudflare 구간은 HTTPS인데, Cloudflare↔원본(origin) 구간이 HTTP이거나, 원본 앱이 “원래 요청이 HTTPS였다”는 정보를 신뢰하지 못해 스킴을 http로 판단하는 겁니다.
이 글에서는 Cloudflare 설정, 프록시 헤더, 그리고 Node/Express·Next.js·Spring Boot 등에서의 실전 수정 포인트를 한 번에 정리합니다.
왜 http로 인식될까: 스킴 결정 로직과 프록시 헤더
웹 앱/프레임워크는 보통 다음 중 하나로 “현재 요청의 스킴(https/http)”을 결정합니다.
- 서버가 실제로 받은 연결의 TLS 여부 (origin이 HTTPS로 받으면 https)
- 리버스 프록시가 넣어주는
X-Forwarded-Proto: https, 또는 표준인Forwarded: proto=https - Cloudflare가 넣어주는
CF-Visitor: {"scheme":"https"}같은 벤더 헤더
Cloudflare를 쓰면 브라우저는 https://로 접속하지만, 원본은 종종 http://로 받습니다(특히 Flexible SSL, Tunnel, 내부 LB). 이때 앱이 (2)를 신뢰하지 않으면 스킴은 http로 결정되고, OAuth 라이브러리가 redirect_uri를 http로 만들어버립니다.
1차 점검: 실제로 어떤 헤더가 들어오는지부터 확인
먼저 원본 서버에서 요청 헤더를 덤프해 보세요. Cloudflare 뒤라면 대개 아래가 보입니다.
X-Forwarded-Proto: httpsX-Forwarded-For: <client-ip>, <cf-ip>CF-Visitor: {"scheme":"https"}CF-Connecting-IP: <client-ip>
Node/Express에서 헤더 확인 예시
import express from 'express';
const app = express();
app.get('/debug/headers', (req, res) => {
res.json({
protocol: req.protocol,
secure: req.secure,
host: req.get('host'),
xfp: req.get('x-forwarded-proto'),
forwarded: req.get('forwarded'),
cfVisitor: req.get('cf-visitor'),
});
});
app.listen(3000);
여기서 x-forwarded-proto는 https인데 req.protocol이 http로 나오면, 프레임워크가 프록시 헤더를 신뢰하지 않는 상태입니다.
Cloudflare 설정에서 가장 흔한 원인: SSL/TLS 모드
Cloudflare 대시보드의 SSL/TLS 모드는 origin까지의 암호화 여부를 좌우합니다.
- Flexible: 브라우저↔Cloudflare는 HTTPS, Cloudflare↔origin은 HTTP
- Full: Cloudflare↔origin도 HTTPS(인증서 검증은 느슨)
- Full (strict): Cloudflare↔origin HTTPS + 유효한 인증서 검증
OAuth 콜백이 http로 떨어지는 사례의 상당수는 Flexible에서 시작합니다. 가능하면 **Full (strict)**로 올리고 origin에도 정상 인증서를 두는 게 정석입니다.
다만, 이미 네트워크 구조상 origin이 HTTP일 수 있습니다(내부망, k8s Ingress, Tunnel). 이 경우에도 해결은 가능합니다. 핵심은 “원래는 HTTPS였다”는 사실을 앱이 알게 하는 것입니다.
해결 전략 A: 앱이 프록시 헤더를 신뢰하도록 설정
Express: app.set('trust proxy', true)
Express는 기본적으로 X-Forwarded-*를 신뢰하지 않습니다. Cloudflare/로드밸런서 뒤라면 아래 설정이 거의 필수입니다.
import express from 'express';
const app = express();
// Cloudflare/프록시 뒤에서 https 판단을 위해 필요
app.set('trust proxy', true);
app.get('/debug/proto', (req, res) => {
res.json({ protocol: req.protocol, secure: req.secure });
});
app.listen(3000);
trust proxy가 켜지면req.protocol이X-Forwarded-Proto를 반영합니다.- OAuth 라이브러리(예: Passport, NextAuth, custom)가 절대 URL을 만들 때 이 값이 중요합니다.
보안 팁: 무조건 true 대신, 가능하면 신뢰할 프록시 범위를 좁히세요.
// 예: 첫 번째 홉만 신뢰
app.set('trust proxy', 1);
Spring Boot: Forwarded 헤더 처리 활성화
Spring은 환경에 따라 X-Forwarded-Proto를 자동 반영하지 않을 수 있습니다. Boot 3 기준으로는 다음 중 하나가 필요합니다.
application.yml
server:
forward-headers-strategy: framework
또는(인프라가 Forwarded 표준 헤더를 쓰는 경우)
server:
forward-headers-strategy: native
이 설정이 없으면 HttpServletRequest#getScheme()가 http로 남아 OAuth redirect URL 생성이 틀어질 수 있습니다.
해결 전략 B: OAuth 라이브러리에 “외부 URL(issuer/baseUrl)”을 명시
프레임워크가 스킴을 잘 판단하게 만드는 게 1순위지만, 운영에서는 구성 실수/미들웨어 순서 때문에 다시 깨지는 경우가 있습니다. 이때는 OAuth 구성에 외부에서 보이는 base URL을 명시해 “절대 URL 생성”을 고정하는 방법이 강력합니다.
NextAuth(Next.js)라면 NEXTAUTH_URL
NEXTAUTH_URL=https://app.example.com
이 값이 없거나 잘못되면, 프록시 뒤에서 콜백이 http로 생성될 수 있습니다.
(일반) OAuth 클라이언트에서 redirect_uri를 고정
서버가 redirect_uri를 동적으로 만들지 말고, 환경변수로 고정하는 방식도 자주 씁니다.
OAUTH_REDIRECT_URI=https://app.example.com/oauth/callback
const redirectUri = process.env.OAUTH_REDIRECT_URI;
const authUrl = new URL('https://idp.example.com/oauth/authorize');
authUrl.searchParams.set('client_id', process.env.CLIENT_ID);
authUrl.searchParams.set('redirect_uri', redirectUri);
authUrl.searchParams.set('response_type', 'code');
장점: 프록시/헤더/프레임워크 변화에도 redirect_uri가 흔들리지 않습니다. 단점: 멀티 도메인/멀티 테넌트에는 추가 설계가 필요합니다.
해결 전략 C: Cloudflare에서 HTTPS 강제(리다이렉트) + Origin 리다이렉트 금지
Cloudflare는 엣지에서 Always Use HTTPS 또는 Redirect Rules로 HTTP→HTTPS를 강제할 수 있습니다. 하지만 여기서 자주 하는 실수가 있습니다.
- origin 앱도 동시에 HTTP→HTTPS 리다이렉트를 수행
- 그런데 origin은 “내가 받은 건 HTTP”라서 https로 리다이렉트
- Cloudflare는 이미 https로 왔는데, origin이 또 리다이렉트하면서 루프/스킴 꼬임 발생
권장 패턴:
- 엣지(Cloudflare)에서 HTTPS 강제
- origin 앱은 프록시 헤더 신뢰(
X-Forwarded-Proto) 후, 필요 시에만 리다이렉트
즉, origin이 무조건적인 스킴 리다이렉트를 하지 않게 조정하는 게 안전합니다.
Cloudflare Tunnel을 쓸 때의 포인트
Cloudflare Tunnel(cloudflared)은 origin과 Cloudflare 간에 터널을 만들고, 로컬 서비스는 보통 HTTP로 붙입니다. 이 구조에서는 원본 앱이 TLS를 직접 보지 못하므로 프록시 헤더 신뢰가 거의 필수입니다.
cloudflared 구성은 대개 이런 형태입니다.
ingress:
- hostname: app.example.com
service: http://localhost:3000
- service: http_status:404
이때도 Cloudflare는 X-Forwarded-Proto: https를 붙여주므로, 앱에서 이를 반영하도록 설정해야 합니다(Express trust proxy, Spring forward-headers-strategy 등).
실전 디버깅 체크리스트(10분 컷)
- Cloudflare SSL/TLS 모드 확인: 가능하면 Full (strict)
- 원본에서
/debug/headers로X-Forwarded-Proto가https인지 확인 - 앱이 그 헤더를 반영하는지 확인
- Express:
trust proxy - Spring Boot:
server.forward-headers-strategy
- Express:
- OAuth 라이브러리가 생성하는
redirect_uri를 실제 로그로 확인 - 쿠키 이슈 동반 시(secure cookie): 콜백이 http로 인식되면
Secure쿠키가 누락될 수 있으니 함께 점검
자주 겪는 함정: “Cloudflare만 믿었는데” 앱 레벨에서 깨지는 이유
- 미들웨어 순서: Express에서
trust proxy를 너무 늦게 설정하거나, 프록시 앞단에서 헤더를 덮어씀 - 다중 프록시: LB → Nginx → app처럼 홉이 여러 개인데
trust proxy를 1로 고정해 잘못 판단 - 리다이렉트 URL 생성 위치: OAuth 핸들러가 request를 참조하지 않고 별도 유틸에서 URL을 만들면서 스킴이 기본값(http)으로 고정
이런 경우에는 구성으로만 해결하려다 시간이 새기 쉽습니다. 로그로 redirect_uri 문자열을 직접 찍어 어디에서 http가 결정되는지 추적하는 게 가장 빠릅니다.
보안 관점: 프록시 헤더 신뢰는 “누구를 믿을지”의 문제
X-Forwarded-Proto는 클라이언트가 위조할 수도 있습니다. 그래서 앱은 “인터넷에서 직접 들어오는 요청”에 대해 이 헤더를 무조건 신뢰하면 안 됩니다.
하지만 Cloudflare 뒤에서만 서비스되고, origin이 외부에 직접 노출되지 않도록 방화벽/보안그룹으로 막혀 있다면(Cloudflare IP만 허용, Tunnel 사용 등) trust proxy를 켜는 것이 합리적입니다.
관련해서 함께 보면 좋은 글
OAuth 콜백 문제를 해결하고 나면, 다음 단계로는 토큰 검증/키 회전(JWKS)에서 401을 줄이는 작업이 자주 따라옵니다. 운영에서 특히 효과가 컸던 패턴은 아래 글에 정리해 두었습니다.
또, 프록시/클라우드 환경에서 “정상처럼 보이는데 간헐적으로만 실패”하는 네트워크 류 이슈는 원인 분리가 중요합니다. 비슷한 접근 방식으로 정리한 글도 참고가 됩니다.
결론: 정답은 ‘https를 끝까지 유지’ 또는 ‘https였음을 정확히 전파’
Cloudflare 뒤에서 OAuth 콜백이 http로 떨어지는 문제는 대부분 아래 둘 중 하나로 깔끔하게 끝납니다.
- 가능하면 Cloudflare SSL/TLS를 **Full (strict)**로 올려 origin까지 HTTPS를 유지
- 구조상 origin이 HTTP라면,
X-Forwarded-Proto(또는 Forwarded)를 앱이 신뢰하도록 설정하고, 필요 시 OAuth의 base URL/redirect_uri를 환경변수로 고정
이 조합으로 redirect_uri_mismatch, 무한 리다이렉트, secure cookie 누락 같은 2차 증상까지 함께 정리할 수 있습니다.