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>
55 lines
3.2 KiB
Markdown
55 lines
3.2 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
./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`.
|