MQTT Security

SSL Certificate Verify Failed MQTT Error: Fixing Python Clients

Getting "[SSL: CERTIFICATE_VERIFY_FAILED]" when your Python MQTT client connects on port 8883? Here is how to find the exact cause and fix it properly on every platform, without disabling verification.

SSL certificate verify failed MQTT error illustration with a Python terminal showing a rejected certificate chain on port 8883
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

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

  1. Read the detail after certificate verify failed.
  2. Run the Step 1 script in the same environment.
  3. Use certifi.where() as ca_certs, or repair the OS store.
  4. Connect by hostname, not IP.
  5. Check date -u for clock problems.
  6. Look at the certificate issuer to detect proxies.
  7. 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.

Related articles

How to Rotate MQTT Credentials Without Downtime

A step-by-step dual-credential strategy for rotating MQTT usernames and passwords across a fleet: stage the new credential, switch with fallback, verify and only then revoke the old one.

7 min read

Storing MQTT Credentials Securely on Embedded Devices

A practical comparison of ways to protect MQTT usernames and passwords on ESP32 and Linux devices, from encrypted NVS and flash encryption to secure elements and TPM-backed systemd credentials.

7 min read

MQTT Access Control Lists (ACLs): Concepts and Design

What MQTT access control lists are, how brokers such as Mosquitto express topic-level read and write rules, how denied operations behave and how to design topic namespaces so authorization stays simple.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.