Files
opencode/skill/mqtts-developer/mqtts-client-config.md

14 KiB

MQTTS Client Configuration Guide

Overview

Complete guide for configuring MQTT clients to connect to MQTTS (MQTT over TLS) servers with single-direction TLS authentication.

Authentication Model

Single-Direction TLS (Server Authentication):

  • Client verifies server identity (via TLS certificate)
  • Server authenticates client (via username/password in MQTT layer)
  • Client does NOT need client certificate

What Clients Need

No files needed! Operating system's built-in CA trust store automatically verifies the server certificate.

Advantages:

  • Zero configuration
  • No certificate files to distribute
  • Automatic updates with OS

Requirements:

  • Server certificate issued by trusted CA (Let's Encrypt, ZeroSSL, etc.)
  • System CA trust store up to date

Suitable for:

  • PC applications (Windows, Mac, Linux)
  • Mobile apps (Android, iOS)
  • Server-side applications
  • Docker containers with CA certificates

Option 2: Explicit CA Certificate (fullchain.pem)

Explicitly specify the CA certificate chain for verification.

Advantages:

  • Platform independent
  • No dependency on system configuration
  • Works on embedded devices without CA store

Requirements:

  • Distribute fullchain.pem to clients
  • Update file when server certificate is renewed (if using different CA)

Suitable for:

  • Embedded devices (ESP32, Arduino)
  • Minimal Linux systems without CA certificates
  • Strict security requirements
  • Air-gapped environments

Option 3: Public Key Pinning (Advanced)

Pin the server's public key or certificate fingerprint.

Advantages:

  • Highest security
  • Immune to CA compromise

Disadvantages:

  • Must update all clients when certificate changes
  • Complex to manage

Suitable for:

  • High-security applications (banking, healthcare)
  • Known, controlled client deployments

Language-Specific Examples

Python (paho-mqtt)

import paho.mqtt.client as mqtt
import ssl

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("Connected successfully")
        client.subscribe("test/#")
    else:
        print(f"Connection failed with code {rc}")

def on_message(client, userdata, msg):
    print(f"{msg.topic}: {msg.payload.decode()}")

client = mqtt.Client()
client.username_pw_set("username", "password")

# Use system CA - no files needed
client.tls_set(
    cert_reqs=ssl.CERT_REQUIRED,
    tls_version=ssl.PROTOCOL_TLSv1_2
)

client.on_connect = on_connect
client.on_message = on_message

client.connect("mq.example.com", 8883, 60)
client.loop_forever()

With fullchain.pem

client.tls_set(
    ca_certs="fullchain.pem",
    cert_reqs=ssl.CERT_REQUIRED,
    tls_version=ssl.PROTOCOL_TLSv1_2
)
client.connect("mq.example.com", 8883, 60)

Skip Verification (Testing Only)

# NOT RECOMMENDED FOR PRODUCTION
client.tls_set(cert_reqs=ssl.CERT_NONE)
client.tls_insecure_set(True)
client.connect("mq.example.com", 8883, 60)

Node.js (mqtt.js)

const mqtt = require('mqtt');

const options = {
  host: 'mq.example.com',
  port: 8883,
  protocol: 'mqtts',
  username: 'username',
  password: 'password',
  rejectUnauthorized: true  // Verify server certificate
};

const client = mqtt.connect(options);

client.on('connect', () => {
  console.log('Connected successfully');
  client.subscribe('test/#');
});

client.on('message', (topic, message) => {
  console.log(`${topic}: ${message.toString()}`);
});

client.on('error', (error) => {
  console.error('Connection error:', error);
});

With fullchain.pem

const fs = require('fs');

const options = {
  host: 'mq.example.com',
  port: 8883,
  protocol: 'mqtts',
  username: 'username',
  password: 'password',
  ca: fs.readFileSync('fullchain.pem'),
  rejectUnauthorized: true
};

const client = mqtt.connect(options);

Skip Verification (Testing Only)

const options = {
  host: 'mq.example.com',
  port: 8883,
  protocol: 'mqtts',
  username: 'username',
  password: 'password',
  rejectUnauthorized: false  // NOT RECOMMENDED
};

Java (Eclipse Paho)

import org.eclipse.paho.client.mqttv3.*;

String broker = "ssl://mq.example.com:8883";
String clientId = "JavaClient";

MqttClient client = new MqttClient(broker, clientId);
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName("username");
options.setPassword("password".toCharArray());
// Java uses system truststore by default

client.setCallback(new MqttCallback() {
    public void connectionLost(Throwable cause) {
        System.out.println("Connection lost: " + cause.getMessage());
    }
    
    public void messageArrived(String topic, MqttMessage message) {
        System.out.println(topic + ": " + new String(message.getPayload()));
    }
    
    public void deliveryComplete(IMqttDeliveryToken token) {}
});

client.connect(options);
client.subscribe("test/#");

With fullchain.pem

import javax.net.ssl.*;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.io.FileInputStream;

// Load CA certificate
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream fis = new FileInputStream("fullchain.pem");
Certificate ca = cf.generateCertificate(fis);
fis.close();

// Create KeyStore with CA
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("ca", ca);

// Create TrustManager
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
    TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);

// Create SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);

// Set in MQTT options
MqttConnectOptions options = new MqttConnectOptions();
options.setSocketFactory(sslContext.getSocketFactory());
options.setUserName("username");
options.setPassword("password".toCharArray());

client.connect(options);

C# (.NET)

using MQTTnet;
using MQTTnet.Client;

var factory = new MqttFactory();
var mqttClient = factory.CreateMqttClient();

var options = new MqttClientOptionsBuilder()
    .WithTcpServer("mq.example.com", 8883)
    .WithCredentials("username", "password")
    .WithTls()  // Use system CA
    .Build();

mqttClient.ConnectedAsync += async e => {
    Console.WriteLine("Connected successfully");
    await mqttClient.SubscribeAsync("test/#");
};

mqttClient.ApplicationMessageReceivedAsync += e => {
    var payload = System.Text.Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
    Console.WriteLine($"{e.ApplicationMessage.Topic}: {payload}");
    return Task.CompletedTask;
};

await mqttClient.ConnectAsync(options);

With fullchain.pem

using System.Security.Cryptography.X509Certificates;

var certificate = new X509Certificate2("fullchain.pem");

var tlsOptions = new MqttClientOptionsBuilderTlsParameters {
    UseTls = true,
    Certificates = new List<X509Certificate> { certificate },
    SslProtocol = System.Security.Authentication.SslProtocols.Tls12
};

var options = new MqttClientOptionsBuilder()
    .WithTcpServer("mq.example.com", 8883)
    .WithCredentials("username", "password")
    .WithTls(tlsOptions)
    .Build();

Go (paho.mqtt.golang)

package main

import (
    "fmt"
    "crypto/tls"
    mqtt "github.com/eclipse/paho.mqtt.golang"
)

func main() {
    tlsConfig := &tls.Config{}  // Uses system CA by default
    
    opts := mqtt.NewClientOptions()
    opts.AddBroker("ssl://mq.example.com:8883")
    opts.SetUsername("username")
    opts.SetPassword("password")
    opts.SetTLSConfig(tlsConfig)
    
    opts.OnConnect = func(c mqtt.Client) {
        fmt.Println("Connected successfully")
        c.Subscribe("test/#", 0, nil)
    }
    
    opts.DefaultPublishHandler = func(c mqtt.Client, msg mqtt.Message) {
        fmt.Printf("%s: %s\n", msg.Topic(), msg.Payload())
    }
    
    client := mqtt.NewClient(opts)
    if token := client.Connect(); token.Wait() && token.Error() != nil {
        panic(token.Error())
    }
    
    select {}  // Keep running
}

With fullchain.pem

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
)

// Load CA certificate
caCert, err := ioutil.ReadFile("fullchain.pem")
if err != nil {
    panic(err)
}

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

tlsConfig := &tls.Config{
    RootCAs: caCertPool,
}

opts.SetTLSConfig(tlsConfig)

ESP32/Arduino (PubSubClient)

With fullchain.pem (Required)

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

const char* ssid = "your_wifi_ssid";
const char* password = "your_wifi_password";
const char* mqtt_server = "mq.example.com";
const int mqtt_port = 8883;
const char* mqtt_user = "username";
const char* mqtt_pass = "password";

// Copy content from fullchain.pem here
const char* root_ca = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIGZDCCBEygAwIBAgIRAKIoXbOGN1X6u+vS+TbyLOgwDQYJKoZIhvcNAQEMBQAw\n" \
"...\n" \
"-----END CERTIFICATE-----\n" \
"-----BEGIN CERTIFICATE-----\n" \
"...\n" \
"-----END CERTIFICATE-----\n";

WiFiClientSecure espClient;
PubSubClient client(espClient);

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("]: ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect("ESP32Client", mqtt_user, mqtt_pass)) {
      Serial.println("connected");
      client.subscribe("test/#");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" retrying in 5 seconds");
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
  
  espClient.setCACert(root_ca);  // Set CA certificate
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
}

Skip Verification (Testing Only)

void setup() {
  // ...
  espClient.setInsecure();  // NOT RECOMMENDED FOR PRODUCTION
  client.setServer(mqtt_server, mqtt_port);
}

Command Line (Mosquitto)

# Publish
mosquitto_pub -h mq.example.com -p 8883 \
  --capath /etc/ssl/certs \
  -t "test/topic" -m "Hello MQTTS" \
  -u "username" -P "password"

# Subscribe
mosquitto_sub -h mq.example.com -p 8883 \
  --capath /etc/ssl/certs \
  -t "test/#" \
  -u "username" -P "password"

With fullchain.pem

mosquitto_pub -h mq.example.com -p 8883 \
  --cafile fullchain.pem \
  -t "test/topic" -m "Hello MQTTS" \
  -u "username" -P "password"

Skip Verification (Testing Only)

mosquitto_pub -h mq.example.com -p 8883 \
  --insecure \
  -t "test/topic" -m "Hello MQTTS" \
  -u "username" -P "password"

Troubleshooting

SSL Handshake Failed

Symptom: Connection refused, SSL error, handshake failure

Causes:

  1. System doesn't trust the CA
  2. Certificate expired or not yet valid
  3. System time incorrect
  4. Wrong domain name

Solutions:

# Check system time
date

# Update CA certificates (Linux)
sudo update-ca-certificates

# Test with openssl
openssl s_client -connect mq.example.com:8883 -servername mq.example.com

# Use fullchain.pem instead of system CA

Certificate Hostname Mismatch

Symptom: Hostname verification failed

Cause: Connecting to IP address instead of domain name

Solution: Always use domain name (mq.example.com), NOT IP (1.2.3.4)

Connection Timeout

Symptom: Connection times out, no response

Causes:

  1. Port 8883 blocked by firewall
  2. EMQX not listening on 8883
  3. DNS not resolving

Solutions:

# Test port connectivity
nc -zv mq.example.com 8883

# Check DNS
dig mq.example.com +short

# Test with telnet
telnet mq.example.com 8883

ESP32 Out of Memory

Symptom: Heap overflow, crash during TLS handshake

Causes:

  1. Certificate chain too large
  2. Insufficient heap space

Solutions:

// Use only server certificate (not full chain)
// Smaller but less secure
espClient.setCACert(server_cert_only);

// Reduce MQTT buffer size
client.setBufferSize(256);

// Use ECC certificate instead of RSA (smaller)

Security Best Practices

Production Requirements

Must Have:

  • Enable certificate verification (CERT_REQUIRED)
  • Use TLSv1.2 or higher
  • Verify server hostname/domain
  • Use strong username/password authentication
  • Keep client code updated

Never in Production:

  • Skip certificate verification (setInsecure(), rejectUnauthorized: false)
  • Use CERT_NONE or disable verification
  • Hard-code passwords in source code
  • Use deprecated TLS versions (SSLv3, TLSv1.0, TLSv1.1)

Certificate Updates

  • System CA: Automatic with OS updates
  • fullchain.pem: Update when server certificate renewed from different CA
  • Public Key Pinning: Must update all clients before certificate renewal

Credential Management

  • Store credentials in environment variables or config files
  • Use secrets management systems (AWS Secrets Manager, HashiCorp Vault)
  • Rotate passwords regularly
  • Use unique credentials per device/client

Decision Matrix

Client Platform Recommended Method Alternative Testing Method
PC/Mac System CA fullchain.pem Skip verification
Linux Server System CA fullchain.pem Skip verification
Android/iOS System CA fullchain.pem Skip verification
ESP32/Arduino fullchain.pem Server cert only setInsecure()
Docker Container System CA fullchain.pem Skip verification
Embedded Linux fullchain.pem System CA Skip verification

Summary

Single-Direction TLS (Current Setup):

  • Client verifies server (via certificate)
  • Server verifies client (via username/password)
  • Server does NOT verify client certificate

Client Needs:

  • System CA (easiest, no files) OR fullchain.pem (most reliable)
  • Does NOT need: server public key, server private key, server certificate

Key Takeaway: The server certificate (containing the public key) is automatically sent during TLS handshake. Clients only need a way to verify it (system CA or fullchain.pem).