495 SSL Certificate Error
Client error response defined by nginx.org.
Last updated July 27, 2026
Common causes at a glance
- A client certificate signed by a CA not listed in ssl_client_certificate
- An expired or not-yet-valid client certificate
- A certificate revoked and listed in the configured CRL
- ssl_verify_depth too low for a multi-level intermediate chain
Reproduce it
curl -sv --cert wrong.pem --key wrong.key https://mtls.example.com/api/healthWhat 495 SSL Certificate Error means
Under ssl_verify_client on, nginx rejects a bad client
certificate during the handshake itself and the client sees a TLS alert, not an HTTP
response. 495 only becomes reachable when verification is deferred — typically
ssl_verify_client optional — so the handshake completes, the request is
parsed, and nginx decides what to do at the HTTP layer.
That distinction matters when you deploy mTLS. Strict mode gives a hard TLS failure with
a cryptic client-side error. Optional mode plus error_page 495 returns an
explanation someone can act on, at the cost of accepting the connection first.
Where it comes from
495 is unregistered with IANA. It is defined by nginx’s SSL module, whose
error processing section
lists it among several non-standard codes usable with the error_page
directive, described as an error occurring during client certificate verification. The
documentation notes the redirect happens after the request is fully parsed, so variables
such as $request_uri are available in the handler. RFC 9110 §15 tells any
client receiving an unrecognised 4xx to treat it as 400, so do not
build client logic around the number. Its real use is in logs, where 495 versus 496 says
whether the certificate was wrong or absent.
What actually causes it
Expiry and trust-chain gaps cause most of it. Client certificates are
often issued with short lifetimes and rotated by automation that silently stops working,
so a fleet keeps presenting a certificate that expired overnight. The other common case
is a chain problem: the client sends a leaf issued by an intermediate, but
ssl_client_certificate holds only the root and ssl_verify_depth
is too shallow, so nginx cannot build a path. Revocation is third — a CRL configured with
ssl_crl that has gone stale rejects valid certificates once it passes its
own next-update time.
How to debug it
Read the reason out of nginx rather than guessing. At info
level, error.log records the verification failure with the OpenSSL reason
string — “certificate has expired”, “unable to get local issuer certificate” — which
tells you which of the causes above applies.
From the client, check the certificate you are actually sending:
openssl x509 -in wrong.pem -noout -subject -issuer -dates
openssl s_client -connect mtls.example.com:443 -cert wrong.pem -key wrong.key
The s_client output lists the acceptable CA names the server advertises; if
your issuer is not among them, the fix is on the server. The
TLS inspector covers the server side of the same handshake. With
ssl_verify_client on you never see 495 at all, because the handshake fails
first.
Working examples
curl
# Inspect what you are presenting before blaming the server.
openssl x509 -in wrong.pem -noout -subject -issuer -dates
# Full handshake detail, including the CA names nginx will accept.
openssl s_client -connect mtls.example.com:443 -cert wrong.pem -key wrong.key </dev/null
# The HTTP-layer result, which only exists under ssl_verify_client optional.
curl -sv --cert wrong.pem --key wrong.key https://mtls.example.com/api/health
# nginx side:
# ssl_verify_client optional;
# ssl_client_certificate /etc/nginx/ca-chain.pem;
# error_page 495 496 = @certerror;
Python (requests)
import requests
# cert=(client_cert, client_key). A rejected cert under ssl_verify_client on
# fails at the TLS layer; under optional you get an HTTP status back.
try:
resp = requests.get(
"https://mtls.example.com/api/health",
cert=("wrong.pem", "wrong.key"),
timeout=10,
)
print(resp.status_code) # 495 if nginx exposes it, else 400
except requests.exceptions.SSLError as exc:
print("handshake rejected:", exc)
Node (fetch)
// fetch() cannot carry a client certificate; use undici's Agent for mTLS.
import { readFileSync } from "node:fs";
import { Agent } from "undici";
const agent = new Agent({
connect: {
cert: readFileSync("wrong.pem"),
key: readFileSync("wrong.key"),
},
});
try {
const res = await fetch("https://mtls.example.com/api/health", { dispatcher: agent });
console.log(res.status); // 495 only when verification is deferred to HTTP
} catch (err) {
console.log("TLS rejected:", err.cause?.code ?? err.message);
}Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request