조회수: 8

nginx 496: 클라이언트 인증서 미전송(mTLS)

No required SSL certificate was sent는 nginx mTLS가 클라이언트 인증서를 못 받은 것. 클라이언트·서버를 3단계로 격리. 무료 즉시 진단으로 바로 확인.

내 도메인에 이 문제가 있는지 지금 확인

무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.

문제

nginx 엔드포인트로 보낸 요청이 400 Bad Request와 본문 No required SSL certificate was sent로 돌아오고, 서버 접근 로그는 상태를 496으로 기록합니다. TLS 핸드셰이크가 서버 인증서에서 실패한 게 아닙니다 — 자물쇠 쪽은 멀쩡합니다. nginx는 상호(mutual) TLS를 강제하는 중입니다: 클라이언트에게 인증서로 자신의 정체를 증명하라고 요구했는데, 클라이언트가 하나도 보내지 않은 것이죠. 이건 서버 인증서·암호·이름 불일치 문제가 아닙니다. 당신이 충족하지 못한 클라이언트 인증 요구입니다. 관건은 당신 클라이언트가 인증서를 보내야 하는데 안 보내는 건지, 아니면 당신과 오리진 사이 무언가가 조용히 그것을 떨궜는지입니다.

증상

  • 클라이언트가 No required SSL certificate was sent라는 정확한 본문과 함께 400 Bad Request를 봅니다. nginx 접근 로그의 상태는 496입니다.
  • 클라이언트 인증서 인증으로 설정된 엔드포인트(호스트 전체, 또는 특정 경로·API)에서만 발생합니다 — 서버의 나머지는 정상적으로 열립니다.
  • curl--cert--key를 넘기지 않으면 재현되고, 브라우저는 올바른 클라이언트 인증서를 임포트하고 프롬프트에서 선택하지 않으면 재현됩니다.
  • 어떨 땐 간헐적입니다 — 대부분의 요청은 성공하고, 장수·풀링된 연결에서 어쩌다 한 요청이 496을 반환합니다(FAQ 참고).

이 오류의 실제 의미

496은 nginx의 비표준 내부 상태 코드 중 하나로, 495(클라이언트 인증서가 제시됐으나 검증 실패)·497(HTTPS 포트로 도착한 평문 HTTP 요청)과 나란히 소스에 정의돼 있습니다. 496 — 내부적으로 NGX_HTTP_NO_CERT — 는 서버가 요구했는데 클라이언트가 인증서를 하나도 제시하지 않았다는 뜻입니다. 496은 등록된 HTTP 상태가 아니라서 nginx는 그것을 와이어에 내보내지 않고, 클라이언트에 400 Bad Request를 반환하며 당신을 위해 496을 로그에 남깁니다.

이건 서버 블록이 ssl_verify_client on;으로 클라이언트 인증서 검증을 켜고 ssl_client_certificate로 클라이언트 인증서용 신뢰 CA 번들을 지정했을 때 나타납니다. TLS 핸드셰이크에서 nginx는 CertificateRequest를 보내는데, 클라이언트가 빈 인증서 목록으로 답하면 검증은 시작조차 못 하고 nginx는 496을 반환합니다. 관련 변수 $ssl_client_verify는 이 경우 NONE입니다 — 좋은 인증서면 SUCCESS, 거부된 인증서면 FAILED:<이유>인 것과 대비됩니다. 그래서 496은 아주 구체적인 선언입니다: “당신 인증서가 나쁘다”가 아니라 “이 엔드포인트가 요구하는 인증서를 안 가져왔다”.

주요 원인 3가지

  1. 클라이언트가 정말로 인증서를 안 보낸다 - 가장 흔한 원인. --cert/--key 없는 curl, TLS 컨텍스트에 클라이언트 인증서가 설정되지 않은 HTTP 라이브러리, 인증서가 임포트되지 않은(또는 “인증서 선택” 프롬프트를 사용자가 닫아 버린) 브라우저, 비어 있거나 경로가 틀린 키스토어. 엔드포인트는 mTLS를 요구하는데 요청이 그냥 자격증명 없이 도착한 것입니다.
  2. 리버스 프록시나 로드밸런서가 인증서를 벗겨냈다 - 당신은 인증서를 보내고 있지만, 중간 장비가 오리진 앞에서 TLS를 종단하고 그것을 전달하지 않았습니다. 앞단 nginx/LB가 백엔드로 연결을 새로 여는데 그 백엔드도 역시 mTLS를 요구하면, 백엔드는 클라이언트 인증서를 보지 못해 496을 반환합니다. 해법은 인증서를 통과시키거나(예: 백엔드가 신뢰하는 헤더로 $ssl_client_escaped_cert 전달) TLS 패스스루로 오리진이 직접 검증하게 하는 것입니다.
  3. ‘인증서 없음’으로 표출되는 CA 불일치 - 클라이언트가 인증서를 보내지만, 그것을 발급한 CA가 서버의 ssl_client_certificate 번들에 없습니다. 설정에 따라 이건 496이 아니라 검증 실패(495)로 나타날 수 있지만, 오설정·잘린 신뢰 번들 — 또는 클라이언트 인증서의 발급자에 닿기엔 너무 얕은 ssl_verify_depth — 는 보낸 인증서가 인정받지 못하는 잦은 이유입니다.

DechoNet으로 진단하기

  • SSL 진단은 서버의 TLS 핸드셰이크와 인증서를 바깥에서 — 자기 클라이언트 인증서 없이 — 읽습니다. 엔드포인트가 모두에게 mTLS를 강제한다면 DechoNet의 인증서 없는 프로브도 거부되고, 이는 요구가 서버 쪽에 있으며 당신의 해법이 클라이언트 측(인증서 제시)임을 확인해 줍니다. 또한 서버 자신의 인증서와 체인이 건강한지 확인해 서버 인증서 문제를 배제하고 클라이언트 인증 계층에 집중하게 합니다.
  • HTTP 진단은 엔드포인트가 평문 요청에 400 / No required SSL certificate was sent를 반환하는지 확인하고 응답을 잡아냅니다. 그리고 당신 네트워크 바깥에서 테스트하므로, 요구가 호스트 전역인지 아니면 당신 경로의 프록시가 뭉개는 특정 경로에 한정된 것인지 알려 줍니다.

해결 체크리스트

  • 엔드포인트가 정말 클라이언트 인증서를 원하는지 확인하세요. 올바른 인증서가 있는 기기에서: curl -v --cert client.crt --key client.key https://YOUR_HOST/PATH. 이게 성공하고 인증서 없는 curl이 496/400을 반환하면, 요구는 진짜이고 클라이언트 측입니다.
  • 당신이 클라이언트라면 인증서를 제시하세요. curl--cert/--key, 브라우저는 클라이언트 인증서(PKCS#12)를 키스토어에 임포트하고 프롬프트에서 선택, 애플리케이션 코드는 TLS/HTTP 클라이언트 컨텍스트에 인증서와 개인키를 붙입니다. 키가 인증서와 맞는지, 파일 경로가 해석되는지 확인하세요.
  • 서버를 소유했다면 신뢰 번들을 확인하세요. ssl_client_certificate는 당신이 수용하는 클라이언트 인증서의 발급 체인 전체를 담아야 하고, ssl_verify_depth는 발급자에 닿을 만큼 깊어야 합니다. 잘린 번들은 유효한 클라이언트 인증서를 거부로 만듭니다.
  • 앞단에 프록시가 있다면 인증서를 전달하는지 검증하세요. 종단하는 nginx/LB는 재검증하는 백엔드로 클라이언트 인증서를 넘겨야 합니다($ssl_client_escaped_cert$ssl_client_verify 전달, 또는 그 홉을 TLS 패스스루로 전환). “직접은 되는데 프록시를 통하면 안 되는” 흔한 이유입니다.
  • 재사용 연결에서의 간헐 496은, 재개 시 인증서가 유실되지 않게 하세요: ssl_verify_clientlocationif로 게이팅하지 말고 서버 레벨에 두고, 패턴이 keepalive와 함께 움직이면 연결 재사용을 끈 채 테스트해 진단을 확정한 뒤 튜닝하세요.

언제 에스컬레이션할까

  • 인증서로 인증한 curl은 성공하는데 애플리케이션은 여전히 496을 받는다면, 간극은 당신 클라이언트가 TLS 컨텍스트를 구성하는 방식에 있습니다 — 인증서가 나가는 요청에 붙지 않는 것이죠. 서버 변경이 아니라 애플리케이션/SDK 설정 수정입니다.
  • 인증서는 올바르고 직접 호출은 되는데 당신 인프라를 통한 호출이 실패한다면, 중간 장비가 인증서를 벗기는 것입니다. 직접 대 프록시 경유 증거와 함께 그 프록시/로드밸런서 담당자에게 넘기세요. 그들이 클라이언트 인증서를 전달하거나 검증을 옮겨야 합니다.
  • 496이 간헐적으로만, 그리고 부하나 장수 연결에서만 나타난다면 인증서 결함이 아니라 당신 클라이언트 스택과 mTLS 엔드포인트 사이의 연결 재사용 동작으로 다루세요 — 재발급할 것은 없습니다.

관련 도구

관련 가이드

가이드 공유

[Ad] Guide Detail Inline
← 전체 가이드 보기