# SSL CERTIFICATE_VERIFY_FAILED Behind a Proxy

> A plain proxy tunnel never causes CERTIFICATE_VERIFY_FAILED. We reproduced it 42 times across 7 clients: the error tail names the cause, and which fix works.

[Home](https://quanticdata.io/)/[Blog](https://quanticdata.io/blog/)/SSL CERTIFICATE_VERIFY_FAILED Behind a Proxy

# SSL: CERTIFICATE_VERIFY_FAILED behind a proxy: what the error tail tells you

TroubleshootingOct 5, 2026·8 min read·By [Aldo Morese](https://quanticdata.io/about/), founder of QuanticData

A Python traceback reading SSL CERTIFICATE_VERIFY_FAILED with the last words of the message highlighted; the card maps each ending to its cause: self-signed certificate in certificate chain means an intercepting proxy, unable to get local issuer certificate means a missing or replaced CA bundle, self-signed certificate means a single self-signed leaf.

On this page [Tunnel or interception: the one question that matters](/blog/ssl-certificate-verify-failed-proxy/#tunnel-vs-interception) [What each error tail means](/blog/ssl-certificate-verify-failed-proxy/#error-tails) [Python: which setting your library actually reads](/blog/ssl-certificate-verify-failed-proxy/#python-matrix) [pip behind a proxy](/blog/ssl-certificate-verify-failed-proxy/#pip) [curl and Node: two traps](/blog/ssl-certificate-verify-failed-proxy/#curl-node) [When the error is not about certificates at all](/blog/ssl-certificate-verify-failed-proxy/#not-a-cert-problem) [Where this applies on QuanticData](/blog/ssl-certificate-verify-failed-proxy/#quanticdata) [A five-step checklist](/blog/ssl-certificate-verify-failed-proxy/#checklist)

A normal HTTP proxy cannot cause `SSL: CERTIFICATE_VERIFY_FAILED`. With a CONNECT tunnel, TLS runs from your client to the website and the proxy never sees a certificate. If you get this error behind a proxy, one of two things is true: something in the path is re-signing the site with its own certificate, or your client is checking against the wrong CA bundle. On 5 October 2026 we reproduced the error 42 times across seven clients. The last words of the message tell you which case you have.

## Tunnel or interception: the one question that matters

An HTTPS request through a proxy normally starts with `CONNECT example.com:443`. The proxy answers `200 Connection established` and then just copies bytes. The certificate your client checks is the website's own, signed by a public CA, so verification passes just as it would without a proxy. In our lab, a plain tunnel returned 200 to requests, urllib, curl and Node with no extra settings. Proxy credentials, rotation and country targeting change nothing here, because none of them touch TLS.

An intercepting proxy also accepts the CONNECT, but then finishes the TLS handshake itself. It shows your client a certificate for `example.com` that it issued on the fly from its own root CA. Corporate SSL inspection, antivirus "web shields", debugging proxies and web unlockers all work this way. Your client has never seen that root, so it refuses the connection. That refusal is the error you're looking at. It isn't a bug: it's TLS doing its job.

You can tell which case you have in one command. Ask for the certificate through the proxy and read the issuer:

```
openssl s_client -proxy PROXY_HOST:PORT -connect example.com:443 \
  -servername example.com </dev/null 2>/dev/null | grep -E " s:| i:|Verify return"
```

Through our plain tunnel, the issuer was a public CA and verification succeeded. Through the intercepting proxy, it was the lab's own root, and openssl printed `Verify return code: 19 (self-signed certificate in certificate chain)`. If the issuer names your company, your antivirus or a debugging tool, you've found the cause.

**Try it on your own targets.** Every QuanticData account gets $2 of free API usage per month.

[Start free — $2/month included](https://quanticdata.io/signup/?utm_source=blog&amp;utm_medium=website&amp;utm_campaign=blog-inline&amp;utm_content=ssl-certificate-verify-failed-proxy)[Explore Residential Proxies from $0.80/GB](https://quanticdata.io/residential-proxies/)

## What each error tail means

Python, curl and Node all start the message the same way and put the diagnosis at the end. These are the three endings we reproduced and what each one meant in our tests:

| End of the message | curl / Node wording | What happened | Fix |
| --- | --- | --- | --- |
| `self-signed certificate in certificate chain` | curl (60) same text; Node `SELF_SIGNED_CERT_IN_CHAIN` | An intercepting proxy sent its own root along with the forged certificate. Your trust store doesn't include that root. | Trust that root CA for this client only |
| `unable to get local issuer certificate` | curl (60) same text; Node `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | Either the proxy sent only the leaf, or your CA bundle lacks the issuer. We also triggered it on a plain tunnel by pointing `REQUESTS_CA_BUNDLE` at a single CA file. | Use a bundle that contains both the public roots and the proxy's CA |
| `self-signed certificate` | curl (60) same text; Node `DEPTH_ZERO_SELF_SIGNED_CERT` | The proxy presents one self-signed certificate per host, with no CA behind it | Fix the proxy, or trust that exact certificate |

The middle row causes the most confusion. The same text appears when an interceptor sends an incomplete chain and when your own configuration has thrown away the public roots. If the error started right after you set a CA variable, suspect the variable first.

## Python: which setting your library actually reads

This is where most of the time goes, because each library reads a different setting. We ran the same intercepted request with each setting, from a separate process so nothing leaked between runs:

| Setting | requests 2.34.2 | httpx 0.28.1 | Effect on a normal site |
| --- | --- | --- | --- |
| `REQUESTS_CA_BUNDLE=ca.pem` | Intercepted request passes | Ignored, still fails | requests now fails on a plain tunnel with "unable to get local issuer certificate" |
| `SSL_CERT_FILE=ca.pem` | Ignored, still fails | Intercepted request passes | Not tested for httpx |
| `verify="ca.pem"` (requests) / SSL context with `cafile` (httpx, aiohttp) | Passes | Passes | Scoped to that call or client |
| `verify=` a bundle of public roots plus the proxy CA | Passes | not run | Plain tunnel also passes: 200 |
| `verify=False` | Passes, emits `InsecureRequestWarning` | not run | Verification is off for everything |

Two results from this table are worth remembering. First, `REQUESTS_CA_BUNDLE` *replaces* the trust store rather than adding to it. Point it at your proxy's CA alone and every site you reach through a normal tunnel breaks with a different error. Build a combined file instead:

```
import certifi, requests

# public roots + the intercepting proxy's root, in one file
with open("combined.pem", "w") as out:
    out.write(open(certifi.where()).read())
    out.write(open("proxy-ca.pem").read())

proxies = {"https": "http://USER:PASS@PROXY_HOST:PORT"}
r = requests.get("https://example.com/", proxies=proxies,
                 verify="combined.pem", timeout=30)
print(r.status_code)
```

Second, httpx and requests don't share environment variables. A fix that works in a requests script won't carry over when you switch libraries. With httpx or aiohttp, pass an explicit context and you won't depend on either variable:

```
import ssl, httpx

ctx = ssl.create_default_context(cafile="combined.pem")
with httpx.Client(proxy="http://USER:PASS@PROXY_HOST:PORT", verify=ctx) as client:
    print(client.get("https://example.com/").status_code)
```

aiohttp takes the same context through its `ssl=` argument. With the default settings, aiohttp raised `ClientConnectorCertificateError` wrapping the same "self-signed certificate in certificate chain" text. Python's built-in urllib behaved like the others: 200 through the plain tunnel, the same CERTIFICATE_VERIFY_FAILED through the interceptor.

## pip behind a proxy

"pip proxy ssl certificate_verify_failed" is the top autocomplete for this error, and pip 25.2 behaves differently from the libraries above. It checks certificates against the operating system's trust store. On macOS, our intercepted install failed with a system message saying the lab CA "is not trusted", not the usual OpenSSL wording. With `--use-deprecated=legacy-certs`, pip went back to its bundled roots and printed the classic `self-signed certificate in certificate chain`.

That has two practical consequences. If your company already pushes its inspection CA into the system store, current pip usually works with no flags. If it doesn't, give pip the CA explicitly and keep verification on:

```
pip install --proxy http://USER:PASS@PROXY_HOST:PORT --cert proxy-ca.pem package-name
# or once, in pip.conf / pip.ini:
# [global]
# cert = /path/to/proxy-ca.pem
```

In our run, `--cert` got pip past the handshake. Avoid `--trusted-host` as a permanent fix, because it turns off verification for that host.

## curl and Node: two traps

**curl `--proxy-insecure` does not help.** It skips checking the certificate of an *HTTPS proxy*, meaning the proxy's own TLS. It doesn't affect the certificate presented for the target. Through our intercepting proxy, curl 8.7.1 still exited with code 60 when given `--proxy-insecure`. `--cacert proxy-ca.pem` returned 200, as did `-k`, which turns verification off entirely.

```
curl --proxy http://PROXY_HOST:PORT --proxy-user 'USER:PASS' \
  --cacert ./proxy-ca.pem 'https://example.com/'
```

**Node appends, Python replaces.** Starting Node with `NODE_EXTRA_CA_CERTS=proxy-ca.pem` fixed the intercepted request and left the plain tunnel working: both returned OK in the same process. That's the behaviour most people expect from `REQUESTS_CA_BUNDLE`, and it's exactly what that variable does not do.

## When the error is not about certificates at all

A common mistake is writing the proxy URL as `https://` when the proxy speaks plain HTTP. It looks like a TLS problem but it isn't one. With `proxies={"https": "https://PROXY_HOST:PORT"}`, requests opened TLS to the proxy itself. When our test proxy replied with a plain-text 400, requests raised a `ProxyError` saying the proxy "appears to only use HTTP and not HTTPS". When the proxy stayed silent instead, the call hung until the 15-second read timeout. Neither case produces CERTIFICATE_VERIFY_FAILED. The fix is the scheme: the key is `"https"` because the *target* is HTTPS, and the value stays `http://` because the proxy is HTTP. We cover how to read that error in [requests.exceptions.ProxyError, explained](https://quanticdata.io/blog/requests-exceptions-proxyerror/), and the tunnel-level failures in [ERR_TUNNEL_CONNECTION_FAILED](https://quanticdata.io/blog/err-tunnel-connection-failed/).

## Where this applies on QuanticData

The [residential](https://quanticdata.io/residential-proxies/), mobile, ISP and datacenter gateways are CONNECT tunnels: TLS stays between your client and the site. If you see CERTIFICATE_VERIFY_FAILED on one of them, look for an interceptor on your side (office network, antivirus, a debugging proxy left running) or a CA variable you set yourself. The [proxy tester](https://quanticdata.io/tools/proxy-tester/) tells you whether the gateway itself answers.

The [Web Unlocker](https://quanticdata.io/web-unlocker/) is different, and deliberately so. It ends your TLS session so it can read and rewrite the request, then opens its own connection to the site. Every HTTPS request through it fails verification until your client trusts its CA. Download the certificate with your API key and pass it to the client. Don't turn verification off:

```
curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://app.quanticdata.io/api/v1/scraper/unlock/ca -o qp-unlocker-ca.pem

curl --proxy http://unlock.quanticdata.io:9000 \
  --proxy-user 'unlock-YOUR_ID-country-us:YOUR_PROXY_PASSWORD' \
  --cacert ./qp-unlocker-ca.pem 'https://example.com/'
```

If you'd rather not manage certificates at all, the [web scraping API](https://quanticdata.io/web-scraping-api/) takes a URL over a normal HTTPS call to our API and returns the page. There's no proxy in your client and no CA to trust, and failed requests are not billed.

## A five-step checklist

1. Read the end of the error message and match it to the table above.

2. Run `openssl s_client -proxy` and check who issued the certificate.

3. If it's an interceptor you trust, get its root CA as a PEM file.

4. Add that CA for this client only: a combined bundle for requests, an SSL context for httpx and aiohttp, `--cert` for pip, `--cacert` for curl, `NODE_EXTRA_CA_CERTS` for Node.

5. Re-run against both an intercepted site and a normal one, so a replaced bundle can't hide behind a fix that only works for one of them.

Our full checklist for proxy errors that aren't about certificates is in [proxy not working: the checklist](https://quanticdata.io/blog/proxy-not-working-checklist/).

### Sources & further reading

- [Requests documentation: SSL certificate verification](https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification)

- [HTTPX documentation: SSL](https://www.python-httpx.org/advanced/ssl/)

- [pip documentation: HTTPS certificates](https://pip.pypa.io/en/stable/topics/https-certificates/)

- [cURL: SSL certificate verification](https://curl.se/docs/sslcerts.html)

- [Node.js CLI: NODE_EXTRA_CA_CERTS](https://nodejs.org/api/cli.html#node_extra_ca_certsfile)

## FAQ

Quick answers on ssl certificate_verify_failed proxy.

[Something else? Ask us](mailto:hello@quanticdata.io)

### Can a normal proxy cause SSL CERTIFICATE_VERIFY_FAILED?

No. With a CONNECT tunnel, TLS runs end to end and the client checks the website's own certificate. In our 5 October 2026 tests, a plain tunnel returned 200 to requests, urllib, curl and Node. The error means something is intercepting TLS or your CA bundle is wrong.

### What does "self-signed certificate in certificate chain" mean behind a proxy?

An intercepting proxy, such as corporate SSL inspection, an antivirus or a debugging tool, re-signed the site with its own root CA, and your client does not trust that root. Trust that CA for the client you are using instead of disabling verification.

### Why did REQUESTS_CA_BUNDLE break other sites?

Because it replaces the trust store instead of adding to it. Pointing it at a single proxy CA made a normal site fail with "unable to get local issuer certificate". Use a file that contains the certifi roots plus the proxy CA.

### Is verify=False an acceptable fix?

It makes the request pass, and requests warns with InsecureRequestWarning, but it turns off verification for everything that call touches. Use it only to confirm the diagnosis, then switch to verify pointing at a bundle that includes the proxy CA.

### How do I fix pip SSL CERTIFICATE_VERIFY_FAILED behind a corporate proxy?

pip 25.2 checks the operating system trust store, so it works when IT has installed the inspection CA there. Otherwise pass the CA with --cert proxy-ca.pem, or set cert in pip.conf. Avoid --trusted-host as a permanent setting.

### Do I need a CA certificate for QuanticData proxies?

Not for the residential, mobile, ISP or datacenter gateways, which are plain CONNECT tunnels. Yes for the Web Unlocker, which terminates TLS by design: download its CA with your API key and pass it to your client.

## Proxies that leave your TLS alone

QuanticData residential, mobile, ISP and datacenter gateways are plain CONNECT tunnels: no certificate to install, country targeting in the username. Failed API requests are never billed.

[Start free — $2/month included](https://quanticdata.io/signup/?utm_source=blog&amp;utm_medium=website&amp;utm_campaign=blog-end&amp;utm_content=ssl-certificate-verify-failed-proxy)[Explore Residential Proxies from $0.80/GB](https://quanticdata.io/residential-proxies/)

## Related reading

[Troubleshooting requests.exceptions.ProxyError: How to Fix ProxyError says the connection to the proxy failed, never that the site blocked you. We ran ten isolated failure modes against a controlled proxy on three requests/urllib3 stacks and mapped each cause to the exception class and the exact text it prints — including two causes that stopped being ProxyError in urllib3 2.x. Read more](https://quanticdata.io/blog/requests-exceptions-proxyerror/) [Troubleshooting IPv6 Proxy Not Working? The Five Checks Five checks that settle almost every "IPv6 proxy not working" ticket: whether the target publishes an AAAA record at all (57 of 102 major sites do not), DNS resolved at the exit with socks5h, credentials the client can actually pass, the error you really got, and pacing by /64 when the site answers but blocks. Read more](https://quanticdata.io/blog/ipv6-proxy-not-working/) [Troubleshooting Read the Block: 4 Shapes, 4 Settings That Work A refused reply has a shape, and the shape tells you the setting. Across 53 sites measured between 26 and 28 September 2026 we met four: a short page with no canonical (Amazon, 2,007 bytes, 0 words), a redirect to the login form (Glassdoor, rendered: canonical /member/profile/login, 54 words), a rate-limit reply (Expedia, rendered: 381,468 bytes, 45 words, no h1) and an empty rendered page (Zillow, 759 words plain, 0 rendered). Each one resolves into a fetch mode, an exit and, where the page never exists, a collector with rows and seconds. Read more](https://quanticdata.io/blog/read-the-block-and-what-to-do/)

---

Source: https://quanticdata.io/blog/ssl-certificate-verify-failed-proxy/ · Site index for AI: https://quanticdata.io/llms.txt
