pypa/pip · error · SSLVerificationError

ssl-verification-failed

ssl-verification-failed

Error message

Failed to establish a secure connection to {host} while fetching {url}

What it means

Raised as SSLVerificationError (the bare, non-MaxRetryError path) when a urllib3.exceptions.SSLError occurs during a non-streamed response, after the TLS handshake. It means certificate verification against the host failed and TLS could not be established.

Solutions

  1. Update CA roots: pip install --upgrade certifi, or update OS ca-certificates.
  2. Point pip at the correct CA bundle with --cert corporate-ca.pem.
  3. Configure the corporate proxy's CA properly; fix system time if skewed.
  4. Only as a last resort, use --trusted-host to bypass verification for that host (insecure).

Example fix

# before
pip install pkg   # MITM proxy with untrusted CA

# after
pip install --cert /etc/ssl/corporate-ca.pem pkg
Defensive patterns

Strategy: validation

Validate before calling

import ssl, certifi

def trust_store_ok(host: str) -> bool:
    ctx = ssl.create_default_context(cafile=certifi.where())
    try:
        with ctx.wrap_socket(socket.create_connection((host, 443)), server_hostname=host):
            return True
    except ssl.SSLError:
        return False

Try / catch

from pip._internal.exceptions import SSLVerificationError

try:
    run_pip_install(["-i", url, "pkg"])
except SSLVerificationError:
    run_pip_install(["--cert", "/etc/ssl/private-ca.pem", "-i", url, "pkg"])

Prevention

When it happens

Trigger: Server presents a self-signed, expired, or untrusted certificate; CA bundle missing or out of date; MITM proxy injecting its own cert; system clock skewed so notBefore/notAfter checks fail; TLS version/cipher mismatch.

Common situations: Corporate TLS-intercepting proxies; stale certifi/ca-certificates; air-gapped environments with private CAs; container images with old CA bundles.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/596ad64fddd2664e. Report an issue: GitHub.

Appendix: source

Thrown at src/pip/_internal/network/utils.py:180

    Note: requests.ConnectionError is the parent class of
          requests.ProxyError, requests.SSLError, and requests.ConnectTimeout
          so these errors are also handled here.
    """
    url = redact_auth_from_url(url)
    raw_hostname = urlsplit(url).hostname or urlsplit(url).netloc
    reason = error.args[0] if error.args else error

    # NewConnectionError is a subclass of TimeoutError for some reason...
    if isinstance(reason, urllib3.exceptions.TimeoutError) and not isinstance(
        reason, urllib3.exceptions.NewConnectionError
    ):
        # A bare timeout error can occur during non-streamed responses. Don't
        # ask me how.
        _raise_timeout_error(reason, url, raw_hostname, timeout)
    if isinstance(reason, urllib3.exceptions.SSLError):
        # A bare SSL error can occur during non-streamed responses, after the
        # initial connection and TLS handshake have completed.
        raise SSLVerificationError(url, raw_hostname, reason)

    # At this point, all errors should be wrapped in MaxRetryError.
    if not isinstance(reason, urllib3.exceptions.MaxRetryError):
        raise ConnectionFailedError(url, raw_hostname, reason)

    max_retry_error = reason
    assert isinstance(max_retry_error.pool, urllib3.connectionpool.HTTPConnectionPool)
    host = max_retry_error.pool.host
    proxy = max_retry_error.pool.proxy
    # Narrow the reason further to the specific error from the last retry.
    reason = max_retry_error.reason

    if isinstance(reason, urllib3.exceptions.SSLError):
        raise SSLVerificationError(url, host, reason)
    if isinstance(reason, urllib3.exceptions.TimeoutError) and not isinstance(
        reason, urllib3.exceptions.NewConnectionError
    ):
        _raise_timeout_error(reason, url, host, timeout)

View on GitHub (pinned to f399c37189)