Skip to content
Control Plane Labs

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/health

What 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