Files
mqtt-security-test/docker
RyanandClaude Sonnet 5.5 bf40b92c52
Test and deploy enrollment demo / test (push) Successful in 7s
Test and deploy enrollment demo / deploy (push) Successful in 1m12s
Add MQTT enrollment demo with Gitea deploy pipeline
Go backend plus C (libsodium) and C++ (OpenSSL) simulated boards enroll
through a Mosquitto 2.1 broker with Dynamic Security using X25519/HKDF/
Ed25519; a web view shows the run live. The Gitea workflow validates and
builds the images, redeploys the stack on the server with the viewer on
port 8099 and fails unless both boards pass all checks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:25:14 +03:30
..

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)

  1. random Client ID, X25519 key pair, nonce
  2. anonymous connect; own enroll/response/<cid> granted, other boards' and wildcard DENIED
  3. publish request; backend derives the password, creates client FN-xxxxxx (Client ID bound), signs the reply
  4. verify the Ed25519 signature with the pinned public key; tampered signature, other nonce, swapped FN rejected
  5. derive the same password locally (compare the logged fingerprints; the password is never logged or sent)
  6. wrong password refused; right password with a different Client ID refused; correct login accepted
  7. FN topics only: own board/<FN>/cmd granted, other FN's denied, hello accepted and acknowledged

Findings from building it

  • %c / %u substitution 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 addRoleACL needs an explicit allow/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.