- 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>
114 lines
3.9 KiB
Bash
Executable File
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."
|