#!/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 "# tokenpasswordconfig 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."