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
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
| Argument | Default in paho 2.x | Use it when |
|---|---|---|
ca_certs | None → OS trust store | The broker uses a private or self-signed CA: path to the CA PEM file |
certfile | None | The broker requires client certificates (mutual TLS) |
keyfile | None | Private key for certfile, if not in the same file |
keyfile_password | None | The key is encrypted; otherwise paho may prompt on the terminal |
cert_reqs | ssl.CERT_REQUIRED | Almost never change it; ssl.CERT_NONE disables verification |
tls_version | ssl.PROTOCOL_TLS_CLIENT (negotiates the highest version) | Rarely; prefer setting a minimum version on your own context |
ciphers | OpenSSL defaults | You must comply with a specific cipher policy |
alpn_protocols | None | The 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 aftertls_set(), otherwise paho raisesValueError: Must configure SSL context before using tls_insecure_set.tls_set_context(ctx)accepts your ownssl.SSLContext. Use it when you need settingstls_set()does not expose, and note you can only configure TLS once per client: a second call raisesValueError: SSL/TLS has already been configured.
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):
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.
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:
certfilecan 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 -nokeysandopenssl 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
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.commandon macOS, orpip install certifiand usetls_set(ca_certs=certifi.where()).
self-signed certificate (in certificate chain)
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 asca_certs, or better, issue it from a small CA.
Hostname mismatch
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
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
notAfterdate, 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
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 orConnectionResetError); a port that answers with HTTP or other non-TLS bytes givesWRONG_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)
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; addcertfile/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 tocertfile.PEM libor[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. Compareopenssl x509 -noout -modulus -in device.crt | openssl md5withopenssl rsa -noout -modulus -in device.key | openssl md5for 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:
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:
mosquitto_sub -h broker.connectmq.com -p 8883 \
--capath /etc/ssl/certs \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/tls-demo/#' -v -C 1Finally run your Python script with logging enabled, so any remaining problem is printed:
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().

