Skip to content

feat(api): operator-or-SPIFFE auth on /v0 + node-pubkey enrollment - #16

Merged
NetworkTheoryAppliedResearchInstitute merged 1 commit into
mainfrom
feat/v0-operator-auth
Jul 13, 2026
Merged

NetworkTheoryAppliedResearchInstitute merged 1 commit into
mainfrom
feat/v0-operator-auth

Conversation

@NetworkTheoryAppliedResearchInstitute

Copy link
Copy Markdown
Contributor

What

Closes the operator-authentication contract gap found in the #4 contract check: the operator credential (2-of-7 sohocloud/operator/v0 transmission) was verified nowhere on the coordination wire. OperatorAuth was fully built but mounted on no route, and /v0 was gated only by RequireSPIFFE + bindNodeID, which requires the caller to hold a per-node SVID matching each message's node_id. That is incompatible with the Cloudy-as-operator model, where one front end relays many members' nodes as a single authenticated operator.

Change — an operator-or-SPIFFE selector

api.OperatorOrSPIFFE dispatches on the presence of the X-SohoCloud-Operator header:

  • header present → OperatorAuth (2-of-7 transmission) — the operator-relay path; no per-node SVID.
  • header absent → the existing RequireSPIFFE + bindNodeID path — for direct/satellite nodes that hold their own SVID.

Neither path is downgradable to the other: a present-but-invalid transmission is a hard 401, never a fall-through to SPIFFE. Node authenticity in both paths comes from each message's own ed25519 signature, which the adapter already verifies against node_protocol_keys — so the operator path adds no per-node SVID and loses no node identity.

Applied to:

  • /v0 node-side subtree — the selector is injected from cmd/orchestrator (protocoladapter.NewHandler gains a gate param, so the adapter package needs no api import; a nil gate preserves the prior SPIFFE-only behavior).
  • POST /nodes/pubkey — an admitted operator may enroll its members' node keys (first-write-wins still prevents any overwrite); a direct node still binds its own SVID. The handler branches on OperatorFromContext.
  • api.New gains (opVerifier, coordinatorID); both nil/absent ⇒ SPIFFE-only (backward compatible).

Verification

  • go build and go vet clean with and without the integration tag; signature changes swept (api.New: 1 caller; protocoladapter.NewHandler: 1 caller + 4 tests).
  • New api unit tests cover the selector's three arms: no header → SPIFFE arm (verifier untouched); valid header → operator arm; malformed header → hard 401 with no downgrade to either arm.
  • internal/api and internal/protocoladapter package tests green.

Contract this fixes for Cloudy

Cloudy authenticates to /v0 and enrolls node keys by sending the X-SohoCloud-Operator header — base64(JSON) of the 2-of-7 transmission (EncodeOperatorHeader is the exact wire; internal/opcred already produces this shape) — and relays each member node's own-signed protocol messages. No per-node SVID.

Honest residual

No operator has completed enrollment end-to-end yet — that is blocked on the 2FA mail transport (SMTP unconfigured) and the governance console not being deployed. So this path is verified in test, not yet in production. CONFORMANCE.md updated accordingly.

Author does not self-merge — for review.

The operator credential (2-of-7 sohocloud/operator/v0 transmission) was
verified nowhere on the coordination wire: OperatorAuth was built but mounted
on no route, and /v0 was gated only by RequireSPIFFE+bindNodeID — which
requires the caller to hold a per-node SVID matching each message's node_id.
That is incompatible with the Cloudy-as-operator model, where one front end
relays many members' nodes as a single authenticated operator.

Add api.OperatorOrSPIFFE, a selector chosen by the presence of the
X-SohoCloud-Operator header:
  - header present -> OperatorAuth (2-of-7 transmission); the operator-relay
    path, no per-node SVID.
  - header absent  -> the existing RequireSPIFFE+bindNodeID path, for
    direct/satellite nodes.
Neither path is downgradable to the other. Node authenticity in BOTH paths
comes from each message's own ed25519 signature, which the adapter already
verifies against node_protocol_keys — so dropping SPIFFE from the operator
path loses no node identity.

- /v0 node-side subtree: selector injected from cmd/orchestrator (protocoladapter
  gains a  param so it needs no api import; nil gate = prior SPIFFE-only).
- POST /nodes/pubkey: an admitted operator may enroll its members' node keys
  (first-write-wins still prevents overwrite); a direct node still binds its own
  SVID. Handler branches on OperatorFromContext.
- api.New gains (opVerifier, coordinatorID); both nil/absent => SPIFFE-only.

Signature changes swept: api.New (1 caller), protocoladapter.NewHandler
(1 caller + 4 tests). Build + vet clean with and without the integration tag.
New api unit tests cover the selector's three arms (no header -> SPIFFE;
valid header -> operator; malformed header -> hard 401, no downgrade).
CONFORMANCE.md updated: frontend-as-operator auth moves from design-target to
built-on-the-wire, with the honest residual that no operator has enrolled
end-to-end yet (blocked on the 2FA mail transport).

Signed-off-by: Jodson Graves <info@ntari.org>
@NetworkTheoryAppliedResearchInstitute
NetworkTheoryAppliedResearchInstitute merged commit baa4632 into main Jul 13, 2026
7 checks passed
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.

1 participant