Skip to content
ecdsa.com
Python ssl module (OpenSSL)Python cryptography

Error message

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain

Also printed as

  • [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain

OpenSSL 3 prints "self-signed", OpenSSL 1.1.1 "self signed"; Python appends a source location such as "(_ssl.c:1082)". Through urllib it appears inside "<urlopen error …>", through requests inside "Max retries exceeded … (Caused by SSLError(…))".

Python ssl module (OpenSSL) — ssl.SSLCertVerificationError, X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN (19)

What it means

The chain the server presented ends in a self-signed root certificate that is not among the CAs this Python process trusts. The chain itself is complete; the root is simply unknown. When it happens for public sites like pypi.org or api.github.com, the root belongs to a TLS-inspecting proxy or antivirus on your network or machine.

Why it happens

How to fix it

  1. 1.

    Find out whose root it is

    Print the chain for a public host. A root named after your company or a security product is the one to trust; an unknown name on an untrusted network is a reason to stop, not to trust it.

    bash
    openssl s_client -connect pypi.org:443 -servername pypi.org -showcerts </dev/null 2>/dev/null \
      | grep -E '^ *[0-9]+ s:|^ *i:'
    # A top certificate named after your company or a security vendor = interception proxy
  2. 2.

    Verify with the operating system's trust store

    truststore.inject_into_ssl() makes Python's TLS use the OS trust store, which already contains roots deployed by your organisation. It is the cleanest fix on managed Windows and macOS machines.

    python
    # pip install truststore==0.10.5
    import truststore
    truststore.inject_into_ssl()   # the proxy root deployed in the OS store is now trusted
    
    import requests
    requests.get("https://api.example.com/")
  3. 3.

    Build a bundle with public roots plus your root

    REQUESTS_CA_BUNDLE (requests) and SSL_CERT_FILE (ssl, urllib, httpx) replace the default bundle, so concatenate certifi's roots with the company root rather than pointing at the company root alone.

    bash
    # Keep the public roots and add the company/proxy root
    cat "$(python3 -m certifi)" corp-root-ca.pem > ~/.config/corp-ca-bundle.pem
    export REQUESTS_CA_BUNDLE=~/.config/corp-ca-bundle.pem   # requests
    export SSL_CERT_FILE=~/.config/corp-ca-bundle.pem        # ssl, urllib, httpx
  4. 4.

    conda: configure its own setting

    conda reads ssl_verify from .condarc; set it to the combined bundle so conda install and conda update work behind the proxy.

    bash
    # conda keeps its own OpenSSL and CA bundle
    conda config --set ssl_verify /path/to/corp-ca-bundle.pem

Related errors

Browse the full signature error database