- Published on
Nginx JWT 검증 401? auth_jwt 핵심 정리
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 앞단에서 JWT를 검증해 애플리케이션 부담을 줄이려다, 갑자기 Nginx가 401을 뱉기 시작하면 원인 파악이 생각보다 까다롭습니다. 특히 Nginx의 auth_jwt(JWT 인증 모듈)는 “토큰이 없거나”, “서명이 맞지 않거나”, “클레임 조건이 불만족”인 경우를 대부분 동일하게 401로 처리해, 로그를 제대로 열지 않으면 감으로 때려 맞추는 상황이 생깁니다.
이 글은 Nginx에서 auth_jwt로 JWT를 검증할 때 401이 나는 핵심 원인을 토큰 추출 단계부터 키/알고리즘, 클레임 검증, **운영 환경(리로드/캐시/프록시)**까지 순서대로 정리합니다. 마지막엔 바로 붙여 넣어 테스트할 수 있는 설정과 디버깅 커맨드도 제공합니다.
네트워크 계층 문제(예: Ingress/ALB 502, MTU/PMTUD 등)로 보이지만 사실은 인증 계층에서
401인 경우도 많습니다. 인프라 레벨에서 헷갈릴 때는 EKS ALB Ingress 502 Target reset 원인과 해결, Pod 통신 이슈는 EKS Pod간 통신만 실패? MTU·PMTUD 10분 진단도 함께 참고하세요.
auth_jwt가 401을 내는 지점 3가지
Nginx의 auth_jwt 기반 흐름을 단순화하면 아래 3단계입니다.
- 요청에서 JWT를 어디서 꺼낼지 결정한다 (대개
Authorization: Bearer ...) - JWT의 서명과 헤더/알고리즘을 키로 검증한다
iss,aud,exp,nbf같은 클레임 조건을 만족하는지 확인한다
이 중 어디에서 실패하든 결과는 보통 401입니다. 따라서 문제를 빨리 풀려면 “내 요청에 토큰이 실제로 들어오고 있는가”부터 역순이 아니라 정방향으로 확인하는 게 가장 빠릅니다.
1) 토큰이 Nginx까지 실제로 도착했나
가장 흔한 401은 “토큰이 없어서”입니다. 애플리케이션은 토큰을 보내고 있다고 생각하지만, 중간 프록시/Ingress/클라이언트에서 헤더가 누락되는 경우가 많습니다.
체크 포인트
- 클라이언트가
Authorization헤더를 실제로 보내는지 - 앞단 프록시가
Authorization을 삭제/미전달하지 않는지 - CORS 환경에서 브라우저가
Authorization을 보내도록 허용했는지
빠른 확인 커맨드
아래처럼 Nginx 앞(또는 Ingress 앞)에서 직접 요청을 날려 헤더 포함 여부를 확인합니다.
curl -i \
-H 'Authorization: Bearer YOUR.JWT.HERE' \
https://api.example.com/protected
Nginx 접근 로그에 Authorization 값 자체를 남기면 보안상 위험하지만, 존재 여부 정도는 디버깅에 유용합니다. 예를 들어 디버깅 기간에만 아래처럼 헤더 존재 여부를 변수로 남길 수 있습니다.
map $http_authorization $has_auth_header {
default 1;
"" 0;
}
log_format jwtdebug '$remote_addr $request $status has_auth=$has_auth_header';
access_log /var/log/nginx/access.log jwtdebug;
has_auth=0이면 JWT 검증 이전에 이미 게임이 끝난 겁니다.
Ingress/프록시에서 Authorization이 사라지는 전형적 패턴
- Nginx 앞단에 또 다른 Nginx/Envoy/ALB가 있고, 기본 설정으로
Authorization을 전달하지 않거나 특정 경로에서만 전달 - 인증이 필요한 경로에서만
proxy_set_header Authorization $http_authorization;가 빠짐
아래는 업스트림으로 전달이 필요할 때(예: Nginx에서 검증 후 백엔드에서도 사용자 식별이 필요) 기본적으로 넣어두는 패턴입니다.
location /api/ {
proxy_set_header Authorization $http_authorization;
proxy_pass http://backend;
}
2) 키/알고리즘 불일치: kid, JWKS, RS256 vs HS256
토큰이 들어오는데도 401이면 다음은 거의 항상 서명 검증 실패입니다.
흔한 원인
- IdP는
RS256(비대칭키)로 서명했는데 Nginx는HS256(대칭키)로 검증하려고 함 - 토큰 헤더에
kid가 있고 키가 회전(rotating)되는데, Nginx가 최신 키를 못 가져옴 - JWKS URL이 내부망에서 접근 불가, DNS/방화벽, 또는 TLS 검증 실패
- PEM 파일 포맷/경로 문제(권한, 줄바꿈 깨짐)
토큰 헤더 먼저 확인하기
JWT는 header.payload.signature 형태입니다. 헤더를 디코딩해서 alg와 kid를 확인하세요.
python - <<'PY'
import base64, json
jwt='YOUR.JWT.HERE'
header=jwt.split('.')[0]
header += '=' * (-len(header) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(header)), indent=2))
PY
여기서 alg가 RS256인데 Nginx에 대칭키 문자열을 넣고 있었다면 100% 실패합니다.
Nginx 설정 예시: RS256 + JWKS
Nginx 버전/빌드에 따라 지시어가 다를 수 있지만, 운영에서 가장 많이 쓰는 방식은 JWKS를 가져와 kid로 키를 선택하는 패턴입니다.
# 예시: /etc/nginx/conf.d/api.conf
server {
listen 443 ssl;
server_name api.example.com;
location /protected/ {
# 1) JWT 인증 활성화
auth_jwt "protected area";
# 2) JWKS (공개키 묶음)로 서명 검증
# 실제 지시어는 배포판/모듈에 따라 다를 수 있으니 문서 확인 필요
auth_jwt_key_file /etc/nginx/jwks.json;
proxy_pass http://backend;
}
}
운영 포인트는 jwks.json을 어떻게 최신으로 유지하느냐입니다. 키 회전이 있는 IdP라면, 사이드카/크론으로 주기적으로 내려받고 Nginx를 reload 하거나, 모듈이 자동 갱신을 지원하는지 확인해야 합니다.
키 회전/크론 운영은 결국 “정기 작업이 제대로 도는지”가 중요합니다. 크론이 말썽이면 리눅스 cron 미실행? PATH·메일로그·권한 점검 체크리스트가 그대로 도움이 됩니다.
3) 클레임 검증 실패: iss, aud, 시간(exp/nbf)이 핵심
서명은 맞는데도 401이면 클레임 조건에서 떨어지는 경우가 많습니다.
iss(issuer) 불일치
- IdP 설정에서 issuer URL이 환경별로 다름 (dev/stage/prod)
https://idp.example.com/vshttps://idp.example.com처럼 슬래시 하나 차이
aud(audience) 불일치
- 토큰의
aud가api인데 Nginx가api://default를 기대 - 일부 IdP는
aud를 배열로 넣음. 모듈이 배열을 지원하는지 확인 필요
exp/nbf 시간 문제 (서버 시간 드리프트)
- 노드 시간이 틀어져 있으면 아직 유효한 토큰도 만료로 판정
- Kubernetes 노드/VM에서 NTP 동기화 문제
Nginx 단에서 시간을 직접 보정할 수는 없으니, 호스트 시간을 먼저 의심하세요.
date -u
# systemd 환경이면
timedatectl status
4) Bearer 파싱/공백 문제: 사소하지만 치명적
클라이언트가 Authorization: Bearer token처럼 공백을 여러 개 넣거나, 프록시가 줄바꿈을 섞는 등 “문자열은 있는데 파싱이 실패”하는 케이스도 있습니다.
- 헤더 값이 정확히
Bearer로 시작하는지 - 토큰 앞뒤로 불필요한 공백이 붙지 않았는지
이 유형은 특히 모바일/레거시 클라이언트에서 발생합니다. 가능하면 클라이언트와 계약을 엄격히 하되, 서버에서 트리밍을 지원하는지(모듈/버전별 상이)도 확인하세요.
5) Nginx가 백엔드로 넘기는 사용자 정보: sub 매핑 실수
auth_jwt는 “검증”만 통과하면 끝이 아니라, 보통 백엔드에서 사용자 식별을 위해 sub나 이메일 같은 클레임을 헤더로 전달합니다. 여기서 매핑이 잘못되면 백엔드가 추가 인증을 하다가 401을 다시 내기도 합니다.
예를 들어 검증은 통과했는데 백엔드가 X-User-Id 헤더를 기대하고 있고, Nginx가 다른 이름으로 보내면 백엔드는 “인증 정보 없음”으로 처리할 수 있습니다.
location /protected/ {
auth_jwt "protected";
auth_jwt_key_file /etc/nginx/jwks.json;
# 클레임을 변수로 꺼내 헤더로 전달 (지시어/변수명은 모듈에 따라 다를 수 있음)
proxy_set_header X-User-Sub $jwt_claim_sub;
proxy_set_header X-User-Email $jwt_claim_email;
proxy_pass http://backend;
}
운영에서는 “Nginx에서 401인지, 백엔드에서 401인지”를 구분하기 위해 응답 헤더에 디버깅용 마커를 잠깐 넣는 것도 유용합니다.
add_header X-Auth-Layer nginx always;
백엔드에서도 X-Auth-Layer: app 같은 헤더를 넣어두면, 클라이언트에서 어느 레이어가 401을 냈는지 바로 보입니다.
6) 로그 레벨과 디버깅: 에러 로그 없이는 답이 안 나온다
auth_jwt 문제는 접근 로그만 보면 대부분 401만 남습니다. 가능한 한 짧은 시간이라도 에러 로그 레벨을 올려 원인을 잡는 게 중요합니다.
error_log /var/log/nginx/error.log info;
# 더 강하게 보고 싶으면 (운영에서는 주의)
# error_log /var/log/nginx/error.log debug;
그리고 설정 변경 후에는 반드시 문법 체크와 reload를 분리해서 하세요.
nginx -t
nginx -s reload
컨테이너 환경이면 프로세스 1번이 Nginx인지, reload 시그널이 제대로 들어가는지도 함께 확인해야 합니다.
7) 재현 가능한 최소 설정 예시 (보호 구간 분리)
인증이 필요한 경로와 공개 경로를 분리하면, 장애 시에도 영향 범위를 줄이고 디버깅이 쉬워집니다.
server {
listen 443 ssl;
server_name api.example.com;
# 공개 엔드포인트
location /healthz {
return 200 'ok';
add_header Content-Type text/plain;
}
# 보호 엔드포인트
location /v1/ {
auth_jwt "api";
auth_jwt_key_file /etc/nginx/jwks.json;
# 백엔드로 사용자 식별 전달
proxy_set_header X-User-Sub $jwt_claim_sub;
proxy_pass http://backend;
}
}
이렇게 해두면 /healthz가 살아있고 /v1/만 401이면 네트워크/라우팅보다 인증 계층을 먼저 보면 됩니다.
8) 401 원인별 초단기 체크리스트
401인데 Nginx 접근 로그에has_auth=0이다: 클라이언트/프록시에서Authorization이 누락- 토큰 헤더의
alg가RS256인데 대칭키로 검증 중이다: 키 타입 불일치 - 토큰에
kid가 있는데 키 파일/JWKS가 오래됐다: 키 회전 대응 필요 iss값이 환경별로 다르다: issuer 설정/슬래시 차이 점검- 서버 시간이 틀어져
exp/nbf에서 떨어진다: NTP/시간 동기화 - Nginx는 통과했는데 백엔드가
401이다: 클레임 헤더 매핑/전달 헤더 점검
마무리: auth_jwt는 “요청이 들어오는 경로”부터 본다
JWT 검증 401은 보통 “키가 틀렸다”로 단정하기 쉽지만, 실제 현장에서는 토큰이 Nginx까지 오지 않는 문제가 1순위입니다. 그 다음이 alg/kid/JWKS 같은 서명 검증, 마지막이 iss/aud/시간 같은 클레임 검증입니다.
위 순서대로 좁혀가면, 401을 하루 종일 붙잡는 대신 10분 안에 재현과 원인 분리가 가능합니다. 특히 키 회전이 있는 환경이라면 JWKS 갱신과 Nginx reload를 “운영 자동화”로 묶어두는 것이 장기적으로 가장 큰 비용 절감 포인트입니다.