Compare commits

..

5 Commits

Author SHA1 Message Date
Brent Perteet
e552ae4e5c fix(S2-a): mount broker auth files outside source tree 2026-08-20 15:53:20 -05:00
Brent Perteet
8aea87c3cd fix(S2-a): keep broker credential database outside Git 2026-08-20 15:52:14 -05:00
Brent Perteet
8f638bfa4f fix(S2-a): keep plaintext MQTT inside compose network 2026-08-20 15:44:38 -05:00
Brent Perteet
9ffe021354 fix(S2-a): expose scoped app MQTT through WSS 2026-08-20 15:43:42 -05:00
Brent Perteet
b66ae2cc47 fix(S2-a): load public MQTT TLS cert from broker volume 2026-08-20 15:27:03 -05:00
4 changed files with 49 additions and 19 deletions

View File

@@ -12,12 +12,27 @@ 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) |
| `1883` | MQTT (plaintext, Compose network only) | username/password | internal backend service; not host-published |
| `8883` | MQTT over TLS | **client certificate** | field devices |
| `8884` | MQTT over TLS | username/password (server cert only) | scoped app clients and administrators |
| `443` (`/mqtt` → loopback `9001`) | MQTT over WSS/TLS | username/password | scoped app clients |
| `8884` | MQTT over TLS | username/password (server cert only) | scoped app clients and administrators on networks that expose the raw port |
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.
The former anonymous WebSocket listener on `9001` is now authenticated, uses the same ACL as the
TLS listeners, and is bound to host loopback only. Nginx exposes it as WSS at `/mqtt` on port 443;
the web portal itself continues to receive live updates from the backend.
The TLS listeners use `mosquitto/certs/public-fullchain.pem` and
`mosquitto/certs/public-privkey.pem`, copied from the host's Let's Encrypt certificate during
deployment with owner `1883:1883` and mode `0600`. These files are deployment secrets/artifacts
and are excluded from Git.
The live password database is likewise outside Git at
`/home/ubuntu/.config/ul-platform/mosquitto.passwd`, bind-mounted read-only as
`/run/secrets/mosquitto_passwd`. The deployed ACL is copied to
`/home/ubuntu/.config/ul-platform/mosquitto.acl` and bind-mounted beside it. Both files are owned
by broker uid/gid 1883 with mode 0600. Provisioning updates the host password file and restarts Mosquitto; the
tracked `mosquitto/config/passwd` is only a legacy/bootstrap sample and must not receive new
organization credentials.
Device authentication happens on **port 8883**. A device presents a client
certificate signed by the app's own Certificate Authority (CA); Mosquitto

View File

@@ -11,14 +11,13 @@ services:
mosquitto:
image: eclipse-mosquitto:2
ports:
- "1883:1883"
- "8883:8883"
- "8884:8884"
- "127.0.0.1:9001:9001"
volumes:
- ./mosquitto:/mosquitto
# Publicly trusted server identity for app MQTTS. The device listener still
# validates client certificates against /mosquitto/certs/ca.crt.
- /etc/letsencrypt:/etc/letsencrypt:ro
- /home/ubuntu/.config/ul-platform/mosquitto.passwd:/run/secrets/mosquitto_passwd:ro
- /home/ubuntu/.config/ul-platform/mosquitto.acl:/run/secrets/mosquitto_acl:ro
backend:
build:

View File

@@ -2,8 +2,16 @@ per_listener_settings true
# Plain MQTT — internal services and clients authenticate with username/password on port 1883
listener 1883 0.0.0.0
password_file /mosquitto/config/passwd
acl_file /mosquitto/config/devices.acl
password_file /run/secrets/mosquitto_passwd
acl_file /run/secrets/mosquitto_acl
allow_anonymous false
# Authenticated MQTT over WebSocket for app clients. Docker binds this listener only to
# host loopback; nginx supplies the public WSS/TLS endpoint at /mqtt on port 443.
listener 9001 0.0.0.0
protocol websockets
password_file /run/secrets/mosquitto_passwd
acl_file /run/secrets/mosquitto_acl
allow_anonymous false
# TLS MQTT — devices authenticate with client certificates (port 8883)
@@ -14,19 +22,19 @@ allow_anonymous false
# since there's no config/cert hot-reload.
listener 8883 0.0.0.0
cafile /mosquitto/certs/ca.crt
certfile /etc/letsencrypt/live/dev.hub.umagul.net/fullchain.pem
keyfile /etc/letsencrypt/live/dev.hub.umagul.net/privkey.pem
certfile /mosquitto/certs/public-fullchain.pem
keyfile /mosquitto/certs/public-privkey.pem
require_certificate true
use_identity_as_username true
allow_anonymous false
acl_file /mosquitto/config/devices.acl
acl_file /run/secrets/mosquitto_acl
# TLS MQTT — app/admin username+password access (port 8884). App usernames are orgIds;
# devices.acl confines them to ul/{orgId}/app/... . No anonymous listener is exposed.
listener 8884 0.0.0.0
certfile /etc/letsencrypt/live/dev.hub.umagul.net/fullchain.pem
keyfile /etc/letsencrypt/live/dev.hub.umagul.net/privkey.pem
certfile /mosquitto/certs/public-fullchain.pem
keyfile /mosquitto/certs/public-privkey.pem
require_certificate false
password_file /mosquitto/config/passwd
password_file /run/secrets/mosquitto_passwd
allow_anonymous false
acl_file /mosquitto/config/devices.acl
acl_file /run/secrets/mosquitto_acl

View File

@@ -21,8 +21,16 @@ server {
client_max_body_size 20m;
location /mqtt {
# Anonymous MQTT-over-WebSocket was removed for Sprint 2 security criterion B1.
return 410;
# Authenticated, ACL-confined MQTT-over-WebSocket. Mosquitto is bound to loopback;
# nginx terminates publicly trusted TLS so phones can use standard port 443.
proxy_pass http://127.0.0.1:9001/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
}
location / {