JWKS Pinning

Microbus uses short-lived JWTs to carry user identity (the actor) end-to-end across a thread of microservice-to-microservice calls. These actor JWTs are issued by the access token service and bearer token service, and every receiving microservice verifies their signatures before trusting the claims they carry.

The verification step is where JWKS pinning matters. The Microbus connector does not delegate to the JWT’s iss claim to decide where to fetch the issuer’s public keys from. It ships with exactly two issuers hardcoded — the access token service at access.token.core and the bearer token service at bearer.token.core — and always fetches JWKS from those. This document explains what that buys and how it composes with the rest of the security model.

What Pinning Does

The connector’s actor-JWT verifier has a hardcoded list of trusted issuers:

The verifier always fetches JWKS from these pinned hostnames over the trust-root tier (:666/jwks). The iss claim on an incoming token is validated against the expected pinned hostname for the token type. Access tokens must carry iss=access.token.core and bearer tokens must carry iss=bearer.token.core. A mismatch causes the token to be rejected immediately, before any signature check or JWKS fetch.

Why Pinning, Given That ACL Already Bounds Issuers

It is tempting to frame JWKS pinning as the thing that prevents forged tokens — a naive verifier that trusted iss would happily fetch keys from an attacker-controlled hostname and accept whatever the attacker signed. On closer inspection, the interservice ACL, combined with broker-pinned source identity and true-to-code .creds, already blocks the obvious impersonation attack:

  • To serve JWKS under hostname X, a microservice needs a SUB rule for danger.666.X.GET.jwks in its signed .creds. The broker rejects subscriptions outside that allow-list.
  • gencreds is true-to-code, so it only emits that SUB rule when the microservice’s source actually declares a JWKS handler.
  • A compromised peer that does not already own a JWKS endpoint cannot subscribe to one. Its broker subscriptions are bounded by the signed creds, even with full code execution inside the process.

So a peer cannot just “serve JWKS from a hostname they control.” They would need a microservice deployed under that hostname with a :666/jwks handler in its source and operator-approved .creds granting that SUB. At that point the attack is symmetric to today’s :666/Mint attack: an operator who misses a danger-tier grant during creds review hands the attacker token-issuing capability either way. Pinning does not shrink the operator-review surface.

What pinning does change is what happens after a review miss, and where iss-enforcement lives.

Centralizing Mint Observability

When every actor JWT flows from one of the two pinned issuers, every mint event passes through access.token.core (or bearer.token.core). The issuer is the natural place to:

  • Emit OpenTelemetry spans for each mint.
  • Audit-log the caller, the claims requested, and the claims granted.
  • Rate-limit per caller, per tenant, or per claim shape.
  • Alert on anomalies — a microservice suddenly minting at 10× its baseline rate, claims appearing that have never been requested before, mints to unusual subject populations.

Without pinning, any microservice with a :666/jwks handler in its source can become a co-equal issuer, signing tokens locally with its own key. Those mints are invisible to the framework. There is no shared chokepoint to instrument and no centralized place to notice “team-x’s microservice is issuing admin tokens at 10k an hour.” A review miss on a SUB grant becomes a silent issuer; a review miss on a PUB grant to access.token.core.POST.Mint becomes a noisy one that the real issuer can see and report.

Keeping iss Enforcement at the Verifier

A pinned verifier rejects any token whose iss does not match the expected hostname for the token type, before signature verification. Without pinning, that check moves into application code — every requiredClaims expression on a sensitive endpoint would need to assert iss=access.token.core (or whatever set of issuers the deployment treats as trusted), and any expression that forgets the check accepts tokens from any issuer the verifier has keys for.

The defense matrix lists “application bugs in requiredClaims” as something the framework explicitly does not defend against. Centralizing iss-enforcement at the verifier removes one whole class of requiredClaims bug. The application’s expression evaluates against claims the framework has already vouched as coming from a pinned issuer.

Integrating External Identity Providers

A consequence of pinning is that tokens signed by external IDPs (Auth0, Okta, Google, enterprise SSO, and similar) cannot be consumed directly by Microbus microservices. Their iss points at their own hostnames, which are not in the pinned list.

The integration pattern is the wrapper microservice:

  1. The external IDP authenticates the user and issues its own token.
  2. A wrapper microservice (typically an ingress login endpoint, or a dedicated IDP-bridge microservice) receives the external token, validates it against the IDP, and calls the access token service’s Mint endpoint to obtain a Microbus access token with claims derived from the external assertion.
  3. Downstream Microbus microservices see only the Microbus access token, signed by the access token service.

This adds one indirection layer but yields a single, framework-controlled issuer for every actor JWT on the bus — preserving the centralized-observability property described above. The wrapper microservice is a natural place to enforce IDP-specific policy (claim mapping, group lookups, MFA requirements) without leaking that complexity into every consumer.

Customizing the Token Issuers

The pinned list is fixed. The framework trusts exactly two issuer hostnames, access.token.core and bearer.token.core, and there is no runtime API or deployment knob to add a third.

If you need to customize what an issuer does — for example to source claims from a different identity backend, change how tokens are minted, or adjust TTL policy — the path is to replace the standard implementation. Build a microservice that satisfies the same interface as the standard issuer and registers under the same hostname, then add your replacement to the application instead of the standard one. Verifying microservices keep pinning the JWKS lookup to the same hostname; they neither know nor care which implementation is serving it.

This is intentionally not a configuration change. The set of trusted issuers is a security-critical decision that should be explicit in code and reviewable in pull requests. The replacement implementation lives in source, gets the same scrutiny as any framework-adjacent code, and ships with the application like any other microservice.

For most external-IDP integrations, the simpler path is the wrapper-microservice pattern described in the previous section. Replacing the standard issuer is reserved for cases where the wrapper pattern is not enough.