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. Microbus does not delegate to the JWT’s iss claim to decide where to fetch the issuer’s public keys from. The connector 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 why that design choice is necessary on a multi-tenant message bus, and how it composes with the rest of the security model.

Why Pinning Matters on a Multi-Tenant Bus

A JWT carries an iss (issuer) claim that identifies who signed it. The standard JWT verification pattern is to fetch the issuer’s JSON Web Key Set from a URL derived from iss, and verify the signature with the matching key. That works fine when iss points at a known authority on the public internet (https://accounts.google.com), but it is dangerous on a multi-tenant message bus, where any peer can claim any iss and serve JWKS from a hostname they control.

A bus peer who controls any hostname’s namespace could become a fake “issuer”:

  1. Generate a fresh signing keypair locally.
  2. Hand-craft a JWT with arbitrary claims (roles: ["admin"], anything), signed with the local key. Set iss to whatever hostname the peer can serve from.
  3. Serve the matching public key as a JWKS endpoint on the controlled hostname.
  4. Send the forged token to a victim microservice.

A naive verifier reads iss=payments.core, fetches JWKS from payments.core, gets the attacker’s public key, and the signature checks out. The attacker has just minted a token with arbitrary claims, bypassing every requiredClaims gate in the application, without ever calling a real Mint endpoint.

What Pinning Means

The Microbus connector’s actor-JWT verifier does not read iss to decide where to fetch JWKS from. It has a hardcoded list of trusted issuers:

The verifier always fetches JWKS from these pinned hostnames. The iss claim of 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 is attempted. There is no path by which a peer-controlled JWKS endpoint can influence verification.

How Pinning Composes with the Trust-Root Tier

Pinning and the :666 port restriction close different attack vectors. Both are necessary, and neither is sufficient on its own.

  • Pinning prevents the “forge tokens client-side and serve fake JWKS” attack. The verifier never federates to attacker-chosen issuers.
  • :666 prevents the “ask the real issuer to mint a token for me” attack. The interservice ACL on the trust-root port allows only the small named set of callers whose code actually invokes Mint.

Removing pinning would let an attacker forge tokens without ever calling Mint. Removing the :666 restriction would let any bus peer call Mint on a legitimate issuer directly and obtain genuine tokens with whatever claims the policy allows.

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, so signature verification fails.

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. 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. New hostnames cannot become trusted issuers.

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 the TTL policy, the path is to replace the standard implementation. Build a microservice that satisfies the same interface as the access token service or bearer token service 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 code, 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: keep the standard issuer, and have a wrapper microservice translate external assertions into mint calls. Replacing the standard issuer is reserved for cases where the wrapper pattern is not enough.