To test an MQTT TLS connection, check three layers in order: the TLS handshake itself with openssl s_client, then a real MQTT session with mosquitto_sub or mosquitto_pub, then the client library your devices use. Each layer can fail for different reasons, and testing them separately tells you exactly which one is broken.
All examples use broker.connectmq.com on port 8883 (MQTT over TLS). Replace the host with your own broker.
Step 1: check the handshake with openssl
openssl s_client speaks TLS but not MQTT, which is exactly what you want: it shows the certificate the broker sends and whether your machine trusts it, without any MQTT library in the way.
openssl s_client -connect broker.connectmq.com:8883 \
-servername broker.connectmq.com \
-verify_hostname broker.connectmq.com \
-showcerts </dev/nullWhat each option does:
-servernamesends SNI (Server Name Indication). Brokers and load balancers that host several names use it to pick the certificate. Without it you may get a default certificate and a hostname mismatch.-verify_hostnamemakes openssl check that the certificate is valid for that name, the same check your MQTT client does.-showcertsprints every certificate the server sends, so you can see whether the intermediate is included.</dev/nullcloses the connection right after the handshake instead of waiting for input.
At the end of the output, look for:
Verify return code: 0 (ok)Anything else is a problem you will also see in your MQTT client:
| openssl result | Meaning |
|---|---|
Verify return code: 20 (unable to get local issuer certificate) | The CA (or a missing intermediate) is not in your trust store |
Verify return code: 18 (self-signed certificate) | Server certificate is self-signed; pass its CA with -CAfile |
Verify return code: 19 (self-signed certificate in certificate chain) | Chain ends in a private root you have not trusted |
Verify return code: 10 (certificate has expired) | Renew the certificate on the broker |
Verify return code: 62 (hostname mismatch) | The certificate does not list the name you connected to |
wrong version number | That port is plain MQTT, not TLS (often 1883) |
connect: Connection refused / timeout | Port closed or filtered — not a TLS problem |
For a broker with a private CA, add -CAfile ca.crt and run the command again. If it then returns 0 (ok), your certificates are fine and the client only needs the same CA file.
Inspect the certificate quickly
openssl s_client -connect broker.connectmq.com:8883 \
-servername broker.connectmq.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltNameCheck that subjectAltName contains the exact hostname your devices use. Modern clients ignore the certificate's Common Name, so a name that only appears in CN does not count. For a CI check that fails two weeks before expiry, pipe the certificate into openssl x509 -noout -checkend 1209600.
To test which TLS versions the broker accepts, repeat the first command with -tls1_2 or -tls1_3. A device with an old TLS stack that only speaks TLS 1.2 needs the broker to keep 1.2 enabled — see TLS 1.2 vs 1.3 for MQTT.
Step 2: run a real MQTT session over TLS
A good handshake does not prove the MQTT listener works behind it. Subscribe and publish over 8883:
mosquitto_sub -h broker.connectmq.com -p 8883 \
--cafile /etc/ssl/certs/ca-certificates.crt \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t testmqtt/tls/check -v -dmosquitto_pub -h broker.connectmq.com -p 8883 \
--cafile /etc/ssl/certs/ca-certificates.crt \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t testmqtt/tls/check -m "hello over tls"mosquitto clients only enable TLS when you give them a CA option. Use --cafile with your CA bundle (the path above is Debian/Ubuntu; on macOS with Homebrew it is under $(brew --prefix)/etc/ca-certificates/cert.pem), --capath /etc/ssl/certs, or --tls-use-os-certs on mosquitto 2.x. For a private CA, point --cafile at your ca.crt. For mutual TLS add --cert client.crt --key client.key — see client certificates and mTLS.
When it fails, mosquitto is terse: it prints Error: A TLS error occurred. and nothing more. Go back to step 1 — openssl tells you why. If openssl succeeds but mosquitto fails, compare the hostname: -h must be the name in the certificate, not an IP address.
Step 3: test from your client code
Finally, test with the library your application uses. The certificate store and defaults differ per language, so a connection that works in mosquitto can still fail here.
import os
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, reason_code, properties):
print("CONNACK:", reason_code)
client.publish("testmqtt/tls/check", "hello from python over tls", qos=1)
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
c.tls_set() # system CAs, hostname check on; ca_certs="ca.crt" for a private CA
c.username_pw_set(os.environ["MQTT_USER"], os.environ["MQTT_PASS"])
c.on_connect = on_connect
c.connect("broker.connectmq.com", 8883)
c.loop_forever()import mqtt from "mqtt";
const client = mqtt.connect("mqtts://broker.connectmq.com:8883", {
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
// ca: fs.readFileSync("ca.crt"), // only for a private CA
reconnectPeriod: 0,
});
client.on("connect", () => {
console.log("connected over TLS");
client.publish("testmqtt/tls/check", "hello from node over tls", () => client.end());
});
client.on("error", (err) => console.error(err.code, err.message));The errors these produce are the ones people search for:
| Library | Error | Usual cause |
|---|---|---|
| Python | [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate | Missing CA or intermediate |
| Python | certificate verify failed: Hostname mismatch, certificate is not valid for '...' | Connected by IP or wrong name |
| Python | [SSL: WRONG_VERSION_NUMBER] wrong version number | TLS client on a plain port |
| Node.js | UNABLE_TO_VERIFY_LEAF_SIGNATURE — unable to verify the first certificate | Server does not send the intermediate |
| Node.js | SELF_SIGNED_CERT_IN_CHAIN | Private CA not passed in ca |
| Node.js | ERR_TLS_CERT_ALTNAME_INVALID — Hostname/IP does not match certificate's altnames | Name not in the SAN list |
The fixes are covered one by one in certificate verify failed, TLS handshake failed and hostname mismatch. The code generator produces TLS-enabled connection snippets for other languages.
A quick TLS test checklist
openssl s_clientwith-servernameand-verify_hostnamereturnsVerify return code: 0 (ok).-showcertslists the server certificate and the intermediate.- The SAN contains the exact hostname clients use; the certificate is not close to expiry.
mosquitto_subandmosquitto_pubexchange a message on8883with verification on.- Your application library connects with its default trust store (or the private CA you ship).
- Plain port
1883is closed or disabled in production if TLS is required. - Devices with older TLS stacks connect with the TLS version and cipher suites you allow.
Want a browser-based check as well? The online MQTT client connects over secure WebSockets on 8084, which uses the same certificate in many setups — a fast way to confirm the broker's TLS side before you debug a device.
Testing TLS layer by layer turns a vague "TLS error" into a specific cause in a few minutes. Keep the openssl command and the expiry check in your scripts, and run them whenever a certificate is renewed or a load balancer changes.
Frequently asked questions
How do I check if an MQTT broker supports TLS?
Run openssl s_client -connect host:8883 -servername host. If the handshake succeeds you see the certificate chain and 'Verify return code: 0 (ok)'. A 'wrong version number' error means the port speaks plain MQTT, not TLS.
Why does TLS work in my browser but fail on my device?
Usually the server does not send its intermediate certificate. Browsers can fetch missing intermediates themselves; openssl, mosquitto and embedded TLS stacks cannot. Check that s_client -showcerts lists the intermediate, and fix the broker's certificate file.
Is it safe to use --insecure or tls_insecure_set(True)?
Only as a temporary diagnostic on your own machine. These options disable hostname verification, which makes the connection open to man-in-the-middle attacks. Fix the certificate or hostname instead and keep verification on.
