Files
internet_use_control/deploy/mosquitto/generate-tls.sh
Sam Rolfe 14b304e0b0 Phase 3: MQTT transport — retained config, status/telemetry, LWT presence, TLS/ACL design
- internal/mqtt: publish each device's desired state retained on
  restrict/<device>/config; subscribe to restrict/+/status and
  restrict/+/telemetry and write reports to SQLite; publish on every state
  change, republish on connect and every 15 minutes from SQLite, so the broker
  is repaired from the source of truth.
- store: presence (devices.online and last_seen, audited only on transitions),
  telemetry upsert per (device, day, app, domain) via migration 002, allow and
  block lists read into the config payload, device lookup by topic.
- config: MQTT settings. TLS plus per-device credentials are required; the
  pre-TLS shared broker needs an explicit CONTROL_MQTT_INSECURE escape hatch.
- web: republish config after lock, unlock, expiry, create and edit; broker card
  on the status partial.
- deploy/mosquitto: TLS listener, topic ACL, credential provisioning and a
  verifier that proves the policy on a throwaway broker. Not applied to the
  shared Mosquitto on .13 — that change needs Sam's approval.
- cmd/control: mqtt-clients prints the MQTT identity of every device.
- paho.mqtt.golang v1.5.1, which pulls golang.org/x/net and with it the
  golang.org/x/crypto bump to v0.42.0.

Verified on .13: retained config delivered to a fresh subscriber; a client
killed without DISCONNECT drove the broker's retained will and the service
flipped the device offline; telemetry landed in SQLite; make mqtt-verify passed
13/13; the service connected over TLS with credentials and a CA.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-13 14:37:43 +11:00

114 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Generate the lab CA and the broker certificate for the Internet Use Control
# MQTT plane (AGENTS.md §11 Phase 3, §10 rule 6).
#
# The CA is what makes "TLS" mean something on a home LAN: every device and the
# Control Service trust this CA, and the broker presents a certificate it signed.
# A rogue machine cannot impersonate the broker and hand a child new rules.
#
# Output (all gitignored, mode 0644 so the Mosquitto container user can read it):
#
# tls/ca.crt the lab CA certificate — ship this to every device
# tls/ca.key the CA private key — keep offline, never ship
# tls/server.crt the broker certificate
# tls/server.key the broker private key
#
# Re-run with --force to replace an existing set. Existing files are kept
# otherwise, so this is safe to call from `make mqtt-config`.
#
# Hostname override:
# MQTT_TLS_HOSTNAME=broker.example.lan MQTT_TLS_SANS=DNS:broker.example.lan,IP:10.0.0.5 \
# deploy/mosquitto/generate-tls.sh --force
#
# The certificates are NOT for the public internet. They exist to authenticate
# the broker to the kids' devices on the LAN.
set -euo pipefail
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
tls_dir="$here/tls"
hostname="${MQTT_TLS_HOSTNAME:-mosquitto.lab.audasmedia.com.au}"
sans="${MQTT_TLS_SANS:-DNS:${hostname},DNS:localhost,DNS:host.docker.internal,IP:127.0.0.1,IP:192.168.20.13}"
days="${MQTT_TLS_DAYS:-3650}"
force=0
case "${1:-}" in
--force | -f) force=1 ;;
--help | -h)
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
"") ;;
*)
echo "usage: $(basename "$0") [--force]" >&2
exit 2
;;
esac
mkdir -p "$tls_dir"
work_dir="$PWD"
cd "$tls_dir"
if [[ -s "$tls_dir/ca.crt" && -s "$tls_dir/server.crt" && $force -eq 0 ]]; then
echo "generate-tls: $tls_dir already holds a CA and a server certificate; leaving it alone."
echo "generate-tls: re-run with --force to replace it."
exit 0
fi
# Prefer a host openssl. `.13` has no openssl, so fall back to a bounded
# container run (never an unbounded `docker run`, in line with the project
# guardrails). The container needs root to install the package, so it hands the
# generated files back to the calling user before it exits.
openssl_run() {
if command -v openssl >/dev/null 2>&1; then
openssl "$@"
return
fi
timeout 300 docker run --rm --network host \
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
-v "$tls_dir":/tls -w /tls alpine:3.21 sh -c '
apk add --no-cache openssl >/dev/null 2>&1 || exit 1
openssl "$@" || exit 1
chown -R "$HOST_UID:$HOST_GID" /tls' -- "$@"
}
echo "generate-tls: CA + broker certificate for $hostname"
echo "generate-tls: SANs $sans"
# 1. The lab CA.
openssl_run req -x509 -newkey rsa:4096 -sha256 -days "$days" -nodes \
-keyout ca.key -out ca.crt \
-subj "/O=Family Home Lab/CN=Internet Use Control Lab CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
# 2. The broker certificate, signed by the CA. The extension file lives in the
# certificate directory so the same relative path works for a host openssl and
# for the fallback container, which is started with that directory as its cwd.
ext=.ext.cnf
cat >"$ext" <<EOF
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=$sans
EOF
trap 'rm -f "$tls_dir/$ext"' EXIT
openssl_run req -newkey rsa:2048 -sha256 -nodes \
-keyout server.key -out server.csr \
-subj "/O=Family Home Lab/CN=$hostname"
openssl_run x509 -req -in server.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days "$days" -sha256 -extfile "$ext"
rm -f server.csr ca.srl "$ext"
chmod 644 ca.crt ca.key server.crt server.key
cd "$work_dir"
echo "generate-tls: wrote $tls_dir/{ca.crt,ca.key,server.crt,server.key}"
echo "generate-tls: ship ca.crt to every device; keep ca.key and server.key off-device."