SSLCertVerificationError is raised when a TLS client cannot validate the server's certificate against its trusted CA store, the requested hostname, or the current validity period. The error message usually names the specific failure, such as an expired certificate, a self-signed certificate, or an inability to get the local issuer certificate. This article explains the verification mechanism, walks through a reproducible diagnostic procedure using openssl and Python, and shows how to fix each failure class without disabling verification. It also covers the tradeoffs of custom CA bundles, proxy interception, and when to escalate to the server operator.
What SSLCertVerificationError actually means
ssl.SSLCertVerificationError is Python's exception for a failed TLS handshake caused by certificate validation. The client reached the server, received its certificate chain, and then rejected it because one or more checks did not pass. The exception is a subclass of ssl.SSLError, so code that catches SSLError will also catch it.
The error text after CERTIFICATE_VERIFY_FAILED is the important part. Common suffixes include certificate has expired, self-signed certificate, unable to get local issuer certificate, hostname mismatch, and certificate is not yet valid. Each points to a different fix, so read the suffix before changing any code.
This error is distinct from a connection refusal or timeout. If the TCP connection never completes, you will see ConnectionError or TimeoutError instead. Verification errors mean the network path worked and the TLS layer made a trust decision.
- Expired or not-yet-valid: the certificate's validity window does not include the current time.
- Self-signed or untrusted issuer: the chain terminates at a certificate the client does not trust.
- Unable to get local issuer certificate: the server did not send an intermediate certificate, or the client's CA bundle is incomplete.
- Hostname mismatch: the certificate's subject alternative names do not include the host you requested.
Assumptions and prerequisites for the diagnostic workflow
The examples below assume Python 3.8 or later, the requests library, and the openssl command-line tool. They also assume you can reach the target host on port 443. If you are working inside a container or CI runner, confirm that outbound TLS is allowed before debugging certificates.
For blockchain RPC endpoints, the same verification rules apply. A managed endpoint such as those described in the multi-chain RPC endpoints guide should present a publicly trusted certificate. If it does not, treat that as a signal to inspect the endpoint configuration rather than to disable verification.
The TLS verification mental model
Certificate verification has four independent checks. First, the client builds a chain from the server's leaf certificate through any intermediates to a root in its trust store. Second, it checks that every certificate in the chain is within its validity period. Third, it checks that the leaf certificate's subject alternative names match the hostname used in the connection. Fourth, it checks that the leaf certificate is authorized for server authentication.
A failure in any check produces SSLCertVerificationError. The error message identifies which check failed, but the underlying cause can be on either side. An expired certificate is a server problem. An outdated CA bundle is a client problem. A corporate proxy that terminates TLS is a network policy problem. Separating these is the core of diagnosis.
Reproducing the error with a minimal Python client
Start with a small script that makes one HTTPS request and prints the full exception. This isolates the failure from application logic and retry wrappers.
import requests
from requests.exceptions import SSLError
url = "https://RPC_HOST" # replace with the host you are debugging
try:
response = requests.get(url, timeout=10)
print("status", response.status_code)
except SSLError as exc:
print(type(exc).__name__)
print(exc)Inspecting the server certificate chain with openssl
The openssl s_client command shows exactly what the server sends. Run it with -servername so SNI is set correctly, and with -showcerts to print the full chain.
openssl s_client -connect RPC_HOST:443 -servername RPC_HOST -showcerts </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltNameInterpreting the verification result
The output of the previous command tells you which check is likely failing. Compare the notBefore and notAfter dates to the current UTC time. Compare the subjectAltName entries to the hostname you requested. Compare the issuer to the root certificates in your client's trust store.
The following table is a procedure, not a set of measured results. Fill in the observed values from your own environment.
- | Check | Command or field | What a pass looks like | What a failure implies |
- | Validity |
-datesoutput | Current time is between notBefore and notAfter | Server certificate expired or clock skew | - | Hostname |
subjectAltName| Requested host appears in the list | Wrong certificate deployed or wrong hostname used | - | Chain |
-showcertsoutput | Leaf plus intermediates, ending at a known root | Missing intermediate or untrusted root | - | Trust store |
certifi.where()in Python | Path exists and file is recent | Outdated or missing CA bundle |
Fixing client-side CA bundle problems
If the error is unable to get local issuer certificate and the server chain looks complete, the client's trust store is the likely culprit. Python's requests uses certifi by default, but some environments override it with REQUESTS_CA_BUNDLE or SSL_CERT_FILE. Check which bundle is actually in use before replacing anything.
On macOS, the python.org installer ships an Install Certificates.command script that populates the default bundle. On Linux containers, the ca-certificates package provides the system store. In both cases, updating the bundle is safer than pinning a single server certificate.
import certifi
import os
print("certifi bundle:", certifi.where())
print("REQUESTS_CA_BUNDLE:", os.environ.get("REQUESTS_CA_BUNDLE"))
print("SSL_CERT_FILE:", os.environ.get("SSL_CERT_FILE"))Handling corporate proxies and custom roots
Many enterprise networks terminate TLS at a proxy and re-sign traffic with an internal root. In that case the server certificate is valid but the client does not trust the proxy's root. The correct fix is to add the corporate root to the client's trust store, not to disable verification.
If you must use a custom bundle for one application, set REQUESTS_CA_BUNDLE to a combined file that contains both the public roots and the corporate root. Keep that file under configuration management so it does not drift. For infrastructure that talks to blockchain RPC endpoints, review the dedicated node and API service options to see whether a private network path avoids proxy interception entirely.
When the server is at fault
If the certificate is expired, self-signed, or missing an intermediate, the fix belongs to the server operator. You can confirm a missing intermediate by checking whether openssl s_client shows only the leaf certificate. Browsers sometimes hide this by caching intermediates, which is why a Python client can fail where a browser succeeds.
For RPC providers, certificate problems are an operational signal. A managed endpoint should be monitored for expiry and chain completeness. If you are evaluating providers, the RPC endpoints guide and the RPC pricing page describe what to expect from a production-grade service.
Why disabling verification is a last resort
Setting verify=False in requests suppresses the exception but also removes authentication of the server. An attacker on the network path can then present any certificate and read or modify traffic. The same applies to ssl._create_unverified_context() and to curl -k.
If you need to test a self-signed endpoint during development, pass the specific certificate file with verify="/path/to/cert.pem" instead of disabling verification. That keeps the trust decision explicit and scoped to one host.
import requests
# Development only: trust one specific certificate file.
response = requests.get(
"https://RPC_HOST",
timeout=10,
verify="/path/to/server-cert.pem",
)
print(response.status_code)Verifying the fix and monitoring for recurrence
After changing the CA bundle or server certificate, rerun the minimal client and the openssl s_client command. A successful request returns an HTTP status without an SSLError. A successful chain inspection shows a leaf, any intermediates, and an issuer that matches a root in your trust store.
Certificate expiry is predictable, so monitor it. A simple check is to parse the notAfter date and alert when it is within 30 days. For teams running many endpoints, centralize this check rather than relying on individual developers to notice. The networks directory lists the chains OnFinality supports, which is a useful starting point for inventory.
Next steps for endpoint and provider decisions
Once verification is working, the remaining question is which endpoint to use. That is a provider and network selection problem rather than a TLS problem. Use the OnFinality Learn library for protocol and integration guidance, and consult RPC Assistant for short, direct recommendations on endpoints and networks.
If you are building against a specific chain, start from the network guide and then confirm the endpoint's certificate chain with the procedure above. Verification should be boring and automatic; if it is not, fix the trust path before adding application complexity.