Published on

Nginx OAuth2 프록시 502와 JWT 검증 실패 해결

Authors

서버 앞단에 Nginx를 두고, 그 뒤에 OAuth2 Proxy를 붙여서 인증을 위임하는 구성은 운영에서 매우 흔합니다. 그런데 이 조합은 장애가 났을 때 증상이 두 갈래로 나타납니다.

  • Nginx가 502 Bad Gateway 를 뱉는다: 업스트림(oauth2-proxy 또는 앱) 연결/응답 문제
  • 인증은 통과한 것 같은데 앱에서 JWT 검증 실패401/403 이 난다: 헤더 전달/토큰 형식/JWK 로테이션/클레임 불일치 문제

이 글은 “502”와 “JWT 검증 실패”를 한 흐름으로 묶어, 어디에서 끊겼는지 빠르게 분기하고 고치는 방법을 다룹니다.

전체 아키텍처와 실패 지점

대표적인 흐름은 다음과 같습니다.

  1. 클라이언트 요청이 Nginx에 도착
  2. Nginx가 auth_request 로 OAuth2 Proxy의 인증 엔드포인트를 호출
  3. OAuth2 Proxy가 세션 쿠키 또는 OIDC 토큰을 확인
  4. 인증 성공 시 Nginx가 원래 업스트림(앱)으로 프록시
  5. 앱은 (선택적으로) Authorization: Bearer ... 또는 X-Auth-Request-* 헤더 기반으로 JWT를 검증

여기서 장애는 크게 3군데에서 납니다.

  • Nginx auth_request 단계에서 OAuth2 Proxy로 붙지 못함(502)
  • OAuth2 Proxy가 IdP와 통신/리다이렉트 과정에서 꼬임(로그에 upstream/redirect 관련 에러)
  • 앱의 JWT 검증 단계에서 실패(401/403)

502 Bad Gateway: 원인별로 빠르게 분기하기

502는 Nginx가 “뒤쪽에서 정상 응답을 받지 못했다”는 뜻입니다. 먼저 Nginx 에러 로그를 봐야 합니다.

1) Nginx 에러 로그에서 패턴 읽기

다음 명령으로 에러 로그를 실시간 확인합니다.

sudo tail -f /var/log/nginx/error.log

자주 보이는 패턴은 다음과 같습니다.

  • connect() failed (111: Connection refused) while connecting to upstream
    • OAuth2 Proxy 프로세스가 죽었거나 포트가 다름, 또는 로컬 바인딩 문제
  • upstream timed out (110: Connection timed out) while reading response header from upstream
    • OAuth2 Proxy가 응답을 못하고 걸림(네트워크, DNS, IdP 지연, 리소스 부족)
  • no live upstreams
    • upstream 블록의 서버가 모두 비정상으로 판단됨(헬스체크/리졸브 문제)

2) OAuth2 Proxy가 실제로 listen 중인지 확인

sudo ss -lntp | grep oauth2
  • 기대 포트(예: 4180)가 떠 있는지
  • 127.0.0.1 에만 바인딩했는데 Nginx가 다른 네트워크로 붙으려는지

컨테이너라면 다음도 확인합니다.

docker ps
# 또는
kubectl get pod -n your-ns -o wide

3) Nginx 업스트림 주소 실수: localhost 착각

Nginx가 컨테이너 밖에서 돌고 OAuth2 Proxy가 컨테이너 안에서 돌면, 127.0.0.1:4180 은 서로 다른 네임스페이스일 수 있습니다. 이 경우 Nginx는 OAuth2 Proxy로 붙지 못해 connection refused 로 502가 납니다.

  • 같은 호스트 네트워크인지
  • Docker bridge라면 컨테이너 IP/서비스 DNS로 붙는지
  • Kubernetes라면 Service 로 붙는지

4) auth_request 경로와 location 충돌

auth_request 는 내부 서브리퀘스트로 동작합니다. 이때 OAuth2 Proxy의 엔드포인트를 internal 로 분리하지 않으면 리다이렉트/정적 처리와 섞여 이상한 동작을 할 수 있습니다.

권장 패턴:

# OAuth2 Proxy 업스트림
upstream oauth2_proxy {
  server 127.0.0.1:4180;
  keepalive 32;
}

server {
  listen 443 ssl;
  server_name example.com;

  # 1) auth_request가 호출할 내부 엔드포인트
  location = /oauth2/auth {
    internal;
    proxy_pass http://oauth2_proxy/oauth2/auth;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Uri $request_uri;

    proxy_set_header Connection "";
    proxy_http_version 1.1;

    proxy_connect_timeout 3s;
    proxy_read_timeout 5s;
  }

  # 2) 로그인/콜백 등 OAuth2 Proxy 공개 엔드포인트
  location /oauth2/ {
    proxy_pass http://oauth2_proxy;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  # 3) 보호할 애플리케이션
  location / {
    auth_request /oauth2/auth;
    error_page 401 = /oauth2/sign_in;

    # 필요 시 auth_request 결과 헤더를 앱으로 전달
    auth_request_set $user $upstream_http_x_auth_request_user;
    auth_request_set $email $upstream_http_x_auth_request_email;

    proxy_set_header X-Auth-Request-User $user;
    proxy_set_header X-Auth-Request-Email $email;

    proxy_pass http://app_upstream;
  }
}

여기서 502가 난다면 location = /oauth2/auth 가 실제 OAuth2 Proxy의 엔드포인트로 잘 프록시되는지부터 curl 로 확인합니다.

curl -i https://example.com/oauth2/auth
  • 202 또는 401이 정상적인 흐름일 수 있습니다(인증 여부에 따라 다름)
  • 아예 연결이 안 되면 Nginx와 OAuth2 Proxy 연결 문제입니다

5) 리다이렉트 루프가 502로 보이는 경우

OAuth2 Proxy는 인증이 필요하면 로그인으로 리다이렉트합니다. 그런데 X-Forwarded-Proto 가 누락되면, OAuth2 Proxy가 자신을 http 로 인식해 콜백 URL을 잘못 만들고, 반복 리다이렉트/세션 불일치로 이어질 수 있습니다.

  • Nginx에서 반드시 proxy_set_header X-Forwarded-Proto $scheme; 설정
  • TLS terminate 위치(Nginx/로드밸런서)와 redirect_url(또는 --redirect-url) 일치

IdP 쪽 에러가 invalid_grant 로 보인다면, PKCE/코드 교환 단계에서 꼬였을 가능성이 큽니다. 아래 글도 함께 참고하면 원인 분기에 도움이 됩니다.

JWT 검증 실패(401/403): “토큰이 없거나, 다르거나, 오래됐다”

Nginx+OAuth2 Proxy 구성에서 JWT 검증 실패는 보통 다음 중 하나입니다.

  1. 앱이 기대하는 위치에 토큰이 없다(헤더 미전달)
  2. 토큰이 있는데 서명키(JWK) 검증이 실패한다(키 로테이션/캐시)
  3. 토큰 클레임이 앱 정책과 다르다(issuer, audience, clock skew)

1) 가장 흔한 원인: Authorization 헤더가 앱까지 전달되지 않음

OAuth2 Proxy는 기본적으로 “세션 쿠키 기반 인증”을 많이 씁니다. 이때 앱이 Authorization: Bearer ... 를 기대하면, 앱 입장에서는 토큰이 없어서 401이 납니다.

해결 방향은 두 가지입니다.

  • 앱이 X-Auth-Request-User, X-Auth-Request-Email 같은 헤더를 신뢰하도록 변경
  • OAuth2 Proxy가 Authorization 헤더를 만들어 전달하도록 설정

OAuth2 Proxy에서는 배포/버전에 따라 옵션이 다를 수 있지만, 흔히 다음 계열 옵션을 검토합니다.

  • --set-authorization-header=true
  • --pass-authorization-header=true
  • --pass-access-token=true
  • --set-xauthrequest=true

그리고 Nginx에서는 업스트림으로 Authorization 을 전달하도록 명시하는 게 안전합니다.

location / {
  auth_request /oauth2/auth;

  # oauth2-proxy가 내려준 Authorization을 앱으로 전달(있는 경우)
  auth_request_set $authz $upstream_http_authorization;
  proxy_set_header Authorization $authz;

  proxy_pass http://app_upstream;
}

주의할 점:

  • auth_request_set 으로 받을 수 있는 헤더는 auth_request 서브리퀘스트의 응답 헤더입니다.
  • OAuth2 Proxy가 실제로 Authorization 헤더를 내려주는지부터 확인해야 합니다.

확인은 Nginx access log 포맷을 확장하거나, 임시로 OAuth2 Proxy 앞단에서 캡처합니다.

2) JWT 서명 검증 실패: JWK 로테이션과 캐시

IdP는 보안상 주기적으로 서명키를 로테이션합니다. 이때 앱이 JWK를 캐싱해두고 갱신을 못하면 갑자기 JWT 검증이 실패합니다.

증상:

  • 어제까지 되던 토큰이 오늘부터 전부 401
  • 로그에 kid not found, unable to find a signing key, signature verification failed 같은 메시지

대응:

  • JWK 캐시 TTL을 짧게 하거나, kid 미스 시 강제 재조회
  • 다중 인스턴스라면 캐시 일관성(로컬 메모리 캐시 vs 공유 캐시) 점검

이 주제는 Spring Security 기준으로 잘 정리된 글이 있어 함께 보면 좋습니다.

3) issuer, audience, clock skew 불일치

JWT 검증은 서명만 맞으면 끝이 아니라, 보통 다음을 함께 검증합니다.

  • iss 가 기대값과 같은지
  • aud 가 내 서비스 클라이언트 ID와 같은지
  • exp, nbf, iat 에 대해 시간 오차(clock skew)를 허용하는지

특히 프록시 체인에서 X-Forwarded-ProtoHost 가 흔들리면, OAuth2 Proxy가 받는 issuer/redirect URL 구성과 앱이 기대하는 리소스 서버 설정이 어긋날 수 있습니다.

점검 체크리스트:

  • 앱의 리소스 서버 설정(issuer URI, jwk set URI)이 IdP 메타데이터와 일치하는지
  • 토큰의 aud 가 API용인지(프론트용 ID 토큰을 API에서 검증하려 하면 실패할 수 있음)
  • 서버 시간 동기화: chrony 또는 systemd-timesyncd 상태 확인

502와 JWT 실패를 같이 잡는 운영용 디버깅 루틴

1) 요청 단위로 상관관계 ID를 찍기

Nginx에 요청 ID를 넣고, OAuth2 Proxy와 앱 로그에 전달하면 “어디서 실패했는지”가 빨라집니다.

map $http_x_request_id $req_id {
  default $http_x_request_id;
  "" $request_id;
}

log_format main_ext '$remote_addr - $req_id [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    'rt=$request_time uct=$upstream_connect_time '
                    'urt=$upstream_response_time ustatus=$upstream_status '
                    'ua="$http_user_agent"';

access_log /var/log/nginx/access.log main_ext;

location / {
  proxy_set_header X-Request-Id $req_id;
  auth_request /oauth2/auth;
  proxy_pass http://app_upstream;
}
  • ustatus502 인지, 아니면 앱에서 401 을 내려준 것인지 구분됩니다.

2) OAuth2 Proxy 로그 레벨을 올려서 “인증 실패”와 “통신 실패” 분리

  • 통신 실패: DNS, TLS, timeout, connection reset
  • 인증 실패: 쿠키 복호화 실패, CSRF mismatch, nonce mismatch, invalid grant

통신 실패가 502를 유발하고, 인증 실패는 보통 302/401로 나타납니다. 둘이 섞여 보이면 우선 로그로 분리하세요.

3) 타임아웃을 계층별로 정렬

Nginx, OAuth2 Proxy, IdP(또는 외부 네트워크) 타임아웃이 제각각이면 간헐적 502가 생깁니다.

  • Nginx proxy_connect_timeout, proxy_read_timeout
  • OAuth2 Proxy upstream timeout(옵션/환경별)
  • 클라우드 LB idle timeout

원칙:

  • 가장 앞단(Nginx)의 read timeout이 너무 짧으면, OAuth2 Proxy가 IdP 응답을 기다리는 동안 502가 납니다.
  • 반대로 너무 길면 장애 감지가 늦어집니다.

실전 예시: “인증은 되는데 앱에서 JWT가 없다고 함”

증상:

  • 브라우저는 로그인 완료
  • Nginx는 200을 반환
  • 앱 로그에 Missing Authorization header 또는 Bearer token required

해결 시나리오:

  1. OAuth2 Proxy가 세션 쿠키 기반이라 앱으로 토큰을 안 넘기는 구조임을 확인
  2. 앱이 JWT 검증을 반드시 해야 한다면 OAuth2 Proxy에서 access token 전달을 켬
  3. Nginx에서 Authorization 헤더를 명시적으로 프록시

검증:

  • 앱에서 요청 헤더 덤프(개발 환경) 또는 미들웨어로 Authorization 존재 여부 확인
  • 토큰이 있다면 iss, aud, kid 를 디코드해서 설정과 비교

실전 예시: “갑자기 전부 JWT 서명 검증 실패”

증상:

  • 특정 시점 이후 전 요청 401
  • 로그에 kid not found 또는 unable to find signing key

대응:

  • JWK 캐시 갱신 로직 점검(미스 시 재조회)
  • JWK endpoint 접근이 프록시/방화벽으로 막히지 않았는지 확인
  • 멀티 리전/멀티 클러스터에서 issuer가 다른 IdP를 바라보지 않는지 확인

JWK 로테이션 대응은 프레임워크별로 구현 디테일이 달라서, Spring Security를 쓴다면 아래 글의 캐시 전략이 특히 도움이 됩니다.

운영 체크리스트(요약)

502 체크

  • OAuth2 Proxy 프로세스/포트 listen 확인: ss -lntp
  • Nginx 업스트림 주소가 올바른 네트워크를 가리키는지(컨테이너/쿠버네티스)
  • location = /oauth2/authinternal 로 분리했는지
  • X-Forwarded-Proto, Host 전달이 맞는지
  • timeout 계층 정렬 및 로그에서 upstream_response_time 확인

JWT 검증 실패 체크

  • 앱이 기대하는 토큰 위치 확인(Authorization vs X-Auth-Request-*)
  • OAuth2 Proxy 옵션으로 토큰/헤더 전달 설정
  • Nginx가 Authorization 을 덮어쓰거나 누락시키지 않는지
  • JWK 로테이션/캐시 전략(kid 미스 시 재조회)
  • iss, aud, clock skew 정책 일치

마무리

Nginx+OAuth2 Proxy에서 문제를 빨리 푸는 핵심은 “502는 연결/응답 계층”, “JWT 실패는 헤더/키/클레임 계층”으로 나누고, auth_request 서브리퀘스트와 실제 업스트림 요청을 로그로 분리하는 것입니다.

특히 운영에서는 JWK 로테이션과 헤더 전달 누락이 가장 흔한 함정이므로, 토큰 전달 방식(세션 쿠키 기반인지, Bearer 토큰 기반인지)을 먼저 확정하고 그에 맞춰 Nginx와 앱의 계약을 고정하는 것이 재발 방지에 가장 효과적입니다.