· 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-0001kan kun skrive tilmeters/GW-0001/data,meters/GW-0001/statusosv. - Gateway
GW-0002kan ikke seGW-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.