- Published on
Nginx에서 JWT 검증 401 뜰 때 원인 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Nginx 앞단에서 JWT를 검증(또는 인증 서버로 auth_request 위임)하도록 구성하면, 애플리케이션까지 요청이 도달하기 전에 401 Unauthorized가 떨어지는 경우가 많습니다. 문제는 원인이 JWT 자체에만 있지 않고, Nginx의 헤더 전달, 프록시 버퍼링, CORS, 시간 동기화, 키 로테이션 같은 인프라 레이어에 숨어 있는 경우가 많다는 점입니다.
이 글은 Nginx에서 JWT 검증 시 401이 뜨는 대표 원인 7가지를 “어디를 보면 바로 좁혀지는지” 중심으로 정리합니다. (Nginx가 직접 JWT 서명을 검증하는 경우는 보통 OpenResty Lua, auth_jwt 모듈, 또는 외부 인증 서버로 위임하는 auth_request 패턴으로 구현됩니다.)
애플리케이션 레벨(예: Spring Security)에서 401이 반복된다면, Nginx보다 필터체인/시큐리티 설정 이슈일 수 있습니다. 관련 체크리스트는 Spring Boot 3 JWT 인증 401 반복? 필터체인 점검도 함께 참고하세요.
0) 먼저 확인할 3가지: 401을 누가 내는가
원인 7가지를 보기 전에, 401 응답을 만든 주체를 먼저 분리해야 합니다.
- Nginx가 401을 직접 반환:
auth_request가 실패했거나,error_page 401처리, 또는 JWT 검증 모듈/Lua에서ngx.exit(401)같은 처리를 한 경우입니다. - 업스트림(앱/인증 서버)이 401 반환: Nginx는 단순 프록시이고 실제 인증 실패는 뒤에서 발생합니다.
- 프리플라이트(OPTIONS)가 401: 브라우저에서만 재현되고 Postman에서는 정상인 전형적인 케이스입니다.
다음과 같이 액세스 로그에 업스트림 상태를 남기면 분리가 빨라집니다.
log_format main '$remote_addr - $request '
'status=$status upstream_status=$upstream_status '
'uri=$uri ua="$http_user_agent" '
'auth="$http_authorization"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log info;
upstream_status가 비어 있으면 Nginx가 자체적으로 막았을 확률이 높습니다.
1) Authorization 헤더가 업스트림으로 전달되지 않음
가장 흔한 유형입니다. Nginx가 기본적으로 헤더를 잘 전달하긴 하지만, 다음과 같은 상황에서 Authorization이 사라집니다.
proxy_set_header Authorization "";같은 설정이 다른location이나include에서 덮어씀auth_request서브리퀘스트에서 원래 요청의Authorization을 인증 서버로 전달하지 않음- CDN/WAF/Ingress가
Authorization을 제거
증상
- 앱 로그에서 토큰이 항상
null또는 빈 문자열 - Nginx 액세스 로그에
auth=""로 찍힘
해결
업스트림에 명시적으로 전달합니다.
location /api/ {
proxy_set_header Authorization $http_authorization;
proxy_set_header Host $host;
proxy_pass http://app_upstream;
}
auth_request를 쓴다면 인증 엔드포인트로도 전달해야 합니다.
location /api/ {
auth_request /_auth;
proxy_pass http://app_upstream;
}
location = /_auth {
internal;
proxy_set_header Authorization $http_authorization;
proxy_pass http://auth_upstream/verify;
}
2) auth_request 구성에서 401이 의도치 않게 전파됨
auth_request는 “서브리퀘스트가 2xx면 통과, 아니면 차단”이라는 단순 규칙입니다. 인증 서버가 401을 내면 그대로 차단되는데, 여기서 흔한 실수는 다음입니다.
- 인증 서버가
401대신403또는302를 반환(리다이렉트 기반 로그인)해서 예상치 못한 동작 - 인증 서버가 에러 시
500을 내는데, Nginx가 이를401처럼 처리하도록error_page가 걸려 있음 - 인증 서버가 바디를 읽어야 하는데 Nginx가 서브리퀘스트 바디를 보내지 않음
해결 포인트
- 인증 서버는 검증 API라면 리다이렉트 없이
200또는401로 명확히 응답하도록 설계 - Nginx에서 상태별 처리 분기
location /api/ {
auth_request /_auth;
error_page 401 = @error401;
error_page 403 = @error403;
proxy_pass http://app_upstream;
}
location @error401 {
return 401;
}
location @error403 {
return 403;
}
인증 서버가 토큰을 헤더가 아니라 쿠키에서 읽는다면, 쿠키 전달도 확인해야 합니다.
location = /_auth {
internal;
proxy_set_header Cookie $http_cookie;
proxy_pass http://auth_upstream/verify;
}
3) 프리플라이트(OPTIONS) 요청이 JWT 검증에 걸려 401
브라우저는 CORS 요청에서 먼저 OPTIONS 프리플라이트를 보냅니다. 이 요청에는 보통 Authorization 헤더가 없거나, 실제 토큰 검증 대상이 아닙니다. 그런데 Nginx에서 /api/에 무조건 JWT 검증을 걸어버리면 OPTIONS가 401로 차단되어, 실제 요청이 나가지 못합니다.
증상
- 브라우저에서만 실패, 서버-서버 호출은 정상
- 개발자도구 네트워크 탭에
OPTIONS가 401
해결
OPTIONS는 인증 없이 통과시키거나, 최소한 CORS 헤더를 붙여 204로 종료합니다.
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
add_header Access-Control-Allow-Credentials "true";
return 204;
}
auth_request /_auth;
proxy_pass http://app_upstream;
}
if사용이 불편하다면 별도location으로 분리하는 방식도 좋습니다.
4) Bearer 스킴/형식 불일치 (공백, 대소문자, 중복 헤더)
JWT 자체는 맞는데, 헤더 포맷 때문에 검증 로직이 실패하는 경우가 있습니다.
Authorization: Bearer{token}처럼 공백 누락Authorization: bearer token처럼 스킴 파싱이 대소문자 민감한 구현- 프록시 체인에서
Authorization이 중복으로 붙어Bearer a, Bearer b형태가 됨 - 토큰 앞뒤에 개행/공백이 섞임
해결
Nginx에서 형식을 정규화하거나, 최소한 디버깅을 위해 토큰을 별도 헤더로 복사해 관찰합니다.
location /api/ {
# 원본 Authorization을 업스트림으로 전달
proxy_set_header Authorization $http_authorization;
# 디버그용: 토큰 원문을 다른 헤더로도 전달(운영에서는 제거 권장)
proxy_set_header X-Debug-Auth $http_authorization;
proxy_pass http://app_upstream;
}
OpenResty Lua로 직접 검증한다면, Bearer 파싱 시 공백/대소문자/콤마 케이스를 robust 하게 처리해야 합니다.
5) 키 로테이션(JWKS) 캐시/동기화 문제로 서명 검증 실패
Nginx에서 JWT를 직접 검증하거나(모듈/Lua), 인증 서버가 JWKS를 캐싱하는 구조라면 키 로테이션 시점에 401이 급증할 수 있습니다.
- 토큰의
kid에 해당하는 공개키가 캐시에 없음 - JWKS 갱신 주기가 길어 새 키를 못 가져옴
- 네트워크 문제로 JWKS fetch 실패 후 오래된 캐시를 계속 사용
증상
- 특정 시간대(키 교체 직후)부터 401 증가
- 같은 사용자의 토큰도 어떤 노드에서는 되고 어떤 노드에서는 실패(캐시 불일치)
해결
- JWKS 캐시 TTL을 합리적으로 설정하고, 실패 시 백오프/재시도
- 다중 인스턴스라면 캐시를 공유(예: Redis)하거나 갱신 타이밍을 맞춤
kid기반 키 조회 실패 시 즉시 JWKS를 한 번 더 갱신 시도
인증 서버를 별도로 둔다면, Nginx는 auth_request로 위임하고 키 관리는 인증 서버에서만 하도록 단순화하는 것도 안정적입니다.
6) 시간 불일치로 exp/nbf 검증 실패 (NTP, 컨테이너 시간)
JWT 클레임 exp(만료), nbf(not before), iat(issued at)은 시간에 민감합니다. Nginx 노드(또는 검증 주체)의 시간이 틀어지면 정상 토큰도 401이 됩니다.
증상
- 토큰 발급 직후인데도
nbf오류 - 만료까지 남았는데
exp만료로 처리 - 특정 노드에서만 401 (그 노드의 시간이 틀어짐)
해결
- 모든 노드에 NTP 동기화 적용
- 컨테이너/VM에서 시간 소스 점검
- 검증 로직에 clock skew 허용(예: 30초~2분) 옵션이 있으면 활성화
운영에서 401이 간헐적으로 튄다면, 애플리케이션 로그뿐 아니라 노드 시간과 배포 이벤트도 함께 보세요. 장애 분석 관점에서는 OpenTelemetry로 MSA 분산 트랜잭션 추적 실전처럼 추적/상관관계를 잡아두면 재현이 어려운 401도 원인까지 도달하기가 쉬워집니다.
7) 요청 바디/버퍼링/크기 제한으로 인증 서버가 오동작
특히 auth_request로 인증 서버에 위임할 때, 인증 서버가 다음 중 하나를 기대하면 문제가 생깁니다.
- 토큰이 헤더가 아니라 바디에 있음(예:
{"token":"..."}) - 인증 서버가 원 요청의 일부 헤더/바디를 기반으로 서명 검증 또는 정책 판단
- Nginx가 큰 헤더를 잘라먹음(긴 JWT, 큰 쿠키)
또한 Nginx의 크기 제한에 걸리면 401이 아니라 400이나 413이 더 흔하지만, 인증 서버가 이를 401로 래핑해서 응답하는 경우도 있습니다.
점검할 설정
large_client_header_buffersclient_header_buffer_sizeclient_max_body_size
예시:
http {
large_client_header_buffers 4 16k;
client_header_buffer_size 8k;
client_max_body_size 10m;
}
인증 서버가 바디를 필요로 한다면 auth_request는 구조적으로 제약이 큽니다(서브리퀘스트에 원 요청 바디를 자동 전달하지 않음). 이 경우는 설계를 바꾸는 게 낫습니다.
- 토큰은 가능한 한
Authorization헤더로 통일 - 바디 기반 인증이 필요하면 Nginx가 아닌 앱 레벨에서 처리
재현과 진단을 빠르게 하는 체크리스트
아래 순서대로 보면 “JWT 문제”인지 “Nginx 라우팅/헤더 문제”인지 빠르게 갈립니다.
- Nginx 액세스 로그에
auth="$http_authorization"와upstream_status를 남긴다. - 같은 요청을
curl로 재현하되, 헤더를 명시한다.
curl -i \
-H "Authorization: Bearer YOUR_JWT" \
https://api.example.com/api/me
- 브라우저에서만 실패하면
OPTIONS401 여부부터 본다.
curl -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization" \
https://api.example.com/api/me
- 인증 위임(
auth_request)이면, 인증 엔드포인트를 단독 호출해 상태코드와 로그를 본다. - 특정 시간대/특정 노드만 문제면
kid로테이션과 노드 시간(NTP)을 의심한다.
마무리
Nginx에서 JWT 검증 401은 “토큰이 틀렸다”보다 “토큰이 전달되지 않았다”, “프리플라이트가 막혔다”, “키/시간이 어긋났다” 같은 인프라 요인이 더 자주 원인입니다. 특히 auth_request 패턴은 단순하지만, 헤더 전달과 CORS, 인증 서버의 응답 규약(리다이렉트 금지, 명확한 상태코드)이 맞지 않으면 401이 쉽게 발생합니다.
401이 반복된다면, 먼저 누가 401을 만들었는지를 로그로 분리하고(원인 절반은 여기서 끝납니다), 그 다음에 헤더 전달, OPTIONS 처리, 키 로테이션, 시간 동기화를 순서대로 점검해 보세요.