M-Bus Gateway
← Tilbage til blog
· MQTT· Mosquitto· TLS· klientsertifikater· sikkerhed· IoT· gateway· PKI

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.

Se gateway-enrollment guide eller MQTT broker opsætning.