Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Troubleshooting11 min read

SSL Certificate Verify Failed: Diagnosing Trust-Chain Errors in Python, curl, and Web3 RPC Clients

A reproducible method for tracing SSL: CERTIFICATE_VERIFY_FAILED from the client trust store through the TLS handshake to the server chain, with fixes that don't disable verification.

TL;DR

SSL: CERTIFICATE_VERIFY_FAILED means the client could not build a trusted path from the server's leaf certificate to a root CA in its local trust store. The fix is almost never to disable verification; it is to identify which of the four links in the chain is broken: the client trust store, the intermediate certificates the server sends, the hostname/SNI match, or the clock. This article gives a reproducible diagnostic sequence you can run with openssl, curl, and Python, then applies the same method to Web3 RPC clients that fail against HTTPS endpoints.

What the error actually reports

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed is emitted by the TLS library (OpenSSL, BoringSSL, Secure Transport, or SChannel) when path validation fails. The trailing detail is the useful part: unable to get local issuer certificate means the client cannot find the issuer of a certificate in the chain; certificate has expired means a validity window was violated; hostname mismatch means the SAN list does not cover the name you requested; self-signed certificate in certificate chain means an untrusted root was presented.

The error is a client-side statement about trust, not proof that the server is broken. A server can be perfectly valid for browsers while failing for a Python process that ships its own CA bundle, or for a container that has no ca-certificates package installed. Treat the message as a pointer to one of four links: local trust store, server-sent intermediates, name/SNI agreement, and system time.

  • Trust store: does the client have the root CA that signed the chain?
  • Chain: does the server send every intermediate needed to reach that root?
  • Identity: does the certificate cover the hostname and is SNI sent correctly?
  • Time: is the client clock inside the certificate's notBefore/notAfter window?

Assumptions and prerequisites

The procedure below assumes you can run commands on the failing client host, that outbound TCP 443 is permitted, and that you know the exact hostname the client is connecting to. You need openssl (1.1.1 or 3.x), curl, and the same language runtime that produced the error. For Web3 work, you need the RPC URL your application uses; keep it in an environment variable such as RPC_URL rather than hard-coding it.

If you are choosing an endpoint rather than debugging one, the concrete provider decision belongs in RPC Assistant; this page focuses on the diagnostic method that applies to any HTTPS endpoint, including the ones you reach through the OnFinality API service and dedicated node offerings.

Mental model: four independent checks

Path validation is a sequence, and each step can fail independently. First, the client parses the presented chain. Second, it walks from the leaf through intermediates looking for an issuer it already trusts. Third, it checks that the leaf's subjectAltName matches the requested host. Fourth, it checks validity dates against the local clock. Any failure produces a variant of the same headline error, which is why copying a fix from a forum thread often does not work: the thread solved a different link.

This is also why pip install certifi sometimes appears to fix the problem and sometimes does not. Certifi only changes the bundle a Python process uses if that process is actually configured to read it. If the failing code is inside a third-party library, or if the environment variable is set in a shell that the service does not inherit, the bundle swap has no effect.

Step 1: Inspect the chain the server sends

Start at the server. This command prints the leaf and every intermediate the server presents, without relying on the client's trust store:

  • Count the certificates. A typical public chain is leaf plus one or two intermediates; a bare leaf with no intermediate is a common cause of unable to get local issuer certificate.
  • Note the issuer of the last certificate. That name is the root the client must trust.
  • Check notBefore and notAfter on each certificate with openssl x509 -noout -dates.
openssl s_client -connect your-rpc-host.example:443 -servername your-rpc-host.example -showcerts </dev/null 2>/dev/null | openssl crl2pkcs7 -nocrl -certfile /dev/stdin | openssl pkcs7 -print_certs -noout

Step 2: Compare against the client trust store

Now ask the client's own TLS stack to validate the same host. curl uses the system store on most Linux distributions and the platform store on macOS and Windows:

  • If curl succeeds but Python fails, the problem is the Python CA bundle, not the server.
  • If curl fails with certificate has expired, check the client clock before anything else.
  • If curl fails with unable to get local issuer certificate, the server is probably not sending an intermediate.
curl -vI https://your-rpc-host.example 2>&1 | grep -E "SSL certificate|subject:|issuer:|expire|TLS"

Step 3: Reproduce inside the failing runtime

Python's default verification uses the bundle compiled into or configured for the interpreter. Print the path it is actually using, then attempt a minimal connection that isolates the TLS layer from application code:

  • If this snippet fails but curl succeeds, the interpreter's bundle is the fault line.
  • If it succeeds, the failure is in the HTTP client layer (requests, httpx, web3.py, ethers) or in a proxy that terminates TLS.
  • Record the exact exception text; the suffix after the colon is the actionable clue.
import ssl, socket, certifi

print("default verify paths:", ssl.get_default_verify_paths())
print("certifi bundle:", certifi.where())

host = "your-rpc-host.example"
ctx = ssl.create_default_context()
with socket.create_connection((host, 443), timeout=10) as sock:
    with ctx.wrap_socket(sock, server_hostname=host) as tls:
        print("negotiated:", tls.version())
        print("peer subject:", tls.getpeercert().get("subject"))

Step 4: Fix the trust store without disabling verification

When the interpreter bundle is stale or missing, point the process at a maintained bundle explicitly. This is the same mechanism the widely shared SSL_CERT_FILE workaround uses, but applied deliberately rather than as a guess:

  • Set these variables in the environment that actually runs the service, not only in your interactive shell.
  • On Debian/Ubuntu hosts, apt-get install -y ca-certificates and update-ca-certificates fix the system store for curl and most runtimes.
  • On Windows, the platform store is used by some runtimes and not others; the certifi path is the most portable fix for Python.
  • Never ship verify=False or rejectUnauthorized: false as the resolution. It converts a trust error into an undetected man-in-the-middle exposure.
pip install --upgrade certifi
python -c "import certifi; print(certifi.where())"
export SSL_CERT_FILE="$(python -c 'import certifi; print(certifi.where())')"
export SSL_CERT_DIR="$(dirname "$SSL_CERT_FILE")"

# verify the fix in the same shell
python -c "import urllib.request; urllib.request.urlopen('https://your-rpc-host.example', timeout=10); print('ok')"

Applying the method to Web3 RPC clients

The same four checks apply when a JSON-RPC call over HTTPS fails. Node providers, load balancers, and corporate proxies all sit between your client and the chain, and any of them can present a chain your runtime does not trust. A minimal probe separates transport failure from RPC failure:

  • ssl_verify_result=0 means the TLS handshake validated; a non-zero value maps to an OpenSSL verify error code.
  • If curl returns a JSON-RPC result but your application fails, the application's HTTP client has a different trust configuration.
  • If a proxy is in the path, inspect the proxy's certificate rather than the origin's; the client only sees the proxy chain.
  • For endpoint selection and network coverage, see the OnFinality networks directory and the RPC endpoint guide rather than guessing a hostname.
curl -sS -X POST "$RPC_URL" \
  -H 'content-type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' \
  -w '\nhttp_code=%{http_code} tls=%{ssl_verify_result}\n'

Verification: what success looks like

A resolved case produces consistent evidence across layers. Use a small results table to record the before/after state so the fix is auditable rather than anecdotal; fill in the values you observe on your own host.

  • | Check | Command | Expected after fix |
  • | Server chain | openssl s_client -showcerts | Leaf plus intermediates, last issuer is a public root |
  • | System client | curl -vI https://host | SSL certificate verify ok |
  • | Runtime TLS | Python create_default_context() snippet | Handshake completes, peer subject printed |
  • | RPC transport | curl POST with ssl_verify_result | 0 and a JSON-RPC result |
  • | Application | your normal request path | No CERTIFICATE_VERIFY_FAILED in logs |

Troubleshooting the cases that survive the basic fix

Some failures persist after the bundle is corrected. A container built FROM scratch or a minimal image may have no CA store at all; copy the bundle in and set SSL_CERT_FILE in the image. A service running under systemd or Kubernetes may not inherit shell exports; set the variable in the unit file or the pod spec. A corporate TLS-inspecting proxy presents its own root, which must be added to the client bundle deliberately and with the security team's knowledge.

Clock skew is easy to miss on VMs restored from snapshots and on embedded devices; date -u and an NTP sync resolve certificate has expired errors that have nothing to do with the certificate. Finally, if only one hostname fails while others on the same provider succeed, suspect SNI or a virtual-host misconfiguration on the server side and report it with the openssl s_client output.

Limitations and tradeoffs

Pinning a specific bundle path makes behavior reproducible but adds a maintenance task: bundles expire and roots are retired, so the path must be refreshed with the runtime image. Relying on the system store is simpler but varies by distribution and base image, which is why the same code can pass on a laptop and fail in CI. Disabling verification is never a fix; it removes the only signal that distinguishes a misconfigured proxy from an active interception.

For production RPC traffic, the durable answer is to standardize the base image, ship a current CA bundle, and monitor TLS handshake failures as a distinct metric from RPC error rates. That way a trust-store regression surfaces before it looks like a chain outage.

Next steps

Once the handshake is clean, confirm that the endpoint you are using is the one you intend and that its network coverage matches your application. Review the OnFinality API service for managed RPC access, compare plans on the RPC pricing page, and use dedicated nodes when you need isolated throughput and predictable TLS termination. If you are still selecting a provider, the RPC Assistant can return a concrete recommendation while this page remains your diagnostic reference.

  • Standardize the CA bundle in your base image and CI runner.
  • Add a startup probe that performs a TLS handshake against each configured RPC host.
  • Log the OpenSSL verify result alongside RPC errors so trust failures are not misclassified.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started