Tutoriais

MQTT sobre TLS em Python (CA e certificados de cliente)

Conecte o paho-mqtt a um broker MQTT via TLS: opções do tls_set(), arquivos CA próprios, TLS mútuo com certificados de cliente e correções de erros SSL comuns.

Atualizado · 9 min de leitura

MQTT sobre TLS em Python (CA e certificados de cliente)

Para conectar o paho-mqtt via TLS, chame client.tls_set() antes de connect() e use a porta TLS do broker, normalmente 8883. Sem argumentos, o paho 2.x verifica o certificado do broker contra as CAs confiáveis do seu sistema e checa o hostname, exatamente o que você quer para um broker com certificado público. Para uma CA privada você passa ca_certs=, e para TLS mútuo adiciona certfile= e keyfile=.

Este guia cobre cada opção do tls_set(), um exemplo completo de TLS mútuo e os erros SSL exatos que o paho lança, com como confirmar e corrigir cada um.

A conexão TLS mínima

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()  # CAs do sistema, TLS 1.2+, verificação de hostname ativa
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()

Duas regras evitam a maioria dos problemas:

  • Conecte usando o hostname que está no certificado, não um endereço IP. O paho envia o host passado em connect() como SNI e o compara com o certificado.
  • Combine porta e protocolo. TLS vai para a 8883 (ou 8084 para WebSockets seguros); TCP sem criptografia vai para a 1883. Misturar os dois gera erros confusos, explicados abaixo. Veja portas MQTT para a lista completa.

As opções do tls_set()

ArgumentoPadrão no paho 2.xUse quando
ca_certsNone → trust store do SOO broker usa uma CA privada ou autoassinada: caminho para o PEM da CA
certfileNoneO broker exige certificados de cliente (TLS mútuo)
keyfileNoneChave privada do certfile, se não estiver no mesmo arquivo
keyfile_passwordNoneA chave é criptografada; caso contrário o paho pode pedir no terminal
cert_reqsssl.CERT_REQUIREDQuase nunca mude; ssl.CERT_NONE desativa a verificação
tls_versionssl.PROTOCOL_TLS_CLIENT (negocia a versão mais alta)Raramente; prefira definir uma versão mínima no seu próprio contexto
ciphersPadrões do OpenSSLVocê precisa seguir uma política de ciphers específica
alpn_protocolsNoneO endpoint roteia por ALPN, por exemplo MQTT na porta 443

Dois métodos relacionados completam o quadro:

  • tls_insecure_set(True) mantém a verificação da cadeia, mas pula a checagem do hostname. Precisa ser chamado depois de tls_set(), senão o paho lança ValueError: Must configure SSL context before using tls_insecure_set.
  • tls_set_context(ctx) aceita o seu próprio ssl.SSLContext. Use quando precisar de ajustes que o tls_set() não expõe, e lembre que o TLS só pode ser configurado uma vez por cliente: uma segunda chamada lança ValueError: SSL/TLS has already been configured.
custom_context.py
import ssl

ctx = ssl.create_default_context(cafile="/etc/mqtt/ca.crt")  # verificação + hostname
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
client.tls_set_context(ctx)

CA privada ou broker autoassinado

Um broker Mosquitto na sua rede local geralmente tem um certificado assinado pela sua própria CA. Entregue ao paho o certificado da CA (não o certificado do broker, e nunca a chave privada):

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

O hostname ao qual você se conecta precisa aparecer no Subject Alternative Name do certificado do broker. Se você conecta por IP, o IP precisa estar no SAN como entrada IP:; uma entrada DNS: não cobre isso. Na dúvida, gere de novo o certificado do servidor com o SAN correto em vez de desligar as checagens. O passo a passo completo está em como corrigir erros de certificado autoassinado.

TLS mútuo com certificados de cliente

Com TLS mútuo o broker também verifica o cliente. Cada dispositivo recebe o próprio certificado, assinado por uma CA em que o broker confia, e muitos brokers conseguem mapear o CN do certificado para o usuário MQTT.

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 que assinou o certificado do broker
    certfile="/etc/mqtt/device-0042.crt",  # certificado deste cliente (PEM)
    keyfile="/etc/mqtt/device-0042.key",   # sua chave privada (PEM)
    keyfile_password=None,                 # ou leia de um cofre de segredos
    cert_reqs=ssl.CERT_REQUIRED,
)
client.connect("mqtt.example.internal", 8883)
client.loop_forever()

Pontos em que as pessoas tropeçam:

  • certfile pode conter o certificado do cliente seguido das CAs intermediárias, o que é necessário quando o broker só confia na raiz.
  • As chaves precisam estar em PEM. Converta um bundle PKCS#12 com openssl pkcs12 -in device.p12 -out device.crt -clcerts -nokeys e openssl pkcs12 -in device.p12 -out device.key -nocerts -nodes.
  • Proteja o arquivo de chave (chmod 600) e nunca faça commit dele.

Se o seu broker suporta certificados de cliente, e como os mapeia para identidades, depende de cada broker. Os conceitos e a configuração no broker estão em certificados de cliente MQTT e mTLS.

Erros SSL comuns no paho e como corrigi-los

Com connect(), os erros TLS são lançados diretamente como exceções, o que facilita a leitura. Com connect_async() e loop_start(), a thread de rede continua tentando de novo, então chame client.enable_logger() para ver o erro. O sufixo (_ssl.c:NNNN) varia conforme a versão do Python; pesquise pelo texto que vem antes.

certificate verify failed: unable to get local issuer certificate

Erro
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)
  • Causa: o Python não encontra a CA que emitiu o certificado do broker. Ou o broker usa uma CA privada, ou o seu Python não tem bundle de CA (típico dos builds do python.org no macOS).
  • Confirmar: python3 -c "import ssl; print(ssl.get_default_verify_paths())" mostra onde o Python procura; o openssl s_client (veja abaixo) mostra quem emitiu o certificado.
  • Correção: para uma CA privada, passe ca_certs="ca.crt". Se falta o bundle, rode /Applications/Python 3.x/Install Certificates.command no macOS, ou pip install certifi e use tls_set(ca_certs=certifi.where()).

self-signed certificate (in certificate chain)

Erro
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1006)
  • Causa: a cadeia termina em uma raiz em que o seu cliente não confia. Builds antigos do OpenSSL escrevem self signed certificate.
  • Correção: passe a CA raiz com ca_certs=. Se o próprio certificado do broker é autoassinado (sem CA), passe esse arquivo como ca_certs ou, melhor, emita-o a partir de uma pequena CA.

Hostname mismatch

Erro
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1006)
  • Causa: o nome ou IP usado em connect() não está na lista SAN do certificado.
  • Confirmar: openssl s_client -connect host:8883 -servername host </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName.
  • Correção: conecte com um nome que esteja no SAN ou reemita o certificado. tls_insecure_set(True) esconde o problema e não deve ir para produção. Mais em hostname mismatch no MQTT.

certificate has expired

Erro
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)
  • Causa: o certificado do broker (ou um intermediário) passou da data notAfter, ou o relógio do dispositivo está errado. Dispositivos sem RTC que iniciam em 1970 caem no caso oposto: certificate is not yet valid.
  • Correção: renove o certificado; nos dispositivos, sincronize a hora (NTP) antes de conectar.

Porta errada: EOF ou wrong version number

Erros
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: você chamou tls_set() mas conectou em uma porta sem criptografia. Um listener MQTT geralmente só fecha a conexão (EOF ou ConnectionResetError); uma porta que responde com HTTP ou outros bytes não TLS gera WRONG_VERSION_NUMBER.
  • Correção: use a porta TLS (8883). O erro inverso, sem tls_set() contra a 8883, não gera nada útil: o broker derruba a conexão e você nunca recebe um CONNACK.

Certificado de cliente rejeitado (TLS mútuo)

Erros
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: o broker exige um certificado de cliente e você não enviou nenhum; adicione certfile/keyfile. Com TLS 1.3 esse alerta pode chegar logo depois do handshake, então pode aparecer como uma desconexão imediata em vez de uma exceção.
  • unknown ca: o broker não confia na CA que assinou o seu certificado de cliente, ou você não enviou o intermediário. Corrija o arquivo de CA do broker ou acrescente o intermediário ao certfile.
  • PEM lib ou [X509: KEY_VALUES_MISMATCH] key values mismatch: o certificado ou a chave não podem ser lidos, estão criptografados com outra senha, não são PEM ou a chave não pertence ao certificado. Para chaves RSA, compare openssl x509 -noout -modulus -in device.crt | openssl md5 com openssl rsa -noout -modulus -in device.key | openssl md5.

Para uma lista de outros erros do paho, veja erros do paho-mqtt em Python.

Como testar a correção

Verifique primeiro o lado do broker, independentemente do Python:

Inspecionar o certificado do broker
openssl s_client -connect broker.connectmq.com:8883 \
  -servername broker.connectmq.com -showcerts </dev/null
# procure: Verify return code: 0 (ok)

Depois confirme que um cliente conhecido funciona com os mesmos arquivos:

A mesma checagem com 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

Por fim, rode seu script Python com logging ativado, para que qualquer problema restante seja impresso:

debug_tls.py
import logging

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

Se o openssl funciona e o Python falha, a diferença quase sempre é o bundle de CA que o Python usa. Se os dois falham, o problema está no broker ou na rede. Há um checklist passo a passo do caminho inteiro em testar uma conexão MQTT com TLS, e você pode conferir o broker via WebSockets seguros com o cliente MQTT online.

TLS no paho se resume a uma chamada feita corretamente: tls_set() com a CA certa, a porta certa e um hostname que confere com o certificado. Mantenha a verificação ativa, corrija os certificados em vez de desligar checagens, e você terá criptografia e autenticação do broker sem código extra.

Perguntas frequentes

O que o tls_set() faz sem argumentos?

No paho-mqtt 2.x ele cria um contexto TLS de cliente com a versão de TLS mais alta suportada pelo Python, carrega os certificados CA confiáveis do sistema operacional, exige um certificado de servidor válido e verifica se o hostname confere. É a configuração certa para brokers com certificados publicamente confiáveis.

Qual a diferença entre tls_insecure_set(True) e cert_reqs=ssl.CERT_NONE?

tls_insecure_set(True) continua verificando a cadeia de certificados, mas pula a checagem do hostname. cert_reqs=ssl.CERT_NONE desativa a verificação por completo e aceita qualquer certificado. Os dois permitem ataques man-in-the-middle e só servem para testes locais rápidos.

Por que o paho diz certificate verify failed: unable to get local issuer certificate?

O Python não encontrou a CA que assinou o certificado do broker. Ou o broker usa uma CA privada (passe o arquivo dela com ca_certs=) ou o seu Python não tem bundle de CA, algo comum em instalações do python.org no macOS. Resolva rodando Install Certificates.command ou passando ca_certs=certifi.where().