El error "ssl certificate verify failed" en MQTT significa que Python rechazó el certificado del broker durante el handshake TLS. Es una función de seguridad haciendo su trabajo, y la solución casi nunca es desactivarla. Esta guía muestra cómo leer la causa exacta en el mensaje de error y corregirla bien con paho-mqtt en Windows, macOS, Linux y Docker, usando broker.connectmq.com en el puerto 8883 como referencia que funciona.
Cómo leer el mensaje de error
La excepción completa suele tener este aspecto:
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate (_ssl.c:1000)
Lo que importa es la parte que sigue a los dos puntos. Las tres variantes habituales apuntan a causas distintas:
| Detalle del mensaje | Significado | Solución más probable |
|---|---|---|
unable to get local issuer certificate |
Python no encuentra la CA raíz que firmó la cadena | Repara o proporciona el paquete de CA |
self-signed certificate in certificate chain |
Algo presentó un certificado firmado por una CA privada no confiable | Inspección por un proxy o antivirus, o una CA privada del broker que no has configurado |
Hostname mismatch, certificate is not valid for '...' |
El nombre al que te conectaste no figura en el certificado | Conéctate con el hostname correcto, no con una IP |
certificate has expired |
El reloj está mal o el certificado del servidor ha caducado | Corrige la hora del sistema o renueva el certificado del servidor |
Paso 1: confirma que Python puede verificar el broker
Este breve script prueba TLS sin MQTT, con exactamente la misma configuración que paho-mqtt usa por defecto:
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"])
Si este script falla con el mismo error, el problema es el almacén de confianza del entorno de Python, el reloj o la red. Si funciona pero tu código MQTT falla, el problema está en la configuración del cliente MQTT: una ruta ca_certs incorrecta, una conexión por IP o un broker con un certificado privado.
Paso 2: usa una configuración TLS correcta en paho-mqtt
Con paho-mqtt 2.x, esta es la base que funciona con cualquier broker que tenga un certificado de confianza pública, incluido 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 incluye el paquete de CA de Mozilla, independiente del almacén del sistema operativo
client.tls_set(ca_certs=certifi.where(), cert_reqs=ssl.CERT_REQUIRED)
# Broker privado de ConnectMQ:
# 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()
Instala las dependencias con pip install "paho-mqtt>=2.0" certifi. Pasar certifi.where() hace que el comportamiento sea idéntico en todos los sistemas operativos, y a menudo es la solución permanente más rápida. Mantén certifi actualizado, porque las listas de CA cambian con el tiempo.
Soluciones para cada plataforma
macOS
Los instaladores de Python de python.org incluyen su propio OpenSSL y no usan el Llavero de macOS. Hasta que ejecutas el script incluido, no tienen ningún certificado de CA, lo que provoca unable to get local issuer certificate. Soluciónalo ejecutando el script de certificados del instalador:
/Applications/Python\ 3.12/Install\ Certificates.command
Ajusta el número de versión al de tu instalación. El Python de Homebrew usa el paquete de CA de Homebrew y normalmente funciona sin más; si no, ejecuta brew reinstall ca-certificates. Usar certifi.where() de forma explícita evita el problema en cualquier caso.
Windows
En Windows, Python carga los certificados desde el almacén de certificados de Windows. Los fallos suelen tener una de estas dos causas:
- Un almacén de raíces desactualizado en una máquina que no ha recibido actualizaciones, de modo que falta una raíz más reciente. Instala las actualizaciones de Windows o usa certifi.
- Inspección TLS por parte de un antivirus o un proxy corporativo, que produce
self-signed certificate in certificate chain. Revisa el emisor que imprime el script del paso 1; si menciona un producto de seguridad, ese producto está interceptando la conexión.
Linux
Python usa las rutas por defecto de OpenSSL, normalmente /etc/ssl/certs/ca-certificates.crt en Debian y Ubuntu o /etc/pki/tls/certs/ca-bundle.crt en sistemas basados en Red Hat. Si el paquete falta o está obsoleto:
# 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 e imágenes slim
Imágenes como python:3.12-slim suelen incluir certificados de CA, pero las variantes alpine y distroless pueden no hacerlo. Añade el paquete explícitamente:
FROM python:3.12-alpine
RUN apk add --no-cache ca-certificates
O instala certifi en la imagen y haz referencia a él en el código. También puedes apuntar OpenSSL a un paquete con la variable de entorno SSL_CERT_FILE.
La variante hostname mismatch
Hostname mismatch aparece cuando llamas a client.connect("203.0.113.10", 8883) o usas un alias interno que no figura en el certificado. TLS comprueba que el nombre que pediste aparece en los Subject Alternative Names del certificado. Conéctate con el hostname real del broker. En ConnectMQ es broker.connectmq.com. Si necesitas forzar una IP concreta para hacer pruebas, ponla en tu archivo hosts en lugar de conectarte por IP.
Brokers privados o autofirmados
Si operas tu propio broker con una CA privada, el paquete de CA públicas nunca confiará en él. Pasa la CA privada explícitamente con client.tls_set(ca_certs="my-ca.crt") y asegúrate de que el certificado del broker incluye el hostname correcto en su SAN. El proceso y la carga de mantenimiento que implica se explican en certificados autofirmados para MQTT.
Inspeccionar la cadena con OpenSSL
Cuando la salida de Python no basta, OpenSSL muestra la cadena exacta que envía el servidor y dónde se detiene la verificación:
openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts </dev/null
Lee la sección Certificate chain desde arriba: la entrada 0 es el certificado del broker, las siguientes son intermedios, y cada línea i: (emisor) debe coincidir con la línea s: (sujeto) de la entrada siguiente. El Verify return code final te dice si el almacén de confianza del propio OpenSSL acepta la cadena. Si OpenSSL dice 0 (ok) pero Python falla, Python está usando un almacén de confianza distinto del de OpenSSL; imprimir ssl.get_default_verify_paths() como en el paso 1 suele revelarlo. Si el emisor de la entrada 0 menciona al fabricante de tu antivirus o firewall, estás viendo una inspección TLS y no el certificado real del broker.
Para comparar específicamente con certifi, añade -CAfile con la ruta que imprime python -c "import certifi; print(certifi.where())". Un éxito con certifi y un fallo sin él confirman que el problema es el almacén del sistema operativo.
Lo que no debes hacer
Los resultados de búsqueda suelen sugerir estas líneas:
client.tls_set(cert_reqs=ssl.CERT_NONE)
client.tls_insecure_set(True)
Hacen desaparecer el error desactivando la autenticación del servidor. Tus credenciales irán entonces a cualquier servidor que responda, incluido el de un atacante. Úsalas, si acaso, solo durante unos minutos de depuración local contra un broker de laboratorio, nunca en código que se suba al repositorio. El artículo de buenas prácticas de seguridad MQTT explica por qué autenticar al broker importa tanto como el cifrado.
Checklist de solución rápida
- Lee el detalle que sigue a
certificate verify failed. - Ejecuta el script del paso 1 en el mismo entorno.
- Usa
certifi.where()comoca_certs, o repara el almacén del sistema operativo. - Conéctate por hostname, no por IP.
- Comprueba
date -upara descartar problemas de reloj. - Mira el emisor del certificado para detectar proxies.
- En brokers privados, proporciona tu propio archivo de CA.
Si sigues atascado después de arreglar el handshake, nuestra guía sobre cómo solucionar fallos del handshake TLS en MQTT cubre los puertos, SNI y las versiones del protocolo, y el tutorial de TLS en MQTT con Python muestra patrones completos para producción.
Por qué los certificados públicos ahorran tiempo
La mayoría de los fallos de verificación de certificados desaparecen cuando el broker usa un certificado de una CA de confianza pública y lo renueva automáticamente. Así funciona ConnectMQ: un broker MQTT gestionado con un certificado de Let's Encrypt en los puertos 8883 y 8084, de modo que las instalaciones estándar de Python lo verifican sin archivos adicionales. Puedes reproducir todas las pruebas de este artículo en el servidor MQTT público gratuito antes de pasar a credenciales privadas.
Empieza con ConnectMQ
Regístrate en app.connectmq.com, elige el plan gratuito Serverless sin tarjeta de crédito, crea un broker y ejecuta el ejemplo de paho-mqtt anterior con el usuario, la contraseña y el client ID generados.
Preguntas frecuentes
¿Por qué el mismo código funciona en Linux pero falla en macOS?
La compilación de python.org para macOS viene sin certificados de CA hasta que ejecutas "Install Certificates.command". El Python de Linux usa el paquete del sistema, que normalmente ya está instalado.
¿Es seguro usar certifi en lugar del almacén del sistema operativo?
Sí. certifi empaqueta la lista de raíces seleccionada por Mozilla. Solo tienes que mantenerlo actualizado con pip, y ten en cuenta que no incluirá CA privadas ni corporativas.
¿Significa este error que el broker está comprometido?
Rara vez. Los paquetes de CA ausentes, los hostnames incorrectos, los problemas de reloj y la inspección TLS son mucho más habituales. Revisa el nombre del emisor para asegurarte.
¿Necesito el archivo del certificado de ConnectMQ?
No. ConnectMQ usa un certificado de Let's Encrypt de confianza pública, así que basta con un paquete de CA estándar como certifi o el almacén del sistema operativo.



