The Pi's GET/POST /grow/dosing (src/dose-pumps.ts, routed in src/server.ts) check no identity, same as /grow/light and /grow/pump. The relay refuses /grow/dosing from browsers (404), so the page and the MCP can only dose through the controller's limits (src/doser.ts). But anything that reaches the arm server's port directly (the LAN, the tailnet, the forge egress allowlist in forge.json) can wire, prime, calibrate, top off and dose with no controller in front.
What holds there today is only the Pi's own backstop: 60 s in one run and 100 ml per pump per day, plus the top-off cap and float. That bounds the damage (a reservoir can still take 100 ml of pH down a day) but it is not access control.
Proposal: a shared secret between relay and arm server (kept in FORGE_APP_DATA on the relay side and the Pi's data dir, never in the repo), sent as a header on every /grow/* write and checked with a constant-time compare on the Pi; reads of /grow/dosing behind it too. Bind the arm server to the tailnet interface where possible.
Also open: /care/mcp now needs a verified identity and limits 60 calls a minute per identity (#610), but there is no crew-specific token, so a crew agent has to call with the owner's identity.
Found in review of #610 (@juniper).
The Pi's
GET/POST /grow/dosing(src/dose-pumps.ts, routed in src/server.ts) check no identity, same as/grow/lightand/grow/pump. The relay refuses/grow/dosingfrom browsers (404), so the page and the MCP can only dose through the controller's limits (src/doser.ts). But anything that reaches the arm server's port directly (the LAN, the tailnet, the forge egress allowlist in forge.json) can wire, prime, calibrate, top off and dose with no controller in front.What holds there today is only the Pi's own backstop: 60 s in one run and 100 ml per pump per day, plus the top-off cap and float. That bounds the damage (a reservoir can still take 100 ml of pH down a day) but it is not access control.
Proposal: a shared secret between relay and arm server (kept in FORGE_APP_DATA on the relay side and the Pi's data dir, never in the repo), sent as a header on every
/grow/*write and checked with a constant-time compare on the Pi; reads of/grow/dosingbehind it too. Bind the arm server to the tailnet interface where possible.Also open:
/care/mcpnow needs a verified identity and limits 60 calls a minute per identity (#610), but there is no crew-specific token, so a crew agent has to call with the owner's identity.