Published on

Nginx JWT 검증 401? auth_jwt 설정과 디버깅

Authors

서버 앞단에서 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은 크게 다음 범주에서 발생합니다.

  1. 토큰이 아예 없거나 형식이 잘못됨
  2. 서명 검증 실패(키 불일치, 알고리즘 불일치)
  3. 클레임 검증 실패(exp, nbf, aud, iss 등)
  4. Nginx가 토큰을 읽는 위치가 다름(헤더/쿠키/쿼리)
  5. 리버스 프록시 구성에서 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로 검증)

클라이언트 토큰 헤더의 algRS256인데 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을 줄이는 실전 점검 순서

  1. 클라이언트가 실제로 Authorization: Bearer를 보내는지(프록시/브라우저에서 확인)
  2. Nginx가 그 헤더를 받는지(로그 레벨 상향)
  3. 토큰 alg와 Nginx 키 타입이 일치하는지(RS256 공개키 vs HS256 시크릿)
  4. iss, aud가 설정과 정확히 일치하는지(슬래시/배열 여부 포함)
  5. 서버 시간이 맞는지(NTP), exp/nbf 스큐가 없는지
  6. kid 롤오버가 있는 환경이면 키 갱신 전략을 갖췄는지

여기까지 점검하면 대부분의 auth_jwt 기반 401은 원인이 드러납니다. 그래도 막힌다면, 토큰 샘플(헤더/페이로드만, 서명 제외)과 Nginx 설정의 인증 관련 부분을 분리해서 비교해보는 것이 가장 빠른 해결책입니다.