Skip to content

atecontroller publishes actor JWT verification keys for ate-idp-server to serve - #2134

Closed
Max Thompson (thompsonmax) wants to merge 6 commits into
agent-substrate:mainfrom
thompsonmax:actor-jwks-configmap
Closed

Max Thompson (thompsonmax) wants to merge 6 commits into
agent-substrate:mainfrom
thompsonmax:actor-jwks-configmap

Conversation

@thompsonmax

@thompsonmax Max Thompson (thompsonmax) commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #1756. Stacked on #2132, which is stacked on #1961; only the last commit is new.

  • Adds ActorJWKSReconciler to atecontroller. It watches the actor-id-jwt-pool Secret and server-side-applies the public key of every authority, active or not, into the actor-id-jwks ConfigMap as jwks.json, the file ate-idp-server (Add ate-idp-server to serve the discovery document and key set for actor JWTs #2099) reads. ate-idp-server never sees the signing keys.
  • The ConfigMap is owned by the Secret, so deleting the pool removes the published keys. Editing or deleting the ConfigMap by hand is reverted.
  • An unreadable or empty pool fails the reconcile and keeps the last good key set, because relying parties cache what ate-idp-server serves.
  • atecontroller gets ConfigMap write access through a Role scoped to ate-system.

For reviewers: atecontroller's Secret cache now covers all of ate-system rather than only egress-mitm-ca-pool, because a cache takes one field selector per namespace. Its ClusterRole already reads Secrets cluster-wide.

Testing: fake-client tests for publishing, rotation, reverting edits, a missing pool, and bad pools. Not run against a cluster; the e2e PR covers that once the manifests land.

  • Tests pass
  • Appropriate changes to documentation are included in the PR

Add oidcdiscovery.Thumbprint, the RFC 7638 SHA-256 thumbprint of an RSA
or P-256 public key, and replace GenerateECDSAP256Authority with
GenerateAuthority, which generates an RS256 or ES256 key and defaults
its ID to the thumbprint. A thumbprint ID cannot collide across
operators or be reused for a different key.

kubectl ate admin make-jwt-pool gains --alg, defaulting to RS256, and
--key-id now defaults to the thumbprint. ate-setup creates the same kind
of pool. RS256 works with relying parties that do not support ES256.

Existing pools are untouched, so keys named "1" keep working.
New actor JWT pools default to ES256 again, matching every existing
pool. make-jwt-pool --alg defaults to ES256, and ate-setup reads the
algorithm from ACTOR_JWT_ALGORITHM (ES256 or RS256, default ES256), so
an install whose relying parties do not support ES256 can choose RS256
without creating the pool by hand. Key IDs still default to the
thumbprint.
Use go-jose's JSONWebKey.Thumbprint instead of building the RFC 7638
canonical form by hand. Thumbprint still accepts only RSA and P-256
keys, the types the pool generates. go-jose moves from an indirect to a
direct dependency; it was already vendored.
kubectl-ate's make-jwt-pool and ate-setup each generated an authority,
made it active, and marshaled the pool. GeneratePool does that and
returns the serialized pool and the authority's ID, so both callers only
build their Secret.
Add JWKS, which serializes verification keys into the JWK set relying
parties fetch from the issuer, sorted by key ID. It encodes with
go-jose's JSONWebKey, as the Kubernetes service account issuer does, and
adds the checks go-jose leaves to the caller: the set is not empty, key
IDs are present and unique, and each algorithm fits its key (ES256 for
P-256 EC, RS256, RS384, or RS512 for RSA), so a malformed pool cannot
publish a set that verifiers would reject.

localjwtauthority.VerificationKey gains Algorithm so callers can publish
it.
ate-idp-server serves the actor JWT key set but must never see the
signing keys, so something with access to the actor-id-jwt-pool Secret
has to copy the public half out. ActorJWKSReconciler watches that Secret
and server-side-applies the JWK set of every authority in the pool,
active or not, into the actor-id-jwks ConfigMap under jwks.json.

The ConfigMap is owned by the Secret, so garbage collection removes the
published keys along with the pool. An unreadable or empty pool fails
the reconcile and leaves the last good key set in place, since relying
parties cache whatever ate-idp-server serves. Deleting or editing the
ConfigMap by hand is reverted.

atecontroller gains ConfigMap write access through a Role scoped to the
system namespace. Its Secret cache now covers the whole system
namespace: a cache takes one field selector per namespace, and two
controllers each need a different Secret.
@thompsonmax

Copy link
Copy Markdown
Collaborator Author

Closing in favor of having ate-idp-server read the pool Secret directly with RefreshingPool (#2099), which removes the need for this controller and the ConfigMap.

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