feat(S2-a): ingest durable app MQTT points securely

This commit is contained in:
Brent Perteet
2026-08-20 15:22:40 -05:00
parent daa3407b9d
commit 8bcc12ec96
14 changed files with 979 additions and 59 deletions

View File

@@ -8,14 +8,16 @@ describes from a usage/dashboard perspective.
## Overview
The broker (`eclipse-mosquitto`, service `mqtt` / container `ul-hub-mqtt`)
exposes four listeners, each with a different trust model:
exposes three listeners, each with a different trust model:
| Port | Protocol | Auth | Who it's for |
|------|----------|------|---------------|
| `1883` | MQTT (plaintext) | username/password | internal services (e.g. the Laravel subscriber, Python publisher) |
| `9001` (mapped to host `9005`) | MQTT over WebSocket | none (anonymous) | browser clients (dashboard) |
| `8883` | MQTT over TLS | **client certificate** | field devices |
| `8884` | MQTT over TLS | username/password (server cert only) | administrators |
| `8884` | MQTT over TLS | username/password (server cert only) | scoped app clients and administrators |
The former anonymous WebSocket listener on `9001` is disabled. The current web portal receives
live updates from the backend rather than connecting directly to Mosquitto.
Device authentication happens on **port 8883**. A device presents a client
certificate signed by the app's own Certificate Authority (CA); Mosquitto
@@ -123,6 +125,10 @@ since they all share `acl_file /mosquitto/config/devices.acl`:
# Certificate CN becomes the MQTT username — restrict each device to its own namespace.
pattern readwrite devices/%u/#
# App username is orgId; app clients may publish durable points and read acks only in that org.
pattern write ul/%u/app/+/log/points
pattern read ul/%u/app/+/ack
# Admin: <username>
user <username>
topic readwrite #
@@ -169,9 +175,9 @@ Managed via the same `/certificates` page:
| Client | Listener | Auth | Can publish/subscribe |
|---|---|---|---|
| Field device (cert CN = serial) | 8883 (TLS) | client cert | `devices/<serial>/#` only |
| App (username = orgId) | 8884 (TLS) | scoped per-org username/password | publish `ul/<orgId>/app/+/log/points`; read `ul/<orgId>/app/+/ack` |
| Internal service (e.g. subscriber) | 1883 | username/password | depends on ACL entry for that username — none defined by default beyond `devices/%u/#`, so a plain username with no matching device row is effectively scoped to `devices/<username>/#` too, unless added as an admin |
| Admin | 8884 (TLS) or 1883 | username/password | `#` and `$SYS/#` (everything) |
| Browser dashboard | 9001/9005 (WebSocket) | anonymous | no ACL applied — `allow_anonymous true`, so effectively unrestricted; treat this listener as untrusted/read-only in front-end code |
## Known gaps
@@ -180,5 +186,5 @@ Managed via the same `/certificates` page:
needs to be production-grade.
- No automatic reload of `mosquitto.conf`/ACL/passwd changes — every
provisioning action requires a manual `docker compose restart mqtt`.
- The WebSocket listener (9001) is fully anonymous with no ACL, so anything
reachable on port 9005 should be treated as public.
- Sprint 2's per-org app credential permits ack visibility across clients in the same org. This
accepted interim limitation is removed when OIDC-derived per-client broker identity lands.