CERTIFICATE_VERIFY_FAILED is not a single problem with a single fix. It is a family of SSL verification failures, each with a different sub-message that identifies the specific cause. Applying the wrong fix to the wrong cause wastes time and sometimes makes the situation worse. The single worst response to this error is disabling certificate verification entirely, yet that is what a significant fraction of Stack Overflow answers and quick-fix tutorials recommend.
This guide starts with reading the error message precisely, maps each sub-message to its specific root cause, and provides the correct targeted fix for each. It covers Python (requests, urllib, pip), curl, Node.js, and the particularly common corporate network scenario. Code is included where the fix requires exact syntax.
Step Zero: Read the Full Error Message
The CERTIFICATE_VERIFY_FAILED error always includes a sub-message that identifies the specific failure reason. This sub-message is the most important diagnostic information available. Do not skip past it.
| Sub-message | Root Cause | Starting Fix |
| unable to get local issuer certificate | Missing intermediate certificate in the server’s chain, or Python’s CA bundle is outdated | Fix 1: Update certifi / check server certificate chain |
| certificate has expired or is not yet valid | Server’s certificate is expired, or local system clock is wrong | Fix 2: Check server cert expiry and local clock |
| self-signed certificate | Server is presenting a self-signed certificate not in any trusted CA bundle | Fix 3: Add cert to bundle or use private CA trust |
| self-signed certificate in certificate chain | An intermediate or root in the chain is self-signed and untrusted — often a corporate SSL inspection proxy | Fix 4: Add corporate root CA to Python’s bundle |
| certificate verify failed: hostname mismatch | Certificate does not cover the hostname being connected to | Fix 5: Check certificate SANs; use correct hostname |
| certificate verify failed: certificate has been revoked | The certificate was revoked by the issuing CA | Fix 6: Certificate must be replaced by the server owner |
Do not use verify=False as a fix for any of the above. Disabling certificate verification removes all HTTPS security protections, making your application vulnerable to man-in-the-middle attacks. An attacker can intercept all traffic, read credentials, modify data, and inject malicious responses. The correct fix always involves trusting the right certificate, not turning off verification.
Why Python’s Certificate Verification Fails When Browsers Work Fine
The most common source of confusion: the same URL opens fine in a browser but fails with CERTIFICATE_VERIFY_FAILED in Python. This happens because Python and browsers use separate certificate trust stores.
Browsers use the operating system’s trust store, or maintain their own (Firefox uses its own). When a corporate IT department pushes a root CA certificate via group policy or MDM, browsers on managed machines automatically trust it. Python’s requests library uses the certifi package, which is a standalone CA bundle maintained by the Python Software Foundation. It does not automatically read the system trust store or any certificates pushed by IT.
On macOS specifically, Python 3.6 and later ship with their own OpenSSL that is independent of the system’s OpenSSL and does not access the macOS Keychain. This is why a fresh Python installation on macOS often fails certificate verification even for common public sites, until the Install Certificates.command script is run or certifi is updated.
On Linux, Python typically links against the system’s OpenSSL and uses the system CA bundle. Certificate errors on Linux are more often caused by outdated CA packages or missing intermediates on the server side rather than by Python’s own bundle being wrong.
Fix 1: Update certifi and Check the Server’s Certificate Chain
The sub-message ‘unable to get local issuer certificate’ means Python cannot trace the server’s certificate chain to any CA in its trusted bundle. There are two possible causes: Python’s certifi bundle is outdated and is missing a newer root CA, or the server is not sending its intermediate certificate.
Update certifi first
| # Update certifi to the latest CA bundle:
pip install –upgrade certifi
# Verify the certifi path and version: python -m certifi # This prints the path to the cacert.pem file Python is using
# Check certifi version: python -c “import certifi; print(certifi.__version__)” |
Check whether the server is missing its intermediate certificate
If updating certifi does not fix the error, the problem may be on the server side. Run the following to test the connection and see what certificates the server sends:
| # Test the certificate chain the server sends:
openssl s_client -connect yourdomain.com:443 -showcerts
# A complete chain shows 2 or 3 certificates in the output. # If only 1 certificate appears, the server is not sending its intermediate. # Contact the server owner or hosting provider to fix the certificate bundle.
# Also check via SSL Labs (no command line needed): # https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com # A warning about an incomplete chain confirms the server-side problem. |
If the server is one you control, add the intermediate certificate to your server’s certificate bundle. In Nginx, the ssl_certificate file should contain your domain certificate followed by the intermediate certificate(s) concatenated in order. In Apache 2.4.8+, the SSLCertificateFile should contain the full chain. Reload the web server after updating.
Fix 2: Expired Certificate or Wrong System Clock
The sub-message ‘certificate has expired or is not yet valid’ has two possible sources. Check both before concluding which applies.
Check the server’s certificate expiry
| # Check the expiry date of a server’s certificate:
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates
# Output shows: # notBefore=Mar 1 00:00:00 2026 GMT # notAfter=Sep 17 23:59:59 2026 GMT
# If notAfter is in the past, the certificate is expired. # The server owner must renew and redeploy the certificate. # There is no client-side fix for a genuinely expired server certificate. |
Check your system clock
Python validates certificate dates against the local system clock. A clock that is wrong by days or years causes valid certificates to appear expired or not-yet-valid. On Windows, check Date and Time settings and enable Set time automatically. On Mac, check System Settings, General, Date and Time, and enable Set time and date automatically. On Linux:
| # Check current time on Linux:
date
# Sync with NTP immediately: sudo timedatectl set-ntp true # or: sudo ntpdate -u pool.ntp.org |
Fix 3: Self-Signed Certificate (Server You Control)
The sub-message ‘self-signed certificate’ means the server is presenting a certificate that was not signed by any CA in Python’s trusted bundle. This is common in development environments and internal services.
The correct fix is to point Python to the specific certificate file, not to disable verification. This way, Python can still verify the connection is to the expected endpoint, preventing MITM attacks.
| # Point requests to the specific self-signed certificate for verification:
import requests response = requests.get(‘https://internal-server.local’, verify=’/path/to/server.crt’)
# For urllib: import ssl import urllib.request ctx = ssl.create_default_context(cafile=’/path/to/server.crt’) response = urllib.request.urlopen(‘https://internal-server.local’, context=ctx)
# For repeated use, set the environment variable to avoid changing every call: # export REQUESTS_CA_BUNDLE=/path/to/server.crt # Python requests automatically respects this environment variable |
If the self-signed certificate is used across many services, a better approach is creating a local private CA with mkcert, trusting the CA in the OS, and issuing a certificate signed by that CA. This way Python uses the system trust store (on Linux) or the certifi-appended bundle, and no per-request verify path is needed.
Fix 4: Corporate SSL Inspection Proxy (The Most Common Enterprise Cause)
The sub-message ‘self-signed certificate in certificate chain’ when connecting to a well-known public site (pypi.org, github.com, an AWS endpoint) is the fingerprint of a corporate SSL inspection proxy. Zscaler, Palo Alto, Blue Coat, Cisco Umbrella, and similar products decrypt HTTPS traffic, inspect it, and re-encrypt it using the company’s internal root CA. Browsers on corporate machines trust this because IT pushed the root certificate to the OS trust store. Python does not, because it uses certifi rather than the OS store.
The fix is appending the corporate root CA certificate to Python’s certifi bundle.
Step 1: Get the corporate root CA certificate
Ask your IT department for the corporate root CA certificate in PEM format. Alternatively, export it from Chrome: go to Settings, Privacy and Security, Security, Manage certificates, find the corporate root under Trusted Root Certification Authorities, and export as Base64 encoded X.509 (.cer or .pem).
Step 2: Find and append to the certifi bundle
| # Find the certifi CA bundle path:
python -m certifi # Example output: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
# Append the corporate CA to certifi’s bundle (Linux/Mac): cat /path/to/corporate-root-ca.pem >> $(python -m certifi)
# Windows (PowerShell): # $certipath = python -m certifi # Add-Content -Path $certipath -Value (Get-Content C:\path\to\corp-root.pem)
# Verify the append worked: python -c “import requests; requests.get(‘https://pypi.org’)” # Should complete without error |
Alternative: Use environment variable instead of modifying certifi
| # Set environment variable to point to your own bundle
# containing both certifi’s standard CAs and the corporate CA:
# Combine certifi bundle with corporate CA: cat $(python -m certifi) /path/to/corporate-root-ca.pem > /path/to/combined-bundle.pem
# Set environment variable (add to shell profile for persistence): export REQUESTS_CA_BUNDLE=/path/to/combined-bundle.pem export SSL_CERT_FILE=/path/to/combined-bundle.pem
# Python requests and most SSL-using libraries respect REQUESTS_CA_BUNDLE. # SSL_CERT_FILE is respected by the ssl module directly. |
Do not overwrite certifi’s cacert.pem with only your corporate CA. This replaces the full set of public CAs with just the corporate one, breaking connections to all public HTTPS sites that are not intercepted by the proxy. Always append to the existing bundle or create a combined bundle.
Fix 5: Hostname Mismatch
The sub-message about hostname mismatch means the certificate presented by the server does not include the hostname in the URL as a Subject Alternative Name. This is a server-side configuration problem, not a client-side one.
Verify what the certificate actually covers:
| # Check what SANs the server’s certificate includes:
openssl s_client -connect hostname:443 -servername hostname 2>/dev/null | openssl x509 -noout -text | grep -A5 ‘Subject Alternative Name’
# If the hostname you are connecting to does not appear in the SAN list, # the certificate must be reissued to include it. # Client-side workarounds defeat the purpose of hostname verification. |
If you control the server, reissue the certificate with the correct hostname in the SANs. If the server is internal and uses an IP address directly, the certificate must include an iPAddress SAN entry for that IP address, not just a DNS name. If you are connecting to an internal hostname that does not match the certificate CN or any SAN, the certificate is misconfigured and must be replaced.
Fix 6: Revoked Certificate
The sub-message ‘certificate has been revoked’ means the issuing CA marked this certificate as no longer trusted before its expiry date, typically due to a private key compromise or mis-issuance. There is no client-side fix. The server must replace the certificate.
If this appears on a server you control, treat it as a security incident. Generate a new private key, obtain a new certificate, deploy it, and investigate how the original private key was exposed. A revoked certificate on a server you do not control should be reported to the site operator immediately.
Language and Tool Specific Fixes
Python: macOS fresh installation
On macOS, Python installed from python.org ships without the macOS system certificates and does not use the Keychain. Run the certificate installer that ships with Python:
| # Find and run Install Certificates.command:
# Navigate to: /Applications/Python 3.x/Â (replace 3.x with your version) # Double-click: Install Certificates.command
# Or from Terminal: open /Applications/Python\ 3.12/Install\ Certificates.command
# Or manually install certifi and install its certificates: pip install –upgrade certifi /Applications/Python\ 3.12/python.org\ Packages/Install\ Certificates.command |
Python: virtual environments
Virtual environments inherit the base Python’s certifi but sometimes have certificate issues independent of the base installation. Check which Python the virtual environment is using and ensure certifi is updated within it:
| # Inside an active virtual environment:
pip install –upgrade certifi python -m certifi # confirm the path is inside the venv
# If using conda: conda update certifi
# If REQUESTS_CA_BUNDLE is set globally, venvs inherit it: # export REQUESTS_CA_BUNDLE=/path/to/your/bundle.pem |
curl
curl uses its own CA bundle separate from Python. The equivalent error appears as ‘SSL certificate problem: unable to get local issuer certificate’. Fix approaches:
| # Check which CA bundle curl is using:
curl –version | grep CAfile
# Point curl to a specific CA bundle for one request: curl –cacert /path/to/ca-bundle.pem https://example.com
# For corporate CA, add to the system CA bundle on Ubuntu/Debian: sudo cp corporate-root-ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates
# On RHEL/CentOS: sudo cp corporate-root-ca.crt /etc/pki/ca-trust/source/anchors/ sudo update-ca-trust
# macOS: add to system keychain (curl uses system certificates on macOS): sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain corporate-root-ca.crt |
Node.js
Node.js uses its own bundled CA list and does not use the system trust store by default. For corporate environments or self-signed certificates:
| # Point Node.js to additional CA certificates via environment variable:
export NODE_EXTRA_CA_CERTS=/path/to/corporate-root-ca.pem
# Or in code using the https module: const https = require(‘https’); const fs = require(‘fs’); const options = { ca: fs.readFileSync(‘/path/to/corporate-root-ca.pem’) }; https.get(‘https://internal-server’, options, (res) => { … });
# NODE_EXTRA_CA_CERTS appends to Node’s built-in CA list, # rather than replacing it. This is the correct approach. |
What Not to Do: Why verify=False Is Not a Fix
Setting verify=False in Python requests, using curl -k, or setting NODE_TLS_REJECT_UNAUTHORIZED=0 in Node.js completely disables certificate verification for that connection. The application still encrypts data in transit, but it no longer verifies that the server it is talking to is the server it intended to reach. An attacker positioned between the client and server can present any certificate and the client will accept it, decrypt and read all traffic, modify responses, and re-encrypt. This is a textbook man-in-the-middle attack.
The scenarios where disabling verification is sometimes defended: local development environments with no sensitive data and no production systems accessible, one-time debugging to confirm that certificate verification is the only issue, and throwaway scripts that connect only to servers you own and have physical access to. Even in these cases, the correct next step is immediately fixing the certificate trust configuration rather than leaving verification disabled.
Any code deployed to a production environment with verify=False or its equivalent is a serious security vulnerability regardless of what other security measures are in place. The fix is always one of the proper certificate trust approaches described earlier in this guide.
Quick Diagnostic Reference
| You see this | Most likely cause | Fastest first action |
| unable to get local issuer certificate — on any public site | certifi bundle outdated | pip install –upgrade certifi |
| unable to get local issuer certificate — only on one site | Server missing intermediate cert | openssl s_client -connect site:443 -showcerts; count certificates |
| self-signed certificate in certificate chain — on public sites | Corporate SSL inspection proxy | Get corporate root CA from IT; append to certifi bundle |
| self-signed certificate — on internal/dev server | Self-signed cert not in trust bundle | Use verify=’/path/to/cert.pem’ or add cert to OS trust store |
| certificate has expired | Server certificate is expired or local clock is wrong | Check clock first; then check server cert expiry with openssl |
| hostname mismatch | Certificate SANs do not include the hostname being used | Check SANs with openssl; reissue cert to include correct hostname |
| Works in browser, fails in Python on macOS | Python not using system certs after fresh install | Run Install Certificates.command from Python’s Applications folder |
| Works in browser, fails in Python on corporate network | Corporate CA not in certifi bundle | Add corporate CA to certifi bundle or set REQUESTS_CA_BUNDLE |
Frequently Asked Questions
What is the CERTIFICATE_VERIFY_FAILED error?
CERTIFICATE_VERIFY_FAILED is an SSL error raised by Python’s ssl module (and surfaced through requests, urllib, and other HTTP libraries) when the TLS certificate presented by a server cannot be verified against Python’s trusted CA bundle. The full error message includes a sub-message identifying the specific failure: the certificate chain is incomplete, the certificate is expired, the certificate is self-signed, or the hostname does not match. Each sub-message points to a different root cause requiring a different fix.
Why does the error occur in Python but not in a browser?
Browsers use the operating system’s certificate trust store, which is maintained by IT departments on corporate machines and by OS updates on personal machines. Python’s requests library uses the certifi package, which is a separate standalone CA bundle that does not automatically read the OS trust store. When a corporate network pushes a custom root CA to the OS, browsers trust it automatically but Python does not. Similarly, on macOS, Python installs its own OpenSSL independent of the system, so it does not access the macOS Keychain automatically.
Is it safe to use verify=False to fix CERTIFICATE_VERIFY_FAILED?
No. Setting verify=False disables all certificate verification, removing the protection that HTTPS provides against man-in-the-middle attacks. An attacker between your application and the server can present any certificate, decrypt all traffic, read credentials, and modify responses undetected. The correct fix for CERTIFICATE_VERIFY_FAILED is always to configure proper certificate trust, never to disable verification. The only acceptable context for verify=False is temporary debugging in a completely isolated local environment to confirm that certificate verification is the root cause, immediately followed by implementing the proper fix.
How do I fix CERTIFICATE_VERIFY_FAILED behind a corporate proxy?
Corporate SSL inspection proxies (Zscaler, Palo Alto, Blue Coat) decrypt and re-encrypt HTTPS traffic using a company-issued root CA. Python rejects connections because this corporate root CA is not in certifi’s bundle. Fix: obtain the corporate root CA certificate in PEM format from your IT department or export it from your browser’s trust store. Append it to certifi’s cacert.pem file (found by running python -m certifi), or create a combined bundle and set the REQUESTS_CA_BUNDLE environment variable to point to it. Do not replace certifi’s bundle; append to it.
How do I fix CERTIFICATE_VERIFY_FAILED on macOS after installing Python?
Python installed from python.org on macOS ships without the macOS system certificates. Run the Install Certificates.command script included in Python’s installation. It is in the Python folder in your Applications directory. Double-click it or run it from Terminal as shown in Fix 3 of this guide. This installs certifi and configures Python to use it. If the script is not available, run pip install –upgrade certifi directly.
Can CERTIFICATE_VERIFY_FAILED be caused by a server misconfiguration?
Yes. The sub-message ‘unable to get local issuer certificate’ often means the server is not sending its intermediate certificate as part of the TLS handshake. Without the intermediate, Python cannot build the chain from the server’s certificate to a trusted root. The server owner needs to add the intermediate certificate to their certificate bundle and reload the web server. Verify this with openssl s_client -connect hostname:443 -showcerts and count the certificates in the output. A properly configured server sends at least two: the leaf certificate and the intermediate.
