adllm Insights logo adllm Insights logo

Conquering SSLError: CERTIFICATE_VERIFY_FAILED in Python requests with Enterprise CAs

Published on by The adllm Team. Last modified: . Tags: Python requests SSLError CERTIFICATE_VERIFY_FAILED SSL TLS Custom CA Enterprise CA HTTPS Troubleshooting certifi

When working with Python’s requests library to interact with HTTPS services, particularly within corporate environments, developers frequently encounter the dreaded requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed. This error signals that Python, through its underlying SSL/TLS mechanisms, could not validate the SSL certificate presented by the server. This typically occurs when the server uses a certificate issued by a custom enterprise Certificate Authority (CA) that isn’t part of the public CAs requests trusts by default.

The requests library, for security, relies on the certifi package, which provides Mozilla’s collection of trusted root CAs. If your organization’s internal CA isn’t in this public set, or if you’re behind an SSL-inspecting corporate proxy that re-signs certificates, verification will fail. This article provides a comprehensive guide to understanding, troubleshooting, and correctly resolving this common SSL error, ensuring secure and successful connections.

Understanding the CERTIFICATE_VERIFY_FAILED Error

At its core, SSL/TLS (Secure Sockets Layer/Transport Layer Security) ensures that communication between a client (your Python script) and a server is encrypted and that the server is authentic. This authenticity is established through digital certificates issued by Certificate Authorities (CAs). Your system maintains a list of trusted root CAs. If a server’s certificate is signed by a CA (or an intermediate CA that chains up to a root CA) not in this trusted list, the CERTIFICATE_VERIFY_FAILED error occurs.

Enterprise networks often use their own internal CAs to issue certificates for internal servers and services. These internal CAs are trusted within the enterprise but not globally. Similarly, SSL-inspecting proxies decrypt, inspect, and then re-encrypt traffic using a certificate signed by the proxy’s own CA, which clients must also trust.

Core Solutions for Trusting Enterprise CAs

The primary goal is to inform requests about your enterprise CA so it can be trusted. Here are the recommended methods:

1. The verify Parameter in requests Calls

The most direct way to specify a custom CA for a particular request or set of requests is by using the verify parameter. You provide the path to your CA certificate file (or a bundle of CA certificates) in PEM format.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import requests

url = 'https://internal.example.com/api/data'
# Path to your enterprise CA certificate or a bundle containing it.
# This bundle might include root and any necessary intermediate CAs.
path_to_custom_ca_pem = '/etc/ssl/certs/enterprise_ca_bundle.pem'

try:
    response = requests.get(url, verify=path_to_custom_ca_pem)
    response.raise_for_status()  # Raise HTTPError for bad responses (4XX, 5XX)
    print(f"Successfully connected to {url}")
    # print(f"Response: {response.json()}")
except requests.exceptions.SSLError as e:
    print(f"SSL verification failed: {e}")
    # Ensure '<path>' is correct and accessible.
    print(f"Ensure '{path_to_custom_ca_pem}' is correct and accessible.")
except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")

This method offers fine-grained control, specifying the CA on a per-request basis.

2. The REQUESTS_CA_BUNDLE Environment Variable

For a more global approach within your application’s environment, requests automatically honors the REQUESTS_CA_BUNDLE environment variable. If this variable is set to the path of your CA bundle file, requests will use it for all SSL verifications by default.

First, set the environment variable in your shell:

1
2
# In your terminal session or .bashrc/.zshrc
export REQUESTS_CA_BUNDLE="/path/to/your/enterprise_ca_bundle.pem"

Then, your Python code can be simpler:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import os

url = 'https://internal.example.com/api/status'

# requests will automatically use the CA bundle specified by
# REQUESTS_CA_BUNDLE if the 'verify' parameter is not explicitly set.
ca_bundle_path = os.getenv('REQUESTS_CA_BUNDLE')
if ca_bundle_path:
    print(f"Using CA bundle from REQUESTS_CA_BUNDLE: {ca_bundle_path}")
else:
    print("REQUESTS_CA_BUNDLE is not set. Using default CAs.")

try:
    response = requests.get(url) # No 'verify' needed if env var is set
    response.raise_for_status()
    print(f"Successfully connected to {url} using env var config.")
except requests.exceptions.SSLError as e:
    print(f"SSL verification failed: {e}")
    if ca_bundle_path:
        print(f"Check the CA bundle at: {ca_bundle_path}")
except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")

requests also checks for CURL_CA_BUNDLE as a fallback if REQUESTS_CA_BUNDLE is not defined.

3. Using requests.Session Objects

If your application makes multiple requests to the same host(s) or requires persistent parameters (like custom CA verification), using a requests.Session object is more efficient. You can set the CA bundle path on the session’s verify attribute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

base_url = 'https://internal.example.com/api'
path_to_custom_ca_pem = '/etc/ssl/certs/enterprise_ca_bundle.pem'

with requests.Session() as session:
    session.verify = path_to_custom_ca_pem
    
    endpoints = ['/users', '/products']
    for endpoint in endpoints:
        try:
            url = f"{base_url}{endpoint}"
            print(f"Requesting {url}...")
            response = session.get(url)
            response.raise_for_status()
            print(f"Successfully fetched from {endpoint}")
            # print(f"Data: {response.json()}")
        except requests.exceptions.SSLError as e:
            print(f"SSL verification failed for {url}: {e}")
            break # Stop on first SSL error with session
        except requests.exceptions.RequestException as e:
            print(f"Request to {url} failed: {e}")

Preparing Your CA Bundle File

The CA certificate(s) must be in PEM (Privacy Enhanced Mail) format. A PEM file is a Base64 encoded text file with -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- markers.

  • Single CA Certificate: If your enterprise uses a single root CA that directly signs server certificates, this file will contain just that root CA’s certificate.
  • CA Bundle (Root + Intermediates): If your server certificate is signed by an intermediate CA, the bundle should contain the intermediate CA’s certificate and its issuing root CA’s certificate. The order is typically the signing CA followed by its issuer, up to the root. However, for a trust bundle, including just the enterprise root CA is often sufficient if servers provide necessary intermediates. Your IT/Security department should provide the correct CA certificate(s).

You can concatenate multiple PEM-formatted certificates into a single file:

1
2
3
# Example: Concatenating root and intermediate CA certificates into a bundle
cat enterprise-intermediate-ca.pem enterprise-root-ca.pem > \
  enterprise_ca_bundle.pem

Ensure this bundle file is readable by your Python application.

System-Wide Trust (Platform Dependent)

An alternative is to install the enterprise CA certificate into your operating system’s trust store. This makes the CA trusted by many applications on your system, not just Python requests. The method for adding a CA to the system trust store varies by OS:

  • Linux (Debian/Ubuntu): Copy PEM to /usr/local/share/ca-certificates/ and run sudo update-ca-certificates. (See man update-ca-certificates).
  • Linux (RHEL/CentOS): Copy PEM to /etc/pki/ca-trust/source/anchors/ and run sudo update-ca-trust extract. (See man update-ca-trust).
  • macOS: Use Keychain Access utility to import the certificate into the System keychain and mark it as trusted.
  • Windows: Use Certificate Manager (certmgr.msc) to import into “Trusted Root Certification Authorities”.

After installing the CA system-wide, the pip-system-certs package can patch requests (and pip itself) to use the OS trust store.

1
pip install pip-system-certs

Once installed, requests may automatically pick up CAs from the system store. However, this approach has broader implications and often requires administrative privileges. Using verify or REQUESTS_CA_BUNDLE provides more explicit, application-level control.

Diagnosing SSL Issues

If you’re still facing problems, these diagnostic steps can help:

1. openssl s_client

The OpenSSL command-line tool is invaluable for testing SSL/TLS connections and inspecting certificates directly, independent of Python.

To check connectivity and verify against your CA bundle using openssl s_client:

1
2
openssl s_client -connect internal.example.com:443 \
                 -CAfile /path/to/your/enterprise_ca_bundle.pem

Look for Verify return code: 0 (ok). Any other code indicates a problem.

To view the certificate chain presented by the server:

1
openssl s_client -connect internal.example.com:443 -showcerts

This helps you see exactly which certificates the server is sending, which can be useful for assembling your CA bundle.

2. Python http.client Debugging

For low-level insight into the SSL handshake process within Python, you can enable debugging for http.client (used by urllib3, which requests uses).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import http.client
import logging
import requests

# Enable http.client debugging
http.client.HTTPConnection.debuglevel = 1

# Setup logging to see the debug output
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

url = 'https://internal.example.com/api/health'
# Set your CA bundle path if not using an environment variable
# path_to_custom_ca_pem = '/path/to/your/enterprise_ca_bundle.pem'

try:
    # response = requests.get(url, verify=path_to_custom_ca_pem)
    response = requests.get(url) # Assuming REQUESTS_CA_BUNDLE or system trust
    print(f"Connection to {url} successful. Status: {response.status_code}")
except requests.exceptions.SSLError as e:
    print(f"SSL Error: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")
finally:
    # Important: Reset debug level if you don't want it for other connections
    http.client.HTTPConnection.debuglevel = 0

This will print verbose information about the SSL handshake, including certificate details and potential error messages from the OpenSSL library.

3. Inspecting Certificate Contents with openssl x509

If you have a certificate file (e.g., my_ca.crt or my_ca.pem) and want to inspect its contents or convert its format using openssl x509:

To view details of a PEM-encoded certificate:

1
openssl x509 -in my_ca.pem -text -noout

To convert a certificate from DER format to PEM format:

1
openssl x509 -in my_ca.der -inform DER -out my_ca.pem -outform PEM

Common Pitfalls and Anti-Patterns

  • NEVER use verify=False in Production: Disabling SSL verification (requests.get(url, verify=False)) silences the error but exposes your application to serious security risks, including Man-in-the-Middle (MITM) attacks. Only use this for temporary, isolated local testing with full awareness of the implications.
  • Incorrect Path: Double-check the path to your CA bundle file. Typos are common.
  • Permissions: Ensure your application has read permissions for the CA bundle file.
  • Incorrect Bundle Content:
    • Using the server’s certificate instead of the CA’s certificate.
    • Missing intermediate CA(s) in the bundle. The bundle needs the certificate(s) that your system doesn’t already trust but are needed to complete the chain to a trusted root.
    • File not in valid PEM format.
  • Environment Variable Not Set/Exported: If using REQUESTS_CA_BUNDLE, ensure it’s correctly set and exported in the environment where your script runs. Changes to .bashrc or similar might require a new terminal session or sourcing the file.
  • Outdated CA Bundle: Ensure your custom CA bundle or the certifi package is reasonably up-to-date, although for enterprise CAs, the issue is usually trust, not expiry of the CA itself.

Advanced: Custom SSLContext with requests.adapters.HTTPAdapter

For scenarios requiring fine-grained control over SSL/TLS parameters (like specific TLS versions, cipher suites, or other options not exposed directly by requests), you can create a custom ssl.SSLContext and use it with requests via an HTTPAdapter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import ssl
import requests
from requests.adapters import HTTPAdapter

class CustomSSLContextAdapter(HTTPAdapter):
    def __init__(self, ssl_context, **kwargs):
        self.ssl_context = ssl_context
        super().__init__(**kwargs)

    def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
        # Override the ssl_context used by the PoolManager
        pool_kwargs['ssl_context'] = self.ssl_context
        super().init_poolmanager(connections, maxsize, block, **pool_kwargs)

# Create a custom SSL context
# This context will load CAs from your specified PEM file.
# It also enforces TLS 1.2 or higher by default with Python 3.6+
custom_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
custom_context.load_verify_locations(
    cafile="/path/to/your/enterprise_ca_bundle.pem"
)
# Example: Optionally set specific ciphers (use with caution)
# custom_context.set_ciphers('ECDHE+AESGCM:CHACHA20')
# custom_context.minimum_version = ssl.TLSVersion.TLSv1_2 # Be explicit

session = requests.Session()
adapter = CustomSSLContextAdapter(custom_context)
session.mount('https://', adapter)

url = 'https://internal.example.com/api/secure-resource'
try:
    response = session.get(url)
    response.raise_for_status()
    print(f"Successfully connected to {url} with custom SSLContext.")
except requests.exceptions.SSLError as e:
    print(f"SSL Error with custom context: {e}")
except requests.exceptions.RequestException as e:
    print(f"Request error with custom context: {e}")

This is an advanced technique and typically not required for standard enterprise CA issues but offers maximum flexibility.

Considerations for Containerized Environments (Docker/Kubernetes)

When running Python applications in containers:

  1. Mount CA Bundle: The preferred method is to mount your custom CA bundle file into the container (e.g., using Docker volumes) and then use REQUESTS_CA_BUNDLE or the verify parameter to point to its path within the container.
  2. Add to Container’s System Trust Store: Alternatively, during the Docker image build process (Dockerfile), you can copy the CA certificate into the container’s system trust store (e.g., /usr/local/share/ca-certificates/ on Debian/Ubuntu, then run update-ca-certificates). If you use this method, also install pip-system-certs in your container.

Conclusion

The SSLError: CERTIFICATE_VERIFY_FAILED error in Python requests is a common hurdle in enterprise settings but is entirely solvable by correctly informing requests about your organization’s custom Certificate Authorities. Prioritize using the verify parameter for specific calls, REQUESTS_CA_BUNDLE for environment-wide configuration, or Session.verify for repeated requests. Always ensure your CA bundle is correctly formatted (PEM) and contains the necessary certificates.

Resist the dangerous temptation to disable SSL verification (verify=False). Instead, invest the effort in proper CA management and diagnostic techniques like openssl s_client. By understanding the underlying SSL/TLS trust mechanisms and applying these solutions, you can build secure, reliable Python applications that seamlessly integrate with internal HTTPS services.