Feature Description
Currently, AgentCard signing is a manual, pre-deployment step: operators must generate keys, run sign-agent-card.py with --spiffe-id, and bake the signed card JSON into a ConfigMap before deploying. This works, but requires the operator to know the workload's SPIFFE ID upfront and handle key management outside the cluster. The spiffe_id in the JWS protected header is also self-asserted the signer claims an identity with no cryptographic proof.
With SPIRE available in the cluster, we can automate this entirely at deploy time using an init container that:
- Fetches the workload's X.509-SVID from the SPIRE Workload API
- Uses the SVID private key to sign the AgentCard JSON
- Includes the full X.509 certificate chain via the
x5c JWS header parameter (RFC 7515), enabling chain-of-trust verification
- Writes the signed card to a shared volume for the agent container to serve
The workload's SPIFFE ID is extracted from the leaf certificate's SAN URI during verification it is no longer a self-asserted claim in the JWS protected header. The x5c chain proves the signer's identity was issued by the SPIRE CA.
This removes the manual signing workflow, eliminates self-asserted identity claims, and makes the SPIFFE ID cryptographically verifiable through the certificate chain.
Proposed Solution
Init container
Add a signing init container (Go binary) that runs before the agent container.
Example manifest snippet
initContainers:
- name: sign-agentcard
image: kagenti/agentcard-signer:latest # pin to digest in production
env:
- name: SPIFFE_ENDPOINT_SOCKET
value: unix:///run/spire/sockets/agent.sock
- name: UNSIGNED_CARD_PATH
value: /etc/agentcard/agent.json
- name: AGENT_CARD_PATH
value: /app/.well-known/agent.json
- name: SIGN_TIMEOUT
value: "30s"
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: spire-agent-socket
mountPath: /run/spire/sockets
readOnly: true
- name: unsigned-card
mountPath: /etc/agentcard
readOnly: true
- name: signed-card
mountPath: /app/.well-known
volumes:
- name: spire-agent-socket
csi:
driver: "csi.spiffe.io"
readOnly: true
- name: unsigned-card
configMap:
name: weather-agent-card-unsigned
- name: signed-card
emptyDir:
medium: Memory # tmpfs signed card never touches disk
sizeLimit: 1Mi
Init container responsibilities
- Read the unsigned AgentCard JSON from a ConfigMap-mounted volume (
unsigned-card)
- Connect to the SPIRE Workload API via the Unix domain socket
- Fetch the workload's X.509-SVID (private key + certificate chain)
- Sign the card in JWS format using the SVID private key, including the full certificate chain in the
x5c header (RFC 7515). The spiffe_id is not included as a separate header field it is embedded in the leaf certificate's SAN URI
- Write the signed card to the shared
emptyDir volume (signed-card) for the agent container to serve
- Zero the private key material in memory (best-effort) and exit
Verification side
The operator's verification side changes significantly from Step 1. A new X5CProvider replaces the existing SecretProvider, JWKSProvider, and NoOpProvider:
X5CProvider validates the x5c certificate chain against the SPIRE X.509 trust bundle, then uses the leaf public key for JWS verification
- The SPIFFE ID is extracted from the leaf certificate's SAN URI (cryptographically proven by the chain), not from a self-asserted JWS header field
computeBinding() uses trust-domain-only validation: the operator confirms the SPIFFE ID from the cert SAN belongs to the configured trust domain no manual allowedSpiffeIDs allowlist required
- Legacy providers removed:
SecretProvider, JWKSProvider, and NoOpProvider are all removed. Single verification path via X5CProvider with no bypass option
Key verification checks:
ExtKeyUsage set to x509.ExtKeyUsageAny (Go defaults to ServerAuth when nil, which would reject valid SPIRE SVIDs)
- Leaf certificate must have exactly one SAN URI with
spiffe:// scheme
- Chain depth limited to 3 (leaf + intermediate + root)
- Algorithm validation rejects
alg: none
Trust bundle distribution
The SPIRE X.509 trust bundle (CA certificates, not JWKS) must be available to the operator for chain validation.
Recommended approach:
spire-controller-manager native sync (preferred): syncs the X.509 trust bundle to a Kubernetes Secret automatically
spiffe-helper (fallback): watches the SPIRE bundle endpoint and syncs to a Secret via kubectl
Note: SPIRE's JWKS endpoint exposes JWT-SVID signing authority keys, not X.509 CA certificates. It cannot be used to verify x5c certificate chains from X.509-SVIDs.
Key rotation
SPIRE rotates SVIDs automatically.
- The init container re-signs the AgentCard on each pod restart, so rotation is handled naturally by the pod lifecycle
- The operator refreshes the trust bundle periodically; on refresh failure, it continues using the last successfully-loaded bundle (fails startup if no bundle was ever loaded)
Want to contribute?
Additional Context
Design Decision
An init container signs the card at pod startup using the workload's own SVID. Key design choices:
x5c certificate chain in JWS header: enables chain-of-trust verification against the SPIRE trust bundle. The SPIFFE ID is extracted from the leaf cert SAN (cryptographically proven), not self-asserted in the header
- Trust-domain-only binding: replaces
allowedSpiffeIDs allowlist. SPIRE attestation (namespace + service account selectors) is the authorization layer; the operator only validates trust domain membership as defense-in-depth
- Single verification path (
X5CProvider): all legacy providers removed (SecretProvider, JWKSProvider, NoOpProvider). No configuration path silently disables verification
- Separate input/output volumes: unsigned card from ConfigMap, signed card to memory-backed
emptyDir. Prevents the “empty emptyDir” problem from the original design
- Explicit
securityContext: compatible with OpenShift restricted SCC
Alternatives considered:
-
Sidecar (spiffe-helper)
- Adds permanent resource overhead
- Unnecessary since pod restarts naturally re-sign
-
Operator-side signing
- Breaks the security model (the operator signing doesn't prove card authenticity)
-
CSI driver + agent-native signing
- Requires agent authors to implement SPIRE/signing logic, violating the principle of keeping agents agnostic
-
JWKS endpoint for verification
- SPIRE JWKS contains JWT-SVID signing keys, not X.509 leaf keys — cryptographically invalid for verifying X.509-SVID signatures
-
Manual allowedSpiffeIDs allowlist
- Creates a silent dependency on
ClusterSPIFFEID path conventions; SPIRE attestation already provides the authorization guarantee
Feature Description
Currently, AgentCard signing is a manual, pre-deployment step: operators must generate keys, run
sign-agent-card.pywith--spiffe-id, and bake the signed card JSON into a ConfigMap before deploying. This works, but requires the operator to know the workload's SPIFFE ID upfront and handle key management outside the cluster. Thespiffe_idin the JWS protected header is also self-asserted the signer claims an identity with no cryptographic proof.With SPIRE available in the cluster, we can automate this entirely at deploy time using an init container that:
x5cJWS header parameter (RFC 7515), enabling chain-of-trust verificationThe workload's SPIFFE ID is extracted from the leaf certificate's SAN URI during verification it is no longer a self-asserted claim in the JWS protected header. The
x5cchain proves the signer's identity was issued by the SPIRE CA.This removes the manual signing workflow, eliminates self-asserted identity claims, and makes the SPIFFE ID cryptographically verifiable through the certificate chain.
Proposed Solution
Init container
Add a signing init container (Go binary) that runs before the agent container.
Example manifest snippet
Init container responsibilities
unsigned-card)x5cheader (RFC 7515). Thespiffe_idis not included as a separate header field it is embedded in the leaf certificate's SAN URIemptyDirvolume (signed-card) for the agent container to serveVerification side
The operator's verification side changes significantly from Step 1. A new
X5CProviderreplaces the existingSecretProvider,JWKSProvider, andNoOpProvider:X5CProvidervalidates thex5ccertificate chain against the SPIRE X.509 trust bundle, then uses the leaf public key for JWS verificationcomputeBinding()uses trust-domain-only validation: the operator confirms the SPIFFE ID from the cert SAN belongs to the configured trust domain no manualallowedSpiffeIDsallowlist requiredSecretProvider,JWKSProvider, andNoOpProviderare all removed. Single verification path viaX5CProviderwith no bypass optionKey verification checks:
ExtKeyUsageset tox509.ExtKeyUsageAny(Go defaults toServerAuthwhen nil, which would reject valid SPIRE SVIDs)spiffe://schemealg: noneTrust bundle distribution
The SPIRE X.509 trust bundle (CA certificates, not JWKS) must be available to the operator for chain validation.
Recommended approach:
spire-controller-managernative sync (preferred): syncs the X.509 trust bundle to a Kubernetes Secret automaticallyspiffe-helper(fallback): watches the SPIRE bundle endpoint and syncs to a Secret viakubectlKey rotation
SPIRE rotates SVIDs automatically.
Want to contribute?
Additional Context
scripts/sign-agent-card.pywith--spiffe-idflag (manual, pre-deployment)internal/signature/verifier.go,VerifyJWS()extracts public key and validates JWS signaturesagentcard_controller.go,computeBinding()uses JWSspiffe_idexclusively (no fallback paths, per reviewer guidance in ✨ A2A AgentCard Signature Verification #176)Design Decision
An init container signs the card at pod startup using the workload's own SVID. Key design choices:
x5ccertificate chain in JWS header: enables chain-of-trust verification against the SPIRE trust bundle. The SPIFFE ID is extracted from the leaf cert SAN (cryptographically proven), not self-asserted in the headerallowedSpiffeIDsallowlist. SPIRE attestation (namespace + service account selectors) is the authorization layer; the operator only validates trust domain membership as defense-in-depthX5CProvider): all legacy providers removed (SecretProvider,JWKSProvider,NoOpProvider). No configuration path silently disables verificationemptyDir. Prevents the “emptyemptyDir” problem from the original designsecurityContext: compatible with OpenShiftrestrictedSCCAlternatives considered:
Sidecar (
spiffe-helper)Operator-side signing
CSI driver + agent-native signing
JWKS endpoint for verification
Manual
allowedSpiffeIDsallowlistClusterSPIFFEIDpath conventions; SPIRE attestation already provides the authorization guarantee