Skip to content
Control Plane Labs

496 SSL Certificate Required

Client error response defined by nginx.org.

Last updated July 27, 2026

Common causes at a glance

  • A client with no client certificate configured for an mTLS-only route
  • A Kubernetes secret or file mount missing so the pod has no cert to send
  • A browser or tool that cannot present client certificates hitting the endpoint
  • ssl_verify_client optional applied to a location that should have been open

Reproduce it

curl -s -o /dev/null -w '%{http_code}\n' https://mtls.example.com/api/health

What 496 SSL Certificate Required means

The difference between 495 and 496 is presence, not validity. 496 means the client completed the TLS handshake without supplying a certificate to a location that requires one. As with 495, you only see it when verification is deferred with ssl_verify_client optional; under on nginx aborts during the handshake and the client gets a TLS alert instead.

Splitting the two codes is useful. 496 almost always means a caller was never configured for mTLS — wrong environment, missing secret mount, a tool that cannot present certificates. 495 means someone tried and got it wrong.

Where it comes from

Like its neighbours, 496 has no RFC and no IANA registration. It is listed in the nginx SSL module’s error processing section as a non-standard code for use with error_page, meaning a client has not presented the required certificate; the trust anchors come from ssl_client_certificate. Under RFC 9110 §15 an unrecognised 4xx is treated as 400, so no client reads 496 as “retry with a certificate”. If you want callers to react, return a documented body from your error_page handler and keep 496 for logs, where it separates unconfigured clients from broken credentials.

What actually causes it

Deployment gaps dominate. A service works in staging where mTLS is off, then fails in production because the certificate comes from a secret nobody created in that namespace, or sits at a path the process does not read. Health checks are the other reliable source: probes and uptime monitors rarely carry client certificates, so they generate a steady baseline of 496 against a healthy service and make the error rate look alarming. Browsers are third — a human opening an mTLS URL without an installed certificate gets 496 and, unless you wrote an error_page, no explanation of what to install.

How to debug it

First separate “no certificate” from “bad certificate”; the fixes are unrelated. Request without one and read the code:

curl -s -o /dev/null -w '%{http_code}\n' https://mtls.example.com/api/health

496 there and 200 with --cert confirms the endpoint is behaving and the caller is unconfigured. Check the server really is requesting a certificate by looking for the acceptable CA names block in openssl s_client -connect mtls.example.com:443; if it is absent, ssl_verify_client is off for the server block you reached.

For health checks, carve out the probe path with ssl_verify_client off; rather than issuing certificates to every monitoring agent. Use the TLS inspector to confirm which server block a given SNI value lands on.

Working examples

curl

# No client certificate: expect 496 (or 400 if nginx does not expose it).
curl -s -o /dev/null -w '%{http_code}\n' https://mtls.example.com/api/health

# With a valid certificate the same URL should return 200.
curl -s -o /dev/null -w '%{http_code}\n' \
  --cert client.pem --key client.key https://mtls.example.com/api/health

# Does the server even ask for a certificate?
openssl s_client -connect mtls.example.com:443 </dev/null 2>&1 \
  | grep -A5 'Acceptable client certificate CA names'

Python (requests)

import requests

url = "https://mtls.example.com/api/health"

# No cert argument: nginx sees an empty certificate list.
print("without cert:", requests.get(url, timeout=10).status_code)  # 496

# With one, the same request should succeed.
print("with cert:", requests.get(
    url, cert=("client.pem", "client.key"), timeout=10
).status_code)

# Health checkers usually look like the first call. Exempt the probe path
# in nginx rather than shipping certificates to every monitoring agent.

Node (fetch)

import { readFileSync } from "node:fs";
import { Agent } from "undici";

const url = "https://mtls.example.com/api/health";

// Plain fetch never sends a client certificate.
console.log("without cert:", (await fetch(url)).status); // 496

const agent = new Agent({
  connect: {
    cert: readFileSync("client.pem"),
    key: readFileSync("client.key"),
  },
});
console.log("with cert:", (await fetch(url, { dispatcher: agent })).status);

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