error:0A000086:SSL routines::certificate verify failed
A fix for OpenSSL's generic 'certificate verify failed' (error:0A000086): how to extract the specific verify code with -verify_return_error, what the common codes mean (missing intermediate vs stale CA bundle), and how to fix each side. Use when OpenSSL-based clients like Python, Ruby, or nginx upstreams reject a cert that browsers accept. Triggers: 'certificate verify failed', 'SSL routines'. Not for: expired certs specifically, TLS version errors, or cipher mismatches.
error:0A000086:SSL routines::certificate verify failed
TL;DR
This is OpenSSL's generic way of saying certificate verification failed, and the message alone does not tell you why. Re-run the handshake with -verifyreturnerror to get the specific verify code: code 21 means the server is not sending its intermediate certificates (fix the server chain), code 20 or 19 means the client's CA bundle is missing or stale (update the bundle). Fix the side the code points at.
error:0A000086:SSL routines::certificate verify failedUse this when
- Python, Ruby, or another OpenSSL-based client rejects an HTTPS endpoint
- nginx fails to handshake with an upstream and logs this error
- Browsers accept the same certificate without complaint
- It broke after a server cert change or a client OS update
Not for this skill when
- The cert is expired (the verify code will say so; treat that separately)
- The client and server cannot agree on a TLS version or cipher
- The hostname does not match the certificate
Steps
Get the specific reason instead of guessing:
echo | openssl s_client -connect example.com:443 -servername example.com -verify_return_errorRead the "Verify return code" line at the end. That number names the real problem. Expected: a specific code like 21 (unable to verify the first certificate) or 20 (unable to get local issuer certificate).
Code 21 means the server is not sending intermediates. Fix the server: bundle the intermediate certificates with the server certificate, in order, in the file the server presents. Reload the server and repeat step 1. Expected: the verify return code becomes 0.
Code 20 or 19 means the client's trust store is at fault. Update the CA bundle the client uses: OS package update for system clients, certifi upgrade for Python, the distribution's CA package in containers. Then retest. Expected: the client handshake succeeds with verification enabled.
For nginx upstreams, check proxyssltrusted_certificate. It must point at a current bundle containing the upstream's chain roots. A stale path here produces exactly this error while browsers stay happy. Expected: nginx error log stops showing the verify failure and upstream proxying works.
Variant: Python ssl.CertificateError wrapping this
Python surfaces OpenSSL's failure as its own exception, but the cause is the same. Diagnose with the openssl command above, then fix the bundle Python uses (certifi), not the OS store.
Variant: Ruby OpenSSL::SSL::SSLError
Same diagnosis. Ruby typically uses the OS default bundle; update the OS CA certificates and retest.
Variant: self-signed certificate in the chain
The verify code says so explicitly. Either the server should present a proper chain, or the client needs that CA added to its trust store deliberately, not by disabling verification.
Variant: works without -servername but the app still fails
Then SNI is the difference: without a server name the server presents a default cert that happens to verify. Always test with -servername matching the real hostname.
Why this happens
OpenSSL reports verification as a single pass/fail at the API level, and this error string is the fail case with the detail stripped out. The numeric verify code underneath distinguishes "the server gave me an incomplete chain" from "my trust store is missing the root," which are fixed on opposite sides of the connection.
Edge cases and pitfalls
- The generic message tempts people to disable verification "temporarily." Get the code first; the fix is usually ten minutes away.
- Intermediate order matters: leaf first, then intermediates down toward the root. Wrong order fails on strict clients.
- A bundle that validated last year can fail today if a root expired or was removed. "Nothing changed" usually means the bundle aged.
- Some clients pin or override the bundle path in config. Updating the OS store does nothing if the app points elsewhere. Check the app's actual setting.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_3GbkGkjuFUfq0GMFpGLAIQ