fix(nats): security review — secure-by-default + per-tenant inbox isolation
All checks were successful
CI / backend-types (push) Successful in 10s
CI / frontend-build (push) Successful in 16s
CI / agent-tests (push) Successful in 43s
CI / integration (push) Successful in 23s

Two HIGH findings from automated review on the generator, both fixed:
1. Cross-tenant inbox access: per-license users were granted _INBOX.>,
   letting license A subscribe to license B's request-reply responses.
   Now scoped to corrosion.{license}.> ONLY; replies must ride the
   license namespace (corrosion.{license}.reply.<id>) — documented in
   PROTOCOL.md. Agent unchanged (responds to msg.reply); constraint is
   on the requester (internal user has full >).
2. Default-open auth bypass: generator defaulted to stage=open with a
   full-access anonymous user — a stale regen left the broker wide open.
   Now defaults to enforce (secure by default); the explicit 'open'
   migration stage maps anonymous to a harmless corrosion.unclaimed.>
   namespace, never real tenant subjects. Committed bootstrap default
   hardened the same way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 12:39:31 -04:00
parent 00cff51ce5
commit 463908b18e
3 changed files with 50 additions and 18 deletions

View File

@@ -179,6 +179,23 @@ service that attempts connections to the customer's public IP/ports on
request; that is specified as a Phase 1+ feature and will reuse this report
format with `direction: "inbound"`.
## Authentication & tenant isolation
The broker enforces per-license auth: an agent connects with `user = license_id`,
`password = HMAC-SHA256(license_id, NATS_TOKEN_SECRET)` (shown on the panel
Server page), and is scoped to `corrosion.{license_id}.>` only. The backend uses
a privileged internal user. This makes cross-tenant access impossible at the
broker, not just by convention.
**Reply-subject rule:** per-license users have NO `_INBOX` permission (granting
it would let one license read another's request-reply traffic). Therefore any
backend→agent request-reply MUST use a reply subject inside the license
namespace — e.g. `corrosion.{license_id}.reply.<id>` — never the client's
default global `_INBOX`. The agent is unaffected: it responds to whatever
`msg.reply` it receives. The constraint is on the requester (the internal user
has full access). The contract/CI tests run against an unauthenticated broker
and use the default inbox; production request-reply must follow this rule.
## Versioning
- The agent embeds semver + git hash + build timestamp (`--version`,