Private CA and troubleshooting
OpenSSL error messages explained: diagnose and fix TLS errors
Each common OpenSSL error message below comes with the command that diagnoses it and the fix. Messages are quoted as OpenSSL 3.6.1 prints them; where 1.1.1 or 3.0 wording differs, both are given. For a public website, start with the SSL Checker; use these commands to see exactly what OpenSSL sees.
Decode any OpenSSL error code
openssl errstr turns the hex code in an error line into its reason string.
openssl errstr 0A000086 0A000410 0308010C
error:0A000086:SSL routines::certificate verify failed
error:0A000410:SSL routines::ssl/tls alert handshake failure
error:0308010C:digital envelope routines::unsupported
A full line looks like 80FF14F601000000:error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:ssl/statem/statem_clnt.c:2126:. The first field is a thread ID, then the code, library, function and reason. For certificate verify failed, the actual cause is the verify error:num= line printed directly above it.
Fix "unable to get local issuer certificate" (error 20)
The client cannot find the certificate that signed the leaf; usually the server omits the intermediate, sometimes the client lacks the root.
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts < /dev/null 2>&1 \
| grep -E 'verify error| [0-9] s:|Verify return'
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
0 s:CN=www.example.com
Verify return code: 21 (unable to verify the first certificate)
One certificate in the chain means the server is missing its intermediate: configure the full chain (fullchain.pem for Certbot, leaf plus CA bundle for commercial certificates). If the chain is complete but the error persists, the client's trust store lacks the root: update ca-certificates or pass -CAfile. Same root cause as curl error 60 and missing intermediate certificate.
Fix "certificate has expired" (error 10) and "not yet valid" (error 9)
Compare the certificate dates with the clock of the machine doing the check.
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts < /dev/null 2>/dev/null \
| grep -E ' [0-9] s:|v:NotBefore'
date -u
0 s:CN=www.example.com
v:NotBefore: Nov 3 13:44:34 2026 GMT; NotAfter: Feb 1 13:44:33 2027 GMT
1 s:C=CN, O=Example Corp, CN=Example Intermediate CA
v:NotBefore: Sep 3 00:00:00 2025 GMT; NotAfter: Sep 2 23:59:59 2028 GMT
Tue Nov 10 02:30:00 UTC 2026
If the leaf expired, renew it and reload the server: a renewed file on disk does nothing until Nginx or Apache reloads. If an intermediate in the served chain expired, replace the bundle with the CA's current one. Error 9 or an expiry error on a valid certificate points to a wrong system clock, common on embedded devices without NTP. Browsers show this as NET::ERR_CERT_DATE_INVALID.
Fix "hostname mismatch" (error 62)
The name you connect to is not in the certificate's Subject Alternative Name.
openssl s_client -connect www.example.com:443 -servername shop.example.com \
-verify_hostname shop.example.com < /dev/null 2>/dev/null | grep -E 'verify error|Verify return'
openssl x509 -in cert.pem -noout -ext subjectAltName
verify error:num=62:hostname mismatch
Verify return code: 62 (hostname mismatch)
X509v3 Subject Alternative Name:
DNS:www.example.com, DNS:example.com
Either reissue the certificate with the missing name in the SAN, or fix the client to use a name that is listed. A wildcard covers exactly one label. If the certificate shown belongs to a completely different site, the server picked a default certificate because SNI was missing or the virtual host is not configured for that name. Browser equivalent: NET::ERR_CERT_COMMON_NAME_INVALID.
Fix "self-signed certificate in certificate chain" (error 19)
The chain ends in a root the client does not trust, typically a private CA or a TLS-inspecting proxy.
openssl s_client -connect www.example.com:443 -servername www.example.com < /dev/null 2>&1 \
| grep -E '^depth|verify error' | head -4
depth=2 C=CN, O=Example Corp, CN=Example Root CA
verify error:num=19:self-signed certificate in certificate chain
depth=1 C=CN, O=Example Corp, CN=Example Intermediate CA
depth=0 CN=www.example.com
If the root at the top is your organisation's CA, add it to the client's trust store (see running a private CA) instead of disabling verification. If it is an unexpected corporate or antivirus CA, traffic is being intercepted. Error 18 (self-signed certificate) means the leaf itself is self-signed. OpenSSL 1.1.1 spelled these without a hyphen (self signed). More in self-signed certificate in certificate chain.
Fix "wrong version number" and "packet length too long"
These mean the other side did not answer with TLS at all, most often because you hit a plain HTTP port.
openssl s_client -connect www.example.com:80 < /dev/null
curl -sI http://www.example.com:443/ | head -1
...:error:0A0000C6:SSL routines:tls_get_more_records:packet length too long:...
...:error:0A000139:SSL routines::record layer failure:...
HTTP/1.1 200 OK
OpenSSL 1.1.1 and 3.0 typically report wrong version number; newer releases (as tested with 3.6) may report packet length too long for the same situation. The curl line confirms that port 443 is answering plain HTTP. Check the listen 443 ssl directive in Nginx, the port in your client configuration, or whether a proxy expects CONNECT first. Mail clients see the same when they use implicit TLS on a STARTTLS port (587 vs 465).
Fix "handshake failure" (alert 40) and "no shared cipher"
Client and server found no common protocol version, cipher suite, group or signature algorithm.
openssl s_client -connect www.example.com:443 -servername www.example.com < /dev/null 2>&1 | grep -m1 alert
openssl s_client -connect www.example.com:443 -servername www.example.com -tls1_2 -cipher 'ALL:@SECLEVEL=0' -brief < /dev/null
...:error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:...:SSL alert number 40
On the server side the matching log line is no shared cipher. Typical causes: a cipher list that excludes everything the client offers, an ECDSA-only certificate with a client that only accepts RSA, an over-restrictive ssl_ecdh_curve/groups setting, or a TLS 1.2 server that requires a client certificate. Older releases such as 1.1.1 print sslv3 alert handshake failure for the same alert. Browsers show ERR_SSL_VERSION_OR_CIPHER_MISMATCH; compare lists with ciphers and protocols.
Fix "no protocols available" and "alert protocol version" (alert 70)
no protocols available is your own client refusing; alert protocol version is the server refusing the version you offered.
openssl s_client -connect www.example.com:443 -tls1_1 < /dev/null
openssl s_client -connect www.example.com:443 -tls1_1 -cipher 'DEFAULT:@SECLEVEL=0' < /dev/null
...:error:0A0000BF:SSL routines:tls_setup_handshake:no protocols available:...
...:error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version:...:SSL alert number 70
OpenSSL 3.x disables TLS 1.0/1.1 at the default security level, so legacy clients built on it cannot reach old servers until the server supports TLS 1.2. If an old device must keep working, upgrade it; lowering @SECLEVEL is a stopgap. Browsers show ERR_SSL_PROTOCOL_ERROR or a version mismatch error.
Fix "key values mismatch"
The certificate and private key do not belong together, or the chain file starts with the intermediate instead of the leaf.
openssl x509 -in fullchain.pem -noout -subject
openssl x509 -in fullchain.pem -noout -pubkey | openssl sha256
openssl pkey -in privkey.pem -pubout | openssl sha256
subject=C=CN, O=Example Corp, CN=Example Intermediate CA
SHA2-256(stdin)= d6f7b12a13ec99a751aad52deee4ce9339710d60fb07acefa86eb1e8d753648d
SHA2-256(stdin)= 3152719d149366da47e37904e2740fb6d296ade304092ce5308560b45d75e458
Here the first certificate in the file is the intermediate, so Nginx compares the wrong public key: put the leaf first (cat leaf.pem intermediate.pem > fullchain.pem). If the subject is right but the hashes differ, the key belongs to another certificate, often an older CSR; find the matching key or reissue. Nginx reports this as key values mismatch.
Fix "unsupported" when reading a PKCS#12 file
OpenSSL 3.x moved RC2 and other old algorithms to the legacy provider; read old .pfx/.p12 files with -legacy.
openssl pkcs12 -in old.pfx -info -noout
openssl pkcs12 -in old.pfx -info -noout -legacy
MAC: sha1, Iteration 2048
MAC length: 20, salt length: 16
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048
Error outputting keys and certificates
...:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:...:Algorithm (RC2-40-CBC : 0), Properties ()
...
Certificate bag
Certificate bag
The second command succeeds. To modernise the file, extract with -legacy and re-export without it, which produces AES-256-CBC with PBKDF2 (the 3.0+ default). Do the opposite (-export -legacy) only when an old consumer cannot read modern files. See PKCS#12 commands.
Fix "Mac verify error: invalid password?"
The PKCS#12 password is wrong, or the file has an empty password and you supplied none (or vice versa).
openssl pkcs12 -in bundle.p12 -noout -passin pass:wrong
openssl pkcs12 -in bundle.p12 -noout -passin pass:
Mac verify error: invalid password?
The second form passes an explicitly empty password, which some tools (Windows export without password, Java keystores converted with empty passwords) produce. If neither works and the file came from Windows or a Java keystore, check whether it was exported with a different password for the key and the store. Never put real passwords on the command line on shared machines; use -passin file:pass.txt or env:VAR.
Fix "bad decrypt" when loading a private key
The passphrase for an encrypted private key is wrong.
openssl pkey -in privkey.pem -noout
head -2 privkey.pem
Could not find private key of key from privkey.pem
...:error:1C800064:Provider routines:ossl_cipher_unpadblock:bad decrypt:...
...:error:11800074:PKCS12 routines:PKCS12_pbe_crypt_ex:pkcs12 cipherfinal error:...:maybe wrong password
-----BEGIN ENCRYPTED PRIVATE KEY-----
ENCRYPTED PRIVATE KEY (PKCS#8) or a Proc-Type: 4,ENCRYPTED header (traditional format) confirms the key is encrypted. Web servers cannot prompt for a passphrase at startup, so after confirming the right one, write an unencrypted copy with restrictive permissions: openssl pkey -in privkey.pem -out privkey-plain.pem && chmod 600 privkey-plain.pem.
Fix "Could not find certificate" and "no start line"
OpenSSL could not find a PEM block of the expected type: wrong file, DER instead of PEM, or damaged content.
file cert.pem
head -1 cert.pem
openssl x509 -in cert.pem -noout -subject
openssl asn1parse -in cert.pem | head -3
cert.pem: ASCII text
-----BEGIN CERTIFICATE-----
Could not find certificate from cert.pem
...:error:1E08010C:DECODER routines:OSSL_DECODER_from_bio:unsupported:...:No supported data to decode. Input structure: Certificate
...:error:0680009B:asn1 encoding routines:ASN1_get_object:too long:...
OpenSSL 1.1.1 prints unable to load certificate ... no start line instead. asn1parse failing with too long or header too long means the Base64 body is truncated or edited, typically by a copy-paste that dropped a line; get the file again. If file says data or Certificate, it is DER: add -inform DER. OpenSSL 3.6 itself tolerates a UTF-8 BOM and Windows CRLF line endings, but Java, Go and some control panels do not; normalise with dos2unix cert.pem or sed -i '1s/^\xEF\xBB\xBF//' cert.pem. Feeding a key where a certificate is expected gives the same error, so check the header line. Conversions are in converting formats.
Fix "unknown ca" (alert 48) and "certificate required" (alert 116)
In mutual TLS these alerts come from the server: it rejected the client certificate, or it got none.
openssl s_client -connect api.example.com:443 -servername api.example.com -brief < /dev/null 2>&1 | grep alert
openssl s_client -connect api.example.com:443 -servername api.example.com \
-cert client.crt -key client.key -brief < /dev/null 2>&1 | grep alert
...:error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:...:SSL alert number 116
...:error:0A000418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca:...:SSL alert number 48
Run s_client without -brief and read Acceptable client certificate CA names to see which CAs the server accepts, then present a certificate issued by one of them, including its intermediate (-cert_chain). Public TLS certificates increasingly lack the clientAuth EKU, so servers that check EKU reject them; see client auth EKU removed. Servers log the matching certificate verify failed with the exact reason.
FAQ
How do I read a long OpenSSL error line?
The format is thread-id:error:code:library:function:reason:file:line:extra. The reason (for example certificate verify failed) and the hexadecimal code are what matter; openssl errstr prints the reason for a code you found in a log. The file and line point into OpenSSL's source and rarely help.
Why does the same certificate work in the browser but fail in curl, Python or Java?
Browsers ship their own root stores and work around missing intermediates; command-line tools and language runtimes use the system store, a bundled store (certifi, Java cacerts) or nothing. Fix the server chain first, then make sure the runtime trusts the right root. See curl error 60 and Java PKIX path building failed.
Node.js prints error:0308010C:digital envelope routines::unsupported. Is that a certificate problem?
Usually not. The code only means OpenSSL 3 refused an algorithm that now lives in the legacy provider. In Node.js 17 and later it is typically an old build tool hashing with MD4; upgrade the tool. With PKCS#12 files it means RC2 or 3DES encryption, fixed with -legacy.