- Published on
Nginx JWT 검증 401? auth_jwt 설정과 디버깅
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 앞단에서 JWT를 검증하면 애플리케이션까지 요청이 도달하기 전에 인증 실패를 걸러낼 수 있어 성능과 보안 모두에 이점이 있습니다. 하지만 Nginx에 auth_jwt를 붙이는 순간부터 401 Unauthorized가 쏟아지는 경우가 많습니다. 원인은 대개 설정의 작은 불일치(알고리즘, 키 포맷, aud/iss, 만료 시간, 헤더 전달 방식)인데, 로그가 친절하지 않아 삽질이 길어집니다.
이 글은 Nginx에서 auth_jwt를 사용할 때의 기본 설정법과, 401이 발생하는 대표 원인을 “재현 가능한 체크리스트”로 정리합니다. (참고로 auth_jwt는 NGINX Plus 또는 상용 JWT 모듈이 필요할 수 있습니다. 오픈소스 Nginx만 사용한다면 대안으로 auth_request를 통해 외부 인증 서비스로 위임하는 패턴을 고려하세요.)
auth_jwt가 401을 내는 대표 시나리오
Nginx에서 JWT 검증 401은 크게 다음 범주에서 발생합니다.
- 토큰이 아예 없거나 형식이 잘못됨
- 서명 검증 실패(키 불일치, 알고리즘 불일치)
- 클레임 검증 실패(
exp,nbf,aud,iss등) - Nginx가 토큰을 읽는 위치가 다름(헤더/쿠키/쿼리)
- 리버스 프록시 구성에서 Authorization 헤더가 유실됨
이 중 2, 3, 5가 실제 운영에서 가장 흔합니다.
최소 구성 예시: Authorization Bearer 토큰 검증
가장 단순한 목표는 다음입니다.
- 클라이언트가
Authorization: Bearer ...로 JWT를 보냄 - Nginx가 JWT 서명을 검증하고 유효하면 upstream으로 프록시
- 실패하면 401 반환
아래는 개념적으로 가장 많이 쓰는 형태의 설정 예시입니다. (배포 환경에 맞게 키/클레임을 조정하세요.)
server {
listen 443 ssl;
server_name api.example.com;
# (생략) ssl_certificate / ssl_certificate_key
location / {
# JWT 인증 활성화
auth_jwt "api";
# 키 제공 방식(예: 공개키)
auth_jwt_key_file /etc/nginx/jwt/public.pem;
# (선택) 클레임 검증
# auth_jwt_require claim_iss eq "https://issuer.example.com";
# auth_jwt_require claim_aud eq "api";
proxy_pass http://app_upstream;
}
}
핵심은 auth_jwt와 키 지정(auth_jwt_key_file)입니다. 여기서부터 401이 나면, 거의 항상 “키/알고리즘/클레임/헤더” 중 하나가 맞지 않습니다.
401 디버깅을 위한 로그 레벨 올리기
JWT 실패 원인을 빨리 찾으려면, 해당 서버 블록 또는 전역에서 error_log 레벨을 올려 원인을 확인하는 것이 가장 빠릅니다.
error_log /var/log/nginx/error.log info;
# 더 공격적으로 보고 싶으면
# error_log /var/log/nginx/error.log debug;
운영에서 debug는 로그 폭증이 있을 수 있으니, 재현되는 짧은 시간에만 적용하거나 특정 IP만 디버깅하는 형태로 제한하는 것이 안전합니다.
체크리스트 1: 토큰이 실제로 들어오나
Authorization 헤더가 upstream까지 전달되는지
Nginx가 JWT를 검증하는 경우에도, 디버깅을 위해 upstream으로 Authorization을 전달하거나(혹은 전달하지 않도록) 의도를 명확히 해야 합니다.
- 클라이언트에서 Nginx까지 Authorization이 잘 들어오는지
- Nginx에서 upstream으로 프록시할 때 Authorization을 지우지 않는지
프록시 설정 중 실수로 헤더가 비워지는 경우가 있습니다.
location / {
auth_jwt "api";
auth_jwt_key_file /etc/nginx/jwt/public.pem;
proxy_set_header Authorization $http_authorization;
proxy_pass http://app_upstream;
}
또는 보안 정책상 upstream에는 Authorization을 넘기지 않고, 검증된 사용자 식별자만 넘기는 방식도 흔합니다.
location / {
auth_jwt "api";
auth_jwt_key_file /etc/nginx/jwt/public.pem;
# 예: JWT의 sub를 upstream으로 전달(지원되는 변수/클레임 추출 방식은 모듈에 따라 다름)
# proxy_set_header X-User-Id $jwt_claim_sub;
proxy_pass http://app_upstream;
}
쿠키로 토큰을 보내는 경우
프론트엔드가 Authorization이 아니라 쿠키에 토큰을 넣는 경우가 있습니다. 이때 Nginx가 어디서 토큰을 읽는지 설정이 필요합니다. 모듈/버전에 따라 토큰 소스 지정 지시어가 다를 수 있으니, 사용 중인 auth_jwt 모듈 문서에서 “token in cookie” 또는 “token variable” 항목을 확인하세요.
체크리스트 2: 알고리즘과 키 포맷 불일치
JWT 401의 절반은 여기서 납니다.
RS256 vs HS256 혼동
HS256: 대칭키(서버와 발급자가 같은 secret 공유)RS256: 비대칭키(private key로 서명, public key로 검증)
클라이언트 토큰 헤더의 alg가 RS256인데 Nginx에 HMAC secret을 넣으면 무조건 실패합니다(반대도 마찬가지).
토큰 헤더는 아래처럼 확인합니다.
python - << 'PY'
import base64, json
jwt = "eyJ..." # 토큰
h = jwt.split('.')[0]
print(json.loads(base64.urlsafe_b64decode(h + '==')))
PY
여기서 {"alg": "RS256", ...}처럼 나오면 공개키 검증이 필요합니다.
공개키 파일 포맷(PEM) 문제
public.pem이 PEM 포맷이 아니거나, 줄바꿈이 깨졌거나, 인증서 체인 파일을 잘못 넣는 경우가 많습니다.
- 공개키만 있는 PEM인지
BEGIN PUBLIC KEY또는BEGIN RSA PUBLIC KEY형태가 맞는지- 발급자가 제공한 JWK를 PEM으로 변환했는지
JWK 기반으로 운영한다면 “JWK를 그대로 쓰는지, PEM으로 변환해서 쓰는지”를 명확히 정해야 합니다. 또한 키 롤오버를 고려하면 JWK 세트 엔드포인트를 연동하는 구성이 운영 친화적입니다.
체크리스트 3: iss, aud 검증에서 막히는 경우
애플리케이션에서는 느슨하게 검증했는데 Nginx에서 엄격히 검증하도록 설정하면 401이 발생합니다.
iss(issuer)가 정확히 일치하는지aud(audience)가 토큰에 존재하는지, 배열인지 문자열인지- 클레임 값의 공백/슬래시 유무까지 동일한지
예를 들어 issuer가 https://issuer.example.com/인데 설정에는 슬래시 없는 https://issuer.example.com을 넣으면 실패할 수 있습니다.
location / {
auth_jwt "api";
auth_jwt_key_file /etc/nginx/jwt/public.pem;
# 예시: issuer/audience를 엄격히 맞춤
auth_jwt_require claim_iss eq "https://issuer.example.com";
auth_jwt_require claim_aud eq "api";
proxy_pass http://app_upstream;
}
aud가 배열로 들어오는 IdP도 있으니(예: aud: ["api", "other"]) 이 경우 모듈이 배열 매칭을 지원하는지 확인해야 합니다. 지원이 애매하면 auth_request로 외부 검증 서비스에 위임하는 편이 예측 가능합니다.
체크리스트 4: exp, nbf, iat 시간 문제(서버 시간/타임스큐)
JWT는 시간 클레임에 매우 민감합니다.
- Nginx 서버 시간이 틀림(NTP 미동기화)
- 컨테이너/VM의 시간대 설정 문제
- 발급자와 검증자 사이에 수 초~수 분 스큐가 있음
이 경우 토큰이 “방금 발급했는데도 만료됨”처럼 보이며 401이 납니다.
운영에서는 반드시 NTP를 맞추고, 필요하다면 스큐 허용 옵션(모듈이 제공하는 leeway/clock skew 설정)을 사용하세요. leeway가 없다면 발급 측에서 exp를 너무 타이트하게 잡지 않는 것도 방법입니다.
체크리스트 5: kid(키 식별자)와 키 롤오버
IdP가 kid를 바꿔가며 키를 롤오버하는데, Nginx에는 단일 키만 박아두면 특정 시점부터 401이 폭발합니다.
- 토큰 헤더에
kid가 있는지 - 현재 서명에 사용된 키가 Nginx에 반영되어 있는지
- 키 로테이션 시 Nginx reload가 필요한지
키 롤오버를 안정적으로 처리하려면 JWK 세트 기반으로 자동 갱신되는 구성이 이상적입니다(지원 여부는 사용하는 auth_jwt 구현에 따라 다릅니다).
401을 더 친절하게: 에러 응답 커스터마이징
기본 401만 던지면 클라이언트 디버깅이 어렵습니다. 운영 정책상 상세 사유를 노출하지 않더라도, 최소한 “인증 필요”와 “토큰 만료”를 구분하고 싶을 수 있습니다.
Nginx에서는 error_page로 401 응답을 JSON으로 바꾸는 패턴을 자주 씁니다.
server {
listen 443 ssl;
error_page 401 = @jwt_unauthorized;
location / {
auth_jwt "api";
auth_jwt_key_file /etc/nginx/jwt/public.pem;
proxy_pass http://app_upstream;
}
location @jwt_unauthorized {
default_type application/json;
return 401 '{"error":"unauthorized","message":"invalid or missing token"}';
}
}
주의: 실패 사유(서명 불일치, 만료 등)를 그대로 노출하면 공격자에게 힌트를 줄 수 있어, 외부 공개 API에서는 메시지를 단순화하는 편이 안전합니다. 대신 내부 관측(로그/메트릭/트레이싱)으로 원인을 추적하세요.
오픈소스 Nginx라면: auth_request로 외부 검증 위임
auth_jwt를 쓸 수 없는 환경(오픈소스 Nginx만 사용)이라면, Nginx는 “인증 서브요청”만 수행하고 JWT 검증은 별도 인증 서비스가 담당하도록 구성할 수 있습니다.
location / {
auth_request /_auth;
proxy_pass http://app_upstream;
}
location = /_auth {
internal;
proxy_pass http://auth_upstream/verify;
proxy_set_header Authorization $http_authorization;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
이 방식은 다음 장점이 있습니다.
- 검증 로직을 코드로 구현 가능(각종 IdP/JWK/캐시/롤오버 대응)
- Nginx 기능 제약을 덜 받음
- 장애 시 fallback 정책을 유연하게 설계 가능
MSA에서 인증을 공통 컴포넌트로 떼어내면, 장애 전파를 막기 위한 패턴(예: 재시도, 캐시, 아웃박스 등)도 함께 고려하게 됩니다. 분산 환경에서 실패를 다루는 관점은 MSA 사가 실패로 중복결제 터질 때 Outbox로 막기 글도 함께 참고하면 도움이 됩니다.
운영 팁: 단계적 롤아웃과 관측 포인트
- 먼저 특정 경로(
/api/private)에만 JWT를 걸어 점진 적용 401비율, 원인별 로그 카운트, 키 갱신 실패 등을 메트릭화- 배포 파이프라인에서 설정 변경 시 캐시/리로드 타이밍을 명확히
CI/CD에서 구성 변경이 잦다면 캐시나 설정 반영 문제도 같이 터집니다. 빌드/배포 문제를 빠르게 점검하는 관점에서는 GitHub Actions 캐시가 안 먹을 때 디버깅 체크리스트도 유용합니다.
빠른 결론: 401을 줄이는 실전 점검 순서
- 클라이언트가 실제로
Authorization: Bearer를 보내는지(프록시/브라우저에서 확인) - Nginx가 그 헤더를 받는지(로그 레벨 상향)
- 토큰
alg와 Nginx 키 타입이 일치하는지(RS256 공개키 vs HS256 시크릿) iss,aud가 설정과 정확히 일치하는지(슬래시/배열 여부 포함)- 서버 시간이 맞는지(NTP),
exp/nbf스큐가 없는지 kid롤오버가 있는 환경이면 키 갱신 전략을 갖췄는지
여기까지 점검하면 대부분의 auth_jwt 기반 401은 원인이 드러납니다. 그래도 막힌다면, 토큰 샘플(헤더/페이로드만, 서명 제외)과 Nginx 설정의 인증 관련 부분을 분리해서 비교해보는 것이 가장 빠른 해결책입니다.