Published on

Nginx에서 mTLS+OAuth2 프록시 502/401 잡기

Authors

서버 앞단에 Nginx를 두고, 클라이언트는 mTLS로 1차 인증을 통과한 뒤 OAuth2 프록시(예: oauth2-proxy, Keycloak Gatekeeper, 자체 인증 게이트웨이)를 거쳐 백엔드로 들어가게 구성하면 보안은 강해지지만 장애 포인트도 늘어납니다. 특히 현장에서 가장 많이 보는 조합이 502 Bad Gateway(연결/업스트림/TLS 문제)와 401 Unauthorized(인증/헤더/세션/JWKS 문제)입니다.

이 글은 “무엇이 문제인지 감으로 찍는 방식”이 아니라, 요청이 지나가는 경로를 4단계로 나눠 각 단계에서 502/401이 나는 대표 원인과 확인 방법, 그리고 Nginx 설정 패턴을 정리합니다.

  • 1단계: 클라이언트 --cert 로 Nginx mTLS 핸드셰이크
  • 2단계: Nginx auth_request 또는 리버스 프록시로 OAuth2 프록시 호출
  • 3단계: OAuth2 프록시가 IdP(예: Keycloak)에서 토큰 검증/JWKS 조회
  • 4단계: Nginx가 백엔드 업스트림으로 프록시(필요 시 업스트림도 TLS)

관련해서 JWT kid 회전/JWKS 캐시로 401이 나는 케이스는 아래 글도 같이 보면 디버깅 속도가 빨라집니다.

1) 502와 401을 먼저 “어디서 난 응답인지” 분리하기

같은 401이라도 “Nginx가 반환한 401”인지 “OAuth2 프록시가 반환한 401”인지 “백엔드가 반환한 401”인지에 따라 원인이 완전히 다릅니다. 502도 마찬가지로 Nginx가 업스트림과 통신 실패했는지, 업스트림이 502를 만든 뒤 Nginx가 전달한 것인지 구분해야 합니다.

Nginx access log에 업스트림 정보를 남기기

아래처럼 업스트림 상태/응답시간을 함께 로깅하면, 502가 “업스트림 연결 실패”인지 “업스트림이 502를 준 것”인지 바로 갈립니다.

http {
  log_format upstream_timing '$remote_addr - $host "$request" '
                            'status=$status upstream_status=$upstream_status '
                            'rt=$request_time urt=$upstream_response_time '
                            'ua="$http_user_agent" '
                            'xrid="$http_x_request_id"';

  access_log /var/log/nginx/access.log upstream_timing;
  error_log  /var/log/nginx/error.log info;
}
  • upstream_status 가 비어있으면 Nginx가 업스트림에 붙기도 전에 실패한 경우가 많습니다(DNS, connect timeout, TLS handshake 실패 등).
  • upstream_status=401 인데 status=401 이면 OAuth2 프록시 또는 백엔드가 401을 준 것입니다.

curl로 단계별 재현하기

  1. mTLS만 먼저 확인합니다.
curl -vk https://api.example.com/health \
  --cert client.crt --key client.key \
  --cacert ca.crt
  1. OAuth2 프록시 단독 엔드포인트가 있다면(내부망에서) 직접 호출해 401의 주체를 확인합니다.
curl -vk http://127.0.0.1:4180/oauth2/auth \
  -H 'Host: api.example.com'
  1. 백엔드 업스트림도 직접 확인합니다.
curl -vk http://127.0.0.1:8080/health

이 3개가 각각 성공/실패하는 지점이 곧 문제 범위입니다.

2) mTLS에서 502/401이 나는 대표 원인

mTLS는 보통 Nginx가 “서버 인증서”를 내고, 클라이언트가 “클라이언트 인증서”를 제시하는 구조입니다. 이때 실패는 대체로 400/495/496 계열로 보이지만, 앞단에 L4/L7 로드밸런서가 있으면 502로 뭉개져 보이기도 합니다.

체크리스트

  • ssl_client_certificate 에 지정한 CA 체인이 “클라이언트 인증서 발급 CA”와 일치하는가
  • 클라이언트 인증서에 Key Usage, Extended Key Usage 가 클라이언트 인증에 적합한가
  • 인증서 체인이 중간 CA까지 포함되어 있는가
  • SNI/Host가 달라서 다른 server 블록으로 들어가 ssl_verify_client 가 기대와 다르게 동작하지 않는가

Nginx mTLS 기본 패턴

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

  ssl_certificate     /etc/nginx/tls/server.crt;
  ssl_certificate_key /etc/nginx/tls/server.key;

  # 클라이언트 인증서 검증
  ssl_client_certificate /etc/nginx/tls/client-ca-chain.crt;
  ssl_verify_client on;
  ssl_verify_depth 2;

  # 디버깅에 유용
  add_header X-SSL-Client-Verify $ssl_client_verify always;
  add_header X-SSL-Client-DN $ssl_client_s_dn always;

  location /health {
    return 200 "ok\n";
  }
}
  • client-ca-chain.crt 는 “클라이언트 인증서 발급자”의 루트/중간 CA 체인을 포함해야 합니다.
  • ssl_verify_depth 가 너무 낮으면 중간 CA가 있는 체인에서 실패합니다.

3) OAuth2 프록시 연동에서 401이 나는 대표 원인

mTLS를 통과했는데도 401이 나오면, 대부분은 OAuth2 프록시가 인증 정보를 못 찾거나(쿠키/헤더 누락), 토큰 검증에 실패했거나(JWKS, aud/iss, kid 회전), Nginx가 인증 결과 헤더를 백엔드로 전달하지 못한 경우입니다.

여기서 가장 흔한 실수는 “Nginx auth_request 로 인증은 했는데, 정작 백엔드로 넘겨야 할 헤더를 빼먹는 것”입니다.

Nginx auth_request + oauth2-proxy 패턴

auth_request 는 서브리퀘스트로 인증 엔드포인트를 호출해 2xx면 통과, 401이면 리다이렉트/차단하는 구조입니다.

# oauth2-proxy가 4180에서 동작한다고 가정
upstream oauth2_proxy {
  server 127.0.0.1:4180;
  keepalive 32;
}

upstream backend_api {
  server 127.0.0.1:8080;
  keepalive 64;
}

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

  ssl_certificate     /etc/nginx/tls/server.crt;
  ssl_certificate_key /etc/nginx/tls/server.key;

  ssl_client_certificate /etc/nginx/tls/client-ca-chain.crt;
  ssl_verify_client on;

  # OAuth2 프록시 자체 엔드포인트
  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-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  # 인증 서브리퀘스트
  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-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # 쿠키 기반 세션이라면 원본 쿠키가 auth_request로도 전달되어야 함
    proxy_set_header Cookie $http_cookie;

    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
  }

  # 실제 API
  location / {
    auth_request /oauth2/auth;

    # 401이면 oauth2-proxy의 sign-in으로 보냄
    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;
    auth_request_set $token $upstream_http_authorization;

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

    # Bearer 토큰을 백엔드로 넘기는 구성이면 Authorization 전달을 명시
    proxy_set_header Authorization $token;

    proxy_pass http://backend_api;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
  }
}

여기서 401이 계속 날 때 우선 확인할 것

  • oauth2-proxy가 세션을 쿠키로 유지한다면 Cookie 헤더가 /oauth2/auth 서브리퀘스트로 전달되는지
  • X-Forwarded-Proto 가 올바른지(외부는 HTTPS인데 내부에서 HTTP로 인식하면 리다이렉트 루프/쿠키 secure 문제)
  • error_page 401/oauth2/sign_in 으로 제대로 연결되는지

4) 502의 핵심: “업스트림 TLS/네트워크” vs “프록시 버퍼/타임아웃”

mTLS+OAuth2 프록시 구성이면 업스트림이 최소 2개입니다.

  • Nginx -> oauth2-proxy
  • Nginx -> backend
  • oauth2-proxy -> IdP(JWKS/토큰 인트로스펙션)

이 중 어디서든 연결이 끊기면 Nginx는 502를 낼 수 있습니다.

4-1) 업스트림이 HTTPS일 때 502: SNI/검증 설정

백엔드가 HTTPS라면 Nginx의 업스트림 TLS 설정이 필요합니다.

location / {
  proxy_pass https://backend_api;

  proxy_ssl_server_name on;
  proxy_ssl_name api-internal.example.local;

  proxy_ssl_trusted_certificate /etc/nginx/tls/backend-ca.crt;
  proxy_ssl_verify on;
  proxy_ssl_verify_depth 2;
}
  • proxy_ssl_server_name on 이 없으면 SNI가 빠져 인증서가 안 맞아 502가 날 수 있습니다.
  • 내부 사설 CA를 쓰면 proxy_ssl_trusted_certificate 를 지정하지 않으면 검증 실패로 502가 납니다.

4-2) 타임아웃/버퍼로 인한 502/504

OAuth2 프록시는 IdP 응답 지연, JWKS 조회 지연, 네트워크 이슈가 있으면 인증 서브리퀘스트가 느려집니다. 이때 Nginx 기본 타임아웃이 짧으면 502/504로 보입니다.

location = /oauth2/auth {
  internal;
  proxy_pass http://oauth2_proxy/oauth2/auth;

  proxy_connect_timeout 3s;
  proxy_read_timeout 10s;
  proxy_send_timeout 10s;
}

location / {
  proxy_connect_timeout 3s;
  proxy_read_timeout 60s;
  proxy_send_timeout 60s;
}

스트리밍(SSE)나 장시간 응답에서 프록시 버퍼/타임아웃 이슈가 섞이면 증상이 더 복잡해집니다. 프록시 뒤에서 스트리밍이 끊기는 케이스는 아래 글의 체크리스트가 그대로 도움이 됩니다.

5) “mTLS 통과 후 OAuth2 401”에서 자주 놓치는 헤더/쿠키 포인트

5-1) X-Forwarded-Proto 와 Secure 쿠키

oauth2-proxy는 외부 스킴을 기준으로 리다이렉트 URL과 쿠키 속성을 결정합니다. 외부는 HTTPS인데 X-Forwarded-Protohttp 로 들어가면,

  • Secure 쿠키가 기대대로 설정되지 않거나
  • 콜백 URL이 http로 생성되어 IdP 설정과 불일치

로 인해 401 또는 로그인 루프가 발생합니다.

Nginx에서 항상 명시하세요.

proxy_set_header X-Forwarded-Proto $scheme;

로드밸런서가 앞에 있고 Nginx는 HTTP로 받는 구조라면 $schemehttp 가 됩니다. 이때는 로드밸런서가 넣어주는 헤더를 신뢰하거나 고정값을 써야 합니다.

proxy_set_header X-Forwarded-Proto https;

5-2) auth_request 에서 쿠키 미전달

auth_request 는 별도 서브리퀘스트이므로, 쿠키를 명시적으로 전달하지 않으면 oauth2-proxy가 세션을 못 찾아 401을 반환합니다.

location = /oauth2/auth {
  internal;
  proxy_set_header Cookie $http_cookie;
}

5-3) Authorization 헤더 전달/차단

구성에 따라 oauth2-proxy가 Authorization 을 만들어 백엔드로 넘기길 기대할 수 있습니다. 그런데 Nginx가 기본 동작/설정으로 헤더를 덮어쓰거나 비워버리면 백엔드는 항상 401을 냅니다.

  • 백엔드가 Bearer 토큰을 요구하면 proxy_set_header Authorization ... 를 명시
  • 백엔드가 토큰을 직접 검증하지 않고 X-User 같은 헤더만 신뢰한다면, 그 헤더가 실제로 세팅되는지 확인

6) JWKS/토큰 검증 계열 401: “간헐적”이면 캐시/회전 의심

증상이 “어떤 토큰은 되고 어떤 토큰은 401” 또는 “배포/키 회전 직후만 401”이면, 대개 IdP의 키 회전과 캐시가 엮인 문제입니다.

  • oauth2-proxy(또는 API 게이트웨이)가 JWKS를 캐시
  • IdP에서 새 kid 로 서명 시작
  • 캐시가 갱신되기 전까지는 새 토큰이 401

이 케이스는 Nginx만 봐서는 답이 안 나오는 경우가 많고, IdP/프록시 로그에서 kid 불일치가 찍힙니다. 대응 전략은 아래 글에 자세히 정리되어 있습니다.

7) 운영에서 바로 쓰는 “원인별 빠른 처방” 표

401 빠른 분류

  • Nginx access log에 upstream_status=401 이면 oauth2-proxy 또는 백엔드가 401
  • upstream_status 가 없고 status=401 이면 Nginx 자체 deny(설정/satisfy/allow/deny) 또는 auth_request 연결 실패 후 처리 로직 가능

502 빠른 분류

  • upstream_status 비어있고 error log에 connect() failed 이면 포트/방화벽/프로세스 다운
  • error log에 SSL_do_handshake() failed 이면 업스트림 TLS(SNI, CA, 프로토콜)
  • error log에 upstream timed out 이면 타임아웃/지연

8) 디버깅을 쉽게 만드는 Nginx 에러 로그 레벨과 포인트

장애 재현 중에는 일시적으로 에러 로그 레벨을 올려 원인을 빨리 잡을 수 있습니다.

error_log /var/log/nginx/error.log debug;

다만 debug 는 로그가 매우 많아 운영 상시 적용은 비추입니다. 재현 후 info 또는 warn 으로 되돌리세요.

또한 oauth2-proxy/백엔드에도 X-Request-ID 를 전달해 요청 단위로 추적 가능하게 만드는 것이 좋습니다.

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

proxy_set_header X-Request-ID $xrid;
add_header X-Request-ID $xrid always;

9) 마무리: “인증은 통과했는데 401”과 “연결이 안 돼서 502”를 분리하라

mTLS+OAuth2 프록시 조합에서 장애 대응의 핵심은, 문제를 인증 계층과 연결 계층으로 분리하고, 다시 업스트림 단위로 쪼개는 것입니다.

  • mTLS는 인증서 체인/검증 깊이/서버 블록 매칭이 핵심
  • OAuth2 프록시는 쿠키/헤더 전달과 X-Forwarded-Proto 가 핵심
  • 502는 업스트림 TLS(SNI/CA) 또는 타임아웃이 핵심

위의 로깅 포맷과 curl 단계별 재현만 갖춰도, “감으로 설정을 바꾸다 더 꼬이는 상황”을 상당 부분 줄일 수 있습니다.