Repository navigation
feat(api): operator-or-SPIFFE auth on /v0 + node-pubkey enrollment - #16
Merged
NetworkTheoryAppliedResearchInstitute merged 1 commit intoJul 13, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Closes the operator-authentication contract gap found in the #4 contract check: the operator credential (2-of-7
sohocloud/operator/v0transmission) was verified nowhere on the coordination wire.OperatorAuthwas fully built but mounted on no route, and/v0was gated only byRequireSPIFFE+bindNodeID, which requires the caller to hold a per-node SVID matching each message'snode_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.OperatorOrSPIFFEdispatches on the presence of theX-SohoCloud-Operatorheader:OperatorAuth(2-of-7 transmission) — the operator-relay path; no per-node SVID.RequireSPIFFE+bindNodeIDpath — 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:
/v0node-side subtree — the selector is injected fromcmd/orchestrator(protocoladapter.NewHandlergains agateparam, so the adapter package needs noapiimport; 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 onOperatorFromContext.api.Newgains(opVerifier, coordinatorID); both nil/absent ⇒ SPIFFE-only (backward compatible).Verification
go buildandgo vetclean with and without theintegrationtag; signature changes swept (api.New: 1 caller;protocoladapter.NewHandler: 1 caller + 4 tests).apiunit 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/apiandinternal/protocoladapterpackage tests green.Contract this fixes for Cloudy
Cloudy authenticates to
/v0and enrolls node keys by sending theX-SohoCloud-Operatorheader — base64(JSON) of the 2-of-7 transmission (EncodeOperatorHeaderis the exact wire;internal/opcredalready 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.