feat(security): agent-side SPIFFE JWT-SVID verification (proposal 011 PR-4) - #87
Merged
Merged
Conversation
Fourth PR of proposal 011 — when security.signing_mode is "spiffe",
a ROLE_UPDATE carries the arbiter's JWT-SVID in its signature field;
the agent verifies it against the SPIRE trust bundle the
spiffe-helper sidecar (PR-3) materialises onto disk.
Inert by design: every deploy_mode still defaults to
signing_mode=ed25519 in v0.4.x, so the existing Ed25519 path is
unchanged for all current deployments.
New module acc/spiffe_verify.py:
* load_jwt_svid / load_jwt_bundle — read the spiffe-helper outputs.
* verify_jwt_svid(token, bundle, audience, expected_spiffe_id) —
resolves the signing key from the JWKS bundle by kid, verifies
signature + aud + exp via PyJWT, optionally enforces the sub
claim, rejects alg=none.
* SpiffeVerifier — re-reads the bundle on every call so SPIRE
bundle rotation is honoured without an agent restart.
* PyJWT lazy-imported with a clear error; also added as a declared
dependency (small, pure-Python, reuses the cryptography dep).
acc/role_store.py:
* apply_update now dispatches signature verification through
_verify_signature, which switches on signing_mode:
- spiffe -> _verify_spiffe
- ed25519 -> the existing _verify_ed25519 path
* When security.spiffe.allow_ed25519_fallback is true, a SPIFFE
failure degrades to the Ed25519 path — a transient SPIRE problem
doesn't strand the collective during the migration window.
acc/config.py:
* New security.spiffe.arbiter_spiffe_id field (ACC_SPIFFE_ARBITER_ID
env). When set, the verifier enforces the JWT sub claim equals
the arbiter's SPIFFE ID; when blank, arbiter identity rests on
the existing approver_id application-layer check.
operator (spiffe_sidecar.go):
* RenderSpiffeHelperConfig emits jwt_bundle_file_name so
spiffe-helper writes the JWKS bundle the verifier reads.
What a JWT-SVID proves: arbiter identity + audience, NOT
ROLE_UPDATE content integrity — content stays bound by the
approver_id + role-version checks. Documented in the
acc/spiffe_verify.py module docstring.
Test coverage: 25 new tests — 20 in tests/test_spiffe_verify.py
(happy path, wrong audience/subject/signature, expiry, alg=none
rejection, kid-not-in-bundle, bundle rotation, file loading) + 5
in tests/test_role_store.py::TestApplyUpdateSpiffe (valid SVID
accepted, invalid rejected, ed25519 fallback, arbiter_spiffe_id
enforced, expired SVID). 123/123 green on the combined
config+role_store+spiffe_verify surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
flg77
added a commit
that referenced
this pull request
Jun 15, 2026
fix(tui): Golden Prompts selection/editor + proposal for versioning/traces
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.
Summary
Fourth PR of proposal 011. When
security.signing_modeisspiffe, a ROLE_UPDATE carries the arbiter's JWT-SVID in itssignaturefield; the agent verifies it against the SPIRE trust bundle thespiffe-helpersidecar (PR-3) writes to disk.Inert by design — every
deploy_modestill defaults tosigning_mode: ed25519in v0.4.x. Branched offmain(the 011-PR-2/3 + 012-PR-2 chain is merged) — not stacked.What's new
acc/spiffe_verify.py(new):verify_jwt_svid(token, bundle, audience, expected_spiffe_id)— resolves the signing key from the JWKS bundle bykid, verifies signature +aud+expvia PyJWT, optionally enforcessub, rejectsalg=none.SpiffeVerifier— re-reads the bundle each call → SPIRE bundle rotation honoured without an agent restart.PyJWTadded as a declared dep (small, pure-Python, reusescryptography).acc/role_store.py:apply_updatedispatches through_verify_signature, switching onsigning_mode:spiffe→_verify_spiffe,ed25519→ existing path.allow_ed25519_fallback: true→ a SPIFFE failure degrades to the Ed25519 path (migration-window safety).acc/config.py: newsecurity.spiffe.arbiter_spiffe_id(ACC_SPIFFE_ARBITER_ID) — when set, the verifier enforces the JWTsubclaim.operator:
RenderSpiffeHelperConfigemitsjwt_bundle_file_nameso spiffe-helper writes the JWKS bundle.Security note
A JWT-SVID attests arbiter identity + audience, not ROLE_UPDATE content integrity — content stays bound by the existing
approver_id+ role-version checks. The SPIFFE upgrade replaces "trust a static Ed25519 key forever" with "trust a SPIRE-attested, short-lived, rotatable identity". Full reasoning in theacc/spiffe_verify.pymodule docstring.Test plan
tests/test_spiffe_verify.py(happy path, wrong audience/subject/signature, expiry,alg=none, kid-not-in-bundle, bundle rotation, file loading), 5 intests/test_role_store.py::TestApplyUpdateSpiffespiffe_sidecar.gojwt_bundle_file_namechange)Proposal 011 reference
011 - SPIFFE workload identity for ACC agents.md. PR-1 #81, PR-2 #84, PR-3 #85 merged. PR-5 (docs + rhoai v0.5.0 default-flip plan) is the last 011 PR.🤖 Generated with Claude Code