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>
This commit is contained in:
113
deploy/mosquitto/generate-tls.sh
Executable file
113
deploy/mosquitto/generate-tls.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/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."
|
||||
Reference in New Issue
Block a user