M-Bus Gateway
← Tilbage til blog
· MQTT· Mosquitto· Hetzner· TLS· ACL· sikkerhed· gateway· integration· IoT

MQTT broker opsætning på Hetzner — TLS, ACL og gateway-isolering

Sådan konfigurerer du Mosquitto MQTT broker på Hetzner VPS med TLS 1.3, per-gateway ACL, client certificates og integration med FastAPI-backend for wM-Bus data.

Af M-Bus Gateway

MQTT er protokollen der binder wM-Bus gateways til serveren. En korrekt konfigureret Mosquitto-broker med TLS og per-gateway ACL er fundamentet for sikker IoT-kommunikation.


Arkitektur-oversigt

[Raspberry Pi gateway]
        │
        │ TLS 1.3 port 8883 (client certificate)
        ↓
[Mosquitto broker — Hetzner VPS]
        │
        │ intern Unix socket
        ↓
[FastAPI subscriber — server/src/mqtt/subscriber.py]
        │
        │ asyncpg
        ↓
[TimescaleDB — reading hypertable]

Mosquitto installation og grundkonfiguration

# På Hetzner VPS (Ubuntu 22.04):
apt install mosquitto mosquitto-clients -y

# Generér CA-certifikat (self-signed til interne gateways):
mkdir -p /etc/mosquitto/certs && cd /etc/mosquitto/certs

# CA-nøgle og certifikat:
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/CN=mbus-gateway-CA/O=MBusGateway/C=DK"

# Server-certifikat:
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
  -subj "/CN=178.105.90.8/O=MBusGateway/C=DK"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt -days 3650

Mosquitto konfiguration med TLS

# /etc/mosquitto/mosquitto.conf

# Ingen uautentificeret adgang:
allow_anonymous false

# TLS-listener port 8883:
listener 8883
protocol mqtt
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
require_certificate true
use_identity_as_username true   # Client cert CN = MQTT-brugernavn

# Intern Unix socket til FastAPI:
listener 1883 127.0.0.1
allow_anonymous true            # Kun lokal adgang (firewall)

# ACL-fil:
acl_file /etc/mosquitto/acl.conf

# Logging:
log_type error
log_type warning
log_dest file /var/log/mosquitto/mosquitto.log

Per-gateway ACL

Hver gateway har ét client certificate. ACL begrænser adgang til kun egne topics:

# /etc/mosquitto/acl.conf

# Pattern: %u = MQTT-brugernavn (= gateway_id fra client cert CN)
# Hvert gateway kan kun publish/subscribe til egne topics:

pattern readwrite meters/%u/#

# FastAPI-subscriber (internt - bruger Unix socket, ingen ACL):
# Sæt ingen ACL for 127.0.0.1-listener

Effekt:

  • Gateway GW-0001 kan kun skrive til meters/GW-0001/data, meters/GW-0001/status osv.
  • Gateway GW-0002 kan ikke se GW-0001's data — selv hvis kompromitteret

Client certificate pr. gateway

# Per-gateway certifikat (kør ved enrollment):
GATEWAY_ID="GW-0001"

openssl genrsa -out ${GATEWAY_ID}.key 2048
openssl req -new -key ${GATEWAY_ID}.key -out ${GATEWAY_ID}.csr \
  -subj "/CN=${GATEWAY_ID}/O=MBusGateway/C=DK"
openssl x509 -req -in ${GATEWAY_ID}.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out ${GATEWAY_ID}.crt -days 3650

# Komprimér og send til gateway ved enrollment:
tar czf ${GATEWAY_ID}-certs.tar.gz ${GATEWAY_ID}.key ${GATEWAY_ID}.crt ca.crt

FastAPI MQTT-subscriber

# server/src/mqtt/subscriber.py
import asyncio
import msgpack
import zlib
from aiomqtt import Client, MqttError

async def run_subscriber(db_session_factory):
    async with Client("127.0.0.1", port=1883) as client:
        await client.subscribe("meters/+/data")
        await client.subscribe("meters/+/status")
        await client.subscribe("meters/+/alarm")

        async for message in client.messages:
            topic_parts = str(message.topic).split("/")
            gateway_id = topic_parts[1]
            msg_type = topic_parts[2]

            if msg_type == "data":
                # MessagePack + zlib decompress:
                payload = msgpack.unpackb(zlib.decompress(message.payload))
                await handle_readings(gateway_id, payload, db_session_factory)

            elif msg_type == "status":
                await handle_heartbeat(gateway_id, message.payload, db_session_factory)

            elif msg_type == "alarm":
                await handle_alarm(gateway_id, message.payload, db_session_factory)

Gateway MQTT-klient (Python, Raspberry Pi)

# gateway/src/mqtt/client.py
import paho.mqtt.client as mqtt
import msgpack, zlib, ssl

class MQTTClient:
    def __init__(self, config):
        self.client = mqtt.Client(client_id=config["GATEWAY_ID"], protocol=mqtt.MQTTv5)

        # TLS med client certificate:
        self.client.tls_set(
            ca_certs=config["MQTT_CA_CERT"],
            certfile=config["MQTT_CLIENT_CERT"],
            keyfile=config["MQTT_CLIENT_KEY"],
            tls_version=ssl.PROTOCOL_TLS_CLIENT,
        )
        self.client.tls_insecure_set(False)
        self.client.connect(config["MQTT_HOST"], int(config["MQTT_PORT"]), keepalive=60)

    def publish_readings(self, readings: list[dict]):
        topic = f"meters/{self.gateway_id}/data"
        payload = zlib.compress(msgpack.packb(readings))
        self.client.publish(topic, payload, qos=1, retain=False)

Topics-oversigt

meters/{gateway_id}/data      → Daglig payload (MessagePack+zlib)
meters/{gateway_id}/status    → Heartbeat hvert 5. min (JSON)
meters/{gateway_id}/alarm     → Alarm JSON (batteri, signal, fejl)
meters/{gateway_id}/cmd       → Kommandoer server→gateway (JSON)
meters/{gateway_id}/ota       → OTA trigger (url+sha256+version)

Firewall-konfiguration (UFW)

# Åbn kun MQTT TLS fra internet:
ufw allow 8883/tcp comment "MQTT TLS"

# Bloker plaintext MQTT fra internet (kun lokal):
ufw deny 1883/tcp

# FastAPI:
ufw allow 8000/tcp comment "FastAPI"

# SSH (begræns til dit IP):
ufw allow from [DIT-IP] to any port 22

ufw enable

Overvågning

# Tjek aktive forbindelser:
mosquitto_sub -h 127.0.0.1 -p 1883 -t "meters/+/status" -v

# Broker-statistik:
mosquitto_sub -h 127.0.0.1 -p 1883 -t "\$SYS/#" -v | grep "connected\|messages"

# Log-overvågning:
tail -f /var/log/mosquitto/mosquitto.log

Konklusion

En korrekt Mosquitto-opsætning med TLS 1.3, per-gateway client certificates og pattern-baseret ACL giver stærk gateway-isolering. Selv hvis én gateway kompromitteres, kan den ikke tilgå andre gateways' data. FastAPI-subscriberen kører internt på Unix socket uden TLS-overhead.

Se gateway-arkitektur eller teknisk dokumentation.