Repository navigation
controlplane: Border Crossing lines name the origin peer; act.sub is the egress node - #750
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces support for nesting the origin actor node as a prior actor (act.act.sub) in border JWTs when a node requests a token using a delegated Biscuit. This change is reflected in the STS token handler, associated unit tests, and documentation. The review feedback highlights a style guide violation where raw peer ID strings are compared directly in borderJWTActClaim instead of being canonicalized first. Additionally, the reviewer noted that the unit tests must be updated to use valid, dynamically generated peer IDs to prevent decoding failures once canonicalization is implemented.
3de94fc to
e47fb39
Compare
|
Thanks for digging into this and writing up #742 and this PR! After looking closely at how
Suggested directionInstead of adding
|
… is the egress node The FactActorNode comment and security-architecture.md §4.3 said actor_node feeds act.sub in border JWTs. It never did: act.sub is the peer ID of the egress node that called /sts/token, as sts.go, identity.md and the rest of security-architecture.md define it. The mention was left over from when FactActorNode was introduced. The allow Border Crossing line at /sts/token now logs client_peer_id next to actor_node. Member, Task and Delegated Session Biscuits all carry client_peer_id in Block 0, while only Delegated Session Biscuits carry actor_node, so audit logs can join the origin peer and the egress node for every Biscuit kind.
e47fb39 to
1a9930d
Compare
|
Thanks for the careful look. I agree on all three points, and I've reworked the PR to exactly that:
I'll take the real-time side of the question to #742. |
…ode for /oauth/* Biscuits The deny Border Crossing line at /sts/token logged only the egress node, so a refused attempt, the one an operator looks at first, could not be joined to the origin peer. authorizeBiscuitForEgress now returns the verified claims along with the refusal, and the deny line logs their client_peer_id, empty only when the Biscuit itself did not verify. TestSTSTokenBorderCrossingNamesTheOriginPeer reads the control plane's log through a go-log pipe and checks client_peer_id on the allow and the deny line for a Task Biscuit attenuated from a Member Biscuit, the case that has client_peer_id and no actor_node. The FactActorNode comment and security-architecture.md §4.3 now say what actor_node is for /oauth/* Biscuits, and the Mint endpoint row says what actor_node and client_peer_id mean in each Border Crossing line, since actor_node is the egress node in /sts/token lines and the exchanging node in /token/exchange lines.
asivanadi0
left a comment
There was a problem hiding this comment.
I read both commits at be008a5 and ran go test -race ./internal/controlplane/ ./api/ ./internal/identity/, which passes. I also put sts.go back to main with the new test kept, and TestSTSTokenBorderCrossingNamesTheOriginPeer fails on the allow line (client_peer_id = <nil>), so the test pins the change.
The contract change in authorizeBiscuitForEgress looks safe. Its only callers are HandleSTSToken and the ban test at sts_test.go:738, which discards the claims. The deny line can't log an unvalidated peer string either: InspectVerifiedBiscuit already rejects a client_peer_id that doesn't decode (canonicalPeer, internal/identity/biscuit.go:958), so whatever reaches the log is the canonical form, or empty when the Biscuit has none.
One doc suggestion inline, plus a note for #742 that is out of scope here.
Not for this PR, just a note for #742. The served_by refusal (sts.go:1025) and the audience refusal (:1036) return 403 before authorizeBiscuitForEgress and write no Border Crossing line, so an audit join on deny lines won't see those attempts. The new table text ("on deny once the Biscuit has verified") is accurate as written, because both happen before verification, so nothing needs to change here.
| // FactActorNode names the origin agentmesh-node PeerID of a delegated Biscuit: | ||
| // the node that exchanged the subject credential at POST /token/exchange. For | ||
| // a Biscuit the control plane mints at /oauth/*, it is the node whose | ||
| // credential came with the request, or else the actor_peer_id the client | ||
| // named (checked only for admission) or the control plane's own peer ID. It | ||
| // is for audit logs and for ext_proc attributes["mesh"].actor_node. It grants | ||
| // nothing: no baseline rule and no policy binding reads it. |
There was a problem hiding this comment.
Since this comment is now the place that says what actor_node is for, it should also mention that it's one of the two authority facts the request binding checks. For a Delegated Session Biscuit, identity.RequireAuthorityRequestBinding (internal/identity/biscuit.go:247) accepts a token without node() only when both actor_node(peer) and client_peer_id(peer) match the presenting peer, and /sts/token runs it at sts.go:1201. "It grants nothing" stays true for Datalog policy, but someone reading "for audit logs and for ext_proc attributes" could fairly conclude the fact is safe to drop. Maybe something like:
// ... It is for audit logs, for ext_proc attributes["mesh"].actor_node, and,
// together with client_peer_id, for the authority-block request binding
// (identity.RequireAuthorityRequestBinding). No baseline rule and no policy
// binding reads it, so it grants nothing.The same clause would fit the actor_node bullet in security-architecture.md (line 309).
There was a problem hiding this comment.
Good point. Done in 2b6fee0: both the FactActorNode comment and the §4.3 bullet now say that actor_node, together with client_peer_id, is how identity.RequireAuthorityRequestBinding binds a token without node() to the presenting peer. They also say that no Datalog rule reads it, so it still grants nothing.
The FactActorNode comment and the actor_node bullet in §4.3 listed the fact's uses as audit logs and ext_proc attributes, which could read as safe to drop. Together with client_peer_id, actor_node is how identity.RequireAuthorityRequestBinding binds a token without node() to the presenting peer, and /sts/token runs that check. Both now say so. No Datalog rule reads it, so it still grants nothing.
|
@asivanadi0 thanks for the review, and for putting Agreed on the |
asivanadi0
left a comment
There was a problem hiding this comment.
Re-checked at 2b6fee0. The new commit only touches comments and docs. It adds the request-binding clause to the FactActorNode comment and to the actor_node bullet in §4.3, and both now match what identity.RequireAuthorityRequestBinding does. go test -race ./api/ ./internal/controlplane/ still passes. LGTM.
|
Thanks |
Follow-up to #742, reworked as suggested in review.
What
FactActorNodecomment inapi/datalog.goand §4.3 ofsecurity-architecture.mdsaidactor_nodefeedsact.subin border JWTs. It doesn't:act.subis the peer ID of the egress node that called/sts/token, assts.go,identity.mdand the rest ofsecurity-architecture.mddefine it. The border JWT is unchanged.Border Crossinglines atPOST /sts/tokennow logclient_peer_idnext toactor_node. Member, Task and Delegated Session Biscuits all carryclient_peer_idin Block 0, while only Delegated Session Biscuits carryactor_node. Audit logs can now join the origin peer and the egress node for every Biscuit kind.client_peer_idis the origin peer.authorizeBiscuitForEgressnow returns the verified claims along with the refusal, so refused attempts name the origin too. The field is empty only when the Biscuit itself did not verify.actor_nodeis. TheFactActorNodecomment and §4.3 now say what it holds for/oauth/*Biscuits: the node whose credential came with the request, or else a claimed peer (theactor_peer_idform value, or the control plane's own peer ID).actor_nodeandclient_peer_idmean in eachBorder Crossingline.actor_nodeis the egress node in/sts/tokenlines and the exchanging node in/token/exchangelines.Tests
TestSTSTokenBorderCrossingNamesTheOriginPeerreads the control plane's log through a go-log pipe. It checksclient_peer_idon both the allow and the deny line for a Task Biscuit attenuated from a Member Biscuit, which is the case withclient_peer_idand noactor_node.go test -race ./internal/controlplane/ ./api/ ./internal/identity/passes, andgolangci-lintv2.14.0 is clean.🤖 Generated with Claude Code