Skip to content

feat(security): agent-side SPIFFE JWT-SVID verification (proposal 011 PR-4) - #87

Merged
flg77 merged 1 commit into
mainfrom
spiffe/agent-verifier
May 15, 2026
Merged

flg77 merged 1 commit into
mainfrom
spiffe/agent-verifier

Conversation

@flg77

@flg77 flg77 commented May 15, 2026

Copy link
Copy Markdown
Owner

Summary

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) writes to disk.

Inert by design — every deploy_mode still defaults to signing_mode: ed25519 in v0.4.x. Branched off main (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 by kid, verifies signature + aud + exp via PyJWT, optionally enforces sub, rejects alg=none.
  • SpiffeVerifier — re-reads the bundle each call → SPIRE bundle rotation honoured without an agent restart.
  • PyJWT added as a declared dep (small, pure-Python, reuses cryptography).

acc/role_store.py:

  • apply_update dispatches through _verify_signature, switching on signing_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: new security.spiffe.arbiter_spiffe_id (ACC_SPIFFE_ARBITER_ID) — when set, the verifier enforces the JWT sub claim.

operator: RenderSpiffeHelperConfig emits jwt_bundle_file_name so 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 the acc/spiffe_verify.py module docstring.

Test plan

  • 25 new tests — 20 in tests/test_spiffe_verify.py (happy path, wrong audience/subject/signature, expiry, alg=none, kid-not-in-bundle, bundle rotation, file loading), 5 in tests/test_role_store.py::TestApplyUpdateSpiffe
  • 123/123 Python green — validated on Windows (3.14) and lighthouse (Linux, 3.9)
  • Go build clean + operator suite green on lighthouse (the spiffe_sidecar.go jwt_bundle_file_name change)

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

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>
@flg77
flg77 merged commit a800ff2 into main May 15, 2026
@flg77
flg77 deleted the spiffe/agent-verifier branch May 15, 2026 18:42
flg77 added a commit that referenced this pull request Jun 15, 2026
fix(tui): Golden Prompts selection/editor + proposal for versioning/traces
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant