M-Bus Gateway
← Tilbage til blog
· IoT· netværkssikkerhed· VLAN· firewall· Cloudflare Tunnel· SSH· Raspberry Pi· sikkerhed· gateway

IoT netværkssikkerhed til gateway — VLAN, firewall og Cloudflare

Netværkssikkerhed til IoT gateway: VLAN-isolering, firewall-regler, Cloudflare Tunnel vs VPN, SSH-sikring og defense-in-depth for Raspberry Pi i kælderrum.

Af M-Bus Gateway

En IoT-gateway i kælderrummet er et potentielt angrebspunkt. Her er den defense-in-depth arkitektur M-Bus Gateway bruger til at beskytte gateways i felten.


Trusselsbillede: Gateway i kælderrum

Trusler mod en IoT-gateway:

Fysisk adgang:
  → Uautoriseret SD-kort-udtræk
  → USB-enhed med malware
  → Password reset via fysisk konsol

Netværksangreb:
  → Scanning af åbne porte (Shodan/Censys)
  → Brute force SSH
  → MQTT-afsnifning
  → Man-in-the-middle (usikker WiFi)

Supply chain:
  → Kompromitteret firmware (verificeres med GPG/SHA256)
  → Python-pakkeer med bagdøre

Mitigering:
  → Ingen åbne indgående porte
  → Cloudflare Tunnel (on-demand, max 30 min)
  → Mutual TLS på MQTT
  → SD-kort kryptering (luks2)
  → Minimal OS + automatiske sikkerhedsopdateringer

VLAN-isolering

Anbefalet netværksarkitektur (Unifi/pfSense):

VLAN 10 — IoT-netværk (gateway):
  → IP: 10.0.10.0/24
  → Gateway: 10.0.10.1 (router)
  → Tillader: Udgående MQTT 8883, NTP 123, DNS 53
  → Blokerer: Al trafik til LAN/WLAN
  → Blokerer: Indgående fra internet (ingen port forward)

VLAN 1 — Ejendomsnet (normal WiFi):
  → Ingen adgang til VLAN 10

Fordel:
  → Kompromitteret gateway har IKKE adgang til
    lejernes netværk, NAS, routeren etc.
  → Kan kun kommunikere med Hetzner MQTT-broker
# Firewall-regler på gateway (ufw):
ufw default deny incoming
ufw default deny outgoing

# Tillad kun nødvendig udgående trafik:
ufw allow out to 178.105.90.8 port 8883 proto tcp   # MQTT
ufw allow out to any port 53 proto udp               # DNS
ufw allow out to any port 123 proto udp              # NTP
ufw allow out to any port 443 proto tcp              # Cloudflare Tunnel + OTA
ufw allow out to any port 80 proto tcp               # HTTP (redirect til 443)

# Ingen indgående porte åbne
ufw enable

SSH-sikring

# /etc/ssh/sshd_config — sikkerhedshærdning:

# Deaktivér password-login (kun SSH-nøgle):
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile /home/mbus/.ssh/authorized_keys

# Ingen root-login:
PermitRootLogin no

# Kun mbus-brugeren:
AllowUsers mbus

# Tidsout inaktive sessioner (5 min):
ClientAliveInterval 300
ClientAliveCountMax 0

# Deaktivér X11 og port forwarding (unødvendigt):
X11Forwarding no
AllowTcpForwarding no

# Port → non-standard (mindsker scanner-noise):
# Port 2222  ← Kun hvis ejendomsnet ikke er VLAN-isoleret

# Genstart sshd:
systemctl restart ssh
# Fail2ban mod SSH brute force:
apt install fail2ban

# /etc/fail2ban/jail.local:
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 1h
findtime = 10m

Cloudflare Tunnel: Sikker fjernadgang

Cloudflare Tunnel eliminerer behovet for åbne indgående porte:

Uden tunnel (USIKKER):
  Router: Port 22 → 10.0.10.5 (gateway)
  Problem: SSH eksponeret på internet

Med Cloudflare Tunnel:
  Gateway initierer udgående forbindelse til Cloudflare
  Tekniker tilgår via cloudflared-klient (authed)
  Ingen indgående porte nødvendige

M-Bus Gateway politik:
  → Tunnel KUN aktiveres on-demand (max 30 min)
  → Aktiveres via platform API: POST /api/v1/gateways/{id}/tunnel/start
  → Auto-stopper efter 30 min via systemd timer
  → Al tunnel-aktivitet logges i audit_log

Cloudflare Access:
  → Kræver godkendelse fra Cloudflare Access policy
  → Email-OTP eller SSO inden tunnel-adgang gives
# gateway/src/connectivity/tunnel.py

import subprocess
import asyncio
from pathlib import Path

TUNNEL_TIMEOUT = 30 * 60  # 30 minutter

async def start_tunnel_session(token: str) -> asyncio.subprocess.Process:
    """
    Start Cloudflare Tunnel session.
    Stoppes automatisk efter TUNNEL_TIMEOUT sekunder.
    """
    proc = await asyncio.create_subprocess_exec(
        "cloudflared", "tunnel", "--no-autoupdate", "run",
        "--token", token,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )

    # Auto-stop efter 30 min:
    asyncio.create_task(_auto_stop(proc, TUNNEL_TIMEOUT))
    return proc

async def _auto_stop(proc: asyncio.subprocess.Process, timeout: int):
    await asyncio.sleep(timeout)
    if proc.returncode is None:
        proc.terminate()
        await asyncio.sleep(5)
        if proc.returncode is None:
            proc.kill()

Firmware-verifikation

# gateway/src/ota.py — Verificer firmware inden installation:
import hashlib
import subprocess
from pathlib import Path

def verify_firmware(firmware_path: Path, expected_sha256: str) -> bool:
    """SHA256-verificér firmware inden installation."""
    sha256 = hashlib.sha256(firmware_path.read_bytes()).hexdigest()
    if sha256 != expected_sha256:
        return False  # Afvis — korrupt eller manipuleret
    return True

# OTA-kommando fra MQTT indeholder:
# {"url": "...", "sha256": "abc123...", "version": "1.2.3"}
# Gateway downloader ALDRIG og installerer ALDRIG uden SHA256-match

SD-kort og fysisk sikkerhed

# Read-only root filesystem (forhindrer persistens ved kompromittering):
# /etc/fstab:
/dev/mmcblk0p2  /       ext4    ro,noatime  0 1

# Writable overmounts for nødvendige writes:
tmpfs   /tmp        tmpfs   defaults,size=64M   0 0
tmpfs   /var/log    tmpfs   defaults,size=32M   0 0
tmpfs   /run        tmpfs   defaults            0 0

# Data-partition (SQLite + config) — separat, krypteret:
/dev/mmcblk0p3  /data   ext4    rw,noatime  0 2

Konklusion

Defense-in-depth for IoT-gateway: VLAN-isolering begrænser blast radius ved kompromittering, ufw-firewall begrænser udgående trafik til kun MQTT/NTP/DNS/HTTPS, Cloudflare Tunnel eliminerer indgående porte, SSH-hærdning med kun nøgle-auth og Fail2ban. Det vigtigste enkelt-tiltag: Ingen åbne indgående porte — aldrig.

Se Mosquitto TLS guide eller Raspberry Pi gateway guide.