Version: 1.0 — June 26, 2026
Status: Design (approved for implementation planning)
Scope: The identity plane the whole credential track stands on — how every session acquires a
per-session SPIFFE identity bound to its human user, who mints it, how that binding stays
trustworthy across scale-to-zero, and the abstract CredentialInjector interface that the
credential-using milestones (Z2/Z3/Z4/Z5) consume. This spec delivers identity, not the
credential stores themselves (those are Z5 / a separate dependency, §12).
Milestone: Z1 (Phase 2). Reframes the parent's M7 ("egress/identity spine"): the harness
gets a SPIFFE identity but no egress waypoint (Z2 §2.4 — its egress is fixed-destination); the
per-user egress plane is the sandbox's (Z5).
Source of truth for numbering: Milestone Registry.
Parent design: Zero-Trust, Multi-Agent Extensions — §2.1 principle, §2.3 portable-core/kagenti-binding, §3.2/§3.4 identities for sandbox & subagents, §4.1 log invariants (refined here, §6).
Consumed by: Z2 Harness Lock-Down (harness SPIFFE id for mTLS to the injector; pods/exec-only RBAC), Z3 Inference Injector (mTLS peer authz), Z4 MCP code-mode + Z5 Generalized Egress (per-user (actor SVID ⊕ subject) resolution).
The one-sentence thesis. Per-user isolation in a shared namespace, under scale-to-zero, reduces to: a trusted orchestrator mints a per-session SPIFFE identity with the user in the attested path, derived from a durable binding it alone can write, and reconstructs it on every wake — and everything else (harness, sandbox) is built untrusted around that.
Give every session a per-session, mesh-verifiable identity bound to its human user, such that the egress planes (Z3 inference, Z5 sandbox) can resolve credentials per-user without any workload being able to assert or spoof who it is — including when many users share one namespace and when sessions scale to zero between turns.
- The trust-tier model (orchestrator / harness / sandbox) — §2.
- The per-session identity model: SPIFFE id with the user in the attested path; why per-namespace identity is insufficient — §3.
- The portable
CredentialInjectorinterface + the kagenti binding (SPIRE + Istio ambient- waypoint + AuthBridge) — §4.
- The orchestrator: responsibilities, lifecycle, and the integrity invariant that only it can mint identity — §5.
- The authoritative binding store, integrity-protected and separate from the model-influenced log (a refinement of parent §4.1) — §6.
- Scale-to-zero: reconstruct identity from the durable binding on wake; nothing per-session persists in the mesh while idle — §7.
- The per-user external-credential store (linking/consent/rotation/revocation of GitHub PATs, OAuth grants, etc.). Z1 issues identity; Z5 resolves credentials keyed by that identity. Z1 records the dependency's shape (§12) but does not build it.
- The egress proxies themselves — the inference injector (Z3) and the sandbox waypoint/forward proxy (Z5). Z1 defines the interface they implement.
- Sandbox filesystem/state persistence across scale-to-zero — a separate track (parent §7).
- Subagent fan-out mechanics (Z6) — they reuse this plane (each subagent = its own session identity); no new identity mechanism is added there.
Per-user isolation requires a control tier above both the brain and the hands. The identity plane introduces it explicitly:
| Tier | Component | Trust | pods/create? |
Mints identity? | Holds secrets? |
|---|---|---|---|---|---|
| Control | Orchestrator | trusted; not model-influenced | yes (sole) | yes (sole) | no |
| Brain | Harness | semi-trusted (untrusted data) | no | no | no (Z2) |
| Hands | Sandbox | untrusted (model code) | no | no | no (Z5; uses, never reads) |
The orchestrator is the new element. It is shared, long-lived control-plane infrastructure (in kagenti: the operator extended with a Session controller), not a per-session pod and not scale-to-zero. It provisions identity; it never touches per-user secrets, which keeps it out of the credential blast radius even though it is the identity-side crown jewel (§9).
Each session's sandbox pod (egress originates in the hands, not the brain) receives a unique SPIFFE id whose path carries the attested user, e.g.:
spiffe://<trust-domain>/ns/team1/user/alice/session/<session-id>
The egress planes resolve credentials by reading this id at the proxy — no separate lookup, no asserted header. The id is issued by the trusted identity provider (SPIRE) only to a pod whose node-attested selectors match the registration the orchestrator created; the workload cannot choose its own path.
Mesh workload identity is per-ServiceAccount by default (…/ns/<ns>/sa/<sa>). If many users'
sessions share one namespace and one SA, the mesh sees a single identity for all of them —
and any per-user signal degrades to a spoofable header, letting a compromised session claim
another user and steal their credentials. The namespace is not the isolation boundary; the
per-session identity is. Therefore identity must be minted per session, finer than the
namespace/SA.
- Recommended: SPIRE per-pod registration. kagenti already runs SPIRE and injects a
spiffe-helpersidecar, so per-pod SVIDs are native and avoid per-session ServiceAccount/RBAC churn; the SPIFFE path can encode the user. The orchestrator creates a registration entry (via the SPIRE controller-managerClusterSPIFFEID/CRs or the SPIRE API) at provisioning. - Fallback: per-session ServiceAccount (identity from the SA token) if ambient/ztunnel ↔ SPIRE identity integration proves rough. Same result, more K8s object churn.
- Verify before building: which layer carries the trusted per-session identity to the resolver — ztunnel's L4 mTLS principal vs. AuthBridge's SPIFFE JWT-SVID actor assertion (kagenti's AuthBridge already uses the latter for RFC 8693). The resolver keys on whichever is authoritative; this choice gates the SPIRE-vs-SA decision above.
The spine is one abstract contract: identity → egress credential injection.
interface CredentialInjector:
# at egress, given the verified workload identity and the destination,
# resolve and inject the credential. The caller never sees the secret.
inject(verifiedIdentity, destination, request) -> request' # secret added downstream of any log
# identity is a verified reference (SPIFFE id); resolution of identity→secret
# happens ONLY here, never in the harness, sandbox, prompt, or log.
The interface does not depend on SPIRE, Envoy, or Keycloak — a non-kagenti cluster can implement it with a minimal secret-holding sidecar behind the same contract. This is what keeps serverless-harness portable (parent §2.3) and what makes Z3 (provider key) and Z5 (per-user egress) two implementations of one idea rather than two designs.
| Concern | Binding |
|---|---|
| Identity issuance | SPIRE issues per-session SVIDs (§3.3); spiffe-helper delivers them in-pod. |
| Transport identity | Istio ambient ztunnel — L4 mTLS, verified source.principal per pod. |
| L7 injection point | waypoint (Envoy) — the only place headers/credentials are set. ztunnel (L4) cannot inject. One shared waypoint per namespace serves many per-session identities; it keys on source.principal, so it does not churn per user. |
| Resolution | AuthBridge ext-proc reads the verified identity → (actor SVID ⊕ subject) → mint (RFC 8693) or fetch stored grant (Z5) → inject. |
The harness path (Z3) is the trivial implementation: identity gates mTLS to the injector; the credential is a non-per-user provider key. The sandbox path (Z5) is the full implementation: identity → user → that user's grant.
- Create: authenticate the user (or accept the gateway/Keycloak assertion) → allocate
session-id→ write the authoritativesession-id → userbinding (§6) → mint the per-session SPIRE registration with the user in the attested path → provision the identity-bearing sandbox pod. This is the one point a live user identity is required. - Wake: receive the wake signal for
session-id→ read the binding from the durable store → reconstruct the SPIRE entry + sandbox pod with the correct identity → hand the harness a reference to connect to. No live user re-auth — wake is unattended (§7). - Idle (scale-to-zero): tear down the sandbox pod and reap the SPIRE entry; keep the binding.
- End / GC: delete binding + entry; TTL-reap abandoned sessions.
Only the orchestrator can (a) create session pods and (b) create SPIRE entries. Nothing else has the RBAC.
This is the single rule that makes per-user isolation real. Consequences enforced elsewhere:
- The harness has no
pods/create(Z2 L5 scopes it topods/execon sandbox pods only) → a compromised harness cannot mint a pod bearing another user's identity; it can only connect to a sandbox the orchestrator already provisioned. - Selectors key on attested, orchestrator-set attributes (SA / namespace / node-attested fields), never a workload-settable free-form label.
- Optional admission policy (Kyverno / validating webhook): reject any pod carrying identity labels unless created by the orchestrator's ServiceAccount — belt-and-suspenders.
The durable session record holds two kinds of data that must not share a writer:
- the append-only, model-influenced log — the spine; the harness writes it; and
- the authoritative
session → userbinding — used for credential resolution.
If these share one harness-writable structure, a compromised harness could rewrite
user: bob → alice and escalate. Therefore:
- The binding lives in an orchestrator-owned, integrity-protected store (Redis ACL-scoped key / a Session CR / Keycloak) that the harness cannot write.
- The log may still reference identity for audit (§4.1 holds for audit), but it is not authoritative for resolution.
This sharpens parent §4.1 ("identity referenced by SPIFFE string only"): a SPIFFE string in the log is fine as audit, but the resolution-authoritative binding is orchestrator-owned and write-isolated from the log.
Identity follows the harness's core philosophy — reconstructed from the durable record on wake, never kept alive while idle.
turn for session X arrives
→ orchestrator reads X → user=alice from the AUTHORITATIVE binding store (§6)
→ reconstruct SPIRE entry (path …/user/alice/session/X) + sandbox pod (attested selectors)
→ spiffe-helper fetches a FRESH SVID (same identity, new cert)
→ model code egresses → waypoint → AuthBridge reads SVID(user=alice)
→ fetch/refresh alice's STORED grant (Z5) → placeholder-swap → egress AS alice
→ idle again → pod torn down, SPIRE entry reaped; binding persists
Two properties make this clean:
- No credential is ever cached in-pod — resolution fetches the grant fresh at egress every time, so teardown loses nothing credential-related.
- Stored-grant resolution (Z5 §4.2) means the user may be offline at wake. Scale-to-zero is the payoff case for choosing stored grants over live user tokens: a session can act as Alice while Alice is offline. A live-token design would break here.
A long-idle wake may find the stored grant expired/revoked → egress returns a clean auth error (Z5: no silent fallback).
| # | Decision | Choice |
|---|---|---|
| ID1 | Identity granularity | Per-session, finer than namespace/SA. The namespace is not the isolation boundary (§3.2). |
| ID2 | User binding | User in the attested SVID path, set by the orchestrator at mint time; the resolver reads it — no asserted header (§3.1). |
| ID3 | Issuance | SPIRE per-pod registration (kagenti-native; spiffe-helper already present); per-session SA as fallback (§3.3). |
| ID4 | Interface | Abstract CredentialInjector (identity → egress injection); kagenti binding = SPIRE + ambient + waypoint + AuthBridge (§4). Z3 and Z5 are implementations. |
| ID5 | Minting authority | Orchestrator only creates session pods and SPIRE entries; harness has no pods/create (§5.2). |
| ID6 | Binding store | Orchestrator-owned, integrity-protected, separate from the harness-writable log (§6). |
| ID7 | Scale-to-zero | Reconstruct identity on wake from the durable binding; reap on idle. No per-session mesh state while sleeping (§7). |
| ID8 | Live user auth | At session creation only. Wake is unattended, trusting the durable binding + stored grant (§7). |
| ID9 | Harness reframing | Harness gets a SPIFFE id but no egress waypoint (parent M7 reframed; Z2 §2.4). The per-user egress plane is the sandbox's (Z5). |
- The orchestrator is the identity-side crown jewel. If compromised, it can mint an SVID bound to any user and drive egress to use that user's stored grant — i.e. impersonate any user. It never holds the raw secrets (those stay in the cred store, injected at the waypoint), but it can cause their use. This mirrors the injector's role on the provider-key side (Z3): the trust core is two small, non-model-influenced concentration points.
- Mitigations: minimal non-model-influenced surface; strong RBAC; an audit trail of every
identity-minting action (who minted
…/user/alice/session/X, from which authenticated request); and ideally separation of duties — the component that authenticates the user distinct from the one that mints entries, so neither alone can impersonate. - The fatal anti-pattern (explicitly forbidden): shared namespace SA + a
subjectheader for per-user routing → any session spoofs any user → cross-user credential theft (§3.2). - Spoofing the SVID: a workload cannot obtain another session's SVID without compromising SPIRE or the node; ztunnel enforces identity at the node. The invariant "one session = one pod = one identity" (fresh sandbox per session) keeps identities from collapsing.
- Binding tampering: prevented by §6 (binding is not harness-writable).
Z1 passes when, on a Kind cluster with SPIRE + Istio ambient + the orchestrator + a stub egress resolver, in one shared namespace:
- Per-user isolation: two sessions for distinct users (alice, bob) in the same namespace receive distinct SVIDs with their user in the attested path; the stub resolver, keying only on the verified identity, attributes each correctly.
- No spoof: a session cannot acquire another user's identity — it has no
pods/create, and a pod that tries to carry a foreign identity label (created out-of-band) is rejected by admission and/or fails SPIRE attestation. - Binding integrity: the harness cannot modify the authoritative
session → userbinding (write attempt denied); tampering the log does not change resolution. - Reconstruct-on-wake: scale a session to zero and wake it; it comes back with the same identity (path), minted fresh from the durable binding, with no per-session SPIRE state having persisted during idle.
- Minting audit: every identity mint produced an audit record tying the SVID to an authenticated creating request; no secret appears in it.
- A new trusted tier to operate. The orchestrator (Session controller) is real control-plane surface with powerful RBAC. Justified: per-user isolation has no cheaper root.
- Identity churn per session. Mint on create/wake, reap on idle/end — SPIRE entry/pod lifecycle work, plus a TTL reaper for abandoned sessions. Cold-start now includes SVID issuance (seconds).
- Two concentration points (orchestrator for identity, injector for provider key). Accepted; both are small, non-model-influenced, and the focus of hardening.
- Ambient + SPIRE integration is a verification risk (§3.3) — confirm the trusted identity layer before committing to SPIRE-per-pod vs per-session SA.
- Live user only at creation. Good for unattended wake (§7), but means the create-time authentication + binding write is a critical, audited step.
- Per-user external-credential store (Z5 / parent M7–M9). Z1 issues identity; Z5 resolves
credentials keyed by it. Recorded shape (from M13 §4.2): keyed
(user, destination); consent, rotation, revocation first-class; at rest in the identity plane (Keycloak + proxy-sidecar memory), never in harness/sandbox; on expiry/revoke, a clean auth error with no silent fallback. - The egress proxies (Z3 injector, Z5 waypoint/forward proxy) — implementations of §4's interface.
- Sandbox FS persistence across scale-to-zero — separate track.
- Milestone Registry — Phase 2 numbering; Z1 supersedes parent M7.
- Zero-Trust, Multi-Agent Extensions — §2.1 principle, §2.3 portable-core/kagenti-binding, §3.2/§3.4 identities, §4.1 log invariants (refined §6 here).
- Z2 Harness Lock-Down —
pods/exec-only RBAC (the integrity invariant's enforcement), harness SPIFFE id. - Z3 Inference Injector — the trivial (non-per-user) implementation of §4; mTLS peer authz.
- Z4 MCP code-mode / Z5 Generalized Egress — the full per-user
(actor SVID ⊕ subject)implementation; the credential store Z1 depends on. - kagenti: SPIRE/SPIFFE (
spiffe-helper, controller-managerClusterSPIFFEID), Istio ambient (ztunnel L4 mTLS, waypoint L7), AuthBridge (RFC 8693, SPIFFE JWT-SVID actor assertion). - RFC 8693 — OAuth 2.0 Token Exchange (
subject_token,actor_token,audience).
Assisted-By: Claude (Anthropic AI) noreply@anthropic.com