The "ssl certificate verify failed" MQTT error means Python refused the broker's certificate during the TLS handshake. That is a safety feature doing its job, and the fix is almost never to turn it off. This guide shows how to read the exact reason from the error message and fix it correctly with paho-mqtt on Windows, macOS, Linux and Docker, using broker.connectmq.com on port 8883 as a known-good reference.
Reading the Error Message
The full exception usually looks like this:
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate (_ssl.c:1000)
The part after the colon is what matters. The three common variants point to different causes:
| Message detail | Meaning | Most likely fix |
|---|---|---|
unable to get local issuer certificate |
Python cannot find the root CA that signed the chain | Fix or supply the CA bundle |
self-signed certificate in certificate chain |
Something presented a certificate signed by an untrusted private CA | Proxy or antivirus inspection, or a private broker CA you have not configured |
Hostname mismatch, certificate is not valid for '...' |
The name you connected to is not on the certificate | Connect using the correct hostname, not an IP |
certificate has expired |
Clock is wrong or the server certificate expired | Fix system time or renew the server certificate |
Step 1: Confirm Python Can Verify the Broker
This short script tests TLS without MQTT, using exactly the settings paho-mqtt uses by default:
import socket
import ssl
HOST, PORT = "broker.connectmq.com", 8883
ctx = ssl.create_default_context()
print("OpenSSL:", ssl.OPENSSL_VERSION)
print("Default verify paths:", ssl.get_default_verify_paths())
with socket.create_connection((HOST, PORT), timeout=10) as sock:
with ctx.wrap_socket(sock, server_hostname=HOST) as tls:
cert = tls.getpeercert()
print("Connected with", tls.version())
print("Issuer:", dict(x[0] for x in cert["issuer"]))
print("Valid until:", cert["notAfter"])
If this script fails with the same error, the problem is the Python environment's trust store, the clock, or the network. If it succeeds but your MQTT code fails, the problem is in how the MQTT client is configured: a wrong ca_certs path, connecting by IP, or a broker with a private certificate.
Step 2: Use a Correct paho-mqtt TLS Setup
With paho-mqtt 2.x, this is the baseline that works against any broker with a publicly trusted certificate, including ConnectMQ:
import ssl
import time
import certifi
import paho.mqtt.client as mqtt
BROKER = "broker.connectmq.com"
TOPIC = "connectmq/demo/a8d2/verify-test"
def on_connect(client, userdata, flags, reason_code, properties):
print("CONNACK:", reason_code)
client.subscribe(TOPIC)
client.publish(TOPIC, "certificate verified")
def on_message(client, userdata, msg):
print("Received:", msg.payload.decode())
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="py-verify-a8d2")
client.on_connect = on_connect
client.on_message = on_message
# certifi ships Mozilla's CA bundle, independent of the OS store
client.tls_set(ca_certs=certifi.where(), cert_reqs=ssl.CERT_REQUIRED)
# Private ConnectMQ broker:
# client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.connect(BROKER, 8883, keepalive=60)
client.loop_start()
time.sleep(3)
client.loop_stop()
client.disconnect()
Install the dependencies with pip install "paho-mqtt>=2.0" certifi. Passing certifi.where() makes the behaviour identical on every operating system, which is often the quickest permanent fix. Keep certifi updated, because CA lists change over time.
Fixes for Each Platform
macOS
Python installers from python.org include their own OpenSSL and do not use the macOS Keychain. Until you run the bundled script, they have no CA certificates at all, which produces unable to get local issuer certificate. Fix it by running the installer's certificate script:
/Applications/Python\ 3.12/Install\ Certificates.command
Adjust the version number to match your installation. Homebrew Python uses Homebrew's CA bundle and usually works out of the box; if not, run brew reinstall ca-certificates. Using certifi.where() explicitly avoids the issue either way.
Windows
On Windows, Python loads certificates from the Windows certificate store. Failures typically have one of two causes:
- An outdated root store on a machine that has not received updates, so a newer root is missing. Install Windows updates, or use certifi.
- TLS inspection by antivirus or a corporate proxy, which produces
self-signed certificate in certificate chain. Check the issuer printed by the Step 1 script; if it names a security product, that product is intercepting the connection.
Linux
Python uses OpenSSL's default paths, usually /etc/ssl/certs/ca-certificates.crt on Debian and Ubuntu or /etc/pki/tls/certs/ca-bundle.crt on Red Hat based systems. If the package is missing or stale:
# Debian / Ubuntu / Raspberry Pi OS
sudo apt update && sudo apt install --reinstall ca-certificates
sudo update-ca-certificates
# Fedora / RHEL
sudo dnf reinstall ca-certificates
Docker and Slim Images
Images such as python:3.12-slim usually include CA certificates, but alpine and distroless variants may not. Add the package explicitly:
FROM python:3.12-alpine
RUN apk add --no-cache ca-certificates
Or install certifi in the image and reference it in code. You can also point OpenSSL at a bundle with the SSL_CERT_FILE environment variable.
The Hostname Mismatch Variant
Hostname mismatch appears when you call client.connect("203.0.113.10", 8883) or use an internal alias that is not on the certificate. TLS checks that the name you asked for appears in the certificate's Subject Alternative Names. Connect using the broker's real hostname. For ConnectMQ that is broker.connectmq.com. If you need to force a specific IP for testing, put it in your hosts file rather than connecting by IP.
Private or Self-Signed Brokers
If you run your own broker with a private CA, the public CA bundle will never trust it. Pass the private CA explicitly with client.tls_set(ca_certs="my-ca.crt"), and make sure the broker certificate includes the correct hostname in its SAN. The process and its maintenance burden are explained in self-signed certificates for MQTT.
Inspecting the Chain With OpenSSL
When the Python output is not enough, OpenSSL shows the exact chain the server sends and where verification stops:
openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts </dev/null
Read the Certificate chain section from the top: entry 0 is the broker certificate, the following entries are intermediates, and each i: (issuer) line should match the next entry's s: (subject) line. The final Verify return code tells you whether OpenSSL's own trust store accepts the chain. If OpenSSL says 0 (ok) but Python fails, Python is using a different trust store than OpenSSL; printing ssl.get_default_verify_paths() as in Step 1 usually reveals it. If the issuer of entry 0 names your antivirus or firewall vendor, you are looking at TLS inspection rather than the real broker certificate.
To compare against certifi specifically, add -CAfile with the path printed by python -c "import certifi; print(certifi.where())". A success with certifi and a failure without it confirms that the operating system store is the problem.
What Not to Do
Search results often suggest these lines:
client.tls_set(cert_reqs=ssl.CERT_NONE)
client.tls_insecure_set(True)
They make the error disappear by disabling server authentication. Your credentials then go to whichever server answers, including an attacker's. Use them, if ever, only for a few minutes of local debugging against a lab broker, never in code that gets committed. The MQTT security best practices article explains why authentication of the broker matters as much as encryption.
Quick Fix Checklist
- Read the detail after
certificate verify failed. - Run the Step 1 script in the same environment.
- Use
certifi.where()asca_certs, or repair the OS store. - Connect by hostname, not IP.
- Check
date -ufor clock problems. - Look at the certificate issuer to detect proxies.
- For private brokers, supply your own CA file.
If you are still stuck after the handshake is fixed, our guide on fixing MQTT TLS handshake failures covers ports, SNI and protocol versions, and the Python MQTT TLS tutorial shows complete production patterns.
Why Public Certificates Save Time
Most certificate verification failures disappear when the broker uses a certificate from a publicly trusted CA and renews it automatically. That is how ConnectMQ works: a hosted MQTT broker with a Let's Encrypt certificate on ports 8883 and 8084, so standard Python installations verify it without extra files. You can reproduce every test in this article on the free public MQTT broker before switching to private credentials.
Get Started with ConnectMQ
Sign up at app.connectmq.com, choose the free Serverless plan with no credit card, create a broker, and run the paho-mqtt example above with your generated username, password and client ID.
Frequently Asked Questions
Why does the same code work on Linux but fail on macOS?
The python.org macOS build ships without CA certificates until you run "Install Certificates.command". Linux Python uses the system bundle that is usually already installed.
Is it safe to use certifi instead of the operating system store?
Yes. certifi packages Mozilla's curated root list. Just keep it updated with pip, and note that it will not include private or corporate CAs.
Does this error mean the broker is compromised?
Rarely. Missing CA bundles, wrong hostnames, clock issues and TLS inspection are far more common. Check the issuer name to be sure.
Do I need the ConnectMQ certificate file?
No. ConnectMQ uses a publicly trusted Let's Encrypt certificate, so a standard CA bundle such as certifi or the OS store is enough.



