Edge nodes are deployed to arbitrary locations by arbitrary people, so the
broker has to be reachable from the internet and secured on its own merits
rather than by a VPN.
Three defects made that impossible. The broker only had a plaintext 1883
listener; every node shared one drb-node password; and the ACL pattern used
%c, the client-supplied client id, so any holder of that shared password
could set client_id to another node and take over its namespace. The comment
claiming this cryptographically prevented cross-node access was wrong and is
gone.
Authentication now uses mosquitto 2.x's built-in dynamic-security plugin on
the stock eclipse-mosquitto image. c2-core administers it over the control
topic, creating each node's client on approval with username=<node_id> and
password=<its node_keys api_key>, attached to a role whose ACL is nodes/%u/#
against the authenticated username. One credential, one revocation point.
An HTTP-callback plugin was implemented first and rejected: that project is
archived upstream, which is not an acceptable dependency on an
internet-facing broker.
Because dynsec state is a second source of truth alongside Firestore,
approve/reissue/delete now write to the broker first and surface a 502
rather than drifting, and c2-core reconciles every approved node into dynsec
on startup.
Adds node self-enrollment (POST /nodes/enroll, GET /nodes/{id}/credentials)
so a new node can obtain its key over HTTPS without an operator handling
secrets by hand. Enrolling an already-approved node_id is refused on the
fleet token alone — otherwise a leaked token plus a guessable id would let
an attacker steal a live node's key before the real node asked for it.
Pickup secrets are stored hashed and returned once, and the endpoint is rate
limited per source IP.
Infrastructure: an 8883 TLS listener fed by Caddy's certificate via a
systemd path unit, a firewall rule for it, and Caddy now 404s /internal/*
so the api vhost cannot proxy internal routes.
Also fixes CORS, which allowed https://app.<domain> while the frontend is
served on the bare domain — every call from the portal would have failed —
and widens the vault gitignore to a glob, since ansible-vault leaves
backup siblings that the exact-name rule left committable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 lines
1.9 KiB
YAML
59 lines
1.9 KiB
YAML
services:
|
|
# Auth is mosquitto's own built-in dynamic-security plugin (see
|
|
# mosquitto.conf + app/internal/dynsec.py) — NOT mosquitto-go-auth, that
|
|
# project is archived upstream (no CVE patches), rejected for a
|
|
# public-internet broker. Stock official image, pinned to an exact patch
|
|
# (not the floating `:2` tag). MOSQUITTO_DYNSEC_PASSWORD seeds the
|
|
# plugin's own one-time "admin" bootstrap client on first boot — read
|
|
# directly by the plugin's C code, no entrypoint scripting needed for it.
|
|
mosquitto:
|
|
image: eclipse-mosquitto:2.1.2-alpine
|
|
restart: unless-stopped
|
|
ports:
|
|
- "1883:1883"
|
|
- "8883:8883"
|
|
environment:
|
|
- MOSQUITTO_DYNSEC_PASSWORD=${MOSQUITTO_DYNSEC_PASSWORD}
|
|
volumes:
|
|
- ./drb-c2-core/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
|
|
- mosquitto_data:/mosquitto/data
|
|
- mosquitto_certs:/mosquitto/certs
|
|
|
|
c2-core:
|
|
image: ${REGISTRY}/c2-core:${TAG:-latest}
|
|
build: ./drb-c2-core
|
|
restart: unless-stopped
|
|
ports:
|
|
- "8888:8000"
|
|
env_file: ./drb-c2-core/.env
|
|
depends_on:
|
|
- mosquitto
|
|
|
|
discord-bot:
|
|
image: ${REGISTRY}/discord-bot:${TAG:-latest}
|
|
build: ./drb-server-discord-bot
|
|
restart: unless-stopped
|
|
env_file: ./drb-server-discord-bot/.env
|
|
depends_on:
|
|
- c2-core
|
|
|
|
frontend:
|
|
image: ${REGISTRY}/frontend:${TAG:-latest}
|
|
build: ./drb-frontend
|
|
restart: unless-stopped
|
|
ports:
|
|
- "3000:3000"
|
|
env_file: ./drb-frontend/.env
|
|
depends_on:
|
|
- c2-core
|
|
|
|
volumes:
|
|
# Dev only for both. Prod overrides these to host bind mounts
|
|
# (/opt/drb/mosquitto-data, /opt/drb/mosquitto-certs — the latter fed by
|
|
# the Caddy cert-sync systemd unit) — see docker-compose.prod.yml and
|
|
# infra/ansible/roles/deploy/templates/. mosquitto_data holds
|
|
# dynamic-security.json (node MQTT credentials, see app/internal/dynsec.py)
|
|
# as well as the usual broker persistence state.
|
|
mosquitto_data:
|
|
mosquitto_certs:
|