From 980bcfa0051c8d9505731b6e75390a9dc8222171 Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Tue, 13 Oct 2026 15:30:35 +1100 Subject: [PATCH] Record the applied MQTT TLS listener on the shared Mosquitto Follow-up to the Phase 3 commit (14b304e), after Sam accepted the confirmation on AUD-80. The operational change itself lives outside this repository; this commit updates the documentation so it matches reality. Applied on .13 (backups: mosquitto.conf.bak.aud80-20261013-152648 and docker-compose.yml.bak.aud80-20261013-152648): - mosquitto.conf gains per_listener_settings true and include_dir /mosquitto/config/conf.d; its original 1883 listener and allow_anonymous true are untouched. - docker-compose.yml publishes 8883 and mounts the new conf.d and tls directories (the compose mounts mosquitto.conf as a single file, so the include_dir fails without them). - conf.d/webrestrict.{conf,acl,passwd} and tls/{ca.crt,server.crt,server.key} are installed from deploy/mosquitto/. - The Control Service now uses tls://host.docker.internal:8883 with the control-service account and CONTROL_MQTT_INSECURE=false. Verified live after the restart: the 1883 plane still carries anonymous traffic, 8883 refuses anonymous clients and a wrong password, each device reads only its own retained config, a device cannot write another device's status or its own config, and a client killed without DISCONNECT drove the retained will into an offline transition recorded in SQLite. Docs: deploy/mosquitto/README.md records the exact host diff, the runbook gap that caused one restart-loop (the missing bind mounts), the per_listener_settings deprecation caveat and the rollback. README.md and TRANSPORT_DESIGN.md now describe the applied state. Co-Authored-By: Paperclip --- README.md | 17 ++-- TRANSPORT_DESIGN.md | 25 ++--- deploy/mosquitto/README.md | 111 ++++++++++++++++++---- environment.d/control-service.env.example | 23 +++-- 4 files changed, 132 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 046005a..d3a40ae 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ reads only the process environment. | `CONTROL_MQTT_USERNAME` | — | Control Service broker account | | `CONTROL_MQTT_PASSWORD` | — | its password | | `CONTROL_MQTT_CA_FILE` | `/etc/webrestrict/tls/ca.crt` | PEM bundle that verifies the broker (TLS only) | -| `CONTROL_MQTT_INSECURE` | `false` | **Temporary.** Allow a plaintext, credential-less broker | +| `CONTROL_MQTT_INSECURE` | `false` | Allow a plaintext, credential-less broker. Only for a broker with no TLS listener | Generate a bcrypt hash without host Go: @@ -86,11 +86,12 @@ docker run --rm --entrypoint /app/control webrestrict/control:0.3.0-phase3 \ `CONTROL_MQTT_USERNAME`/`CONTROL_MQTT_PASSWORD` are required unless `CONTROL_MQTT_INSECURE=true`, and a non-TLS broker is refused unless -`CONTROL_MQTT_INSECURE=true` (AGENTS.md §10 rule 6). The shared Mosquitto on -`.13` has no TLS listener yet, so the running deployment sets the escape hatch -and logs a warning. `deploy/mosquitto/README.md` holds the TLS listener, the -per-device credentials and the topic ACL, verified but not yet applied — see -that file for the apply and rollback steps. +`CONTROL_MQTT_INSECURE=true` (AGENTS.md §10 rule 6). The escape hatch exists for +a broker that has no TLS listener; the running deployment on `.13` does not use +it. Since 2026-10-13 the shared Mosquitto also serves a TLS listener on 8883 +with per-device credentials and a topic ACL +(`tls://host.docker.internal:8883`), so `CONTROL_MQTT_INSECURE=false`. +`deploy/mosquitto/README.md` is the design, the apply runbook and the rollback. ## MQTT (Phase 3) @@ -108,6 +109,10 @@ Broker: the shared **Mosquitto on `.13`**. A second broker is not added - The service publishes each device's config **retained** on startup, after every lock, unlock, expiry, create and edit, and again every 15 minutes so a broker that lost its retained store is repaired from SQLite. +- Devices use the **8883 TLS listener** with one account each; the broker's ACL + lets a device read only its own config and write only its own status and + telemetry, and the Control Service is the only identity that may publish + config. - A device registers the will `{"online": false}` on its status topic, so the broker reports the drop the moment the connection dies. That is presence (FR-6); no polling. diff --git a/TRANSPORT_DESIGN.md b/TRANSPORT_DESIGN.md index dc691a5..447689c 100644 --- a/TRANSPORT_DESIGN.md +++ b/TRANSPORT_DESIGN.md @@ -110,19 +110,22 @@ reconnects is locked again **immediately**, with no waiting. own** status. Without this, a child can publish a fake "unlocked". - Rotate device credentials when a device is removed. -**Status (Phase 3).** Written, verified, not yet applied: -`deploy/mosquitto/conf.d/webrestrict.conf` is the TLS listener, `.acl` is the -topic policy, and `deploy/mosquitto/README.md` is the apply/rollback runbook. -`make mqtt-verify` proves the policy on a throwaway broker. The Control Service -is configured for TLS plus credentials, and refuses a plaintext, +**Status (Phase 3). Applied on `.13` 2026-10-13.** The shared Mosquitto now runs +two planes: its original anonymous `1883` listener, unchanged, and a TLS +listener on `8883` that requires an account +(`deploy/mosquitto/conf.d/webrestrict.conf` + `.acl`). The Control Service +connects to `8883` as `control-service` with the lab CA, and refuses a plaintext, credential-less broker unless `CONTROL_MQTT_INSECURE=true` is set explicitly. -The shared Mosquitto on `.13` still has only its original anonymous `1883` -listener, so the running deployment uses that escape hatch and logs a warning. -Applying the TLS listener means editing the shared broker's configuration and -compose file, adding port 8883 and restarting it — a change to a shared -household service, so it waits for Sam's approval instead of being applied -unilaterally. +`make mqtt-verify` proves the policy on a throwaway broker, so it can be +re-checked without touching the shared one. `deploy/mosquitto/README.md` holds +the design, the exact host changes, the notes from the live apply and the +rollback. + +One caveat to carry forward: `per_listener_settings true` is deprecated in +Mosquitto 2.1 and is removed in 3.0. It is kept because it is the only way on +2.1 to give the two listeners different `password_file`/`acl_file` values. Pin +the broker image tag before upgrading, or rework the two planes. --- diff --git a/deploy/mosquitto/README.md b/deploy/mosquitto/README.md index 4573e11..4f61c45 100644 --- a/deploy/mosquitto/README.md +++ b/deploy/mosquitto/README.md @@ -1,5 +1,12 @@ # Phase 3 MQTT security for the shared Mosquitto +> **APPLIED 2026-10-13 (AUD-80).** The shared Mosquitto on `.13` now runs the +> 8883 listener below next to its original anonymous `1883` listener, and the +> Control Service connects over TLS with credentials. Backups: +> `/home/sam/Docker/Containers/mosquitto/mosquitto.conf.bak.aud80-20261013-152648` +> and `docker-compose.yml.bak.aud80-20261013-152648`. See "What was changed on +> `.13`" at the end of this file. + This directory holds the **broker-side** half of Phase 3 (AGENTS.md §11, §10 rule 6; `TRANSPORT_DESIGN.md` "Security"). The Go half lives in `internal/mqtt/`. @@ -78,19 +85,13 @@ wrong password, a device reads only its own config, writes only its own status and telemetry, cannot write another device's topics, and **cannot overwrite its own config to fake an unlock**. -## Why this is not applied to the shared broker yet +## Applying it (approval required first) Applying it changes files outside this project (`mosquitto.conf` and the Mosquitto `docker-compose.yml`), adds port 8883 to a family-facing service, and -restarts that service. The Phase 3 guardrail is explicit: *do not change the -shared Mosquitto config.* So the change is prepared, generated and verified — -**not applied**. Applying it is a one-command step after Sam approves it, and is -tracked as the follow-up on AUD-80. - -## Applying it (after approval) - -Everything below is additive. Nothing existing is rewritten except the three -lines in step 2, and a backup is taken first. +restarts that service — a change to a shared household service, so it needs +Sam's approval first. Everything below is additive and a backup is taken before +anything is written. ```bash # 1. Credentials and certificates. @@ -107,14 +108,23 @@ install -m 644 deploy/mosquitto/conf.d/webrestrict.conf "$BROKER_DIR/conf.d/" install -m 644 deploy/mosquitto/conf.d/webrestrict.acl "$BROKER_DIR/conf.d/" install -m 644 deploy/mosquitto/conf.d/webrestrict.passwd "$BROKER_DIR/conf.d/" install -m 644 deploy/mosquitto/tls/* "$BROKER_DIR/tls/" -# then add these three lines to mosquitto.conf: +# then add to mosquitto.conf (leave `listener 1883 0.0.0.0` and +# `allow_anonymous true` exactly as they are): # per_listener_settings true (before the first listener) # include_dir /mosquitto/config/conf.d -# (leave `listener 1883 0.0.0.0` and `allow_anonymous true` exactly as they are) -# 3. Publish 8883 and restart, bounded. -# add - "8883:8883" to the mosquitto service ports in -# $BROKER_DIR/docker-compose.yml, then: +# 3. Mount the new directories, publish 8883, restart, bounded. +# The shared compose mounts mosquitto.conf as a single file, so the new +# directories need their own lines or the broker will not start +# ("Error: Unable to open include_dir"). Add to the mosquitto service: +# +# ports: +# - "8883:8883" +# volumes: +# - /home/sam/Docker/Containers/mosquitto/conf.d:/mosquitto/config/conf.d:ro +# - /home/sam/Docker/Containers/mosquitto/tls:/mosquitto/config/tls:ro +# +# then: cd "$BROKER_DIR" timeout 120 docker compose up -d timeout 30 docker logs --tail 20 mosquitto # expect both listeners to open @@ -134,16 +144,83 @@ timeout 120 docker compose up -d --build timeout 30 docker logs --tail 30 webrestrict-control # expect "mqtt connected" over tls ``` +### Notes from the live apply (2026-10-13) + +- The broker logs `The 'per_listener_settings' option is now deprecated and will + be removed in version 3.0` and the matching `allow_anonymous` notice. It still + works on the `eclipse-mosquitto:latest` image, which is 2.1.0.1 today. + `per_listener_settings true` is kept because it is the only way on 2.1.0 to + give the two listeners different `password_file`/`acl_file` values: the + per-listener `listener_*` prefix exists only for `allow_anonymous`. **Pin the + image tag before Mosquitto 3.0 arrives**, or rework the two planes. +- The bind mounts in step 3 are not optional. The shared compose mounts + `mosquitto.conf` as a single file, so `include_dir` fails without them and the + broker restart-loops. +- The password file and the TLS key are installed mode 0644 because the broker + runs as uid 1883 and that user does not exist on the host. See `tls/README.md`. + ### Roll back ```bash cd /home/sam/Docker/Containers/mosquitto -cp mosquitto.conf.bak. mosquitto.conf +cp mosquitto.conf.bak.aud80- mosquitto.conf +cp docker-compose.yml.bak.aud80- docker-compose.yml rm -rf conf.d tls -# remove the "8883:8883" port line from docker-compose.yml timeout 120 docker compose up -d ``` Rolling back the broker needs no change in this project: set `CONTROL_MQTT_INSECURE=true` and `CONTROL_MQTT_BROKER=tcp://host.docker.internal:1883` in `environment.d/control-service.env` and restart the service. + +## What was changed on `.13` (2026-10-13) + +Files outside this project were edited for the first time. The backup copies are +listed above; everything is additive. + +`/home/sam/Docker/Containers/mosquitto/mosquitto.conf` — the two original lines +(`listener 1883 0.0.0.0`, `allow_anonymous true`) are untouched: + +```diff + log_dest file /mosquitto/log/mosquitto.log ++ ++# Internet Use Control (AUD-80). Two listeners, two auth planes. Mosquitto ++# requires per_listener_settings before the first listener, and it is what keeps ++# the 1883 listener below anonymous while 8883 requires an account. ++per_listener_settings true ++ + listener 1883 0.0.0.0 + allow_anonymous true # IMPORTANT: For production, configure authentication (username/password) ++ ++# Internet Use Control device plane (AUD-80): TLS on 8883, account required, ++# topic ACL. Roll back by deleting this line and the conf.d directory. ++include_dir /mosquitto/config/conf.d +``` + +`/home/sam/Docker/Containers/mosquitto/docker-compose.yml`: + +```diff + ports: + - "1883:1883" # Standard MQTT port ++ - "8883:8883" # Internet Use Control device plane (TLS, AUD-80) + - "9001:9001" # Websockets port (optional, good for web clients) + volumes: + - /home/sam/Docker/Containers/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro ++ - /home/sam/Docker/Containers/mosquitto/conf.d:/mosquitto/config/conf.d:ro ++ - /home/sam/Docker/Containers/mosquitto/tls:/mosquitto/config/tls:ro + - /home/sam/Docker/Containers/mosquitto/data:/mosquitto/data + - /home/sam/Docker/Containers/mosquitto/log:/mosquitto/log +``` + +New on the host, copied from this directory: +`conf.d/webrestrict.{conf,acl,passwd}` and `tls/{ca.crt,server.crt,server.key}`. + +`environment.d/control-service.env` in this project now points at +`tls://host.docker.internal:8883` with the `control-service` account and +`CONTROL_MQTT_INSECURE=false`. + +Verified on the live broker after the restart: the `1883` plane still carries +anonymous traffic, `8883` refuses anonymous clients and a wrong password, each +device reads only its own retained config, a device's write to another device's +status and to its own config is dropped, and a client killed without DISCONNECT +drove the retained will into an offline transition in the Control Service. diff --git a/environment.d/control-service.env.example b/environment.d/control-service.env.example index ac4f7eb..56ca9b5 100644 --- a/environment.d/control-service.env.example +++ b/environment.d/control-service.env.example @@ -46,21 +46,24 @@ CONTROL_ADMIN_PASSWORD=change-me CONTROL_MQTT_ENABLED=true # The shared Mosquitto on `.13`, seen from inside this container. The compose -# file maps host.docker.internal to the host gateway. -# When the TLS listener exists, change this to tls://host.docker.internal:8883. -CONTROL_MQTT_BROKER=tcp://host.docker.internal:1883 +# file maps host.docker.internal to the host gateway. The TLS listener on 8883 +# has been live since 2026-10-13 (deploy/mosquitto/README.md). +CONTROL_MQTT_BROKER=tls://host.docker.internal:8883 # The Control Service identity. The broker ACL gives it write access to every # config topic and read access to every status and telemetry topic, nothing else. +# Generate the account with deploy/mosquitto/provision-clients.sh, which writes +# the values into deploy/mosquitto/clients.env (gitignored). CONTROL_MQTT_CLIENT_ID=control-service -# CONTROL_MQTT_USERNAME=control-service -# CONTROL_MQTT_PASSWORD=change-me +CONTROL_MQTT_USERNAME=control-service +CONTROL_MQTT_PASSWORD=change-me # PEM bundle used to verify the broker certificate. Required for a tls:// broker. +# docker-compose.yml mounts deploy/mosquitto/tls/ here read-only. CONTROL_MQTT_CA_FILE=/etc/webrestrict/tls/ca.crt -# TEMPORARY. The shared Mosquitto has no TLS listener yet, so a plaintext, -# credential-less link is allowed. The service refuses to start without -# credentials or TLS unless this is true, and it logs a warning when it is. -# Set to false as soon as deploy/mosquitto/ is applied (AGENTS.md §10 rule 6). -CONTROL_MQTT_INSECURE=true +# Escape hatch for a broker that has no TLS listener: it allows a plaintext, +# credential-less link. The service refuses to start without credentials or TLS +# unless this is true, and it logs a warning when it is. Leave it false against +# the shared Mosquitto (AGENTS.md §10 rule 6). +CONTROL_MQTT_INSECURE=false