Skip to content

Fix: Python SSL: CERTIFICATE_VERIFY_FAILED

FixDevs · (Updated: )

Part of:  Python Errors

Quick Answer

How to fix Python SSL CERTIFICATE_VERIFY_FAILED error caused by missing root certificates on macOS, expired system certs, corporate proxies, and self-signed certificates in requests, urllib, and httpx.

The TLS Handshake That Never Trusted You

Personally, I have lost more half-days to this error than to any other Python networking problem. The fix is usually trivial once you know what to check, but each cause looks identical from the traceback. I have learned to walk a fixed diagnostic sequence rather than guess. You run a Python script that makes an HTTPS request and get:

ssl.SSLCertificateError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)

Or through the requests library:

requests.exceptions.SSLError: HTTPSConnectionPool(host='api.example.com', port=443):
Max retries exceeded with url: /data (Caused by SSLError(SSLCertificateError("bad handshake:
Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')])")))

Or via urllib:

urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate (_ssl.c:1129)>

Or after installing Python on macOS:

ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:852)

Note: This error is distinct from the pip-specific SSL error (pip install fails). This article covers SSL errors when Python scripts make HTTPS requests at runtime. For pip install SSL failures, see Fix: pip SSL Certificate Verify Failed.

Quick Reference Before You Dive In

If you arrived here from Google with a fresh traceback, the five facts that resolve roughly 90 percent of cases:

  1. On macOS with Python.org installer, run Install Certificates.command first. This single fix resolves the error for the majority of macOS users. Python.org’s installer does NOT use the system keychain; it ships unconfigured. The Python ssl module docs and the certifi package are the canonical references.
  2. verify=False is the WORST possible fix. It turns every HTTPS request into an unauthenticated TLS handshake. The error is the security model working correctly; never disable verification in production.
  3. Behind a corporate proxy (Zscaler, BlueCoat, Cisco Umbrella) you need the corporate root CA. Get the cert from IT, point REQUESTS_CA_BUNDLE at it; do not bypass.
  4. python -m certifi prints the path to the bundled CA file. Use this in SSL_CERT_FILE and REQUESTS_CA_BUNDLE env vars to pin the trust store explicitly.
  5. In Docker Alpine / distroless images, you must install ca-certificates. Minimal base images ship without a CA bundle; HTTPS fails until you install it.

The rest of this article walks through each cause in detail, plus the failure modes most other guides skip.

Why Python Cannot Verify the Server’s Certificate

When Python makes an HTTPS connection, it verifies the server’s SSL certificate against a set of trusted root certificates (a “CA bundle”). The error means Python could not find a trusted root certificate to verify the server’s certificate chain.

Common causes:

  • macOS Python installation: The official Python installer from python.org does NOT use macOS’s system certificate store. It ships with no CA bundle configured, causing all HTTPS requests to fail until you run a certificate install script.
  • Corporate proxy / firewall: Your company’s network intercepts HTTPS traffic and presents its own certificate, which Python does not trust.
  • Self-signed or private CA certificates: Connecting to an internal server with a self-signed cert or one issued by a private CA.
  • Outdated system CA bundle: The CA bundle bundled with Python or OpenSSL is outdated and missing newer root certificates.
  • Virtual environment isolation: A virtualenv uses different certificate paths than the system Python.
  • Docker containers: Minimal base images often have no CA certificates installed.

The most dangerous thing you can do here is add verify=False. That suppresses the error and turns every HTTPS request into an unauthenticated TLS handshake, meaning a man-in-the-middle on any network you touch can read and modify your traffic. The error is the security model working correctly. Your job is to fix the trust chain, not to disable the check. Every Stack Overflow answer that says “just use verify=False” is teaching a security antipattern that production codebases inherit and never remove.

The other historical wrinkle worth knowing is the 2021 DST Root CA X3 expiration. Let’s Encrypt’s chain was anchored at DST Root CA X3, which expired in September 2021. Older OpenSSL builds (1.0.x and many 1.1.0 builds) did not handle the cross-signed replacement correctly, and Python interpreters built against those OpenSSL versions started failing to verify Let’s Encrypt sites on that date. If you are debugging an old Python (3.6 or earlier) on a long-lived server, the fix may simply be “upgrade Python.” Python 3.10 also tightened hostname checks and dropped some legacy flags, so a script that worked on 3.8 can fail on 3.10 against a server with a sloppy certificate. The error message is the same; the underlying validator changed.

Diagnostic Timeline

Use this sequence instead of skipping straight to verify=False.

Minute 0: Confirm the platform. If you are on macOS and just installed Python from python.org, the fix is almost certainly running Install Certificates.command. Skip the rest of the timeline and try that first.

Minute 2: Identify which CA store Python is reading. Run python -c "import ssl; print(ssl.get_default_verify_paths())". The output shows cafile, capath, and openssl_capath. If cafile is None and the paths point at locations that do not exist, your interpreter has no CA store and every HTTPS call will fail.

Minute 4: Confirm certifi is installed and locate its bundle. Run python -m certifi. This prints the path to the bundled Mozilla CA file. If certifi is missing, pip install certifi. Set SSL_CERT_FILE=$(python -m certifi) and retry the script; if it works, the fix is wiring certifi into the environment permanently.

Minute 7: Reproduce the handshake with the openssl CLI. Run openssl s_client -connect api.example.com:443 -showcerts < /dev/null. The output shows every certificate the server presented, plus the verification status. Look for Verify return code: 21 (unable to verify the first certificate); that means the server is not sending its intermediate certificate. The fix there is on the server, not in your code.

Minute 10: Test against a known-good site. Run python -c "import urllib.request; print(urllib.request.urlopen('https://www.google.com').status)". If this also fails, the problem is your entire CA bundle, not the specific endpoint. If only your target fails, the issue is server-side or related to a private CA.

Minute 13: Inspect for a corporate MITM. Run openssl s_client -connect api.example.com:443 < /dev/null 2>&1 | grep issuer. If the issuer is your employer (“Zscaler”, “BlueCoat”, “Forcepoint”, “Netskope”, “Cisco Umbrella”), you are behind a TLS-intercepting proxy. The fix is to add the corporate root CA to REQUESTS_CA_BUNDLE; never to bypass with verify=False.

Minute 16: Check the date. Run date. If your system clock is more than a few minutes off, certificate notBefore/notAfter checks fail and every site looks expired. sudo ntpdate pool.ntp.org (or restart the time-sync service) and retry.

Minute 18: Print the actual chain Python sees. Use the snippet at the bottom of this article (ssl.SSLContext.wrap_socket) to print the server’s cert. Compare the issuer field against your CA bundle. If the issuer is not in certifi.where(), you have isolated the missing root.

When to Use Which Fix

The next seven sections cover the fixes in detail. The table below maps your situation to the recommended fix.

Your situationRecommended fixWhy
macOS, Python from python.org installerFix 1: Install Certificates.commandOne-click install of certifi bundle
Server / Linux, no install scriptFix 2: certifi.where() + env varsExplicit CA path
Behind corporate TLS-inspecting proxyFix 3: add corp root CA to bundleTrust the inspector
Internal server with self-signed certFix 4: pass verify=path/to/cert.pemTrust the specific cert, never verify=False
Linux server or Docker containerFix 5: install ca-certificates packageMinimal images ship without it
Virtual environment ignores system certsFix 6: set SSL_CERT_FILE per venvvenv isolation
Using httpx, aiohttp, boto3 instead of requestsFix 7: per-library API for CA pathEach library has its own knob

If multiple rows apply, pick the topmost one.

Fix 1: Run the Certificate Install Script (macOS)

This is the most common cause on macOS. The official Python.org installer ships with an Install Certificates.command script that installs the certifi CA bundle.

Open Finder and navigate to:

/Applications/Python 3.x/

Double-click Install Certificates.command. Or run it from the terminal:

/Applications/Python\ 3.11/Install\ Certificates.command

Replace 3.11 with your Python version. This runs:

pip install --upgrade certifi
/Applications/Python\ 3.11/python3 -m certifi

After running this, retry your script. This fix resolves the error for the vast majority of macOS users.

The reason this matters: Python on macOS uses its own bundled OpenSSL rather than the system’s Security framework. The system’s certificate store (used by Safari, curl, etc.) is not accessible to Python by default. The certifi package provides an up-to-date Mozilla CA bundle that Python can use, and the install script just wires that bundle into the interpreter’s default search path.

Fix 2: Use certifi Explicitly in Your Code

If you cannot run the install script (e.g., on a server), point Python to the certifi CA bundle explicitly:

With requests:

import requests
import certifi

response = requests.get("https://api.example.com/data", verify=certifi.where())
print(response.json())

With urllib:

import urllib.request
import ssl
import certifi

context = ssl.create_default_context(cafile=certifi.where())
with urllib.request.urlopen("https://api.example.com/data", context=context) as response:
    print(response.read())

Set it globally for all requests in a session:

import requests
import certifi
import os

# Set the environment variable for the entire process
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
os.environ["SSL_CERT_FILE"] = certifi.where()

Install certifi if you do not have it:

pip install certifi

Fix 3: Fix Corporate Proxy / Man-in-the-Middle Certificates

If your company uses a proxy that intercepts HTTPS traffic, Python sees the proxy’s certificate instead of the server’s. Python does not trust the proxy’s self-signed or corporate CA certificate.

Option A: Add the corporate certificate to your trusted CA bundle:

Get the corporate root certificate file (ask your IT department; it is usually a .pem or .crt file). Then:

import requests

response = requests.get(
    "https://internal.company.com/api",
    verify="/path/to/corporate-root-ca.pem"
)

Or combine it with certifi’s bundle:

import certifi
import shutil
import os

# Append corporate cert to certifi's bundle
corporate_cert = "/path/to/corporate-root-ca.pem"
certifi_bundle = certifi.where()

# Create a combined bundle
combined_bundle = "/tmp/combined-ca-bundle.pem"
shutil.copy(certifi_bundle, combined_bundle)

with open(corporate_cert, "r") as corp, open(combined_bundle, "a") as bundle:
    bundle.write(corp.read())

os.environ["REQUESTS_CA_BUNDLE"] = combined_bundle

Option B: Set the certificate via environment variable:

export REQUESTS_CA_BUNDLE=/path/to/corporate-root-ca.pem
export SSL_CERT_FILE=/path/to/corporate-root-ca.pem
python your_script.py

Fix 4: Fix Self-Signed Certificates for Internal Servers

If you are connecting to a server with a self-signed certificate (common in development environments):

Pass the certificate file directly:

import requests

# Verify against the server's self-signed cert
response = requests.get(
    "https://localhost:8443/api",
    verify="/path/to/server-cert.pem"
)

For mutual TLS (client certificate authentication):

response = requests.get(
    "https://internal-api.company.com/data",
    verify="/path/to/ca-bundle.pem",
    cert=("/path/to/client-cert.pem", "/path/to/client-key.pem")
)

I have personally seen production codebases shipped with verify=False that nobody remembered to remove. Each one was an open door for any MITM on the network path. Treat verify=False the same way you would treat eval(user_input): it is a red-flag pattern that should not survive code review. Suppressing the error does not fix the broken trust chain; it just hides the symptom.

If you must use verify=False in development (not recommended), suppress the InsecureRequestWarning:

import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get("https://localhost:8443/api", verify=False)

Fix 5: Update Certificates on Linux / Docker

On Debian/Ubuntu-based systems:

sudo apt-get update && sudo apt-get install -y ca-certificates
sudo update-ca-certificates

On Alpine Linux (common in Docker):

apk add --no-cache ca-certificates
update-ca-certificates

In a Dockerfile:

FROM python:3.11-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Or install certifi and set the env var
RUN pip install certifi
ENV SSL_CERT_FILE=/usr/local/lib/python3.11/site-packages/certifi/cacert.pem

Fix 6: Fix SSL in Virtual Environments

Virtual environments sometimes do not inherit the system’s certificate configuration:

# Activate your virtualenv
source venv/bin/activate

# Install certifi inside the virtualenv
pip install certifi

# Check which cert file Python is using
python -c "import ssl; print(ssl.get_default_verify_paths())"

Set the cert path for the virtualenv:

export SSL_CERT_FILE=$(python -m certifi)
export REQUESTS_CA_BUNDLE=$(python -m certifi)

Add these exports to your .env file or shell profile to make them persistent. For .env loading issues, see Fix: .env variables not loading.

Fix 7: Fix httpx and Other HTTP Libraries

The fix applies similarly to other HTTP libraries:

httpx:

import httpx
import certifi
import ssl

ssl_context = ssl.create_default_context(cafile=certifi.where())
with httpx.Client(verify=ssl_context) as client:
    response = client.get("https://api.example.com/data")

aiohttp:

import aiohttp
import ssl
import certifi

async def fetch():
    ssl_context = ssl.create_default_context(cafile=certifi.where())
    async with aiohttp.ClientSession() as session:
        async with session.get("https://api.example.com/data", ssl=ssl_context) as response:
            return await response.json()

boto3 (AWS SDK):

export AWS_CA_BUNDLE=/path/to/corporate-ca.pem

Or in code:

import boto3

session = boto3.Session()
client = session.client("s3", verify="/path/to/ca-bundle.pem")

Diagnose the Certificate Chain

Before applying a fix, identify exactly which certificate is failing:

import ssl
import socket

hostname = "api.example.com"
port = 443

context = ssl.create_default_context()
try:
    with socket.create_connection((hostname, port)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            cert = ssock.getpeercert()
            print("Certificate is valid")
            print("Subject:", cert["subject"])
            print("Issuer:", cert["issuer"])
            print("Expires:", cert["notAfter"])
except ssl.SSLCertificateError as e:
    print("SSL Error:", e)

Or use OpenSSL from the command line:

openssl s_client -connect api.example.com:443 -showcerts

This shows the full certificate chain and which CA issued the certificate, helping you determine whether to update certifi, add a corporate cert, or contact the server admin.

Stranger Causes I Have Tracked Down

Check if the site itself has a certificate issue. Use an online SSL checker (e.g., SSL Labs) to verify the server’s certificate is valid and properly chained. If the server is misconfigured, the fix is on the server side.

Check for clock skew. SSL certificates have expiration dates. If your system clock is significantly off, certificates may appear expired or not yet valid. Sync your system clock: sudo ntpdate pool.ntp.org.

Check Python’s OpenSSL version. Older OpenSSL versions bundled with Python may not support newer TLS extensions:

python -c "import ssl; print(ssl.OPENSSL_VERSION)"

If you see OpenSSL 1.0.x, upgrade Python to a version that ships with OpenSSL 1.1.x or 3.x.

Check for SNI issues. Some servers require SNI (Server Name Indication) to present the correct certificate. Python 3.x supports SNI by default. If you are on Python 2 (end of life), the pyOpenSSL and ndg-httpsclient packages add SNI support.

Look for the server skipping intermediate certificates. Many servers misconfigure their chain and send only the leaf certificate. Browsers paper over this with AIA fetching; Python does not. Run openssl s_client -connect host:443 -showcerts; if you see only one certificate in the output, the server admin needs to bundle the intermediate. As a workaround, download the intermediate from the issuing CA and add it to your local bundle.

Rule out a stale pip install certifi from years ago. certifi ships a snapshot of Mozilla’s CA list. A two-year-old version is missing the Let’s Encrypt ISRG Root X2 and newer roots. Run pip install --upgrade certifi and re-export SSL_CERT_FILE=$(python -m certifi).

Check for virtualenv Python pointing at a deleted system Python. A venv’s python is a symlink to the system Python that created it. If the system Python was uninstalled or moved, the venv’s SSL module loads but ssl.get_default_verify_paths() returns nonsense. Recreate the venv with the current system Python.

Verify requests is not picking up a stale REQUESTS_CA_BUNDLE from your shell. Run env | grep -E "(SSL|CA_BUNDLE|CERT)" and remove any leftover exports from previous troubleshooting sessions before testing the fix.

Check for proxy environment variables conflicting with CA settings. HTTPS_PROXY and https_proxy route traffic through an intermediate that may itself terminate TLS. If both are set and the proxy is broken, the symptom looks like a cert error. Unset them temporarily and retry.

What Other Tutorials Get Wrong About This Error

Most Python SSL tutorials list the same fixes but frame them in ways that produce subtle bugs.

They recommend verify=False as a “quick fix.” It is the worst possible default. Disabling verification turns every HTTPS request into an unauthenticated TLS handshake, which means any MITM on the network path can read and modify your traffic. Tutorials that show verify=False as a one-line solution train readers to ship security antipatterns that production codebases inherit and never remove.

They miss that macOS Python ignores the system keychain. Python.org’s installer ships with its own bundled OpenSSL and a CA path that points at empty directories. Articles that say “use the system certs” assume Python reads the keychain (it does not on macOS) and leave the reader more confused.

They omit python -m certifi as the canonical CA path command. This single command prints the path to the current CA bundle. Tutorials that recommend “find your CA bundle” without naming this command send readers searching the filesystem.

They confuse pip SSL errors with runtime SSL errors. The pip-specific failure has its own fixes (pip install --trusted-host); runtime SSL failures need REQUESTS_CA_BUNDLE or SSL_CERT_FILE. Articles that group both errors together send readers to the wrong knob.

They miss the corporate-MITM detection step. A Verify return code: 21 in openssl s_client output strongly suggests TLS-inspecting proxy. Tutorials that focus only on legitimate certificate problems leave corporate-network users guessing.

They omit the boto3 / aws-cli AWS_CA_BUNDLE env var. AWS SDKs honor a separate environment variable from requests. Setting only REQUESTS_CA_BUNDLE does not fix boto3 calls. Articles focused only on requests send AWS users on a long detour.

Frequently Asked Questions

What is the difference between SSL_CERT_FILE, REQUESTS_CA_BUNDLE, and CURL_CA_BUNDLE?

They all point to a CA bundle but different tools honor different variables. SSL_CERT_FILE is read by Python’s stdlib ssl module. REQUESTS_CA_BUNDLE is read by the requests library. CURL_CA_BUNDLE is read by curl. For Python applications, set the first two; the third is for shell environments. AWS SDKs use AWS_CA_BUNDLE separately.

Why does my browser trust the cert but Python does not?

Browsers fetch missing intermediate certificates via Authority Information Access (AIA). Python does not. If a server sends only the leaf certificate without the intermediate chain, browsers succeed by fetching the missing piece; Python fails. The fix is either to add the missing intermediate to your local bundle or to convince the server admin to bundle the chain correctly.

Is pip install --trusted-host the same as verify=False?

Functionally yes, scope no. pip install --trusted-host disables SSL verification for one specific host during installation. verify=False disables it for any request. Both are insecure; pip install --trusted-host is at least scoped to a known endpoint. The right fix is to install the missing CA, not to bypass.

Why does the same script work on my laptop but fail on the server?

Different CA bundles. Your laptop probably ran Install Certificates.command (macOS) or has ca-certificates installed (Linux distribution). The server may be a minimal image (Alpine, distroless) that ships without CAs. Install ca-certificates (Linux) or set SSL_CERT_FILE=$(python -m certifi) (any platform).

Why does Docker Alpine fail when Ubuntu works?

Alpine is intentionally minimal and does not include a CA bundle. apk add --no-cache ca-certificates installs it. Ubuntu’s base image includes ca-certificates by default, which is why HTTPS “just works” there.

Does certifi update itself?

No. certifi is a snapshot of Mozilla’s CA list at the time of release. Run pip install --upgrade certifi periodically to pick up newer roots. A two-year-old certifi is missing Let’s Encrypt’s ISRG Root X2 and other newer roots, which can cause this error against sites that depend on them.

For connection errors that happen before SSL negotiation, see Fix: Python ConnectionError: Max Retries Exceeded.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles