# Python [SSL: CERTIFICATE_VERIFY_FAILED]: causes and fixes

> Python raises CERTIFICATE_VERIFY_FAILED when it cannot verify a server certificate. Fix the server chain or give Python the right CA bundle, not verify=False.

Source: https://ssl.sh/errors/python-certificate-verify-failed/
Last updated: 2026-11-10

**`[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed` means Python reached the server but could not verify its certificate against the CA certificates it trusts.** Usually the server is not sending its intermediate certificate, or the Python installation is looking at an empty, outdated or wrong CA bundle.

## Quick fix

A one-certificate chain means fixing the server; otherwise Python's CA bundle is the problem.

Decide whether the server or your Python environment is at fault. First test the server from a shell:

```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
```

If only one certificate comes back and the result is `Verify return code: 21` or `20`, the server chain is incomplete: fix the server (see [missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/)). If the chain is complete, fix the client side:

```bash
# See where Python's OpenSSL looks for CA certificates
python3 -c "import ssl; print(ssl.get_default_verify_paths())"

# requests: point it at a bundle that contains your root
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

# urllib / ssl: point OpenSSL at the bundle
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
```

On macOS with the python.org installer, run `/Applications/Python 3.x/Install Certificates.command` once per Python version.

## Causes

### 1. The server sends an incomplete chain

An incomplete server chain is the most frequent cause on public sites. OpenSSL needs every intermediate from the server; it does not fetch missing ones.

- **Confirm:** `openssl s_client -showcerts` shows only `depth=0`, or the [SSL checker](https://ssl.sh/tools/ssl-checker/) reports a chain problem.
- **Fix:** configure the server with the full chain (for Nginx, `fullchain.pem`). You cannot fix this permanently from the client.

### 2. Python cannot find any CA certificates

The python.org macOS installer ships its own OpenSSL without a CA bundle until you run Install Certificates.command. Self-compiled Python, minimal containers and some embedded builds behave the same way.

- **Confirm:** `ssl.get_default_verify_paths()` returns `cafile=None` and a `capath` that is empty or missing.
- **Fix:** run Install Certificates.command, install the `ca-certificates` package, or set `SSL_CERT_FILE` to a valid bundle.

### 3. requests does not see your corporate or internal root

requests verifies against certifi, which contains only public roots from Mozilla. A root added to the Windows or macOS store is invisible to it.

- **Confirm:** urllib succeeds but requests fails, or the server issuer is your company CA.
- **Fix:** pass the bundle explicitly:

```python
import requests
requests.get("https://intranet.example.com", verify="/etc/ssl/certs/company-bundle.pem")
```

Alternatively install `truststore` (Python 3.10+) and call `truststore.inject_into_ssl()` at startup so Python uses the OS trust store.

### 4. A TLS-inspecting proxy or antivirus

Proxies and "HTTPS scanning" features re-sign traffic with their own root. The `issuer` in `openssl s_client` output then names the proxy vendor.

- **Fix:** export the proxy root (PEM) from your IT team, append it to a copy of the bundle and point `REQUESTS_CA_BUNDLE` / `SSL_CERT_FILE` at it.

### 5. Strict verification in Python 3.13+

If the message ends with `Missing Authority Key Identifier` or names another certificate extension, the certificate itself does not meet RFC 5280 and strict mode rejects it. Reissue the internal CA and leaf certificates with RFC 5280 extensions.

### 6. Expired certificate or wrong clock

`certificate has expired` means the chain is trusted but the dates are wrong. Renew the certificate, or fix the client's clock.

## Fixes by environment

Every fix gives Python a current CA bundle that includes any private root; the steps depend on how Python was installed.

**macOS (python.org installer):** run Install Certificates.command. It installs certifi and links it as the OpenSSL bundle for that Python.

**macOS (Homebrew Python):** Homebrew's OpenSSL uses Homebrew's CA bundle. Run `brew upgrade` to refresh it; add private roots via `SSL_CERT_FILE`.

**Windows:** `ssl.create_default_context()` loads the Windows ROOT and CA stores, so urllib usually trusts roots that IT deployed by Group Policy. requests does not; use `verify=`, `REQUESTS_CA_BUNDLE` or truststore.

**Debian / Ubuntu / Docker:**

```bash
apt-get update && apt-get install -y ca-certificates
cp company-root.crt /usr/local/share/ca-certificates/ && update-ca-certificates
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
```

**RHEL family:** put the root in `/etc/pki/ca-trust/source/anchors/`, run `update-ca-trust`, and use `/etc/pki/tls/certs/ca-bundle.crt`.

**pip itself** has its own options; see [pip SSL certificate verify failed](https://ssl.sh/errors/pip-ssl-certificate-verify-failed/).

## Don't do this

- **`requests.get(url, verify=False)`.** It disables certificate and hostname checks; any machine on the path can impersonate the server and read tokens or passwords.
- **`ssl._create_default_https_context = ssl._create_unverified_context`.** This monkeypatch turns off verification for the whole process, including libraries you did not write.
- **`urllib3.disable_warnings()` to silence the InsecureRequestWarning.** Hiding the warning leaves the hole open and makes it harder to find later.
- **Clearing `VERIFY_X509_STRICT` globally** to make a malformed internal certificate pass. Fix the certificate instead.
