Mosquitto MQTT med TLS og klientsertifikater — gateway-sikkerhed
Sådan konfigurerer du Mosquitto MQTT broker med TLS 1.3, per-gateway klientsertifikater, ACL-isolering og automatisk certifikatfornyelse til IoT-gateways.
Af M-Bus Gateway
Alle gateways i M-Bus Gateway platformen kommunikerer via MQTT over TLS 1.3 med per-gateway klientsertifikater. Her er opsætningen der sikrer at ingen gateway kan tilgå en anden gateways data.
Arkitektur: Per-gateway PKI
[Root CA] — genereret én gang, opbevares offline
↓
[Server cert] — til Mosquitto broker (Hetzner)
↓
[Client cert pr. gateway] — GW-0001.crt, GW-0002.crt...
↓ TLS 1.3 mutual auth
[Mosquitto ACL] — pattern: meters/%u/# → kun eget topic
Hvert gateway har sit eget certifikat. Common Name (CN) matcher GATEWAY_ID — bruges i Mosquitto ACL.
Certifikatgenerering (provision-gateway.sh)
#!/bin/bash
# scripts/provision-gateway.sh
GATEWAY_ID=${1:?Angiv GATEWAY_ID som argument}
CERTS_DIR="certs/gateways"
CA_DIR="certs/ca"
mkdir -p "$CERTS_DIR/$GATEWAY_ID"
# Generer gateway private key:
openssl genrsa -out "$CERTS_DIR/$GATEWAY_ID/client.key" 4096
# Generer CSR med CN = GATEWAY_ID:
openssl req -new \
-key "$CERTS_DIR/$GATEWAY_ID/client.key" \
-out "$CERTS_DIR/$GATEWAY_ID/client.csr" \
-subj "/CN=$GATEWAY_ID/O=MBusGateway/C=DK"
# Signer med Root CA:
openssl x509 -req \
-in "$CERTS_DIR/$GATEWAY_ID/client.csr" \
-CA "$CA_DIR/ca.crt" \
-CAkey "$CA_DIR/ca.key" \
-CAcreateserial \
-out "$CERTS_DIR/$GATEWAY_ID/client.crt" \
-days 3650 \
-sha256
echo "✅ Certifikat genereret: $CERTS_DIR/$GATEWAY_ID/client.crt"
echo " CN=$GATEWAY_ID, gyldig 10 år"
Mosquitto konfiguration
# /etc/mosquitto/mosquitto.conf
# Ingen uautentificeret adgang:
allow_anonymous false
# TLS port:
listener 8883
protocol mqtt
# Server-certifikater:
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
# Kræv klientcertifikat (mutual TLS):
require_certificate true
use_identity_as_username true # CN fra certifikat = MQTT username
# TLS version:
tls_version tlsv1.3
# ACL-fil:
acl_file /etc/mosquitto/acl
# Persistence:
persistence true
persistence_location /var/lib/mosquitto/
# Logging:
log_dest file /var/log/mosquitto/mosquitto.log
log_type error
log_type warning
log_type notice
ACL-fil: Topic-isolering pr. gateway
# /etc/mosquitto/acl
# %u erstattes med MQTT username = CN fra certifikat = GATEWAY_ID
# Hver gateway har kun adgang til sine egne topics:
pattern readwrite meters/%u/#
# Server-subscriber (ingen klientcertifikat — separat listener):
# (server bruger intern 1883 via Docker netværk)
user server
topic readwrite meters/#
topic readwrite #
Resultatet: GW-0001 kan kun publicere og subscribe på meters/GW-0001/#. En kompromitteret gateway kan ikke tilgå andre gateways' data.
Gateway klient-konfiguration (Python)
# gateway/src/mqtt/client.py
import ssl
import paho.mqtt.client as mqtt
from pathlib import Path
CERTS_DIR = Path("/etc/mbus-gateway/certs")
def create_mqtt_client(gateway_id: str, config: dict) -> mqtt.Client:
client = mqtt.Client(
client_id=gateway_id,
protocol=mqtt.MQTTv5,
)
# TLS med klientcertifikat:
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ssl_context.load_verify_locations(CERTS_DIR / "ca.crt")
ssl_context.load_cert_chain(
certfile=CERTS_DIR / "client.crt",
keyfile=CERTS_DIR / "client.key",
)
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_3
client.tls_set_context(ssl_context)
# Callbacks:
client.on_connect = _on_connect
client.on_disconnect = _on_disconnect
client.on_publish = _on_publish
return client
def _on_connect(client, userdata, flags, rc, properties=None):
if rc == 0:
logger.info("MQTT forbundet", broker=client._host)
else:
logger.error("MQTT forbindelsesfejl", rc=rc)
def _on_disconnect(client, userdata, rc, properties=None):
if rc != 0:
logger.warning("MQTT uventet disconnect — reconnect om 30s", rc=rc)
Server-side subscriber
# server/src/mqtt/subscriber.py
import ssl
import asyncio
import paho.mqtt.client as mqtt
class MQTTSubscriber:
def __init__(self, config):
self.client = mqtt.Client(
client_id="server-subscriber",
protocol=mqtt.MQTTv5,
)
# Server bruger intern Docker-netværk (1883, ingen TLS):
# Ekstern broker på 8883 kræver server-certifikat
self.client.on_message = self._on_message
self.client.connect(config.MQTT_HOST, 1883)
self.client.subscribe("meters/#", qos=1)
def _on_message(self, client, userdata, msg):
topic = msg.topic
# meters/GW-0001/data → gateway_id = "GW-0001"
parts = topic.split("/")
if len(parts) >= 3:
gateway_id = parts[1]
msg_type = parts[2]
asyncio.create_task(
self._process(gateway_id, msg_type, msg.payload)
)
Certifikatfornyelse
# Certifikater har 10-årig levetid (gateways ude af stand i lang tid)
# Fornyelse via OTA-kommando:
# 1. Generer nyt certifikat på server:
./scripts/provision-gateway.sh GW-0001
# 2. Pak certifikat i OTA-payload:
# (gateway/src/ota.py håndterer certs/ mappe separat fra kode-OTA)
# 3. Verificér SHA256:
sha256sum certs/gateways/GW-0001/client.crt
# Monitorer certifikatudløb (Celery task):
# server/src/workers/tasks/cert_monitor.py — advarer 90 dage inden udløb
Mosquitto i Docker Compose
# docker-compose.yml
services:
mosquitto:
image: eclipse-mosquitto:2.0
ports:
- "8883:8883" # Ekstern TLS (gateways)
# 1883 ikke eksponeret eksternt — kun intern Docker-netværk
volumes:
- ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf
- ./mosquitto/acl:/mosquitto/config/acl
- ./certs/ca/ca.crt:/etc/mosquitto/certs/ca.crt:ro
- ./certs/server/server.crt:/etc/mosquitto/certs/server.crt:ro
- ./certs/server/server.key:/etc/mosquitto/certs/server.key:ro
- mosquitto_data:/var/lib/mosquitto
- mosquitto_log:/var/log/mosquitto
restart: unless-stopped
networks:
- internal
Sikkerhedsegenskaber
✅ Mutual TLS — begge parter verificerer certifikat
✅ TLS 1.3 — kun moderne cipher suites
✅ Per-gateway certifikat — kompromis begrænses til ét gateway
✅ ACL pattern-isolering — kan ikke tilgå andre gateways' topics
✅ Ingen adgangskoder — certifikat erstatter password-auth
✅ Root CA offline — signering kræver fysisk adgang
✅ 10-årig certifikatlevetid — passer til gateway-livscyklus
Konklusion
MQTT-sikkerhed i IoT kræver mere end brugernavn/adgangskode. Mutual TLS med per-gateway klientsertifikater og topic-ACL sikrer at en kompromitteret enhed ikke kan tilgå systemets samlede data. Root CA opbevares offline og bruges kun ved provisionering af nye gateways.