Skip to content

Authenticate the node to the broker, with credentials kept out of git (#7) - #24

Merged
bazauto merged 2 commits into
mainfrom
feat/broker-auth
Sep 23, 2026
Merged

bazauto merged 2 commits into
mainfrom
feat/broker-auth

Conversation

@bazauto

@bazauto bazauto commented Sep 23, 2026

Copy link
Copy Markdown
Owner

The bench broker used to accept anonymous clients on the LAN. Any device on that network could publish a fabricated clear on an occupied block, and the orchestrator would believe it. Now every client has its own identity, and an ACL limits each identity to the topics it owns. This PR is the firmware and deploy side of that change. The broker side is already live on the bench.

The ACL

User May
orchestrator read and write layout/#
io-node write sensor/+/reading and point/+/reading; read point/+/query. Pinned to this layout's id.
monitor read layout/# and $SYS/#, for mosquitto_sub
anonymous refused

The io-node rules cover topic kinds, not individual sensor ids, and that is deliberate. Mosquitto acknowledges a publish the ACL denies and then drops it. The node's publish() returns True, and the broker log records nothing at the default level (checked on the bench). If the ACL listed sensor ids, installing a sensor would fail in exactly the silent way rule 3 exists to prevent.

How the node gets its password

  • Where it's kept: the only copy is on the bench, in ~/.config/layout-feedback/<node>.json (mode 0600). It is never in git or on the dev machine.
  • How it reaches the board: scripts/deploy.sh copies it to the board as /mqtt_credentials.json on every deploy. It refuses to deploy if the file is missing, and checks that before touching the board. The file is .json rather than .py, so the root sweep leaves it alone and nothing can import it.
  • Reading and validating it: broker_credentials.py is new, pure code with no machine import. It reads and validates the file. A missing or malformed file raises CredentialsError, which flashes the new LED code 8, and the node never falls back to anonymous. Values are limited to 64 printable ASCII characters with no ", \ or ,, so a bad value fails here and not as "connection refused".
  • The modem command: MQTTATClient gains username and password and sends them in AT+MQTTUSERCFG. The modem echoes that command back, so the password is redacted from the debug log and from failure dumps.

The broker config, the password file, the ACL and the other clients' credentials live on the bench. docs/broker-auth.md records where each one is and how to rotate it.

Verified at cutover

Host suite: 207 passed in 0.12s.

On the bench, 2026-09-23:

  • Staged, then switched. The node was deployed and the orchestrator restarted, both with credentials, while the broker was still anonymous. Only then was authentication switched on. The log shows u'io-node' and u'orchestrator' on every connect. After the broker restart the node reconnected through its own reconnect path.
  • Refused from the LAN: anonymous and wrong-password connects from the dev machine both get Connection refused: Not authorized.
  • ACL: a monitor watcher on an all-zero layoutId received nothing from three probes: io-node → point/…/command, io-node → sensor/…/reading under the wrong layout id, and monitor → anything. The positive control, an orchestrator publish to the same namespace, arrived.
  • Orchestrator health stayed online with no fault reason, and both Goods Shed readings kept re-asserting.

Not covered on the host: whether the real ESP-AT firmware reports a rejected password differently from an unreachable broker. It's treated as the same failure (code 5), and the doc says so.

Docs

  • docs/broker-auth.md is new.
  • docs/startup-and-status-led.md gains code 8, and code 5 now covers rejected credentials.
  • CLAUDE.md gets the doc-index row, the module listing and a trap entry.
  • The deploy skill covers the credentials step.

TLS is out of scope and tracked separately.

Closes #7

🤖 Generated with Claude Code

bazauto and others added 2 commits September 23, 2026 18:12
…#7)

The node sends a username and password in AT+MQTTUSERCFG. They are read from
/mqtt_credentials.json on the board, which deploy.sh copies there from the
bench's ~/.config/layout-feedback/<node>.json. That file is the only copy, so
the password never passes through git or the dev machine. If the file is
missing or malformed, the node flashes new LED code 8 instead of connecting
anonymously. The modem echoes the command back, so the password is also
redacted from the console.

docs/broker-auth.md records the identities, the ACL, and the trap: mosquitto
acknowledges a publish the ACL denies, so the node cannot see the failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bazauto
bazauto enabled auto-merge (squash) September 23, 2026 17:26
@bazauto
bazauto merged commit 4ccdcdf into main Sep 23, 2026
1 check passed
@bazauto
bazauto deleted the feat/broker-auth branch September 23, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Broker accepts loopback connections only, so no node can publish

1 participant