Skip to content

identity: a ban on a compromised node does not reach the delegations re-exchanged downstream of it #767

Open

Description

@ignaciojaneiro

Follow-up to #742. That issue proposed carrying the delegation chain in the border JWT; the review in #750 dropped that part. This issue is about the problem underneath it.

eclipse-biscuit/biscuit-go#186 brings third-party blocks to Go. With them, each agentmesh node can sign its own delegation hop with its libp2p key, offline. That closes a gap in the ban list: today, banning a compromised node doesn't reach the delegations re-exchanged downstream of it.

Problem

Say node O is compromised, for example because its agent is prompt-injected, and drives requests on a user's behalf to node E, which calls another node, P.

  • Past the first hop, E can't act as the user. The provider strips X-Mesh-*, Biscuit included, before the backend (node.go:2531), so E's agent gets the user's identity as plain headers but no credential. Today it either calls P as node E, and the user drops out, or the user's raw JWT has to reach it some other way so its node can re-exchange it at POST /token/exchange. In the second case, every agent on the path holds a bearer credential that isn't bound to any node.
  • Banning O stops O's own calls. The control plane and the nodes reject a token whose node, actor_node or client_peer_id is banned (controlplane/sts.go:646, node/sts.go:96, node/middleware.go:443).
  • It doesn't reach E's requests, in either case.
    • If E calls with its own identity, its token doesn't mention O. Those calls keep working for as long as E is trusted.
    • If E re-exchanged the user's JWT, the result is a new root carrying actor_node(E) and client_peer_id(E) (mintDelegatedBiscuitFromJWT), which doesn't mention O either. It keeps working until it expires, up to DelegatedBiscuitTTL (one hour by default), and the forwarded JWT can be exchanged again from any other node.
Tool What it does here
Ban E Stops E for every user, legitimate traffic included.
Revoke the user's tokens E's token is a new root, so revoking the one O held doesn't reach it. Banning the user's identity stops all of their traffic.
Short TTLs Bound the window but don't close it.
client_peer_id in the Border Crossing lines (#750) Finds the requests afterwards, but can't stop them at request time.

Proposal: node-signed delegation hops in third-party blocks

Everything happens inside the mesh. The border JWT and the control plane's role don't change.

Biscuit (block 0 from the control plane: the user, roles, bound to O)
 + third-party block signed by O:  delegation("<O>", "<E>")
 + third-party block signed by E:  delegation("<E>", "<P>")
  • Signing. Each node signs with its libp2p Ed25519 key. The peer ID embeds the public key, so a verifier needs no lookup, and a rule trusting ed25519/<key of E> sees only the facts E signed.
  • What P checks when E presents the token:
    • block 0 is bound to O, and the chain starts at O;
    • each hop is signed by the node it starts from;
    • the presenting peer signed the last hop, and the last hop names P;
    • no signer is banned.
  • Consent is built in. O signs the hand-off to E, so a node that merely saw the Biscuit can't extend it. A sealed Biscuit ends the chain.
  • The user's JWT is used once, at the entry node. The agent never holds a credential: its node gives it a short-lived, per-request handle and appends the hop for it.
  • Banning O stops every request that passed through O, on every hop, while E keeps serving the rest. The delay is the propagation of /revocations, not the token TTL.
  • No control-plane round trip. It keeps the control plane off the per-request hot path (security-architecture.md §6, invariant 2).
  • Per-node policy. A provider can add its own policy over the chain in its attenuation, for example a depth limit.

What it needs from biscuit-go

eclipse-biscuit/biscuit-go#186 already has:

  • Third-party blocks. Biscuit.ThirdPartyBlockRequest, ThirdPartyBlockRequest.SignRequest (takes the node's Ed25519 private key; a libp2p key converts with ed25519.PrivateKey(raw), where raw comes from PrivKey.Raw()) and Biscuit.AppendThirdPartyBlock. The spec's third-party sample (test024) passes.
  • No extension of a sealed token. ThirdPartyBlockRequest fails on a sealed token.

Still missing there:

  • Public key interning across blocks. Spec sample test026 is still skipped. A token that crosses several hops carries one key per node, so agentmesh needs this.

biscuit-wasm and biscuit-python, which the SDKs use, already support third-party blocks.

What it needs in agentmesh

  • Accept third-party blocks that carry exactly one delegation fact signed by an enrolled node. UnmarshalInbound accepts only tar_block today.
  • Let the request binding follow the chain. It checks only block 0 today.
  • Make the ban check walk the signers.
  • Give the agent a per-request handle, so it can ask its node to delegate on its behalf.

Alternative considered

The control plane could re-mint the Biscuit at each hop, with the chain in block 0. That works with today's biscuit-go, but it puts the control plane on the request path for every new delegation.

Open questions

  • The shape of the per-request handle, and how long it lives.
  • The depth cap. MaxAttenuationBlocks = 8 bounds it today.

Related

Activity

  1. ignaciojaneiro commented on Oct 20, 2026

    @ignaciojaneiro
    ContributorAuthor

    @aojea following up on your review in #750: this is the problem I was trying to get at. How would you contain a compromised node at request time today, without waiting for the tokens to expire? If nothing covers it, does building this on the third-party blocks in eclipse-biscuit/biscuit-go#186 look reasonable to you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions