« Phase 08 · Warmup · Track Overview
Core Contributor Notes — How the Real Systems Work
Table of Contents
- 1. Entra ID: the three features that matter
- 2. Token exchange as products implement it
- 3. SPIRE, in practice
- 4. Service mesh identity
- 5. Verification libraries and their sharp edges
- 6. Vault and dynamic secrets
- 7. Sharp edges
- 8. What the miniature simplifies
- 9. References
1. Entra ID: the three features that matter
In a bank running Azure, Entra is the authorization server and three of its features map directly onto this phase.
On-behalf-of (OBO). A middle-tier API exchanges the token it received for one audienced to a
downstream API, preserving the user. This is RFC 8693's delegation case with Microsoft's parameter
names (requested_token_use=on_behalf_of, assertion=<the incoming token>).
What to know before designing around it:
- OBO requires the middle tier to be a confidential client with its own credential — which should be a certificate or a federated credential, not a secret.
- The chain Entra records is one hop:
xms_ccand related claims capture the immediate actor, not an arbitrary-depthactchain. For a three-hop agent flow you either chain OBO calls (each hop a separate app registration) or carry your own chain in an internal STS. - Token lifetimes are governed by Conditional Access and token-lifetime policies, and the minimums are measured in minutes, not seconds.
That third point is the practical reason banks end up with an internal STS: Entra is optimized for human sessions, and second-scale agent credentials are not what it is sized for.
Managed identity. An Azure resource (a container app, a VM, a function) is given an identity by the platform; code fetches a token from a local endpoint with no secret anywhere. This is Azure's answer to the workload-identity problem, and it is the single highest-value change for removing long-lived secrets from a platform.
Workload identity federation. An external workload — a Kubernetes service account, a GitHub Actions run — presents its own OIDC token, and Entra exchanges it for an Entra token. No stored secret. This is how you remove credentials from CI/CD, and it is the same trust inversion SPIFFE makes: the platform verifies attributes rather than checking a secret.
The federation is configured by trusting an issuer and matching a subject — and the matching is
exact. A common failure is a subject pattern that is broader than intended (repo:org/* rather
than a specific repo and ref), which lets any workflow in the organization obtain the identity.
2. Token exchange as products implement it
| Product | Shape |
|---|---|
| Entra | OBO flow (on_behalf_of), one-hop actor context |
| Keycloak | RFC 8693 token-exchange endpoint, both delegation and impersonation, with per-client permissions |
| Auth0 | Token Exchange with custom token exchange profiles and Actions for claim shaping |
| Okta | token exchange for specific flows |
| AWS STS | AssumeRole — the same idea in IAM's vocabulary, with role chaining |
| Google STS | token.googleapis.com exchange, used for workload identity federation |
Two things worth noticing across all of them.
Impersonation is usually easier to enable than delegation. Keycloak's impersonation is a
checkbox; a full delegation model with an act chain needs claim mapping. That asymmetry is a
trap: the easy path erases the user, and the phase's whole argument is that you want the hard one.
AWS role chaining has a hard limit and a lifetime cliff. Chained AssumeRole calls are capped
(one hour maximum session duration once chained, regardless of the role's setting), which is IAM's
version of "a derived credential cannot outlive its parent". Different mechanism, identical
principle — worth citing when someone argues the constraint is arbitrary.
The claim you will fight over is act. It is standard, and many products do not populate it by
default. Getting a chain of arbitrary depth usually means an internal STS, which is what the lab
builds. When you propose one, the framing that lands is: the enterprise IdP remains the authority
on who the human is; the platform STS is the authority on which agent is acting for them.
3. SPIRE, in practice
SPIRE has two components, and the split is the design.
The SPIRE server holds the registration entries, signs SVIDs, and is the CA for the trust domain. The SPIRE agent runs on each node, attests the node to the server, then attests individual workloads on that node and hands them SVIDs.
Node attestation proves the machine, using something the platform can verify independently: an AWS instance-identity document, a GCP instance token, an Azure MSI token, a Kubernetes projected service-account token, or a TPM. Workload attestation then proves the process: the k8s attestor reads the pod's namespace, service account, labels and image digest by asking the kubelet — the workload does not assert any of it.
Three operational realities:
The Workload API is a Unix domain socket. A workload calls it and receives its SVID plus the
trust bundle, and the socket's peer credentials (SO_PEERCRED) are how the agent knows which
process is asking. That is why the socket's mount and permissions are a security boundary, and why
a sidecar sharing the socket shares the identity.
SVID TTLs are short and rotation is automatic. The default is on the order of an hour with rotation at half-life; for agent workloads, minutes is defensible. Applications must re-read the SVID rather than caching it at startup — a long-running process holding its first SVID will simply stop working, and the symptom is a mysterious failure an hour after deploy.
Federation is a bundle exchange. Two trust domains exchange trust bundles (via the SPIRE federation API or a static bundle), and each then accepts the other's SVIDs for explicitly configured entries. Bundles rotate, and a stale bundle silently breaks cross-domain mTLS — so it needs the same monitoring as a certificate.
4. Service mesh identity
Istio and Linkerd both issue workload certificates and do mTLS transparently, which means a great deal of this phase can be infrastructure rather than application code.
Istio issues SPIFFE-format identities (spiffe://<trust-domain>/ns/<ns>/sa/<sa>) and can use
SPIRE as the CA. AuthorizationPolicy then expresses "which identity may call which service and
which path" declaratively — which is precisely mtls_authorize in the lab, as a CRD.
Linkerd does the same with a simpler surface and its own identity format.
Two things the mesh gives you nearly free once it is in place:
- Sender-constrained tokens. The client certificate is already there, so RFC 8705 binding costs a thumbprint claim and a check.
- Per-workload authorization independent of the application. A service that forgets to check who called it is still protected by the mesh's policy.
And the thing it does not give you: the mesh authenticates the workload, not the user. A mesh policy saying "the investigator agent may call core banking" says nothing about which user the investigator is acting for. Both layers are needed — mesh identity for workload-to-workload, token identity for the delegation chain — and conflating them is a common design error.
5. Verification libraries and their sharp edges
Use a library. PyJWT, python-jose, jose4j, nimbus-jose-jwt, jsonwebtoken. Then check
these, because several have shipped CVEs on exactly these points:
Algorithm must be passed explicitly. jwt.decode(token, key, algorithms=["RS256"]) — never
omit algorithms. Libraries that defaulted to trusting the header's alg are the source of the
algorithm-confusion CVE family.
Audience must be passed explicitly. Most libraries do not validate aud unless you supply
audience=. A verifier that omits it is the confused deputy waiting to happen, and it is the most
commonly missing parameter in real code.
verify=False exists in several libraries for debugging, and it appears in production more
often than anyone would like. Ban it in review.
JWKS fetching needs caching and a rollover story. Fetch on every request and the IdP becomes
a synchronous dependency of every API call. Cache forever and key rotation breaks you. The correct
shape is cache with a TTL, plus a bounded refetch on unknown kid — bounded because otherwise an
attacker with a random kid forces a fetch per request.
Clock skew is a parameter, and its default varies. Some libraries default to zero, which fails intermittently across a fleet.
6. Vault and dynamic secrets
HashiCorp Vault's dynamic secrets are the JIT-credential pattern for things that are not tokens: database credentials, cloud IAM credentials, SSH certificates.
The shape is identical to JitCredentialBroker: a workload authenticates (with a Kubernetes
service-account token, or an SVID via the JWT/cert auth methods), Vault issues a freshly created
credential with a lease, and revokes it when the lease expires.
The properties that matter, and they are the same four:
- the credential did not exist before the request, so it cannot have leaked from anywhere;
- it is scoped to a role;
- it has a lease — expiry, with optional renewal;
- revocation is real here, unlike with a signed token, because Vault can delete the database user it created.
That last point is the genuine difference from §6 of the PRINCIPAL-DEEP-DIVE, and it is worth knowing: stateful credentials can be revoked; stateless assertions can only expire. When someone insists on instant revocation, that distinction is the honest answer — and it is a reason to prefer dynamic secrets for the highest-impact paths.
7. Sharp edges
Symmetric signing lets the verifier mint. HS256 means every service that can verify can also forge. The lab uses it for zero dependencies; production must use RS256/ES256 so verifiers hold only public keys.
aud can be an array. The spec allows a string or a list, and a verifier that assumes a string
will crash or silently mismatch on a multi-audience token. Handle both.
Clock skew is not free. With 60-second tokens, 30 seconds of skew is a 50% extension of every credential's effective life. Shorten skew as you shorten lifetimes.
Refresh-token rotation needs a family. Detecting reuse means tracking which tokens descend from which — and the correct response to a reuse is revoking the whole family, because you cannot tell whether the legitimate client or the attacker holds the current one.
Token size grows with the chain. Each actor adds a nested object, and headers have limits (8 KB is a common proxy default). A deep chain with rich claims can exceed it, and the failure is a confusing 431 from an intermediary rather than an auth error.
Logging tokens. A JWT in a log is a credential in a log, valid for its remaining life. Redact at the logging boundary, not by convention.
The none algorithm is still in the JOSE registry. Some libraries still accept it if you let
them. Explicit algorithms=[...] everywhere.
Kubernetes service-account tokens are now audience-scoped and time-bound (projected volumes), which makes them usable as attestation material — but a legacy long-lived secret-based token is still a long-lived credential in etcd. Check which kind you have.
SPIRE agent socket permissions. Any process that can reach the socket can request an SVID and will be attested by its own properties — so a sidecar in the same pod gets the pod's identity. That is usually intended and occasionally not.
8. What the miniature simplifies
| Miniature | Reality |
|---|---|
| HS256 | RS256/ES256, JWKS endpoint, key rotation with overlap |
| One signer per issuer | key sets, kid selection, rollover windows |
| No refresh tokens | rotation, reuse detection, family revocation |
ReplayCache sweeping a dict | Redis with native TTL, or a bounded LRU |
AuthorizationServer | Entra/Okta/Keycloak with consent, MFA, Conditional Access, discovery, DCR |
TokenExchange | Entra OBO or an internal STS with claim mapping and per-client exchange permissions |
SpireServer | SPIRE with node/workload attestor plugins, X.509 SVIDs, the Workload API socket, trust-bundle rotation |
mtls_authorize | Istio AuthorizationPolicy or Envoy RBAC, with real TLS |
IdentityRegistry | an NHI governance platform, HR-feed-linked ownership, discovery of unmanaged identities |
JitCredentialBroker | Vault dynamic secrets, cloud STS, Entra managed identity |
| Deny-list absent | a pushed, cached, fail-static revocation list for high-impact identities |
Everything the real stack adds is either key management (which is most of the operational burden and none of the conceptual content) or integration (which is where the design decisions in PRINCIPAL-DEEP-DIVE §2 get made). The verification rules, the narrowing algebra and the chain semantics transfer unchanged.
9. References
Specifications
- RFC 8693 (Token Exchange) — §2 for the request, §4.1 for
act. Read both. - OAuth 2.1 draft, and RFC 9700 (Security BCP) for the reasoning behind its removals.
- RFC 7636 (PKCE), RFC 7519 (JWT), RFC 9068 (JWT access tokens), RFC 7515 (JWS),
RFC 7638 (JWK thumbprint), RFC 7800 (
cnf), RFC 8705 (mTLS-bound tokens), RFC 9449 (DPoP). - OpenID Connect Core 1.0 §3.1.3.7 — ID-token validation, step by step.
Implementations
- Microsoft Entra ID — on-behalf-of flow, managed identities, workload identity federation, token-lifetime policies. The three features in §1 are what you will configure.
- SPIFFE / SPIRE — the SPIFFE ID and SVID specs; SPIRE's node and workload attestor documentation; the Workload API.
- Istio security documentation —
PeerAuthentication,AuthorizationPolicy, SPIFFE identity format, and integrating SPIRE as the CA. - Keycloak token exchange, Auth0 Token Exchange, AWS STS
AssumeRoleand role chaining. - HashiCorp Vault — dynamic secrets, the Kubernetes and JWT auth methods, leases and revocation.
Background
- Hardt, The OAuth 2.0 Authorization Framework (RFC 6749) — for what OAuth 2.1 is consolidating.
- OWASP — Top 10 for LLM Applications, Excessive Agency; and the JWT security cheat sheet for the library-level sharp edges in §5.