Tutorials

MQTT over TLS in Python (CA, Client Certificates)

Connect paho-mqtt to an MQTT broker over TLS: tls_set() options, custom CA files, mutual TLS with client certificates and fixes for common SSL errors.

Updated · 9 min read

MQTT over TLS in Python (CA, Client Certificates)

To connect paho-mqtt over TLS, call client.tls_set() before connect() and use the broker's TLS port, usually 8883. With no arguments, paho 2.x verifies the broker certificate against your system's trusted CAs and checks the hostname, which is exactly what you want for a broker with a public certificate. For a private CA you pass ca_certs=, and for mutual TLS you add certfile= and keyfile=.

This guide covers each tls_set() option, a full mutual TLS example, and the exact SSL errors paho raises, with how to confirm and fix each one.

The minimal TLS connection

tls_minimal.py
import os
import time

import paho.mqtt.client as mqtt


def on_connect(client, userdata, flags, reason_code, properties):
    print("CONNACK:", reason_code)
    client.subscribe("testmqtt/tls-demo/#")


def on_message(client, userdata, msg):
    print(msg.topic, msg.payload.decode())


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="tls-demo-1")
client.username_pw_set(os.environ["MQTT_USER"], os.environ["MQTT_PASS"])
client.tls_set()  # system CA store, TLS 1.2+, hostname verification on
client.on_connect = on_connect
client.on_message = on_message

client.connect("broker.connectmq.com", 8883, keepalive=60)
client.loop_start()
client.publish("testmqtt/tls-demo/hello", "encrypted hello", qos=1)
time.sleep(2)
client.loop_stop()
client.disconnect()

Two rules prevent most problems:

  • Connect with the hostname on the certificate, not an IP address. paho sends the host you pass to connect() as SNI and checks it against the certificate.
  • Match port and protocol. TLS goes to 8883 (or 8084 for secure WebSockets); plain TCP goes to 1883. Mixing them up produces confusing errors, covered below. See MQTT ports for the full list.

tls_set() options explained

ArgumentDefault in paho 2.xUse it when
ca_certsNone → OS trust storeThe broker uses a private or self-signed CA: path to the CA PEM file
certfileNoneThe broker requires client certificates (mutual TLS)
keyfileNonePrivate key for certfile, if not in the same file
keyfile_passwordNoneThe key is encrypted; otherwise paho may prompt on the terminal
cert_reqsssl.CERT_REQUIREDAlmost never change it; ssl.CERT_NONE disables verification
tls_versionssl.PROTOCOL_TLS_CLIENT (negotiates the highest version)Rarely; prefer setting a minimum version on your own context
ciphersOpenSSL defaultsYou must comply with a specific cipher policy
alpn_protocolsNoneThe endpoint routes by ALPN, for example MQTT on port 443

Two related methods complete the picture:

  • tls_insecure_set(True) keeps chain verification but skips the hostname check. It must be called after tls_set(), otherwise paho raises ValueError: Must configure SSL context before using tls_insecure_set.
  • tls_set_context(ctx) accepts your own ssl.SSLContext. Use it when you need settings tls_set() does not expose, and note you can only configure TLS once per client: a second call raises ValueError: SSL/TLS has already been configured.
custom_context.py
import ssl

ctx = ssl.create_default_context(cafile="/etc/mqtt/ca.crt")  # verify + hostname check
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
client.tls_set_context(ctx)

Private CA or self-signed broker

A Mosquitto broker on your LAN usually has a certificate signed by your own CA. Give paho the CA certificate (not the broker certificate, and never the private key):

private_ca.py
client.tls_set(ca_certs="/etc/mqtt/ca.crt")
client.connect("mqtt.home.internal", 8883)

The hostname you connect to must appear in the broker certificate's Subject Alternative Name. If you connect by IP, the IP must be in the SAN as an IP: entry; a DNS: entry does not cover it. When in doubt, regenerate the server certificate with the right SAN instead of turning off checks. The full walkthrough is in fixing self-signed certificate errors.

Mutual TLS with client certificates

With mutual TLS the broker also verifies the client. Each device gets its own certificate signed by a CA the broker trusts, and many brokers can map the certificate's CN to the MQTT username.

mtls_client.py
import ssl

import paho.mqtt.client as mqtt

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="device-0042")
client.tls_set(
    ca_certs="/etc/mqtt/ca.crt",          # CA that signed the broker certificate
    certfile="/etc/mqtt/device-0042.crt",  # this client's certificate (PEM)
    keyfile="/etc/mqtt/device-0042.key",   # its private key (PEM)
    keyfile_password=None,                 # or read it from a secret store
    cert_reqs=ssl.CERT_REQUIRED,
)
client.connect("mqtt.example.internal", 8883)
client.loop_forever()

Points that trip people up:

  • certfile can contain the client certificate followed by intermediate CAs, which is required when the broker only trusts the root.
  • Keys must be PEM. Convert a PKCS#12 bundle with openssl pkcs12 -in device.p12 -out device.crt -clcerts -nokeys and openssl pkcs12 -in device.p12 -out device.key -nocerts -nodes.
  • Protect the key file (chmod 600) and never commit it.

Whether your broker supports client certificates, and how it maps them to identities, is broker-specific. The concepts and broker-side setup are covered in MQTT client certificates and mTLS.

Common SSL errors in paho and how to fix them

With connect(), TLS errors are raised directly as exceptions, which makes them easy to read. With connect_async() and loop_start(), the network thread keeps retrying instead, so call client.enable_logger() to see the error. The (_ssl.c:NNNN) suffix varies by Python version; search for the text before it.

certificate verify failed: unable to get local issuer certificate

Error
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)
  • Cause: Python cannot find the CA that issued the broker certificate. Either the broker uses a private CA, or your Python has no CA bundle (typical for python.org builds on macOS).
  • Confirm: python3 -c "import ssl; print(ssl.get_default_verify_paths())" shows where Python looks; openssl s_client (see below) shows who issued the certificate.
  • Fix: for a private CA, pass ca_certs="ca.crt". For a missing bundle, run /Applications/Python 3.x/Install Certificates.command on macOS, or pip install certifi and use tls_set(ca_certs=certifi.where()).

self-signed certificate (in certificate chain)

Error
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1006)
  • Cause: the chain ends in a root your client does not trust. Older OpenSSL builds spell it self signed certificate.
  • Fix: pass the root CA with ca_certs=. If the broker certificate itself is self-signed (no CA), pass that certificate file as ca_certs, or better, issue it from a small CA.

Hostname mismatch

Error
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1006)
  • Cause: the name or IP in connect() is not in the certificate's SAN list.
  • Confirm: openssl s_client -connect host:8883 -servername host </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName.
  • Fix: connect with a name that is in the SAN, or reissue the certificate. tls_insecure_set(True) hides the problem and should stay out of production. More in MQTT hostname mismatch.

certificate has expired

Error
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)
  • Cause: the broker certificate (or an intermediate) is past its notAfter date, or the device clock is wrong. Devices without RTC that boot in 1970 hit the opposite case, certificate is not yet valid.
  • Fix: renew the certificate; on devices, sync time (NTP) before connecting.

Wrong port: EOF or wrong version number

Errors
ssl.SSLEOFError: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1006)
ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1006)
  • Cause: you called tls_set() but connected to a plaintext port. An MQTT listener usually just closes the connection (EOF or ConnectionResetError); a port that answers with HTTP or other non-TLS bytes gives WRONG_VERSION_NUMBER.
  • Fix: use the TLS port (8883). The reverse mistake, no tls_set() against 8883, raises nothing useful: the broker drops the connection and you never get a CONNACK.

Client certificate rejected (mutual TLS)

Errors
ssl.SSLError: [SSL: TLSV13_ALERT_CERTIFICATE_REQUIRED] tlsv13 alert certificate required (_ssl.c:2580)
ssl.SSLError: [SSL: TLSV1_ALERT_UNKNOWN_CA] tlsv1 alert unknown ca (_ssl.c:1006)
ssl.SSLError: [SSL] PEM lib (_ssl.c:3900)
  • certificate required: the broker demands a client certificate and you sent none; add certfile/keyfile. With TLS 1.3 this alert may arrive just after the handshake, so it can show up as an immediate disconnect rather than an exception.
  • unknown ca: the broker does not trust the CA that signed your client certificate, or you did not send the intermediate. Fix the broker's CA file or append the intermediate to certfile.
  • PEM lib or [X509: KEY_VALUES_MISMATCH] key values mismatch: the certificate or key file is unreadable, encrypted with a different password, not PEM, or the key does not belong to the certificate. Compare openssl x509 -noout -modulus -in device.crt | openssl md5 with openssl rsa -noout -modulus -in device.key | openssl md5 for RSA keys.

For a paho-wide list of other errors, see paho-mqtt Python errors.

How to test the fix

Check the broker side first, independently of Python:

Inspect the broker certificate
openssl s_client -connect broker.connectmq.com:8883 \
  -servername broker.connectmq.com -showcerts </dev/null
# look for: Verify return code: 0 (ok)

Then confirm a known-good client works with the same files:

Same check with mosquitto_sub
mosquitto_sub -h broker.connectmq.com -p 8883 \
  --capath /etc/ssl/certs \
  -u "$MQTT_USER" -P "$MQTT_PASS" \
  -t 'testmqtt/tls-demo/#' -v -C 1

Finally run your Python script with logging enabled, so any remaining problem is printed:

debug_tls.py
import logging

logging.basicConfig(level=logging.DEBUG)
client.enable_logger()

If openssl succeeds and Python fails, the difference is almost always the CA bundle Python uses. If both fail, the problem is on the broker or the network. A step-by-step checklist for the whole path is in test an MQTT TLS connection, and you can cross-check the broker over secure WebSockets with the online MQTT client.

TLS in paho comes down to one call made correctly: tls_set() with the right CA, the right port and a hostname that matches the certificate. Keep verification on, fix certificates instead of disabling checks, and you will get encryption and broker authentication with no extra code.

Frequently asked questions

What does tls_set() do with no arguments?

In paho-mqtt 2.x it creates a TLS client context with the highest TLS version Python supports, loads the operating system's trusted CA certificates, requires a valid server certificate and checks that the hostname matches. That is the right setting for brokers with publicly trusted certificates.

What is the difference between tls_insecure_set(True) and cert_reqs=ssl.CERT_NONE?

tls_insecure_set(True) still verifies the certificate chain but skips the hostname check. cert_reqs=ssl.CERT_NONE disables verification entirely, so any certificate is accepted. Both make man-in-the-middle attacks possible and belong only in short local tests.

Why does paho say certificate verify failed: unable to get local issuer certificate?

Python could not find the CA that signed the broker's certificate. Either the broker uses a private CA (pass its file with ca_certs=) or your Python has no CA bundle, which is common with python.org installs on macOS. Fix it by running Install Certificates.command or passing ca_certs=certifi.where().