Para conectar paho-mqtt por TLS, llama a client.tls_set() antes de connect() y usa el puerto TLS del broker, normalmente 8883. Sin argumentos, paho 2.x verifica el certificado del broker contra las CA de confianza de tu sistema y comprueba el hostname, que es exactamente lo que quieres para un broker con certificado público. Para una CA privada pasas ca_certs=, y para TLS mutuo añades certfile= y keyfile=.
Esta guía cubre cada opción de tls_set(), un ejemplo completo de TLS mutuo y los errores SSL exactos que lanza paho, con cómo confirmarlos y corregirlos.
La conexión TLS mínima
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() # almacén de CA del sistema, TLS 1.2+, verificación de hostname
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()Dos reglas evitan la mayoría de los problemas:
- Conecta con el hostname que figura en el certificado, no con una dirección IP. paho envía el host que pasas a
connect()como SNI y lo compara con el certificado. - Haz coincidir puerto y protocolo. TLS va al 8883 (o al 8084 para WebSockets seguros); TCP sin cifrar va al 1883. Mezclarlos produce errores confusos, explicados más abajo. Consulta puertos MQTT para la lista completa.
Las opciones de tls_set()
| Argumento | Valor por defecto en paho 2.x | Úsalo cuando |
|---|---|---|
ca_certs | None → almacén de confianza del SO | El broker usa una CA privada o autofirmada: ruta al PEM de la CA |
certfile | None | El broker exige certificados de cliente (TLS mutuo) |
keyfile | None | Clave privada de certfile, si no está en el mismo archivo |
keyfile_password | None | La clave está cifrada; si no, paho puede pedirla en el terminal |
cert_reqs | ssl.CERT_REQUIRED | Casi nunca lo cambies; ssl.CERT_NONE desactiva la verificación |
tls_version | ssl.PROTOCOL_TLS_CLIENT (negocia la versión más alta) | Rara vez; mejor fija una versión mínima en tu propio contexto |
ciphers | Valores por defecto de OpenSSL | Debes cumplir una política de cifrado concreta |
alpn_protocols | None | El endpoint enruta por ALPN, por ejemplo MQTT en el puerto 443 |
Dos métodos relacionados completan el cuadro:
tls_insecure_set(True)mantiene la verificación de la cadena pero omite la comprobación del hostname. Debe llamarse después detls_set(); si no, paho lanzaValueError: Must configure SSL context before using tls_insecure_set.tls_set_context(ctx)acepta tu propiossl.SSLContext. Úsalo cuando necesites ajustes quetls_set()no expone, y ten en cuenta que TLS solo se puede configurar una vez por cliente: una segunda llamada lanzaValueError: SSL/TLS has already been configured.
import ssl
ctx = ssl.create_default_context(cafile="/etc/mqtt/ca.crt") # verificación + hostname
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
client.tls_set_context(ctx)CA privada o broker autofirmado
Un broker Mosquitto en tu LAN suele tener un certificado firmado por tu propia CA. Dale a paho el certificado de la CA (no el certificado del broker, y nunca la clave privada):
client.tls_set(ca_certs="/etc/mqtt/ca.crt")
client.connect("mqtt.home.internal", 8883)El hostname al que te conectas debe figurar en el Subject Alternative Name del certificado del broker. Si te conectas por IP, la IP debe estar en el SAN como entrada IP:; una entrada DNS: no la cubre. Ante la duda, regenera el certificado del servidor con el SAN correcto en lugar de desactivar comprobaciones. El recorrido completo está en solucionar errores de certificado autofirmado.
TLS mutuo con certificados de cliente
Con TLS mutuo el broker también verifica al cliente. Cada dispositivo recibe su propio certificado firmado por una CA en la que confía el broker, y muchos brokers pueden mapear el CN del certificado al usuario MQTT.
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 que firmó el certificado del broker
certfile="/etc/mqtt/device-0042.crt", # certificado de este cliente (PEM)
keyfile="/etc/mqtt/device-0042.key", # su clave privada (PEM)
keyfile_password=None, # o léela de un gestor de secretos
cert_reqs=ssl.CERT_REQUIRED,
)
client.connect("mqtt.example.internal", 8883)
client.loop_forever()Puntos en los que la gente tropieza:
certfilepuede contener el certificado del cliente seguido de las CA intermedias, algo necesario cuando el broker solo confía en la raíz.- Las claves deben estar en PEM. Convierte un bundle PKCS#12 con
openssl pkcs12 -in device.p12 -out device.crt -clcerts -nokeysyopenssl pkcs12 -in device.p12 -out device.key -nocerts -nodes. - Protege el archivo de clave (
chmod 600) y nunca lo subas al repositorio.
Si tu broker soporta certificados de cliente, y cómo los mapea a identidades, depende de cada broker. Los conceptos y la configuración del lado del broker se explican en certificados de cliente MQTT y mTLS.
Errores SSL comunes en paho y cómo corregirlos
Con connect(), los errores TLS se lanzan directamente como excepciones, lo que los hace fáciles de leer. Con connect_async() y loop_start(), el hilo de red sigue reintentando, así que llama a client.enable_logger() para ver el error. El sufijo (_ssl.c:NNNN) varía según la versión de Python; busca el texto que va antes.
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)- Causa: Python no encuentra la CA que emitió el certificado del broker. O el broker usa una CA privada, o tu Python no tiene bundle de CA (típico en builds de python.org para macOS).
- Confirmar:
python3 -c "import ssl; print(ssl.get_default_verify_paths())"muestra dónde busca Python;openssl s_client(ver abajo) muestra quién emitió el certificado. - Solución: para una CA privada, pasa
ca_certs="ca.crt". Si falta el bundle, ejecuta/Applications/Python 3.x/Install Certificates.commanden macOS, opip install certifiy usatls_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)- Causa: la cadena termina en una raíz en la que tu cliente no confía. Las versiones antiguas de OpenSSL lo escriben
self signed certificate. - Solución: pasa la CA raíz con
ca_certs=. Si el propio certificado del broker es autofirmado (sin CA), pasa ese archivo comoca_certso, mejor, emítelo desde una pequeña CA.
Hostname mismatch
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1006)- Causa: el nombre o la IP de
connect()no está en la lista SAN del certificado. - Confirmar:
openssl s_client -connect host:8883 -servername host </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName. - Solución: conéctate con un nombre que esté en el SAN o vuelve a emitir el certificado.
tls_insecure_set(True)oculta el problema y no debe llegar a producción. Más en hostname mismatch en MQTT.
certificate has expired
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)- Causa: el certificado del broker (o un intermedio) ha superado su fecha
notAfter, o el reloj del dispositivo está mal. Los dispositivos sin RTC que arrancan en 1970 sufren el caso contrario:certificate is not yet valid. - Solución: renueva el certificado; en los dispositivos, sincroniza la hora (NTP) antes de conectar.
Puerto equivocado: EOF o 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)- Causa: llamaste a
tls_set()pero te conectaste a un puerto sin cifrar. Un listener MQTT normalmente solo cierra la conexión (EOF oConnectionResetError); un puerto que responde con HTTP u otros bytes que no son TLS daWRONG_VERSION_NUMBER. - Solución: usa el puerto TLS (8883). El error inverso, sin
tls_set()contra el 8883, no lanza nada útil: el broker corta la conexión y nunca recibes un CONNACK.
Certificado de cliente rechazado (TLS mutuo)
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: el broker exige un certificado de cliente y no enviaste ninguno; añadecertfile/keyfile. Con TLS 1.3 esta alerta puede llegar justo después del handshake, así que puede aparecer como una desconexión inmediata en lugar de una excepción.unknown ca: el broker no confía en la CA que firmó tu certificado de cliente, o no enviaste el intermedio. Corrige el archivo de CA del broker o añade el intermedio acertfile.PEM libo[X509: KEY_VALUES_MISMATCH] key values mismatch: el certificado o la clave no se pueden leer, están cifrados con otra contraseña, no son PEM o la clave no corresponde al certificado. Para claves RSA, comparaopenssl x509 -noout -modulus -in device.crt | openssl md5conopenssl rsa -noout -modulus -in device.key | openssl md5.
Para una lista de otros errores de paho, consulta errores de paho-mqtt en Python.
Cómo probar la solución
Comprueba primero el lado del broker, independientemente de Python:
openssl s_client -connect broker.connectmq.com:8883 \
-servername broker.connectmq.com -showcerts </dev/null
# busca: Verify return code: 0 (ok)Después confirma que un cliente que sabes que funciona va bien con los mismos archivos:
mosquitto_sub -h broker.connectmq.com -p 8883 \
--capath /etc/ssl/certs \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/tls-demo/#' -v -C 1Por último, ejecuta tu script de Python con el logging activado para que se imprima cualquier problema restante:
import logging
logging.basicConfig(level=logging.DEBUG)
client.enable_logger()Si openssl funciona y Python falla, la diferencia casi siempre es el bundle de CA que usa Python. Si fallan ambos, el problema está en el broker o en la red. Tienes una checklist paso a paso de todo el recorrido en probar una conexión MQTT con TLS, y puedes contrastar el broker por WebSockets seguros con el cliente MQTT online.
TLS en paho se reduce a una llamada bien hecha: tls_set() con la CA correcta, el puerto correcto y un hostname que coincida con el certificado. Mantén la verificación activa, arregla los certificados en lugar de desactivar comprobaciones y obtendrás cifrado y autenticación del broker sin código extra.
Preguntas frecuentes
¿Qué hace tls_set() sin argumentos?
En paho-mqtt 2.x crea un contexto TLS de cliente con la versión de TLS más alta que soporte Python, carga los certificados CA de confianza del sistema operativo, exige un certificado de servidor válido y comprueba que el hostname coincide. Es la configuración correcta para brokers con certificados de confianza pública.
¿Qué diferencia hay entre tls_insecure_set(True) y cert_reqs=ssl.CERT_NONE?
tls_insecure_set(True) sigue verificando la cadena de certificados pero omite la comprobación del hostname. cert_reqs=ssl.CERT_NONE desactiva por completo la verificación y acepta cualquier certificado. Ambos permiten ataques man-in-the-middle y solo valen para pruebas locales breves.
¿Por qué paho dice certificate verify failed: unable to get local issuer certificate?
Python no encontró la CA que firmó el certificado del broker. O el broker usa una CA privada (pasa su archivo con ca_certs=) o tu Python no tiene bundle de CA, algo habitual en instalaciones de python.org en macOS. Se arregla ejecutando Install Certificates.command o pasando ca_certs=certifi.where().

