Skip to content

Add ate-idp-server to serve the discovery document and key set for actor JWTs - #2099

Merged
Taahir Ahmed (ahmedtd) merged 5 commits into
agent-substrate:mainfrom
thompsonmax:ate-idp-server
Oct 16, 2026
Merged

Taahir Ahmed (ahmedtd) merged 5 commits into
agent-substrate:mainfrom
thompsonmax:ate-idp-server

Conversation

@thompsonmax

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

Copy link
Copy Markdown
Collaborator

Part of #1756.

  • Adds cmd/ate-idp-server, which serves /.well-known/openid-configuration and /openid/v1/jwks over HTTPS with a servicedns credential bundle, so relying parties can verify actor JWTs.
  • Reads the actor JWT authority pool from --actor-id-jwt-pool with localjwtauthority.RefreshingPool, the reader ate-api-server signs with, and builds each document from it per request.
  • Builds both documents at startup and exits if it can't. After that, a request whose document can't be built gets a 500 rather than an empty or stale set.
  • Requires --issuer rather than deriving a default, so it can't disagree with ateapi, and accepts only https://<host>, so the routes are fixed. The check is the new actoridjwt.ValidateIssuer; the discovery document moves out of internal/oidcdiscovery into the server, its only user.
  • Makes the EC case in localjwtauthority.JWKS explicit: P-256 with fixed 32-byte coordinates and an error for any other curve, rather than reading the coordinate size off the encoded point.
  • Records in docs/metrics/substrate.yaml that it emits no metrics.

For reviewers: the pool holds the signing keys, so ate-idp-server will mount the same Secret ate-api-server does, in place of the controller and ConfigMap in #2134. ate-api-server and ate-setup still accept an issuer with a path until the installer deploys ate-idp-server. It serves /healthz but no /readyz. Nothing deploys it yet.

Testing: unit tests for routing, the host-only issuer check, the discovery document, startup failures, key changes, 500s on a broken set, the P-256 encoding, and required flags, plus a local run over TLS against a generated pool. E2E can't cover it until it's deployed.

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

@wiz-alphabet

wiz-alphabet Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 1 Low
Software Management Finding Software Management Findings -
Total 1 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@thompsonmax
Max Thompson (thompsonmax) force-pushed the ate-idp-server branch 3 times, most recently from 8536af6 to c5dd81b Compare October 7, 2026 20:55
Lior Lieberman (LiorLieberman) pushed a commit to LiorLieberman/agent-substrate that referenced this pull request Oct 8, 2026
…ubstrate#2132)

Part of agent-substrate#1756.

- Adds `localjwtauthority.JWKS`, which turns a pool's verification keys
into the JWK set relying parties fetch, sorted by key ID. It encodes the
keys in-tree with the RFC 7518 parameters, padding EC coordinates to the
curve size.
- Rejects an empty set, missing or duplicate key IDs, and an algorithm
that doesn't fit its key, so a bad pool can't publish a set verifiers
would reject.
- `localjwtauthority.VerificationKey` gains `Algorithm`.
- `Thumbprint` moves from `oidcdiscovery` to `localjwtauthority`, since
key IDs belong to the pool. It is now the base64url SHA-256 of the key's
PKIX encoding, as the Kubernetes service account issuer derives `kid`,
instead of go-jose's RFC 7638 thumbprint. Pools store each key's ID, so
existing keys keep theirs. go-jose is no longer imported; it stays in
go.mod as an indirect dependency.

Nothing calls `JWKS` yet; ate-idp-server (agent-substrate#2099) will.

Testing: the RFC 7517 and RFC 7638 example keys, thumbprints checked
against openssl, EC coordinate padding, a round trip that decodes keys
back to the originals, and the rejection cases. E2E can't cover it until
it's deployed.

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
Comment thread internal/localjwtauthority/jwks.go Resolved
Comment thread internal/localjwtauthority/jwks.go Resolved
@ahmedtd
Taahir Ahmed (ahmedtd) added this pull request to the merge queue Oct 15, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 15, 2026
Add a small read-only HTTPS server for the OpenID discovery document and
JWK set that relying parties use to verify actor JWTs. It serves the key
set from --jwks-file as is, builds the discovery document from the keys'
algorithms with oidcdiscovery.DiscoveryDocument, and reloads when the
file changes, keeping the last good set if a new one does not parse.
Routes sit under the issuer's path and match on the cleaned path.

--issuer is required. The installer passes the same value it gives
ateapi, so the two cannot derive different issuers.

Until a key set loads, /readyz and both documents return 503, so a
relying party never caches an empty set. TLS uses the servicedns
credential bundle through credbundle, which picks up rotated
certificates. The server emits no metrics, which
docs/metrics/substrate.yaml records.
ate-idp-server read a JWK set that a controller was to derive from the
authority pool and copy into a ConfigMap. It now mounts the pool itself
and reads it with localjwtauthority.RefreshingPool, the same reader
ate-api-server signs with, so publishing a key needs no second component
and no internal ConfigMap. --actor-id-jwt-pool replaces --jwks-file and
--reload-interval; the pool is reread at most once a minute.

Each request builds the key set and discovery document from the pool's
verification keys. RefreshingPool fails every call after a bad reread,
so the server keeps the last documents it built and serves those; until
it has built any, it reports not ready and answers with 503.
toJWK now switches on the curve and slices the two 32-byte P-256
coordinates out of the uncompressed point, returning an error for any
other curve, instead of deriving the coordinate size from the point's
length. It also stops aliasing base64.RawURLEncoding.EncodeToString.

The coordinates still come from PublicKey.Bytes rather than the X and Y
fields, which are deprecated.
The server registers GET patterns on a ServeMux for the discovery
document, the JWK set and /healthz, so the mux answers HEAD, 405 with
Allow, and 404 in place of hand-written checks. The patterns use the
issuer's escaped path, so a character such as { in it stays literal.

New builds both documents and fails if it cannot, so ate-idp-server
exits at startup on a pool it cannot publish. After that, each request
builds its document from the pool and answers 500 if that fails, instead
of serving the last good documents. That leaves /readyz nothing to
report, so it is removed.
ate-idp-server now requires an issuer of the form https://<host>, with
no path, and serves /.well-known/openid-configuration and
/openid/v1/jwks at fixed routes. Supporting a path meant building routes
from the escaped issuer path and handling dot segments; an operator who
fronts the server with a load balancer still chooses the host.

The host-only check is actoridjwt.ValidateIssuer so that ate-api-server
and ate-setup can share it. The discovery document moves out of
internal/oidcdiscovery into the server, its only user; ValidateIssuer
stays there for ate-api-server and ate-setup until they switch.

The server also stops special-casing HEAD, since net/http discards the
body and sets Content-Length, and sets only Content-Type, because
net/http would infer text/plain and OpenID Connect Discovery requires
application/json. Relying parties refetch the key set when they see an
unknown key ID, so Cache-Control goes, as does the CORS header.
@ahmedtd
Taahir Ahmed (ahmedtd) added this pull request to the merge queue Oct 16, 2026
Merged via the queue into agent-substrate:main with commit 0877906 Oct 16, 2026
15 checks passed
Timo Derstappen (teemow) pushed a commit to giantswarm/substrate that referenced this pull request Oct 18, 2026
…tor JWTs (agent-substrate#2099)

Part of agent-substrate#1756.

- Adds `cmd/ate-idp-server`, which serves
`/.well-known/openid-configuration` and `/openid/v1/jwks` over HTTPS
with a servicedns credential bundle, so relying parties can verify actor
JWTs.
- Reads the actor JWT authority pool from `--actor-id-jwt-pool` with
`localjwtauthority.RefreshingPool`, the reader ate-api-server signs
with, and builds each document from it per request.
- Builds both documents at startup and exits if it can't. After that, a
request whose document can't be built gets a 500 rather than an empty or
stale set.
- Requires `--issuer` rather than deriving a default, so it can't
disagree with ateapi, and accepts only `https://<host>`, so the routes
are fixed. The check is the new `actoridjwt.ValidateIssuer`; the
discovery document moves out of `internal/oidcdiscovery` into the
server, its only user.
- Makes the EC case in `localjwtauthority.JWKS` explicit: P-256 with
fixed 32-byte coordinates and an error for any other curve, rather than
reading the coordinate size off the encoded point.
- Records in `docs/metrics/substrate.yaml` that it emits no metrics.

For reviewers: the pool holds the signing keys, so ate-idp-server will
mount the same Secret ate-api-server does, in place of the controller
and ConfigMap in agent-substrate#2134. ate-api-server and ate-setup still accept an
issuer with a path until the installer deploys ate-idp-server. It serves
`/healthz` but no `/readyz`. Nothing deploys it yet.

Testing: unit tests for routing, the host-only issuer check, the
discovery document, startup failures, key changes, 500s on a broken set,
the P-256 encoding, and required flags, plus a local run over TLS
against a generated pool. E2E can't cover it until it's deployed.

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR

(cherry picked from commit 0877906)
Signed-off-by: Timo Derstappen <[email protected]>
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.

2 participants