Published on

Nginx 뒤 OAuth 콜백이 http로 바뀌는 원인·해결

Authors

서론

프로덕션에서 HTTPS로 서비스 중인데, OAuth 로그인만 하면 콜백 URL(redirect_uri)이 http://.../callback으로 바뀌어 실패하는 경우가 있습니다. 증상은 대개 다음 중 하나로 나타납니다.

  • IdP(구글/깃허브/카카오/사내 Keycloak 등)에서 redirect_uri mismatch 오류
  • 로그인은 됐는데 콜백 단계에서 302가 http://로 떨어지며 무한 리다이렉트
  • 앱이 생성한 절대 URL이 http로 찍힘(예: 이메일 링크, Swagger, HATEOAS 링크 포함)

핵심 원리는 단순합니다. 앱이 “원래 클라이언트가 HTTPS로 접속했다”는 사실을 모르는 상태에서, 자신이 받은 요청이 HTTP라고 오인하면 redirect_uri를 http로 만들거나, 프레임워크가 스킴을 http로 확정해 버립니다. 이 글에서는 왜 그런 오인이 생기는지(원인)와, Nginx/로드밸런서/애플리케이션에서 무엇을 맞춰야 하는지(해결)를 실전 관점에서 정리합니다.

문제의 본질: 스킴(https)을 누가 결정하나

대부분의 웹앱은 다음 중 하나를 기준으로 “현재 요청의 스킴”을 판단합니다.

  1. 서버가 직접 수신한 프로토콜(예: 앱 컨테이너가 8080 HTTP로 받으면 http로 인식)
  2. 프록시가 전달한 헤더(예: X-Forwarded-Proto: https, Forwarded: proto=https)
  3. 명시적 앱 설정(예: server.forward-headers-strategy, SECURE_PROXY_SSL_HEADER, proxy_set_header)

리버스 프록시(Nginx) 또는 L7 로드밸런서에서 TLS를 종료하면, 프록시↔앱 구간은 HTTP인 경우가 많습니다. 이때 앱은 1번만 보면 당연히 http로 판단합니다. 그래서 2번(Forwarded 헤더)을 정확히 전달하고, 3번(앱이 그 헤더를 신뢰하도록 설정)을 맞춰야 합니다.

대표 원인 6가지

1) Nginx가 X-Forwarded-Proto를 안 보내거나 잘못 보냄

가장 흔한 케이스입니다. Nginx가 업스트림(앱)으로 X-Forwarded-Proto를 전달하지 않으면 앱은 HTTP로 인식합니다.

또는 Nginx 앞단에 또 다른 프록시(ALB/Ingress)가 있는데 Nginx가 $scheme만 보내면, Nginx가 받은 요청이 HTTP였던 경우(예: ALB가 Nginx에 HTTP로 전달) $schemehttp가 되어 버립니다.

2) 앱이 Forwarded 헤더를 “신뢰하지 않도록” 기본 설정됨

보안상 많은 프레임워크는 임의의 클라이언트가 X-Forwarded-Proto: https를 위조할 수 있기 때문에, 신뢰할 수 있는 프록시 뒤에서만 이 헤더를 사용하도록 제한합니다.

  • Spring Boot: forward headers 전략/톰캣 remoteIpValve 설정 필요
  • Django: SECURE_PROXY_SSL_HEADER 필요
  • Express: app.set('trust proxy', ...) 필요

3) Host/포트가 바뀌어 redirect_uri가 깨짐

스킴뿐 아니라 호스트/포트도 중요합니다.

  • X-Forwarded-Host, X-Forwarded-Port 미설정
  • Nginx가 proxy_set_header Host $host;를 안 해서 내부 호스트로 바뀜
  • port_in_redirect/absolute_redirect 영향으로 80/443이 섞임

4) OAuth 라이브러리가 “요청 URL” 대신 “설정값”으로 URL을 만들며 http로 고정

일부 라이브러리/미들웨어는 issuer, baseUrl, callbackURL 같은 설정값을 기반으로 절대 URL을 구성합니다. 이 값이 http://로 되어 있으면 헤더를 아무리 잘 보내도 소용이 없습니다.

5) Nginx/Ingress 다단 프록시에서 헤더가 덮어써짐

ALB/Ingress가 X-Forwarded-Proto=https를 붙여도, 뒤의 Nginx가 다시 X-Forwarded-Proto=$scheme로 덮어쓰면 http가 됩니다. 다단 프록시에서는 “누가 최종적으로 어떤 값을 쓰는지”를 확정해야 합니다.

6) HSTS/리다이렉트 정책 미흡으로 http가 살아 있음

애초에 80 포트가 열려 있고, 443으로 강제 리다이렉트가 불완전하면 OAuth 시작 단계에서 http로 진입해 버릴 수 있습니다. 그 결과 콜백도 http로 등록/생성됩니다.

진단: 지금 앱은 왜 http로 믿고 있나

1) Nginx access log에 헤더를 찍어 확인

Nginx에서 업스트림으로 보내는 값을 확인하려면, 우선 들어오는 요청의 X-Forwarded-Proto$scheme를 로그로 비교해보는 게 빠릅니다.

log_format with_proto '$remote_addr - $host "$request" '
                     'scheme=$scheme '
                     'xfp=$http_x_forwarded_proto '
                     'xff=$http_x_forwarded_for '
                     'ua="$http_user_agent"';

access_log /var/log/nginx/access.log with_proto;
  • 클라이언트가 HTTPS로 들어오는데 scheme=http로 찍히면, Nginx 앞단에서 이미 HTTP로 전달되고 있을 가능성이 큽니다.
  • xfp가 비어 있으면 앞단이 헤더를 안 붙였거나 중간에서 제거됩니다.

2) 앱에서 수신한 헤더를 덤프

애플리케이션 레벨에서 실제 수신 헤더를 찍어보면 “전달은 되는데 신뢰를 안 하는지”를 구분할 수 있습니다.

예: Node/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'),
    xfh: req.get('x-forwarded-host'),
    forwarded: req.get('forwarded'),
  });
});

xfp=https인데도 req.protocolhttp라면 trust proxy 설정이 빠진 겁니다.

해결 1: Nginx에서 Forwarded 헤더를 “정확히” 전달

가장 기본이 되는 Nginx 설정 템플릿입니다.

server {
  listen 80;
  server_name example.com;

  # 80으로 들어오면 무조건 443으로
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name example.com;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  location / {
    proxy_pass http://app:8080;

    # 원본 Host 유지
    proxy_set_header Host $host;

    # 클라이언트 IP 체인
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # 핵심: 스킴/호스트/포트 전달
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Port $server_port;

    # WebSocket/HTTP1.1 필요 시
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
  }
}

Nginx가 TLS 종료를 안 하고, 앞단(ALB/Ingress)이 TLS 종료하는 구조라면?

이때 Nginx가 보는 $scheme는 보통 http입니다. 그러면 X-Forwarded-Proto $scheme;가 오히려 문제를 만듭니다.

해결은 “앞단이 준 X-Forwarded-Proto를 보존”하는 것입니다.

# 앞단이 이미 X-Forwarded-Proto를 넣어준다면 그 값을 우선 사용
map $http_x_forwarded_proto $proxy_xfp {
  default $http_x_forwarded_proto;
  ''      $scheme;
}

server {
  listen 80;
  server_name example.com;

  location / {
    proxy_pass http://app:8080;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $proxy_xfp;
    proxy_set_header X-Forwarded-Host $host;
  }
}

이 패턴은 다단 프록시에서 X-Forwarded-Proto가 http로 덮어써지는 사고를 예방합니다.

해결 2: 애플리케이션이 프록시 헤더를 신뢰하도록 설정

Nginx만 고쳐도 되는 경우가 많지만, 프레임워크가 기본적으로 Forwarded 헤더를 무시하면 여전히 http로 나갑니다. 대표 스택별 설정만 짚습니다.

Spring Boot (Spring Security OAuth 포함)

application.yml에서 Forwarded 헤더 처리를 켭니다.

server:
  forward-headers-strategy: framework

구버전/서블릿 컨테이너에 따라 아래가 필요할 수 있습니다.

server:
  tomcat:
    remoteip:
      protocol-header: x-forwarded-proto
      remote-ip-header: x-forwarded-for

또한 Spring Security에서 리다이렉트 URL 생성 시 X-Forwarded-Proto를 반영하도록 ForwardedHeaderFilter가 필요할 수 있습니다(환경에 따라 자동 등록되기도 함).

Django

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
USE_X_FORWARDED_HOST = True

Express (Passport/OAuth)

// Nginx 같은 프록시 뒤라면 필수
app.set('trust proxy', 1);

이 설정이 없으면 req.protocol이 항상 http로 남아 callback URL이 http로 만들어질 수 있습니다.

해결 3: OAuth 라이브러리/IdP 설정에서 “정답 URL”을 고정

프록시 헤더가 완벽해도, 라이브러리가 설정 기반으로 URL을 만들면 결과가 달라질 수 있습니다.

  • Keycloak/Okta/Auth0 등: Allowed Callback URLs에 정확한 https 콜백 등록
  • 앱 설정의 BASE_URL, PUBLIC_URL, issuer, redirectUri 류 값이 https://인지 확인

예: NextAuth.js는 배포 환경에서 NEXTAUTH_URL이 매우 중요합니다.

export NEXTAUTH_URL="https://example.com"

체크리스트: 재발 방지용 빠른 점검

  • 외부 진입점은 HTTPS만 허용하고 80은 443으로 301 강제
  • Nginx가 X-Forwarded-Proto/Host/Port를 올바르게 전달
  • 다단 프록시라면 X-Forwarded-Proto를 덮어쓰지 않음(map으로 보존)
  • 앱이 프록시 헤더를 신뢰하도록 설정(Spring/Django/Express 등)
  • OAuth 라이브러리의 base URL/redirect URL 관련 환경변수 점검
  • 로그에 xfp=https인데도 앱이 http로 판단하면 “앱 설정 문제”

운영 팁: 쿠버네티스/Ingress 환경에서 특히 자주 터지는 지점

Kubernetes에서는 Ingress Controller(Nginx Ingress, ALB Ingress 등) + 서비스 + 파드로 홉이 늘어나며, 헤더가 중간에서 바뀌는 일이 잦습니다. 특히 AWS ALB Controller를 쓴다면 리다이렉트/헬스체크/웹훅 타임아웃 등 주변 이슈가 함께 얽히기도 합니다. 관련해서는 EKS AWS Load Balancer Controller 500 Webhook 타임아웃 해결도 같이 참고하면 “프록시/컨트롤 플레인 이슈를 로그로 좁히는 방식”에 도움이 됩니다.

또한 서비스 간 TLS나 SNI가 얽히면 “https인데도 내부에서 http로 보이는” 증상과 함께 인증서/프록시 설정이 같이 꼬일 수 있습니다. 이 경우 EKS Pod간 TLS 실패? 인증서·SNI·mTLS 10분 진단을 함께 보면 진단 루트를 빠르게 잡을 수 있습니다.

결론

Nginx 뒤에서 OAuth 콜백이 http로 바뀌는 문제는 대부분 (1) Forwarded 헤더 전달(2) 애플리케이션의 헤더 신뢰 설정 불일치에서 발생합니다.

  • 단일 Nginx에서 TLS 종료: proxy_set_header X-Forwarded-Proto $scheme;만으로 해결되는 경우가 많고
  • 다단 프록시(ALB/Ingress → Nginx → 앱): $scheme로 덮어쓰지 말고 map으로 기존 X-Forwarded-Proto를 보존해야 하며
  • 마지막으로 앱 프레임워크가 해당 헤더를 신뢰하도록 설정해야 redirect_uri가 안정적으로 https로 고정됩니다.

위 설정을 적용한 뒤에는 Nginx 로그(스킴/xfp) → 앱 헤더 덤프(req.protocol/secure) 순서로 확인하면, 원인을 “프록시 vs 앱” 중 어디로든 빠르게 귀속시킬 수 있습니다.