Spec: Shield Identity Bundle (embedded SPIRE + agent governance)
Status: DRAFT — awaiting approval. No code written.
1. Problem & outcome
On-prem customers who want strong agent identity face a three-way assembly job: stand up a workload-identity issuer (SPIRE), buy/operate a non-human-identity product (Aembit/Astrix) for the agent layer, and separately bolt on runtime guardrails. Nobody ships the combination as one on-prem install.
Shield already owns the differentiated half — agent-scoped identity binding
(build_hash/model_version/session), capability minting per tool call,
runtime guardrails, tool RBAC, tamper-evident audit (core/agent_tokens.py,
core/capabilities.py, the guardrail pipelines). What it lacks is a turnkey
issuer/attestation core for on-prem — the piece SPIRE already does well.
Outcome: an opt-in Shield Identity Bundle that packages a proven issuer (SPIRE, embedded) with Shield’s agent-governance behind one install (compose profile / Helm subchart). A partner enables it and gets: attested workload identity → Shield agent token → capability minting → guarded tool calls, end to end, on-prem, with no third-party NHI product and no hand-rolled CA.
The moat is the integration + the governance layer, delivered as one package. Shield consumes SPIRE identity via the modular provider layer (spec-modular-workload-identity.md); it does not reimplement attestation or PKI.
Non-goals
- Not building an issuer/CA/attestation authority. SPIRE provides it; we
embed and integrate, never reinvent. (Rolling our own is precisely how critical
security functionality gets missed — cf. the forgeable X.509 check,
task_02d25da6.) - Not replacing cloud IAM or a customer’s existing SPIRE — if they have one, the bundle federates to / points at it instead of running its own.
- Not on the guard path; no new latency to
/guardrails/*,cap/mint,tools/call. - Not default-on. Existing deploys are unchanged unless the profile is enabled.
1a. Critical-functionality coverage matrix (the “nothing missing” check)
Every load-bearing function of a workload/agent identity system, and who owns it. Bold = the differentiated layer Shield owns.
| # | Function | Owner | Verified by |
|---|---|---|---|
| 1 | Trust domain + SPIFFE naming | SPIRE | bundle boot test |
| 2 | Root/intermediate CA + key mgmt, rotation | SPIRE (+ optional HSM/KMS) | SPIRE health + rotation test |
| 3 | Node attestation (join_token, k8s PSAT, x509pop, cloud IID) | SPIRE | attestation e2e |
| 4 | Workload attestation (uid/path/sha, pod, docker) | SPIRE | attestation e2e |
| 5 | SVID issuance (X.509 + JWT) | SPIRE | SVID fetch test |
| 6 | Short TTL + automatic SVID/key rotation | SPIRE | rotation test |
| 7 | Workload API (local socket; no secrets on disk) | SPIRE Agent | socket fetch test |
| 8 | Real SVID cryptographic verification | Envoy mTLS front door (not the name-only in-Shield check) | forged-cert rejection test |
| 9 | Trust-bundle distribution + rotation to consumers | SPIRE + Envoy SDS | bundle refresh test |
| 10 | Revocation (workload/node) | SPIRE (entry delete) + Shield token/auto-revoke | revoke e2e |
| 11 | Federation (cross-cluster/domain) | SPIRE federation | documented + optional test |
| 12 | Registration entries / selectors | SPIRE (+ Controller Manager on k8s) | registration test |
| 13 | HA datastore + DR | SPIRE (Postgres) | ops doc |
| 14 | Issuance/attestation audit | SPIRE + Shield tamper-evident audit | audit chain test |
| 15 | Identity → Shield agent token exchange | Shield (modular provider) | provider tests |
| 16 | Agent binding: build_hash / model_version / session | Shield agent_tokens |
existing tests |
| 17 | Capability minting per tool call | Shield capabilities |
existing tests |
| 18 | Runtime guardrails + tool RBAC on the identity | Shield pipelines | existing tests |
| 19 | Human-on-behalf (user_sub) binding |
Shield (+ OIDC) | existing tests |
Rows 1–14 are commodity/hard → SPIRE + Envoy. Rows 15–19 are the moat → Shield, already built. The bundle is the wiring between them.
2. Plane & latency contract
- Plane: the bundle is infrastructure packaging (new containers:
spire-server, spire-agent, an Envoy front door) plus data-plane consumption
via the modular provider. No admin-plane change → no
Dockerfile.adminimpact. - Guard path: identity resolution + SVID→token exchange happens only at
token issuance (
/v1/shield/auth/agent-token), per-process. Not on/guardrails/*,cap/mint,tools/call. SPIRE/Envoy run as separate processes/containers — zero added latency to the core guardrail image.
3. Data model
- No new Shield Redis state. SPIRE owns its own datastore (SQLite dev / Postgres prod) — outside Shield’s Redis, its own volume.
- Shield consumes the identity via
request.state(provider layer); no persisted Shield state beyond the existing agent-token/cap stores. - Config surfaces (env / values file), not Redis:
SHIELD_WORKLOAD_IDENTITY_PROVIDERS=spiffe,mtls,SHIELD_SPIFFE_TRUST_DOMAIN,SHIELD_SPIFFE_TRUST_BUNDLE, plus SPIRE server/agent config and Envoy config shipped in the bundle.
4. API / interface
No new public Shield endpoints. This is packaging + integration. Surfaces:
The bundle (opt-in deployment artifact)
- Compose: a
identityprofile indeploy/identity/docker-compose.identity.ymladdingspire-server,spire-agent,envoy(mTLS front door pre-wired to the guardrail server). Enabled viadocker compose --profile identity up. - Helm: an
identitysubchart (deploy/helm/shield/charts/identity/), gated byidentity.enabled=false(default). Includes SPIRE + the SPIRE Controller Manager for auto-registration, and Envoy as the Shield ingress when enabled.
The flow (all existing endpoints)
agent workload
└─ fetch SVID from SPIRE Workload API socket (SPIRE)
└─ mTLS to Shield ingress presenting the SVID (Envoy verifies vs trust bundle)
Envoy injects X-Forwarded-Client-Cert (real crypto here)
└─ POST /v1/shield/auth/agent-token (Shield `spiffe`/`mtls` provider
accepts the XFCC identity)
→ agent_token (bound to build_hash/model/session)
└─ POST /v1/shield/cap/mint per tool call (Shield)
└─ guarded tool calls / /v1/chat/completions (Shield guardrails + RBAC)
New helper (agent side, optional dep)
sdk/spiffe_helper.py (or examples/) — fetch the SVID from the Workload API
socket via py-spiffe, present it, exchange for an agent token, mint caps.
Builds on examples/langchain/spiffe_guarded_e2e.py.
5. Security & backward compatibility
- Opt-in, default off. No profile → no SPIRE, no Envoy, behavior identical to today. Non-breaking.
- Envoy does the real cryptographic verification of the SVID chain against
SPIRE’s trust bundle, then injects
X-Forwarded-Client-Cert. This mitigates the forgeable in-Shield X.509 check (task_02d25da6) for the bundle: Shield trusts only Envoy-injected XFCC.- Hard requirement: the network must ensure only Envoy can reach the guardrail server, and Shield must strip/ignore client-supplied XFCC (accept it only from the trusted proxy). Documented + enforced in the bundle’s network policy. Without this, a client sets XFCC directly and bypasses mTLS.
- The in-Shield X.509 fix (
task_02d25da6) remains a prerequisite for the “Shield validates SVID directly, no Envoy” mode — that mode is not shipped until the fix lands.
- Revocation is two-layer: SPIRE entry delete (stops new SVIDs) + Shield token/auto-revoke (kills live agent tokens/caps immediately).
- No secret on disk: SVIDs come from the Workload API socket, auto-rotated.
- Trust levels recorded per issuance (from the provider layer) so policy can require attested identity for high-risk tools.
6. Packaging & deploy
- New containers, NOT new pip deps in the core image:
spire-server,spire-agent,envoyproxy/envoyare upstream images referenced by the bundle. The core guardrail image gains nothing → norequirements.txtbloat, noDockerfile.adminchange. - Optional agent-side dep:
py-spiffefor the SDK helper goes in an extras group (requirements-spiffe.txt/pip install shield[spiffe]), never in the core runtime requirements. Only agents using the Workload API need it. - New files:
deploy/identity/(compose + SPIRE + Envoy config),deploy/helm/shield/charts/identity/,docs/identity-bundle.md,scripts/smoke_identity_bundle.sh. Self-contained. - Depends on: the modular-workload-identity providers landing first
(
spiffe/mtlsproviders) — that PR is the prerequisite. - Rebuild: none of the core images change; the bundle ships config + upstream images. (If a helper is added to the Shield image later, that PR updates the relevant Dockerfile + import test.)
7. Failure modes & edge cases
- SPIRE server down → no new SVIDs issued; existing SVIDs work until TTL; agent-token issuance fails closed (403) once SVIDs expire. Documented HA setup (3 servers + Postgres) mitigates.
- Envoy misconfig / bypass → covered by the network-policy requirement (§5); a smoke test asserts a direct-to-Shield request (no Envoy) is refused.
- Forged / self-signed SVID → rejected at Envoy (real chain verification); smoke test #forged asserts rejection.
- Trust bundle stale (SPIRE rotated CA) → Envoy SDS / spiffe-helper refresh; test asserts post-rotation SVIDs still verify.
- Clock skew / expired SVID → Envoy rejects; short TTL bounds exposure.
- Customer already runs SPIRE → bundle federates to their trust domain instead of running its own server (config flag); don’t double-run.
- Profile disabled → nothing new runs; core unaffected (regression).
- Revocation race → SPIRE delete + Shield auto-revoke both fire; Shield side is authoritative for live tokens.
8. Test plan (Definition of Done)
scripts/smoke_identity_bundle.sh + tests/test_identity_bundle.py (compose-based
where feasible, mocked where not):
- Bundle boots: spire-server, spire-agent, envoy, shield all healthy.
- Attestation e2e: a registered workload fetches an SVID from the Workload API.
- Token exchange: SVID → mTLS → Envoy XFCC →
/auth/agent-tokenreturns a token bound to the workload’s SPIFFE id. - Forged cert rejected at Envoy (self-signed / wrong CA) — the security-critical check.
- Direct-to-Shield refused: a request bypassing Envoy (no XFCC, or client-set XFCC) does not get a token (network + strip-header enforcement).
- Full chain: token →
/cap/mint→ guarded tool call allowed; a forbidden tool blocked by RBAC (reusesspiffe_guarded_e2e.py). - Rotation: after SVID rotation, a fresh SVID still exchanges successfully.
- Revocation: SPIRE entry delete + Shield revoke → subsequent issuance denied.
- Profile-off regression: with
identity.enabled=false, the core suite passes unchanged and no SPIRE/Envoy runs.
Green in a clean venv for the Python tests; CI pytest gate passes; the
compose-based smoke runs in CI as a separate job (or documented manual gate if CI
can’t run nested containers).
Invariant risk flags
- ✅ Off the hot path — identity only at token issuance; SPIRE/Envoy are separate containers.
- ✅ No core pip deps / no admin import → no
Dockerfile.admindrift.py-spiffeis agent-side extras only. - ✅ Opt-in, default off → non-breaking.
- ⚠️ Network policy is load-bearing: only-Envoy-reaches-Shield + strip client XFCC. Without it the mTLS gate is bypassable. Enforced in the bundle + smoke test #5.
- ⚠️ Prerequisite: modular-workload-identity providers must land first. The direct-SVID-in-Shield mode also needs the X.509 fix (
task_02d25da6); the Envoy mode does not.
Dependencies & sequence
- (prereq) Modular workload-identity providers — spec-modular-workload-identity.md PR 1.
- (prereq, for direct mode only) X.509 validation fix —
task_02d25da6.
Proposed task breakdown (PRs)
- PR 1 — Compose bundle:
deploy/identity/with spire-server + spire-agent + Envoy front door pre-wired to the guardrail server;docs/identity-bundle.md;scripts/smoke_identity_bundle.sh(tests #1-6). The core “one-stop” install. - PR 2 — Agent SDK helper:
py-spiffe-based SVID fetch → token → cap helper (extras dep) + a LangChain e2e that runs fully on the bundle (no cloud). Test #7. - PR 3 — Helm subchart:
identity.enabledk8s deployment with SPIRE + Controller Manager (auto-registration) + Envoy ingress. Tests #8-9. - PR 4 — Federation mode: point-at-existing-SPIRE / cross-cluster federation config for customers who already run SPIRE.
Open decisions for approver
- Ingress choice — Envoy (recommended: SPIRE has first-class Envoy SDS) vs nginx/other for mTLS + XFCC?
- Default trust domain naming —
spiffe://<tenant>.shield.local/...vs a customer-supplied domain in values? - Ship the “Shield validates SVID directly (no Envoy)” mode at all, or
Envoy-only until
task_02d25da6lands? (Recommended: Envoy-only first.) - PR 1 scope — compose-only first (recommended), or compose+Helm together?