· sikkerhed· IoT· gateway· MQTT· TLS· firewall· SSH· OTA· secrets· penetrationstest
IoT security hardening — gateway og cloud backend
Security hardening af IoT-gateway og cloud backend: SSH-hærdning, firewall, MQTT TLS, certificate pinning, OTA signaturer, secrets management og penetrationstest.
Af M-Bus Gateway
Et IoT-gateway med 4G-forbindelse er et angrebspunkt. Her er det komplette security hardening setup for M-Bus Gateway.
Trusselsbillede
Angrebsvektorer for en wM-Bus gateway:
1. Fysisk adgang (gateway i kælder)
→ SD-kort fjernes → direkte adgang til data
Modforanstaltning: Fuld disk-kryptering (dm-crypt/LUKS)
2. Netværksangreb via 4G SIM
→ SIM med offentlig IP → direkte adgang til gateway
Modforanstaltning: Ingen åbne indgående porte (Cloudflare Tunnel)
3. MQTT man-in-the-middle
→ Opfang af varmemålerdata
Modforanstaltning: Mutual TLS (mTLS) + certificate pinning
4. OTA-firmware kompromittering
→ Ondsindet firmware installeres
Modforanstaltning: SHA256 + GPG signatur
5. Cloud backend angreb
→ API-endpoints kompromitteret
Modforanstaltning: WAF, rate limiting, RBAC, audit log
SSH hardening på gateway (Pi)
# /etc/ssh/sshd_config (kun relevante ændringer)
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile /home/mbus/.ssh/authorized_keys
# Tillad kun specifikke brugere:
AllowUsers mbus
# Deaktivér X11 og TCP forwarding:
X11Forwarding no
AllowTcpForwarding no
# Timeout idle sessions:
ClientAliveInterval 300
ClientAliveCountMax 2
# Skift default port (security through obscurity — supplement ikke erstatning):
# Port 2222 ← konfigureres i gateway config, ikke her (SIM-afhængig)
# Genstart sshd:
# systemctl restart sshd
UFW firewall regler
# Gateway (Raspberry Pi) — minimal angrebsflade:
# Reset til default (deny incoming, allow outgoing):
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
# Tillad SSH fra management IP (Hetzner server):
ufw allow from 178.105.90.8 to any port 22
# MQTT udgående (kun til Mosquitto server):
# Udgående tillades som default — ingen speciel regel nødvendig
# Aktivér:
ufw --force enable
ufw status verbose
# Hetzner server — porte:
# 8883 (MQTT TLS): Kun fra kendte gateway-IP'er (1NCE IP-pool)
# 8000 (FastAPI): Via Nginx proxy + Cloudflare (ikke direkte)
# 5432 (PostgreSQL): Kun intern Docker-netværk
# 6379 (Redis): Kun intern Docker-netværk
MQTT certificate pinning på gateway
# gateway/src/mqtt/client.py
import ssl
import hashlib
from pathlib import Path
def create_tls_context(
ca_cert: Path,
client_cert: Path,
client_key: Path,
expected_server_fingerprint: str,
) -> ssl.SSLContext:
"""
Mutual TLS med certificate pinning.
Afviser forbindelser til servere med ukendt certifikat.
"""
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ctx.load_verify_locations(ca_cert)
ctx.load_cert_chain(client_cert, client_key)
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
# Certificate pinning: Verificér server-certifikat fingerprint
def check_fingerprint(conn, cert, err_num, depth, ok):
if depth == 0: # Leaf certificate
der = conn.getpeercert(binary_form=True)
fingerprint = hashlib.sha256(der).hexdigest()
if fingerprint != expected_server_fingerprint:
return False
return ok
# NB: ssl.SSLContext understøtter ikke post_handshake callback direkte
# Implementeres i on_connect callback via getpeercert()
return ctx
OTA signatur verification
# gateway/src/ota/verifier.py
import hashlib
import gnupg
from pathlib import Path
GPG_KEYRING = Path("/etc/mbus-gateway/trusted_keys.gpg")
def verify_firmware(
firmware_path: Path,
signature_path: Path,
expected_sha256: str,
) -> bool:
"""
Trin 1: SHA256 checksum
Trin 2: GPG signatur mod betroet nøgle
Begge skal passere — ellers afvis firmware.
"""
# Trin 1: SHA256
sha256 = hashlib.sha256(firmware_path.read_bytes()).hexdigest()
if sha256 != expected_sha256:
log.error("firmware_sha256_mismatch",
expected=expected_sha256, actual=sha256)
return False
# Trin 2: GPG signatur
gpg = gnupg.GPG(gnupghome=str(GPG_KEYRING.parent))
with open(signature_path, "rb") as sig_file:
verified = gpg.verify_file(sig_file, str(firmware_path))
if not verified.valid:
log.error("firmware_signature_invalid",
fingerprint=verified.fingerprint,
status=verified.status)
return False
log.info("firmware_verified",
sha256=sha256[:16],
signer=verified.username)
return True
Secrets management
# Gateway: Secrets i /etc/mbus-gateway/ med strenge permissions
ls -la /etc/mbus-gateway/
# -rw------- mbus mbus config.env
# -rw------- mbus mbus client.crt
# -rw------- mbus mbus client.key
# -r-------- mbus mbus ca.crt
# -rw------- mbus mbus aes_keys.json.enc ← krypteret med gateway-specifik nøgle
# Kryptering af AES-nøgler:
openssl enc -aes-256-cbc -pbkdf2 \
-in aes_keys.json \
-out aes_keys.json.enc \
-pass file:/etc/mbus-gateway/.master_key
# .master_key gemmes i RAM-tmpfs:
mount -t tmpfs -o size=1m tmpfs /run/mbus-secrets
# Populeres ved boot af init-script inden service starter
# server: Docker Secrets (aldrig environment variables til credentials)
# docker-compose.prod.yml:
# secrets:
# jwt_secret:
# external: true
# environment:
# JWT_SECRET_KEY_FILE: /run/secrets/jwt_secret
def load_secret(name: str) -> str:
"""Læs Docker Secret fra /run/secrets/."""
secret_file = Path(f"/run/secrets/{name}")
if secret_file.exists():
return secret_file.read_text().strip()
# Fallback til environment (dev/test)
return os.environ[name.upper()]
API security headers (Nginx)
# /etc/nginx/sites-enabled/mbus-gateway
server {
listen 443 ssl http2;
server_name api.mbus-gateway.dk;
# HSTS — 1 år
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Forhindre clickjacking
add_header X-Frame-Options "DENY" always;
# Forhindre MIME-sniffing
add_header X-Content-Type-Options "nosniff" always;
# CSP — til API (ingen HTML responses):
add_header Content-Security-Policy "default-src 'none'" always;
# Fjern server-version fra headers:
server_tokens off;
# Rate limiting (defineret i http{} blok):
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
}
Penetrationstest checkliste
Kvartalvis security review:
Gateway:
□ nmap scan fra internet (alle porte skal være lukkede)
□ SSH password login forsøg (skal afvises)
□ AES-nøgler krypterede på disk (verify)
□ wmbusmeters kører som mbus-bruger (ikke root)
MQTT:
□ Forsøg connect uden klientcertifikat (skal afvises)
□ Forsøg subscribe til anden gateways topic (skal afvises)
□ TLS version check (kun TLS 1.2+)
API:
□ OWASP Top 10 gennemgang
□ JWT ekspirering fungerer
□ rate limiting ved 100+ req/min
□ SQL injection forsøg (parametriserede queries beskytter)
□ Tenant isolation: Kan lejer X tilgå lejer Ys data? (skal afvises)
Audit:
□ Alle kritiske handlinger logges i audit_log
□ Ingen credentials i git-historik (git-secrets scan)
□ Dependencies: npm audit + safety check (Python)
Konklusion
IoT security kræver forsvar i dybden: ingen åbne indgående porte på gateway, mutual TLS til MQTT, GPG-signerede OTA-pakker, krypterede secrets på disk, Docker Secrets i cloud og strenge SSH-regler. Quarterly penetrationstest sikrer at nye angrebsvektorer opdages. MQTT ACL-pattern meters/%u/# isolerer gateways fra hinanden på broker-niveau.