Workload identity overview

A workload in axess is a non-human caller: a service in your service mesh, a Kubernetes pod, a CI/CD runner, a batch job, a serverless function. Workloads need to authenticate against your application the same way users do, but the credentials, the lifetimes, and the operational characteristics are different. This part of the book covers how axess models workload identity, how it resolves credentials into a typed Principal::Workload, and how the Cedar policy layer authorises workloads through the same rules it uses for human users.

The unifying claim is the one The principal model in Part II already made: humans and workloads are the same type. A Cedar policy that says resource.tenant_id == principal.tenant_id works for a logged-in user and for a SPIFFE-identified payment service without branching. The chapters in this part cover the specific resolvers that turn each credential kind into a Principal::Workload.

The cookbook chapters are siblings of this overview. Read them in the order that matches your deployment: SPIFFE-based deployments read Inbound: JWT-SVID and Inbound: mTLS-SVID; cloud-platform deployments read Inbound: federation and Cloud STS exchange; applications that call downstream services on a workload's behalf read Outbound: OAuth and Outbound: mTLS.

The resolver model

Every inbound request that carries a workload credential runs through a PrincipalResolver. The resolver inspects the credential (a bearer JWT in a header, a client certificate from the TLS handshake, a projected service-account token), validates it, and returns a Principal::Workload if the validation succeeds. The same trait is implemented for every credential kind axess supports, and applications wire only the resolvers their deployment needs.

        ┌──────────────────────┐
        │  Inbound request     │
        └──────────┬───────────┘
                   │
                   ▼
        ┌──────────────────────┐
        │ PrincipalResolver    │
        │ (per-feature impls)  │
        └──────────┬───────────┘
                   │
      ┌────────────┴───────────┐
      │                        │
      ▼                        ▼
Principal::Human         Principal::Workload
(session + factors)      (with Issuer + WorkloadId)
                                │
      ┌─────────────────────────┘
      │
      ▼
ToCedarEntity bridge
      │
      ▼
Cedar evaluation

Two resolvers ship today plus a generic third for everything else: JwtSvidResolver (SPIFFE JWT-SVID, spec-bound; mandatory spiffe:// URI in sub); MtlsResolver (SPIFFE X.509-SVID over mTLS); and WorkloadResolver, the generic JWT-bearer resolver that covers every non-SPIFFE workload-identity flow (Kubernetes projected service-account tokens, GitHub Actions OIDC, GitLab CI OIDC, Okta, Azure AD, Auth0, axess's own LocalIdP, custom internal JWT formats). The adopter supplies a small claim parser + mapping closure per issuer they care about; see examples/workload-identity/ for ready-made recipes (GitHub Actions, Kubernetes SA). The human side has its own SessionResolver covered in Part II. A MockResolver is available for DST tests. Each resolver implements the same trait and produces the same Principal shape.

Why one type covers both

A traditional auth library treats human and workload identity as two independent stacks. The session layer handles users; a separate JWT-validation middleware handles services. Neither composes with the other, and policies that need to apply to both ("only callers in the finance tenant may read this resource") end up duplicated: one rule for users in code that knows about sessions, another rule for workloads in code that knows about tokens, and the two drift apart over time as the application evolves.

Unifying on Principal removes the duplication. The Cedar policy quoted above works for a human and a workload because the policy matches on a tenant id, which both variants carry. If the policy later needs to discriminate between the two (a rule that demands human-completed MFA for an action, but admits any workload), the discrimination is expressed in one rule:

permit (
    principal,
    action == Action::"transfer-funds",
    resource
) when {
    resource.tenant_id == principal.tenant_id
    && (
        principal has Workload  // workloads bypass MFA requirement
        || (
            principal has Human
            && "Fido2" in principal.factors_completed
        )
    )
};

The discrimination is local, readable, and lives in the policy file rather than scattered across handlers.

SPIFFE and SVIDs

SPIFFE is the industry-standard model for workload identity, and the chapters that follow assume the vocabulary. The two terms worth knowing up front:

A SPIFFE ID is a URI of the form spiffe://<trust_domain>/<path>. The trust domain is the federation namespace (prod.example.com, say); the path identifies a specific workload within that domain (/svc/billing/tenant-acme). The combination uniquely names the workload across the federation.

An SVID (SPIFFE Verifiable Identity Document) is the credential that carries the SPIFFE ID. SVIDs come in two formats: JWT-SVID (a JWT signed by the trust domain's issuing authority) and X.509-SVID (a leaf certificate with the SPIFFE ID in a Subject Alternative Name URI). Both are covered in their own cookbook chapters.

SPIRE is the reference SPIFFE implementation. It handles workload attestation (verifying that a process running on a host is the workload it claims to be), SVID issuance, key rotation, and trust-domain federation. Axess does not replace SPIRE; SPIRE issues, axess validates. The two are designed to compose.

A future SpireWorkloadApiResolver (tracked in the ROADMAP as ) will talk to a local SPIRE agent socket directly, fetching fresh SVIDs on demand rather than relying on adopters to mount them into the filesystem. For now, adopters mount short-lived SVIDs into pod filesystems and configure axess against them.

Federation

A trust domain is a unit of issuance. A workload in trust domain A is identified by an A-issued SVID, validated against A's signing keys. When a workload in domain A needs to call a service in domain B, federation is the mechanism that lets B accept A's identity.

Three federation patterns appear in axess.

Same-domain is the simple case. The resolver validates the SVID against the local trust-domain bundle (the JWKS for JWT-SVIDs, the CA bundle for X.509-SVIDs). The SVID carries the local trust domain; the resolver knows where to fetch the keys.

Federated is the cross-domain case. The resolver validates the SVID against a remote trust-domain bundle, then runs the resulting identity through a TrustDomainFederation policy that maps the foreign identity (which trust domains are accepted, which path prefixes within each are admitted, how the identity is rewritten into the local namespace if at all). The federation policy is deployment configuration; axess validates, the deployment decides the rules.

External-issuer is the non-SPIFFE case. The credential is not a SVID at all (a Kubernetes service-account token, a GitHub Actions OIDC token, an Azure AD workload token). All of these go through the single generic WorkloadResolver: the adopter supplies a claim parser + mapping closure that synthesises a SPIFFE-shape WorkloadId from whichever claims the issuer's JWT carries. The synthesis is what lets the rest of the system (Cedar policies, audit events, the principal type) work uniformly: the external workload looks like any other workload by the time the policy evaluator sees it.

Cloud STS exchange

A workload that needs to call AWS, GCP, or Azure APIs can exchange its workload identity for short-lived cloud credentials. The mechanism is implemented by all three cloud providers under similar names (AWS STS AssumeRoleWithWebIdentity, GCP Workload Identity Federation, Azure Federated Identity Credentials), and axess provides adapters that bridge a validated workload identity to each of them.

The chapter Cloud STS exchange covers the configuration and the credential lifecycle. The benefit is that no long-lived cloud keys ever live on the workload's filesystem; the credentials are minted on demand from the workload identity, used briefly, and discarded.

Outbound

Axess is not only an inbound authenticator. When a service authenticates to a downstream service, it uses the same identity shape it would accept inbound. The chapters Outbound: OAuth and Outbound: mTLS cover the two ways this works: the workload presents an mTLS client certificate to the downstream's TLS server, or the workload exchanges its identity for a bearer token through an OAuth flow.

The pattern matters because it lets one identity (the workload's SVID, or its federated equivalent) carry through an entire chain of service calls. The audit trail records the same identity at every hop; revocation at the issuing authority propagates to every call that was about to use the identity.

Feature flags

The resolvers are individually feature-gated so a deployment only pays the compile cost for the credential kinds it actually uses.

FeatureResolverPurpose
jwt-svidJwtSvidResolverInbound SPIFFE JWT-SVID (spec-bound)
mtlsMtlsResolverInbound SPIFFE X.509-SVID via mTLS
jwt (auto-pulled by jwt-svid etc.)WorkloadResolverGeneric JWT-bearer workload identity for every non-SPIFFE issuer (GitHub Actions, k8s SA, GitLab CI, Okta, Azure AD, Auth0, LocalIdP, …) via adopter-supplied claim parser + mapping closure. No per-company features; see examples/workload-identity/
outbound-mtls(client side)Outbound mTLS with workload SVID
outbound-oauth(client side)Outbound OAuth client
aws-sts, gcp-wif, azure-fic(cloud STS)Exchange workload identity for cloud credentials
workload-idumbrellaSPIFFE adapters + outbound + mTLS bundle

What this part does not cover

Three concerns are intentionally outside scope.

The SPIRE Agent and Server implementations are not part of axess. Axess validates SVIDs; SPIRE issues them. The two are designed to be independent so deployments can use any SPIFFE-compliant issuer (SPIRE, an in-house implementation, a managed service like AWS IAM Roles Anywhere) without changing the axess side.

Trust domain bootstrap and root-of-trust ceremonies are out of scope. Operators manage the trust-domain bundle through SPIRE's federation API or an equivalent mechanism. Axess consumes the bundle; it does not establish it.

Service mesh integration is out of scope. Istio, Linkerd, and Consul handle mesh-level identity at the proxy layer. Axess works at the application layer above the mesh. When the mesh terminates mTLS and forwards a verified identity in a header, a custom PrincipalResolver can pick it up and produce a Principal::Workload the rest of the system understands.

Further reading

The cookbook chapters in this part each cover one resolver in detail. Start with the one matching the credential kind your deployment uses; the others are useful background for the federation and outbound scenarios. Cedar policy fundamentals covers how the policy engine handles workload principals. The principal model in Part II covers the unified Principal type that all of this resolves to.