pypa/pip · error · CertificateError

Unable to verify server certificate for

Error message

Unable to verify server certificate for %s

What it means

Raised by distlib's HTTPSHandler.https_open when an HTTPS request fails with a 'certificate verify failed' URLError. The handler re-wraps the underlying ssl error into a CertificateError naming the offending host, so the caller knows exactly which server's certificate chain could not be validated against the configured CA bundle (ca_certs). This guards against MITM and stale/expired server certificates during pip's package downloads.

Solutions

  1. Install or update the system CA bundle (e.g. apt-get install ca-certificates, update-ca-certificates) so the server's root CA is trusted.
  2. If the server uses a private/internal CA, point ca_certs to a PEM file containing that root certificate.
  3. Verify the server's certificate is not expired and that the hostname matches the SAN field using openssl s_client -connect host:443.
  4. If this is a known-internal mirror, configure pip to trust it via --trusted-host after confirming it is safe, understanding this disables verification.
  5. Renew or re-issue the server certificate if it is expired or has an incomplete chain.

Example fix

// before — no CA bundle, verification fails
handler = HTTPSHandler(ca_certs=None)

// after — supply the correct CA bundle
handler = HTTPSHandler(ca_certs='/etc/ssl/certs/ca-certificates.crt', check_domain=True)
Defensive patterns

Strategy: validation

Validate before calling

import ssl, socket

def verify_cert_chain(host, port=443, ca_certs=None):
    ctx = ssl.create_default_context(cafile=ca_certs)
    with socket.create_connection((host, port)) as sock:
        with ctx.wrap_socket(sock, server_hostname=host) as ssock:
            cert = ssock.getpeercert()
    return cert is not None

Try / catch

from ssl import CertificateError
try:
    opener.open('https://example.com/')
except CertificateError as e:
    log.error('Cert verification failed for %s — check CA bundle', e)
    raise

Prevention

When it happens

Trigger: Calling urlopen/https_open against a host whose TLS certificate is expired, self-signed, signed by an untrusted CA, has a hostname mismatch, or whose chain is incomplete — while ca_certs is configured on the HTTPSHandler. Also triggered when the system CA bundle is empty or missing.

Common situations: Corporate proxy with its own root CA not installed in the trust store; air-gapped environment with no CA bundle; pip pointed at an internal index with a self-signed cert; expired Let's Encrypt cert on a private PyPI mirror; container images that ship without ca-certificates.

Understand the failure class

Related errors


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

Appendix: source

Thrown at src/pip/_vendor/distlib/util.py:1542

            pass a connection class to do_open, but it doesn't actually check for
            a class, and just expects a callable. As long as we behave just as a
            constructor would have, we should be OK. If it ever changes so that
            we *must* pass a class, we'll create an UnsafeHTTPSConnection class
            which just sets check_domain to False in the class definition, and
            choose which one to pass to do_open.
            """
            result = HTTPSConnection(*args, **kwargs)
            if self.ca_certs:
                result.ca_certs = self.ca_certs
                result.check_domain = self.check_domain
            return result

        def https_open(self, req):
            try:
                return self.do_open(self._conn_maker, req)
            except URLError as e:
                if 'certificate verify failed' in str(e.reason):
                    raise CertificateError('Unable to verify server certificate '
                                           'for %s' % req.host)
                else:
                    raise

    #
    # To prevent against mixing HTTP traffic with HTTPS (examples: A Man-In-The-
    # Middle proxy using HTTP listens on port 443, or an index mistakenly serves
    # HTML containing a http://xyz link when it should be https://xyz),
    # you can use the following handler class, which does not allow HTTP traffic.
    #
    # It works by inheriting from HTTPHandler - so build_opener won't add a
    # handler for HTTP itself.
    #
    class HTTPSOnlyHandler(HTTPSHandler, HTTPHandler):

        def http_open(self, req):
            raise URLError('Unexpected HTTP request on what should be a secure '
                           'connection: %s' % req)

View on GitHub (pinned to f399c37189)