- Published on
Nginx에서 mTLS+OAuth2 프록시 502/401 잡기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 앞단에 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로 단계별 재현하기
- mTLS만 먼저 확인합니다.
curl -vk https://api.example.com/health \
--cert client.crt --key client.key \
--cacert ca.crt
- OAuth2 프록시 단독 엔드포인트가 있다면(내부망에서) 직접 호출해 401의 주체를 확인합니다.
curl -vk http://127.0.0.1:4180/oauth2/auth \
-H 'Host: api.example.com'
- 백엔드 업스트림도 직접 확인합니다.
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-Proto 가 http 로 들어가면,
Secure쿠키가 기대대로 설정되지 않거나- 콜백 URL이 http로 생성되어 IdP 설정과 불일치
로 인해 401 또는 로그인 루프가 발생합니다.
Nginx에서 항상 명시하세요.
proxy_set_header X-Forwarded-Proto $scheme;
로드밸런서가 앞에 있고 Nginx는 HTTP로 받는 구조라면 $scheme 이 http 가 됩니다. 이때는 로드밸런서가 넣어주는 헤더를 신뢰하거나 고정값을 써야 합니다.
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 단계별 재현만 갖춰도, “감으로 설정을 바꾸다 더 꼬이는 상황”을 상당 부분 줄일 수 있습니다.