I build MQTT Commander, an MQTT client for iPhone and iPad, so I read a lot of other people's failed TLS handshakes. The same half-dozen causes come up every time, and you can pin most of them down before you touch the broker config.
Plain MQTT on port 1883 either connects or it doesn't. Add TLS and five more things sit between you and a working session: the certificate chain, hostname verification, ALPN, client certificates and broker-side authorization. The MQTT client that reports the failure sees a closed socket and little else.
Three layers, in order
Before the first MQTT packet moves, three things have to succeed one after another.
TCP. The client resolves the broker hostname, opens a socket on 8883 (MQTT over TLS) or 443 / 8084 (MQTT over WebSocket and TLS), and completes the handshake. A firewall, a wrong port or a broker that isn't running fails here, and the fix is on the network side even when the error text mentions TLS. On iOS a LAN broker that never answers has one more possible cause: the app has no Local Network permission in Settings.
TLS handshake. Client and server agree on a cipher suite, exchange certificates and set up the encrypted channel, and most secure-MQTT failures land somewhere in that exchange.
MQTT CONNECT. Only now does the protocol itself start. The client sends CONNECT, the broker answers with CONNACK. A non-zero reason code at this point means the broker refused you on purpose: bad credentials, a client ID it won't accept, an ACL that denies you. TLS finished before any of that.
Find out which of the three failed before you change anything.
Where TLS breaks
Most MQTT clients collapse a TLS failure into "connection failed". To see the real verdict, ask OpenSSL:
openssl s_client -connect broker.example.com:8883 \
-servername broker.example.com -verify_hostname broker.example.com
The verify error lines below are what it prints for each cause. Other TLS stacks word them differently, but the categories are the same.
An expired or incomplete chain
Every certificate in the chain expires, intermediates included. An expired intermediate breaks the connection while the leaf certificate is still valid, and the leaf is the one you check by hand.
verify error:num=10:certificate has expired
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
Codes 20 and 21 mean the chain stops short of a root you trust. Either the broker sends only its leaf certificate and skips the intermediate, or the root is a private CA you haven't imported yet (next section). The first case you fix on the broker: every other client hits the same wall. Code 9, certificate is not yet valid, usually means the device clock is wrong rather than the certificate.
A private CA the device doesn't trust
Mosquitto on a Raspberry Pi, an internal staging broker, an AWS IoT custom endpoint: a private CA signs all of these, and no phone trusts that CA out of the box.
verify error:num=19:self-signed certificate in certificate chain
verify error:num=18:self-signed certificate
Code 19 is a private root; code 18 is a broker that serves a self-signed leaf with no CA at all. Import the root as PEM (for code 18, the broker's certificate is its own root) and compare its SHA-256 fingerprint with the one your CA published before you trust it. Whoever holds the key of an unverified root can impersonate your broker.
The hostname isn't in the certificate
TLS clients check that the hostname you connected to appears in the certificate's SAN (Subject Alternative Name); a name that only appears in CN no longer counts on iOS. The match is exact, so broker.example.com and mqtt.example.com are different names to TLS even when they point at the same box.
verify error:num=62:hostname mismatch
Connect using a name that is in the SAN. If you have to reach the broker by an address that isn't in the certificate, send the right server name during the handshake with an SNI override instead of editing your hosts file.
Missing or wrong ALPN
Some brokers multiplex MQTT onto a shared port and use ALPN to route it. AWS IoT Core takes MQTT with certificate authentication on port 443 only when the client advertises x-amzn-mqtt-ca. Leave it out and the handshake dies with a generic alert, or the socket closes right after it, and nothing in the message says ALPN. One common form of it:
error:0A000438:SSL routines::tlsv1 alert internal error
Standard MQTT over TLS on 8883 needs no ALPN entry in most setups. On 443, ALPN is the first thing to check. The AWS IoT guide on my site lists the exact values for that endpoint.
A TLS version the client won't negotiate
iOS negotiates TLS 1.2 or 1.3 and refuses 1.0 and 1.1. A broker built on an old library that tops out at 1.1, which still happens on legacy gateways and old embedded images, produces:
error:0A000102:SSL routines:ssl_choose_client_version:unsupported protocol
The client is refusing what the broker offered, so the fix is on the broker.
Mutual TLS
Standard TLS authenticates the broker to you. mTLS authenticates you back, which is why AWS IoT device provisioning and industrial fleets use it: each device carries its own certificate instead of a shared password.
You need three things: a client certificate, its private key, and the CA that signed both ends. They arrive as a passphrase-protected .p12 / .pfx bundle, or as separate PEM files.
Three failures cover most mTLS trouble.
The passphrase is wrong. Import fails before anything reaches the network, and OpenSSL calls it mac verify failure. Passphrases are case-sensitive. If you have lost it, re-export the bundle from the CA rather than guessing.
The certificate and key don't match. Import the client identity as a single .p12 bundle and the pair stays together by construction. With separate PEM files, a key from the wrong export surfaces as:
key values mismatch
The handshake succeeds and the broker hangs up anyway. The broker accepted your certificate and then refused the session on authorization grounds. In AWS IoT Core, look for a policy that doesn't grant iot:Connect for the client ID you present. With MQTT 5 the CONNACK reason code says so: 0x87, not authorized. With MQTT 3.1.1 the broker often closes the socket without a CONNACK at all.
Reading CONNACK
If TLS completed and the session still dies, the reason code names the cause:
| MQTT 5 | MQTT 3.1.1 | Meaning |
|---|---|---|
0x87 |
5 |
Not authorized: an ACL or IoT policy denies Connect
|
0x86 |
4 |
Bad username or password |
0x85 |
2 |
Client identifier not valid |
These three cover most post-handshake refusals.
The checklist
- Port matches the transport: 8883 for MQTT over TLS, 8084 for MQTT over WSS, 443 where ALPN does the routing
- Chain is complete and nothing in it has expired, intermediates included
- Private CA root is imported, and you verified its fingerprint before trusting it
- Connection hostname appears in the certificate's
SAN -
ALPNis set if the broker needs it, such asx-amzn-mqtt-cafor AWS IoT on 443 - For mTLS: certificate, key and CA all present, passphrase correct
- Client key matches the certificate's public key
- Broker policy grants
Connectfor this client ID
Work down it in order and stop at the first item that fails. You can't test anything below it until you fix that one. The site version of this checklist sits next to a table of what each diagnostic stage looks like when it fails.
On iOS I do this with MQTT Commander. Its Connection Doctor runs the connection stage by stage, from DNS and TCP through the TLS handshake, chain, expiry, hostname and ALPN/SNI to CONNACK, authentication and a write test against the broker ACL, stops at the first stage that fails and says what to change. The free download shows the transport and MQTT stages plus whichever stage fails, TLS included; the one-time Pro unlock lists each TLS check on its own line and adds a shareable report that carries stage names and statuses only, with no hosts, topics or credentials. The Certificate Manager is free on every tier: it imports a CA as PEM and a client identity as .p12 or .pfx, shows the SHA-256 fingerprint, and flags an expired certificate before you connect.
Top comments (0)