The server deployment cannot mount the Docker socket safely, so the web view's button now opens the Gitea page for a workflow_dispatch workflow that recreates the stack and replays the demo. Gitea handles who may run it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Dockerized enrollment demo
Three programs talk through a real Mosquitto 2.1 broker with Dynamic Security:
| Service | Language / library | Role |
|---|---|---|
backend |
Go (stdlib crypto, paho MQTT) | provisions Dynamic Security, answers enrollment, creates the FN broker client |
board-c |
C, libsodium + libmosquitto | simulated board |
board-cpp |
C++17, OpenSSL 3 + libmosquitto | simulated board |
broker |
eclipse-mosquitto:2.1-alpine | anonymous enrollment listener + Dynamic Security |
Web view
Open http://localhost:8099 after starting the stack. It shows the three programs as side-by-side lanes,
the current step in plain language, a progress card per board (checks passed, password fingerprint match)
and the full timeline. The viewer service tails the LOG_FILE of each program from a shared volume.
Options: follow live, only checks and steps, hide countdown lines. Click a long hex value to expand it.
Run again button (only when started with compose.restart.yaml, which run-docker.sh does): stops the backend and boards, restarts the broker (fresh Dynamic Security state), clears
the old logs and starts everything again, so the whole run replays with its countdowns. It works by letting the
viewer call the Docker API through the mounted /var/run/docker.sock, which gives that container control of
Docker on this machine. The page is bound to 127.0.0.1 only and the endpoint requires a custom header, but
this is for demo use only: never expose port 8099.
Run
./run-docker.sh # builds, starts, streams logs, prints the result
docker compose logs -f # follow logs from another terminal
docker compose down # stop everything
Timing (change in compose.yaml): the backend waits 15 s, board-c 20 s, board-cpp 35 s,
and every step pauses STEP_DELAY = 2 s so the logs can be followed.
What each board does (19 checks, exit code 0 = all passed)
- random Client ID, X25519 key pair, nonce
- anonymous connect; own
enroll/response/<cid>granted, other boards' and wildcard DENIED - publish request; backend derives the password, creates client
FN-xxxxxx(Client ID bound), signs the reply - verify the Ed25519 signature with the pinned public key; tampered signature, other nonce, swapped FN rejected
- derive the same password locally (compare the logged fingerprints; the password is never logged or sent)
- wrong password refused; right password with a different Client ID refused; correct login accepted
- FN topics only: own
board/<FN>/cmdgranted, other FN's denied,helloaccepted and acknowledged
Findings from building it
%c/%usubstitution in Dynamic Security ACLs does not work in Mosquitto 2.0.22 (literal ACLs did, substituted ones silently never matched). It works in 2.1.x, so this demo uses 2.1.- Changing a connected client's roles (
addClientRole) disconnects that client, so the backend uses a separate admin client (control API) and service client (backend-svc). mosquitto_ctrl addRoleACLneeds an explicitallow/deny, otherwise it fails with "Invalid arguments".- Go dependencies are vendored because the Docker build could not reach the Go module proxy here.
- This spike has no TLS and uses demo secrets from
compose.yaml.