nginx 495: 클라이언트 인증서 검증 실패
495 SSL Certificate Error는 nginx가 받은 클라이언트 인증서를 거부한 것 — 만료·틀린 CA·끊긴 체인. 3단계로 원인 특정. 무료 즉시 진단으로 바로 확인.
내 도메인에 이 문제가 있는지 지금 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.
문제
nginx 엔드포인트로 보낸 요청이 400 Bad Request와 본문 The SSL certificate error로 돌아오고, 서버 접근 로그는 상태를 495로 기록합니다. 이번엔 클라이언트 인증서를 보냈습니다 — 빈손으로 온 경우가 아닙니다. nginx는 상호 TLS를 강제하는 중이고, 인증서를 요구했고, 당신은 하나를 제시했고, nginx가 그 인증서를 들여다본 뒤 거부한 것입니다. 서버 자신의 인증서는 멀쩡합니다 — 자물쇠 쪽은 잘 돕니다. 실패한 건 당신 정체의 검증입니다: 인증서가 만료됐거나, 아직 유효 전이거나, 폐기됐거나, nginx가 클라이언트 인증용으로 신뢰하지 않는 CA가 서명했죠. 관건은 그중 무엇이냐이고, 답은 당신이 아직 안 읽었을 로그 한 줄에 들어 있습니다.
증상
- 클라이언트는
400 Bad Request와 본문The SSL certificate error를 봅니다. nginx 접근 로그는 상태를 400이 아니라495로 기록합니다. - 클라이언트 인증서 인증이 설정된 엔드포인트(
ssl_verify_client on)에서만 발생 — 서버의 다른 것들은 정상 로드됩니다. curl --cert client.crt --key client.key https://HOST/도 여전히 실패하는데, 같은 호스트에 인증서 없이 보내면 495 대신496이 납니다.- nginx 에러 로그에 진짜 이유가 담깁니다:
client SSL certificate verify error: (NN:reason) while reading client request headers. - 내 쪽 설정 변경 없이 OpenSSL이나 nginx 패키지 업그레이드 직후 시작되기도 합니다(FAQ 참고).
이 에러의 실제 의미
495는 nginx의 비표준 내부 상태 코드 중 하나로, 소스에서 494(요청 헤더/쿠키 초과), 496(NGX_HTTP_NO_CERT — 클라이언트 인증서를 아예 안 보냄), 497(HTTPS 포트에 온 평문 HTTP) 옆에 정의돼 있습니다. 495 — 내부적으로 NGX_HTTPS_CERT_ERROR — 는 클라이언트 인증서가 제시됐고 검증에 실패했다는 뜻입니다. 495는 등록된 HTTP 상태가 아니라서 nginx는 와이어에 그걸 싣지 않고, 본문 The SSL certificate error와 함께 400 Bad Request를 반환하며 로그에 495를 남깁니다.
이 코드는 서버 블록이 ssl_verify_client on;으로 클라이언트 인증서 검증을 켜고 ssl_client_certificate로 신뢰 번들을 지정했을 때 나타납니다. 핸드셰이크 중 nginx가 CertificateRequest를 보내면 클라이언트가 인증서로 답하고, nginx는 그 번들에 대해 OpenSSL 체인 검증을 돌립니다. 검증이 어떤 오류든 반환하면 nginx는 $ssl_client_verify를 FAILED:<이유>로 설정하고 495로 답합니다 — 아무것도 제시되지 않아 검증이 시작조차 못 한 496의 정반대죠. 유용한 부분은 이유 문자열입니다: OpenSSL X.509 verify 코드이고, 코드마다 해법이 다릅니다.
주요 원인 3가지
- 클라이언트 인증서가 만료·유효 전·폐기됨 - 가장 단순하고 흔합니다. 에러 로그가
(10:certificate has expired),(9:certificate is not yet valid)— 보통 과거로 맞춰진 클라 시계이거나notBefore가 아직 미래인 갓 발급된 인증서 — 또는(23:certificate revoked)를 읽습니다. 이건 서버 오설정이 아니라 클라이언트 정체 문제입니다: nginx가 받은 인증서가 실제로 유효 구간을 벗어났거나 CRL에 올라 있죠. - 발급 CA(또는 중간 인증서)가 서버 신뢰 번들에 없음 - 클라이언트 인증서는 유효하지만 nginx가 신뢰 경로를 세우지 못합니다.
(20:unable to get local issuer certificate)나(21:unable to verify the first certificate)는 클라이언트 인증서를 서명한 CA — 또는 그 사이 중간 인증서 — 가ssl_client_certificate파일에서 빠졌거나ssl_verify_depth(기본 1)가 너무 얕아 발급자에 못 닿는다는 뜻입니다. 사설 루트가 안 들어 있으면(19:self signed certificate in certificate chain)도 납니다. - 건드리지도 않은 설정에서 OpenSSL 업그레이드가 체인 구성을 바꿈 - OpenSSL 1.1.1i(2020년 12월)가 클라이언트 체인 구성 방식을 엄격하게 조였습니다(nginx trac #1847). 수년간 검증되던 인증서가 에러 20/21로 실패하기 시작했는데, 신뢰 번들이 OpenSSL이 제거한 관대한 체인 구성에 기대고 있었기 때문입니다. nginx는 안 바뀌고 플랫폼이 바뀐 것이죠. 해법은 번들에 중간 체인을 완성하는 것 — OpenSSL 다운그레이드는 증상만 가립니다.
DechoNet으로 진단
- SSL 점검은 자체 클라이언트 인증서 없이 바깥에서 서버의 TLS 핸드셰이크와 인증서를 읽습니다. 서버 인증서와 체인이 건강함을 확인해 서버측 문제를 배제하고, 495가 실제로 사는 클라이언트 인증 계층에 집중하게 해 줍니다. (mTLS 엔드포인트에 인증서 없이 보낸 프로브는 거부되는데, 그 자체가 엔드포인트가 모두에게 클라이언트 인증을 강제한다는 확인입니다.)
- HTTP 점검은 엔드포인트가 바깥 요청에
400과The SSL certificate error를 반환하는지 확인하고 응답을 캡처합니다. 그리고 당신 것이 아닌 네트워크에서 테스트하므로, 요구가 호스트 전체에 걸리는지 아니면 당신 경로의 프록시가 망가뜨리는 특정 경로에만 걸리는지 알려 줍니다.
해결 체크리스트
- 접근 로그가 아니라 에러 로그를 읽으세요.
grep "SSL certificate verify error" /var/log/nginx/error.log가(NN:reason)을 줍니다. 아래 모든 게 그 숫자에 달렸습니다 — 추측하지 마세요. -
(10:...)/(9:...)/(23:...)— 만료·유효 전·폐기 — 는 인증서 자체가 문제입니다. 클라이언트 인증서를 재발급/갱신하고, 갓 발급된 인증서에서(9:...)이면 클라이언트 시계를 실제 시각과 대조하세요. -
(20:...)/(21:...)/(19:...)은 서버의 신뢰 경로를 고치세요.ssl_client_certificate가 가리키는 파일에 전체 발급 체인(루트 + 모든 중간)을 넣고,ssl_verify_depth를 클라이언트 인증서 발급자에 닿을 때까지 올리세요. 번들 대비 인증서를 별도로 검증:openssl verify -CAfile ca-bundle.pem client.crt. - 당신이 생각하는 그 인증서를 실제로 제시하는지 확인.
curl -v --cert client.crt --key client.key https://HOST/PATH— curl이 쌍을 실제로 로드하는지(키가 안 맞거나 경로가 틀리면 요청 전에 실패), 인증서 발급자가 서버가 신뢰하는 것과 맞는지 확인하세요. - 설정 변경 없이 패키지 업데이트 후 495가 시작됐다면 OpenSSL 체인 구성 변경으로 취급하세요:
ssl_client_certificate에 중간 체인을 완성하면 됩니다. 옛 OpenSSL을 고정해 “고치지” 마세요. - 앞단 프록시가 TLS를 종단하고 nginx로 다시 연결한다면, 백엔드 번들이 실제로 신뢰하는 인증서를 넘기는지 확인하세요 — 프록시 계층이 재서명하거나 바꿔치기한 인증서가 오리진 495의 흔한 숨은 원인입니다.
언제 에스컬레이션할까
openssl verify -CAfile ca-bundle.pem client.crt가 로컬에선 성공하는데 nginx가 여전히 495를 남기면, nginx가 실제로 로드하는 번들이 당신이 테스트한 것과 다릅니다 — 해석된ssl_client_certificate경로를 확인하고 nginx를 리로드하세요. 인증서 결함이 아니라 배포/설정 드리프트 문제입니다.- 클라이언트 인증서가 유효하고 신뢰되는데 앞에 프록시나 로드밸런서가 있다면, 직접 대 프록시 경유 증거를 그 홉 담당자에게 넘기세요: 오리진이 신뢰하지 않는 인증서를 종단·재제시하는 쪽입니다.
- 495가 OS나 OpenSSL 업그레이드 후 여러 서버에서 한꺼번에 나타났다면 호스트별 문제가 아니라 플랫폼 변경입니다 — 호스트를 하나씩 쫓지 말고, 모든 노드가 완전한 체인을 받도록 구성 관리에서 CA 번들을 고치세요.
관련 도구
관련 가이드
가이드 공유