- 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>
101 lines
3.8 KiB
Bash
Executable File
101 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Create one broker account per registered device, plus the Control Service's
|
|
# own account (AGENTS.md §11 Phase 3, §10 rule 6).
|
|
#
|
|
# The MQTT username of a device is its topic token — the middle level of its
|
|
# config topic. The device list comes from the Control Service itself:
|
|
#
|
|
# docker exec webrestrict-control /app/control mqtt-clients
|
|
#
|
|
# Because conf.d/webrestrict.acl is written with `%u`, no per-device ACL edit is
|
|
# needed: adding the account is the whole onboarding step.
|
|
#
|
|
# Output (both gitignored, mode 0600 for the secrets):
|
|
#
|
|
# conf.d/webrestrict.passwd hashed accounts, read by the broker
|
|
# clients.env the plaintext values, for the devices and .env
|
|
#
|
|
# Nothing secret is printed to the terminal. Read clients.env to collect them.
|
|
|
|
set -euo pipefail
|
|
|
|
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
passwd_file="$here/conf.d/webrestrict.passwd"
|
|
secrets_file="$here/clients.env"
|
|
|
|
container="${CONTROL_CONTAINER:-webrestrict-control}"
|
|
broker_image="${MQTT_IMAGE:-eclipse-mosquitto:latest}"
|
|
service_user="${CONTROL_MQTT_USERNAME:-control-service}"
|
|
service_pass="${CONTROL_MQTT_PASSWORD:-}"
|
|
|
|
die() {
|
|
echo "provision-clients: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
command -v docker >/dev/null 2>&1 || die "docker is not available"
|
|
|
|
# head reads a fixed number of bytes from the device and cut consumes its whole
|
|
# input, so no stage of this pipeline is signalled with SIGPIPE.
|
|
rand_pass() {
|
|
head -c 32 /dev/urandom | base64 | tr -d '/+=' | cut -c1-32
|
|
}
|
|
|
|
[[ -n "$service_pass" ]] || service_pass="$(rand_pass)"
|
|
|
|
# The device list comes from the service, so the broker accounts can never
|
|
# drift from the devices the Control Service knows about.
|
|
clients="$(timeout 30 docker exec "$container" /app/control mqtt-clients)" ||
|
|
die "could not read the device list from container '$container' (is it running?)"
|
|
|
|
tmp_dir="$(mktemp -d)"
|
|
chmod 700 "$tmp_dir"
|
|
trap 'rm -rf "$tmp_dir"' EXIT
|
|
|
|
add_user() { # user password (creates the file on the first call)
|
|
local user="$1" pass="$2" args=(-b)
|
|
[[ -s "$tmp_dir/webrestrict.passwd" ]] || args+=(-c)
|
|
timeout 60 docker run --rm --user "$(id -u):$(id -g)" \
|
|
-v "$tmp_dir":/cfg --entrypoint mosquitto_passwd "$broker_image" \
|
|
"${args[@]}" /cfg/webrestrict.passwd "$user" "$pass" >/dev/null ||
|
|
die "mosquitto_passwd failed for $user"
|
|
}
|
|
|
|
add_user "$service_user" "$service_pass"
|
|
|
|
: >"$tmp_dir/clients.env"
|
|
{
|
|
echo "# GENERATED by deploy/mosquitto/provision-clients.sh — secrets, do not commit."
|
|
echo "#"
|
|
echo "# Control Service account. Put these two lines in"
|
|
echo "# environment.d/control-service.env, then set CONTROL_MQTT_INSECURE=false"
|
|
echo "# and CONTROL_MQTT_BROKER=tls://host.docker.internal:8883."
|
|
echo "CONTROL_MQTT_USERNAME=$service_user"
|
|
echo "CONTROL_MQTT_PASSWORD=$service_pass"
|
|
echo
|
|
echo "# One account per device. The name is the device's topic token; the"
|
|
echo "# password goes into the device's own MQTT settings."
|
|
echo "# token<TAB>password<TAB>config topic"
|
|
} >>"$tmp_dir/clients.env"
|
|
|
|
count=0
|
|
while IFS=$'\t' read -r token topic platform child name; do
|
|
# mqtt-clients prints real tabs; names contain spaces, so the split matters.
|
|
token="$(printf '%s' "${token:-}" | tr -d '[:space:]')"
|
|
[[ -n "$token" ]] || continue
|
|
pass="$(rand_pass)"
|
|
add_user "$token" "$pass"
|
|
printf '%s\t%s\t%s\n' "$token" "$pass" "$topic" >>"$tmp_dir/clients.env"
|
|
count=$((count + 1))
|
|
echo "provision-clients: account '$token' ($name · $platform) -> $topic"
|
|
done <<<"$clients"
|
|
|
|
[[ $count -gt 0 ]] || echo "provision-clients: no devices registered yet; only the Control Service account was created."
|
|
|
|
install -m 600 "$tmp_dir/webrestrict.passwd" "$passwd_file"
|
|
install -m 600 "$tmp_dir/clients.env" "$secrets_file"
|
|
|
|
echo "provision-clients: wrote $passwd_file ($((count + 1)) accounts)"
|
|
echo "provision-clients: wrote $secrets_file — read it, do not commit it."
|