Home > IoT > MQTT mTLS Authentication Setup: Securing IoT Messaging with Client Certificates

MQTT mTLS Authentication Setup: Securing IoT Messaging with Client Certificates

Author: Ganesh Velrajan

Last Updated: Jul 28, 2026

MQTT is the dominant messaging protocol for IoT — lightweight, publish-subscribe, and designed for constrained devices and unreliable networks. But the default MQTT security model (username/password, or no authentication) leaves devices wide open to impersonation. Any machine that knows the password can publish as any device. There is no binding between a credential and a specific physical device.

Mutual TLS (mTLS) closes that gap. When the Mosquitto broker requires a client certificate, every connecting device must prove it holds a private key paired to a certificate signed by the trusted CA. Impersonation is cryptographically impossible without the key.

The standard advice for setting up MQTT mTLS is to run OpenSSL commands to build your own certificate authority. With SocketXP installed, you do not need to do that. SocketXP’s built-in BastionXP CA module is a private certificate authority. A single socketxp ca login command issues a CA-signed X.509 certificate for the MQTT broker and another for each IoT device client. Those certificates are what Mosquitto uses for the mTLS handshake.

For the full PKI architecture behind this model, see: Scaling mTLS IoT Security: The Developer’s Guide.


How It Fits Together

SocketXP issues two types of X.509 certificates through its built-in BastionXP CA module:

CLI flagCertificate typeDefault validityEKUIntended role
--server <name>TLS server certificateLong-livedserverAuth + clientAuthAny endpoint that needs a persistent, long-lived identity — the MQTT broker AND every IoT device
--client <email>TLS client certificate24 hoursclientAuthHuman operators and engineers for short-lived, one-off debugging or monitoring sessions

The key distinction is that the --server certificate carries both the TLS Web Server Authentication and TLS Web Client Authentication Extended Key Usage (EKU) OIDs. This means a --server certificate can authenticate in either direction: an IoT device uses it to authenticate to the broker as a client, and the broker uses it to authenticate to devices as a server. The --client certificate carries only clientAuth and is intentionally short-lived — it is designed for humans who need temporary access, not for devices that run continuously.

Both certificate types are signed by the same BastionXP CA root, which is what makes mutual verification possible.

                    SocketXP BastionXP CA
                          │
          ┌───────────────┴───────────────┐
          │                               │
   tls_server.crt                  tls_server.crt
   tls_server.key                  tls_server.key
  (MQTT Broker)                   (IoT MQTT Device)
  EKU: serverAuth                 EKU: serverAuth
       clientAuth                      clientAuth
          │                               │
          │◄──── mTLS handshake ─────────│
          │     both sides verified       │
          │     by shared CA root         │
          ▼                               ▼
   Mosquitto on :8883          mosquitto_pub / paho / ESP32

Prerequisites

  • SocketXP agent installed on the machine running the MQTT broker and on each IoT device (download)
  • A valid SocketXP auth token (from the SocketXP portal)
  • Mosquitto broker installed: sudo apt-get install -y mosquitto mosquitto-clients

Step 1: Issue the MQTT Broker Server Certificate

On the machine running Mosquitto, use the SocketXP agent to download a server certificate from the BastionXP CA module. The --server argument becomes the Common Name (CN) in the certificate — use a name that identifies the broker endpoint:

sudo socketxp ca login  --server "mqtt-broker.local"

The command downloads two files to /var/lib/socketxp/:

ls -la /var/lib/socketxp/tls_server.*
# tls_server.crt   ← broker's server certificate (signed by BastionXP CA)
# tls_server.key   ← broker's private key

Confirm the certificate is valid and inspect its subject:

openssl x509 -in /var/lib/socketxp/tls_server.crt -noout -subject -issuer -dates
# subject= /CN=mqtt-broker.local
# issuer=  /CN=BastionXP CA  (or similar — the BastionXP CA module's DN)
# notBefore=...
# notAfter=...

Step 2: Issue the MQTT Device Server Certificate

On each IoT device (or on the broker machine during fleet provisioning), download a server certificate for the device using the --server flag:

sudo socketxp ca login  --server "device001.fleet.local"

This generates:

ls -la /var/lib/socketxp/tls_server.*
# tls_server.crt   ← device certificate (signed by BastionXP CA)
# tls_server.key   ← device private key

The name argument (device001.fleet.local) becomes the certificate CN — the device’s unique identity that Mosquitto extracts and uses as the MQTT client username for ACL enforcement.

Why --server for IoT Devices, Not --client

The --server flag issues a long-lived certificate with both serverAuth and clientAuth in the Extended Key Usage extension. This dual EKU means the certificate is accepted by TLS stacks in either role: the device presents it as a client certificate when connecting to the Mosquitto broker, and Mosquitto’s mTLS verification accepts it because clientAuth is present.

The --client flag issues a certificate valid for only 24 hours. That short window is intentional — it is designed for human operators who need quick one-off access for debugging, monitoring, or diagnosing a specific device. An engineer runs socketxp ca login --client at the start of a session and the credential expires automatically at the end of the day.

IoT devices, by contrast, run continuously. They need a long-lived credential that remains valid across reboots, network interruptions, and sleep cycles — without requiring a human to issue a new credential every 24 hours. The --server certificate is the right choice for any device that maintains persistent or recurring MQTT connections.


Step 3: Obtain the BastionXP CA Root Certificate

Both the Mosquitto broker and every MQTT device need the CA root certificate — the shared trust anchor that lets each side verify the other’s certificate. This is the root certificate of the BastionXP CA module that signed both the broker’s and the device’s tls_server.crt.

Extract the issuing CA certificate from the broker’s downloaded chain:

# The tls_server.crt file may contain the full chain (leaf + CA).
# If the CA certificate is bundled, extract it:
openssl x509 -in /var/lib/socketxp/tls_server.crt -noout -text | grep -A 2 "Issuer:"

# To split a chain file into individual certificates (if bundled):
awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++} {print > "/tmp/cert-" n ".pem"}' \
    /var/lib/socketxp/tls_server.crt

# cert-1.pem = leaf (broker cert)
# cert-2.pem = CA certificate (BastionXP CA root)

Place the extracted CA certificate where both Mosquitto and MQTT clients can reference it:

sudo mkdir -p /etc/mosquitto/certs
sudo cp /tmp/cert-2.pem /etc/mosquitto/certs/bastionxp-ca.crt

Refer to the SocketXP documentation for the current procedure to download the BastionXP CA root certificate directly if it is not bundled in the chain.


Step 4: Configure Mosquitto with SocketXP Certificates

Copy the broker certificates into the Mosquitto certificate directory:

sudo cp /var/lib/socketxp/tls_server.crt /etc/mosquitto/certs/mqtt-broker.crt
sudo cp /var/lib/socketxp/tls_server.key /etc/mosquitto/certs/mqtt-broker.key

sudo chown -R mosquitto:mosquitto /etc/mosquitto/certs/
sudo chmod 600 /etc/mosquitto/certs/mqtt-broker.key

Create /etc/mosquitto/conf.d/mtls.conf:

# MQTT over mTLS — port 8883 (standard MQTT/TLS port)
listener 8883

# Broker's server certificate (from socketxp ca login --server)
certfile /etc/mosquitto/certs/mqtt-broker.crt
keyfile  /etc/mosquitto/certs/mqtt-broker.key

# BastionXP CA root — trusts both the broker cert and device client certs
# because both were signed by the same BastionXP CA module
cafile /etc/mosquitto/certs/bastionxp-ca.crt

# Require client certificate — reject any MQTT client without one
require_certificate true

# Use the client certificate CN as the MQTT client username
# Enables per-device ACL rules without a separate password database
use_identity_as_username true

# Minimum TLS version
tls_version tlsv1.2

# Disable anonymous access entirely
allow_anonymous false

Restart Mosquitto and verify it starts cleanly:

sudo systemctl restart mosquitto
sudo systemctl status mosquitto

# Check that Mosquitto is listening on 8883
ss -tlnp | grep 8883
# Expected: LISTEN  ...  *:8883  ...  mosquitto

Step 5: Test the mTLS Connection with mosquitto Clients

With the device’s tls_server.crt and tls_server.key from Step 2, test the mTLS handshake.

First confirm the broker rejects connections without a certificate:

mosquitto_pub \
  --cafile /etc/mosquitto/certs/bastionxp-ca.crt \
  -h mqtt-broker.local -p 8883 \
  -t "sensors/test" -m "hello"
# Expected: error: Connection Refused: not authorised
# The broker rejected the connection because no client cert was presented

Now connect with the SocketXP-issued device server certificate:

# Start a subscriber on the IoT device — using its tls_server.crt as the client credential
mosquitto_sub \
  --cafile /etc/mosquitto/certs/bastionxp-ca.crt \
  --cert /var/lib/socketxp/tls_server.crt \
  --key /var/lib/socketxp/tls_server.key \
  -h mqtt-broker.local -p 8883 \
  -t "sensors/device001/#" -v

# In a second terminal — publish a sensor reading as the authenticated device
mosquitto_pub \
  --cafile /etc/mosquitto/certs/bastionxp-ca.crt \
  --cert /var/lib/socketxp/tls_server.crt \
  --key /var/lib/socketxp/tls_server.key \
  -h mqtt-broker.local -p 8883 \
  -t "sensors/device001/temperature" \
  -m '{"temperature": 24.3, "unit": "C"}'

Verify the authenticated device identity in broker logs:

sudo tail -f /var/log/mosquitto/mosquitto.log
# Expected:
# New connection from 192.168.1.x on port 8883.
# New client connected from 192.168.1.x as device001.fleet.local (c1, k60, u'device001.fleet.local').

The username shown in the log is the CN from the device’s tls_server.crt (device001.fleet.local), extracted by Mosquitto via use_identity_as_username true. Mosquitto accepts the certificate as a valid client credential because its EKU includes clientAuth. No passwords were checked — the mTLS certificate chain verification is the authentication.


Step 6: Per-Device Topic ACLs from Certificate Identity

Because the CN from the device’s tls_server.crt becomes the MQTT username, you can write ACL rules that restrict each device to its own topic namespace — entirely from the certificate identity, with no additional credential management:

# /etc/mosquitto/acl

# device001.fleet.local can only publish/subscribe to its own topic tree
user device001.fleet.local
topic readwrite sensors/device001/#

# device002.fleet.local is isolated to its own namespace
user device002.fleet.local
topic readwrite sensors/device002/#

# Operator with --client cert (24-hour) can subscribe to all sensor feeds (read-only)
user [email protected]
topic read sensors/#

Enable the ACL file in /etc/mosquitto/conf.d/mtls.conf:

acl_file /etc/mosquitto/acl
sudo systemctl reload mosquitto

With this configuration, device001.fleet.local cannot publish to sensors/device002/temperature — the broker checks the certificate CN against the ACL and refuses the publish. Topic-level access control is enforced from the cryptographic device identity, with no shared password database to manage.


Step 7: Python paho-mqtt Client with SocketXP Certificates

For Linux IoT devices running Python, paho-mqtt accepts the SocketXP certificate files directly:

import ssl
import paho.mqtt.client as mqtt

BROKER_HOST   = "mqtt-broker.local"
BROKER_PORT   = 8883
CA_CERT       = "/etc/mosquitto/certs/bastionxp-ca.crt"
CLIENT_CERT   = "/var/lib/socketxp/tls_server.crt"   # --server cert: long-lived, clientAuth EKU
CLIENT_KEY    = "/var/lib/socketxp/tls_server.key"
DEVICE_ID     = "device001"

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print(f"[{DEVICE_ID}] mTLS connection established")
        client.subscribe(f"commands/{DEVICE_ID}/#")
    else:
        print(f"[{DEVICE_ID}] Connection failed: rc={rc}")

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

client = mqtt.Client(client_id=DEVICE_ID, protocol=mqtt.MQTTv5)
client.on_connect = on_connect
client.on_message = on_message

# Configure mTLS — point directly at SocketXP-issued certificate files
client.tls_set(
    ca_certs    = CA_CERT,
    certfile    = CLIENT_CERT,
    keyfile     = CLIENT_KEY,
    tls_version = ssl.PROTOCOL_TLS_CLIENT,
    cert_reqs   = ssl.CERT_REQUIRED,
)

client.connect(BROKER_HOST, BROKER_PORT, keepalive=60)
client.loop_start()

# Publish a sensor reading
client.publish(
    topic   = f"sensors/{DEVICE_ID}/temperature",
    payload = '{"temperature": 24.3, "unit": "C"}',
    qos     = 1,
)

The tls_set() call references /var/lib/socketxp/tls_server.crt and /var/lib/socketxp/tls_server.key — the files downloaded by socketxp ca login --server. The clientAuth EKU in the --server certificate is what allows paho-mqtt to present it as a client credential. No certificate format conversion needed.


Certificate Lifecycle for MQTT

IoT Device Certificates: Automatic Renewal by the SocketXP Agent

The --server certificate issued to each IoT device is long-lived and its renewal is handled automatically by the SocketXP agent running as a background service on the device. No renewal scripts, systemd timers, or OTA deliveries are required.

The agent continuously monitors the expiry date of tls_server.crt. When the remaining validity falls to 30% or below, the agent automatically contacts the BastionXP CA module, obtains a fresh certificate, replaces the file on disk, and re-establishes the mTLS connection — all without any operator action.

For an MQTT device, this means:

  • The initial socketxp ca login --server "device001.fleet.local" is a one-time step at provisioning
  • Every subsequent renewal cycle is handled by the agent in the background
  • The MQTT client service on the device reconnects with the renewed certificate automatically when the agent restarts its connection

The same automatic renewal applies to the broker’s tls_server.crt. The SocketXP agent on the broker machine monitors the broker certificate’s expiry and renews it at the 30% threshold. After renewal, copy the fresh certificate files into Mosquitto’s certificate directory and reload the broker:

# After the agent auto-renews the broker cert, apply it to Mosquitto:
sudo cp /var/lib/socketxp/tls_server.crt /etc/mosquitto/certs/mqtt-broker.crt
sudo cp /var/lib/socketxp/tls_server.key /etc/mosquitto/certs/mqtt-broker.key
sudo systemctl reload mosquitto

Mosquitto’s reload (not restart) re-reads the certificate files without dropping existing client connections — in-flight MQTT sessions from other devices are not interrupted.

Operator Debug Access: 24-Hour --client Certificates

For engineers who need to inspect live MQTT traffic, subscribe to device topics, or debug a specific device, the --client certificate provides short-lived, one-off access. Obtain it fresh at the start of each session:

sudo socketxp ca login  --client "[email protected]"

# Subscribe to all sensor feeds with the 24-hour client cert
mosquitto_sub \
  --cafile /etc/mosquitto/certs/bastionxp-ca.crt \
  --cert /var/lib/socketxp/tls_client.crt \
  --key /var/lib/socketxp/tls_client.key \
  -h mqtt-broker.local -p 8883 \
  -t "sensors/#" -v

The --client certificate expires after 24 hours automatically. The engineer does not need to revoke it after the session ends — it becomes invalid on its own. This is why --client is the right choice for humans: zero credential management overhead for temporary access.


Why This Works: The Shared CA Root

The broker and every IoT device can authenticate each other without any pre-shared secret or out-of-band coordination because all certificates — broker and devices alike — are signed by the same BastionXP CA root. The broker’s cafile directive tells Mosquitto to trust any certificate signed by that CA, regardless of which device presents it.

# Broker verifying device: "Is this device's tls_server.crt signed by my trusted CA?"
openssl verify -CAfile /etc/mosquitto/certs/bastionxp-ca.crt \
    /var/lib/socketxp/tls_server.crt   # ← device's cert (--server flag)
# tls_server.crt: OK   (clientAuth EKU present — accepted as client credential)

# Device verifying broker: "Is the broker's tls_server.crt signed by my trusted CA?"
openssl verify -CAfile /etc/mosquitto/certs/bastionxp-ca.crt \
    /etc/mosquitto/certs/mqtt-broker.crt   # ← broker's cert (--server flag)
# mqtt-broker.crt: OK   (serverAuth EKU present — accepted as server credential)

Adding a new device to the fleet means running one socketxp ca login --server command on the device — the broker automatically trusts the new device’s certificate without any Mosquitto configuration change, because it already trusts all certificates issued by the BastionXP CA. The SocketXP agent on the new device will then keep that certificate current through automatic renewal at the 30% validity threshold.

Removing a device means revoking its --server certificate at the CA. The broker (if configured with a CRL) will reject the revoked certificate on the next connection attempt, without touching any other device’s credentials.


Next Steps

SocketXP IoT Remote Access and Device Management Platform

Remotely access, manage, and update your IoT & AIoT edge fleet with SocketXP's secure and scalable platform.

Start Your Free Trial Now!

Join thousands of satisfied users who trust SocketXP for a secure, reliable, and scalable IoT Edge device management solution. Start your free trial now.