This is a development version of the documentation. Content may change without notice.
Voke Documentation
PLC / Devices

mTLS onboarding

Issue device certificates from Voke's root CA and configure mutual TLS on your PLC.

mTLS is the production default. The PLC presents a client certificate signed by Voke's internal CA; the broker validates it against the CA chain and derives the MQTT username from the certificate's CN field. No password or token is exchanged — the certificate is the credential.

Listener ports: 8886 (production) · 8883 (local dev)


Overview of the PKI

Voke maintains a self-signed Root CA (4096-bit RSA, 10-year validity by default). All scripts live in scripts/pki/:

ScriptPurpose
init-ca.shBootstrap the root CA — run once per environment. Generates ca.key + ca.crt and an empty CRL.
ensure-ca.shIdempotent wrapper: runs init-ca.sh if the CA files are absent, exits silently otherwise. Called by bun run docker:up.
generate-broker-cert.shIssue the broker TLS certificate (SAN for mqtt.voke.lovinka.com, localhost, cpi-mosquitto). Run once after init-ca.sh.
generate-device-cert.shIssue a client certificate for one device. Run once per plant; re-run to rotate. CN = device ID = MQTT username.
generate-mqtt-jwt-keys.shGenerate the RSA key pair for the JWT listener (unrelated to mTLS).

Certificates are written to docker/mosquitto/certs/ and mounted into the broker container at runtime.

Never commit ca.key or any device.key file. They are in .gitignore by convention. The CA private key grants the ability to issue trusted device identities for your entire fleet.


Step 1: Bootstrap the CA

Run this once in the repository root. If the CA already exists the script exits with an error to protect the existing PKI.

cd scripts/pki
./init-ca.sh

Default output:

Generating CPI Root CA...
  Key size: 4096
  Validity: 3650 days
  Subject:  /C=CZ/ST=Prague/O=CPI IoT Platform/CN=CPI Root CA

CA generated successfully!
  CA Certificate: docker/mosquitto/certs/ca/ca.crt
  CA Private Key:  docker/mosquitto/certs/ca/ca.key  (KEEP SECURE)

Override subject or validity with environment variables:

CA_DAYS=7300 CA_SUBJECT="/C=DE/O=Acme/CN=Acme Root CA" ./init-ca.sh

Step 2: Generate the broker certificate

cd scripts/pki
./generate-broker-cert.sh

The broker cert has SANs for mqtt.voke.lovinka.com, localhost, cpi-mosquitto, and 127.0.0.1. Override the CN if your broker domain differs:

BROKER_CN=mqtt.example.com ./generate-broker-cert.sh

Step 3: Issue a device certificate

Each plant gets its own 2048-bit RSA client certificate. Pass the plant UUID as the only argument — this becomes the certificate's CN and therefore the MQTT username.

cd scripts/pki
./generate-device-cert.sh <plantId>

Example:

./generate-device-cert.sh a1b2c3d4-0000-0000-0000-000000000001

Expected output:

Generating device certificate for 'a1b2c3d4-0000-0000-0000-000000000001'...

Device certificate generated!
  Certificate:  docker/mosquitto/certs/devices/a1b2c3d4-0000-0000-0000-000000000001/device.crt
  Private Key:  docker/mosquitto/certs/devices/a1b2c3d4-0000-0000-0000-000000000001/device.key
  Fingerprint:  3A7F...
  Expires:      Apr 18 00:00:00 2028 GMT

Test connection:
  mosquitto_sub --cafile docker/mosquitto/certs/ca/ca.crt \
    --cert docker/mosquitto/certs/devices/.../device.crt \
    --key  docker/mosquitto/certs/devices/.../device.key \
    -h localhost -p 8883 -t 'cpi/<plantId>/command'

Default validity is 730 days (2 years). Override with DEVICE_DAYS:

DEVICE_DAYS=365 ./generate-device-cert.sh <plantId>

Do NOT reuse a certificate across multiple plants. Voke's ACL maps one CN to exactly one plant's topics. A shared cert means both plants share the same ACL scope.


Step 4: Copy files to the device

Transfer the three files to the PLC and set restrictive permissions:

# Conventional paths (adjust for your device OS)
scp docker/mosquitto/certs/ca/ca.crt          plc:/etc/voke/ca.crt
scp docker/mosquitto/certs/devices/<id>/device.crt  plc:/etc/voke/device.crt
scp docker/mosquitto/certs/devices/<id>/device.key  plc:/etc/voke/device.key

ssh plc "chmod 644 /etc/voke/ca.crt /etc/voke/device.crt && chmod 600 /etc/voke/device.key"

Standard path conventions used in the Python examples:

FilePath
Voke CA certificate/etc/voke/ca.crt
Device certificate/etc/voke/device.crt
Device private key/etc/voke/device.key

Step 5: Connect

The full Python example is in apps/docs/content/examples/plc/mqtt_mtls.py. Key configuration:

import ssl
import paho.mqtt.client as mqtt

HOST = "mqtt.voke.lovinka.com"
PORT = 8886          # production; 8883 for local dev

PLANT_ID   = "a1b2c3d4-0000-0000-0000-000000000001"
CA_CERT    = "/etc/voke/ca.crt"
CLIENT_CERT = "/etc/voke/device.crt"
CLIENT_KEY  = "/etc/voke/device.key"

client = mqtt.Client(client_id=PLANT_ID, protocol=mqtt.MQTTv5)
client.tls_set(
    ca_certs=CA_CERT,
    certfile=CLIENT_CERT,
    keyfile=CLIENT_KEY,
    tls_version=ssl.PROTOCOL_TLS_CLIENT,
)
client.connect(HOST, PORT, keepalive=60)

No username_pw_set call is needed — the certificate is the credential. The broker reads use_identity_as_username true and sets the MQTT username to the cert's CN automatically.


ACL scope

mTLS devices share the same pattern-based ACL as JWT devices:

pattern write cpi/%u/telemetry
pattern write cpi/%u/status
pattern write cpi/%u/ack
pattern read  cpi/%u/commands

%u expands to the MQTT username which equals the cert CN which equals the plant UUID. A plant can only write to its own telemetry/status/ack topics and read from its own commands topic.


Cert monitoring and rotation

Expiry. The default device cert validity is 2 years. Track expiry by inspecting the certificate date:

openssl x509 -in /etc/voke/device.crt -enddate -noout
# notAfter=Apr 18 00:00:00 2028 GMT

Rotation. Re-run the issuance script with the same plant ID and redeploy the new device.crt + device.key pair. The broker picks up the new cert on next connection (no broker restart needed):

cd scripts/pki
./generate-device-cert.sh <plantId>   # overwrites existing files with a warning
# copy device.crt + device.key to PLC and restart the PLC's MQTT client

Revocation. The broker loads ca.crl at startup. To revoke a certificate before it expires:

  1. Add the cert serial to the CA's index and regenerate the CRL:
    openssl ca -revoke docker/mosquitto/certs/devices/<id>/device.crt \
      -keyfile docker/mosquitto/certs/ca/ca.key \
      -cert docker/mosquitto/certs/ca/ca.crt
    openssl ca -gencrl \
      -keyfile docker/mosquitto/certs/ca/ca.key \
      -cert docker/mosquitto/certs/ca/ca.crt \
      -out docker/mosquitto/certs/ca/ca.crl
  2. Restart the broker so it reloads the CRL:
    docker compose restart mosquitto

For a full fleet revocation (e.g. CA compromise), rotate the CA with init-ca.sh (after removing ca.key) and re-issue all device certificates.


Troubleshooting

SymptomLikely cause
Connection refusedWrong port (check prod 8886 vs dev 8883)
SSL handshake failedWrong CA cert or cert signed by a different CA
CONNACK rc=5 (not authorised)Cert CN does not match any ACL entry
certificate verify failedDevice trusting wrong CA; update ca.crt
certificate revokedCert serial is in the CRL; issue a replacement

Next steps

On this page