---
key: "workload-identity-models"
title: "Workload identity federation: short-lived, scoped identities for services and CI"
url: "https://softwaresecurity.com/domains/identity-crypto/identity/workload-identity-models/"
api: "https://softwaresecurity.com/api/v1/articles/workload-identity-models.json"
domain: "identity-crypto"
topic: "Identity \u0026 cryptography"
type: "Guide"
identifiers: ["CWE-863","CWE-290","A07:2025","A01:2025","API2:2023","V13.2.1","V13.2.2","V13.3.1"]
tags: ["Workload identity","CI/CD security"]
stages: ["design","build","release"]
roles: ["platform-sre"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Workload identity federation: short-lived, scoped identities for services and CI

Trace how a running workload obtains a short-lived identity, write cloud trust policies that match immutable claims instead of names or wildcards, and keep the resulting credential limited by audience and authorization.

Canonical URL: https://softwaresecurity.com/domains/identity-crypto/identity/workload-identity-models/

## In short

Let the platform issue a short-lived token to the workload, and trust it only through a policy that pins the issuer, an audience reserved for your resource, and exact, immutable claims: for GitHub Actions the `repository_owner_id`, `repository_id`, branch or environment, and workflow. Never use a wildcard subject, a repository name alone or the default audience. Then authorize what the resulting credential may do as narrowly as any other role.

- The trust policy, not the token’s short lifetime, decides which workloads can become your role.
- Match immutable IDs such as `repository_id` and `repository_owner_id`; names can be deleted and claimed again.
- A wildcard subject such as `repo:org/app:*` admits pull request runs and every branch.
- Bind protected deployments to a GitHub environment with protection rules, and allow only expected events.
- Give each consumer its own audience, so a token for one service cannot be replayed to another.
- Revoke by disabling the trust rule or role; waiting for tokens to expire is not incident response.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| Issuer (GitHub Actions) | `https://token.actions.githubusercontent.com` | Tokens are signed with `RS256`. | GitHub OIDC discovery document |
| Audience | AWS `sts.amazonaws.com`; Azure `api://AzureADTokenExchange`; your own service URL | GitHub’s default audience is the repository owner’s URL; set an explicit one. | AWS, Microsoft and GitHub documentation |
| Repository binding | `repository_owner_id` and `repository_id`, exact match | Google Cloud warns that name fields invite cybersquatting. | Google Cloud and AWS documentation |
| Subject | Exact environment or branch, such as `repo:org/app:environment:production` | Repositories created after 15 July 2026 use `repo:org@ID/app@ID:...`; copy the subject from a real token. | GitHub OIDC reference |
| Wildcards | None in the subject | AWS rejects a GitHub trust policy whose `sub` condition is missing or only a wildcard; Azure supports no wildcards. | AWS IAM and Microsoft Entra documentation |

## Verify it

1. Decode a real token from your workflow and confirm the policy pins `repository_owner_id`, `repository_id` and an exact subject or environment.
2. Open a pull request from a branch and confirm its run cannot assume the production role.
3. Request a token with another audience and confirm the cloud provider refuses the exchange.
4. Search every trust policy for `*` in a subject condition and for conditions that use names without IDs.
5. Disable the trust rule or role and confirm a fresh run fails within the documented propagation time.

## What you will learn

- Explain attestation, issuance, exchange, and resource authorization.
- Separate issuer-attested claims from workload-supplied labels.
- Test neighboring workloads and refresh failures.

Audience: Platform and DevOps engineers who connect CI pipelines and services to cloud accounts, and reviewers who check trust policies.

Before you start:

- Distinguish workload identity issuance from the permissions attached to the resulting credential.

Time: About 29 minutes: 6 to read and 23 for the exercise

## Short lifetimes help only after the issuer identifies the workload

A workload identity system issues credentials to a running service based on evidence about where and how it is running. Examples include a platform-issued token or a SPIFFE identity document. The application uses that credential directly or exchanges it for a resource-specific credential. The bootstrap question is who is allowed to claim the workload identity.

A typical sequence is platform attestation, identity issuance, credential retrieval through a controlled local interface, presentation or exchange, and resource authorization. Each step has an audience and trust boundary. A token proving a CI job’s identity is not automatically a database login or permission to administer every cloud resource.

Short-lived credentials reduce the period during which a copied token can be replayed. They do not prevent misuse while valid, nor do they stop a compromised workload from repeatedly obtaining fresh credentials while its attestation and permissions remain valid. Incident response must disable the issuance or authorization path as well as wait for tokens to expire.

## Bind issuer assertions to a narrowly defined runtime identity

For federation, configure the expected issuer, audience and subject constraints using authoritative platform claims. For GitHub Actions a deployment policy might require `repository_owner_id` 123456, `repository_id` 456789, `ref` `refs/heads/main`, the `deploy.yml` workflow in `job_workflow_ref`, and the protected `production` environment. A repository name alone can leave pull request runs, other branches or a recreated repository able to satisfy the policy; the GitHub Actions hardening guide at /domains/supply-chain/ci-cd/github-actions-hardening/ covers the workflow side.

Review which claims the platform signs and which values the job itself can choose. A branch name in an environment variable is not the same evidence as an issuer-signed claim. Prefer numeric IDs to names: GitHub repository, organization and user names can be renamed or deleted and then registered by someone else, while `repository_id`, `repository_owner_id` and `actor_id` never change. For repositories created after 15 July 2026, GitHub also puts those IDs in the subject, as `repo:octo-org@123456/billing@456789:environment:production`.

For SPIFFE, the SPIFFE ID identifies the workload within a trust domain, while an SVID carries a verifiable identity assertion. Trust bundles and federation configuration determine which issuers are accepted. A local identity socket is sensitive: a neighboring process that can retrieve another workload’s credentials can cross the intended identity boundary.

Mutual TLS can authenticate both peers and protect transport. Authorization must still map the peer identity to specific services, actions, and resource scope. A mesh that accepts every workload certificate in one trust domain has established membership, not an application-specific access policy.

## How do AWS, Google Cloud and Azure check a GitHub Actions token?

All three exchange the GitHub token for short-lived cloud credentials, but they match claims differently. Write the narrowest condition each one supports.

- Dangerous: a subject ending in `:*`, which admits `repo:org/app:pull_request` and every branch.
- Dangerous: a repository-name condition alone, which a recreated repository with the same name satisfies.
- Dangerous: no audience condition, which lets a token minted for another service assume the role.
- Dangerous: allowing every event, which includes Dependabot update jobs (`event_name` of `dynamic`).

| Cloud | Where the trust lives | What it matches | Pitfall to avoid |
| --- | --- | --- | --- |
| AWS | IAM role trust policy for the `token.actions.githubusercontent.com` provider | `aud` and `sub`, plus `repository_id`, `repository_owner_id`, `job_workflow_ref`, `ref` and `environment` condition keys | `StringLike` with `repo:org/*` or `repo:org/app:*`; IAM refuses only a missing or pure-wildcard `sub` |
| Google Cloud | Workload identity pool provider with an attribute condition | A CEL condition over mapped claims, such as `assertion.repository_owner_id` | An attribute condition is required, because every GitHub organization shares one issuer |
| Azure | Federated identity credential on an app registration or managed identity | Exact, case-sensitive issuer, subject and audience | One credential per subject; wildcards are not supported |

## Trust one environment of one repository in AWS

This role trust policy admits only jobs that run in the `production` environment of one repository, identified by its immutable owner and repository IDs. Protect the environment in GitHub with required reviewers and deployment branch rules. The JSON was checked for syntax and its conditions were evaluated against sample token claims with IAM’s documented exact-match semantics; AWS itself was not called.

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:octo-org/billing:environment:production",
          "token.actions.githubusercontent.com:repository_owner_id": "123456",
          "token.actions.githubusercontent.com:repository_id": "456789"
        }
      }
    }
  ]
}

```

## Accept a GitHub Actions token in your own service

A deployment service or internal broker can accept GitHub tokens directly. This Python function verifies the token against GitHub’s published keys, requires your own audience, and compares the immutable claims exactly. It was tested with PyJWT 2.15.0 on Python 3.14 against tokens signed by a local key: the deploy workflow is accepted, while a recreated repository, another owner, another workflow file or branch, a different-case environment, a missing environment, pull request and Dependabot events, GitHub’s default audience, another issuer, an expired token and an unknown key are all refused. The JWT validation lab at /labs/token-validation/ practices the same audience and purpose checks.

```python
import jwt  # PyJWT 2.x

ISSUER = "https://token.actions.githubusercontent.com"
jwks = jwt.PyJWKClient(ISSUER + "/.well-known/jwks", cache_keys=True, lifespan=600)

# Exact values for the one workflow allowed to deploy. Use the numeric IDs: a deleted or
# renamed repository's name can be registered again by someone else.
POLICY = {
    "repository_owner_id": "123456",
    "repository_id": "456789",
    "job_workflow_ref": "octo-org/billing/.github/workflows/deploy.yml@refs/heads/main",
    "ref": "refs/heads/main",
    "environment": "production",
}
ALLOWED_EVENTS = {"push", "workflow_dispatch"}


def verify_deploy_token(token: str) -> dict:
    key = jwks.get_signing_key_from_jwt(token)
    claims = jwt.decode(
        token,
        key.key,
        algorithms=["RS256"],
        issuer=ISSUER,
        audience="https://deploy.example",  # your service, never the default audience
        options={"require": ["exp", "iat", "sub", "aud"]},
    )
    for name, expected in POLICY.items():
        if claims.get(name) != expected:  # exact match: no prefixes, wildcards or case folding
            raise PermissionError(f"{name} does not match the deployment policy")
    if claims.get("event_name") not in ALLOWED_EVENTS:  # excludes pull requests and Dependabot runs
        raise PermissionError("event not allowed to deploy")
    return claims

```

## Choose scope, refresh, and outage behavior together

Give each workload a role appropriate to its job, and issue resource credentials with the intended audience and lifetime. If one worker processes multiple tenants, tenant authorization still belongs in each request or job context; the worker’s identity does not by itself prove which customer’s data a job may touch.

Refresh before expiry with jitter and bounded retries so thousands of instances do not stampede the issuer at once. Preserve a clear expiry cutoff and fail closed when no valid credential remains. Avoid a static emergency token embedded in the image, because it can become the permanent bypass around the identity system.

An issuer outage presents a real availability tradeoff. A valid cached credential can continue within its existing authority and lifetime if policy allows; an expired one cannot become valid because refresh failed. Longer lifetimes reduce dependence on refresh availability while increasing theft and revocation exposure.

Record identity, target audience, policy version, and decision outcome in logs. Do not log complete bearer tokens or private keys. For certificate identities, preserve a way to trace the issuing trust domain and key lifecycle without assuming certificate expiry is immediate revocation.

## Exercise the denied neighboring identities

A permitted workload should obtain a correctly scoped credential and complete its intended operation. The same credential at a different audience must fail. A neighboring namespace, forked repository, wrong workflow, or unrelated trust domain must not receive equivalent authority merely because it shares infrastructure.

Test expiry at the exact boundary, refresh failure with a still-valid cache, refresh failure after expiry, and disabling the workload’s issuance rule. Include an authorization test showing that identity success cannot read a different tenant’s resource. These outcomes distinguish a working credential refresh loop from a sound workload trust policy.

## Guided practice

Your production deploy role in AWS must trust only the release job of `octo-org/billing`, which runs in the protected production environment. Below are the decoded claims of four GitHub Actions OIDC tokens. Write the role's trust policy, then decide which tokens it admits. Produce the policy before reading the worked answers.

### Supplied material

```text
J1 {"iss": "https://token.actions.githubusercontent.com", "aud": "sts.amazonaws.com",
    "sub": "repo:octo-org/billing:environment:production", "repository": "octo-org/billing",
    "repository_id": "456789", "repository_owner_id": "123456", "ref": "refs/heads/main",
    "environment": "production", "event_name": "push",
    "job_workflow_ref": "octo-org/billing/.github/workflows/release.yml@refs/heads/main"}
J2 as J1 but "sub": "repo:octo-org/billing:pull_request", "ref": "refs/pull/12/merge",
    "event_name": "pull_request", and no environment claim.
J3 as J1 but "repository_id": "460001": the repository was deleted and a new one created under the same name.
J4 as J1 but "aud": "https://github.com/octo-org", the default audience when the workflow does not request one.
Provider ARN: arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com
```

### 1. Write the trust policy

Write the IAM role trust policy that admits only J1-like tokens.

Worked answer: Principal `{"Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"}`, action `sts:AssumeRoleWithWebIdentity`, and a `StringEquals` condition on `token.actions.githubusercontent.com:aud` = `sts.amazonaws.com`, `:sub` = `repo:octo-org/billing:environment:production`, `:repository_id` = `456789` and `:repository_owner_id` = `123456`. Use `StringEquals`, not `StringLike` with wildcards. The policy shown on this page has exactly this shape.

### 2. Decide the four tokens

For J1 to J4, say whether the policy admits it and which condition decides.

Worked answer: J1 is admitted. J2 fails on `sub`, because a pull request job's subject is `repo:octo-org/billing:pull_request`. J3 fails on `repository_id`: the name matches, but the recreated repository has a new immutable ID. J4 fails on `aud`, because the token was minted for a different audience.

### 3. Find the dangerous variant

A colleague proposes `StringLike` with `sub` = `repo:octo-org/billing:*` and no ID conditions "to make deployments easier". Which tokens would it admit?

Worked answer: J1, J2 and J3 (J4 still fails if `aud` is checked). J2 means any job triggered by a pull request in the repository could assume the production role without passing the environment's reviewers; J3 means whoever recreates the repository name inherits the role. This is the wildcard subject pitfall in the provider table above.

### 4. Add the GitHub-side control

Say what must be configured in GitHub for the environment condition to mean anything.

Worked answer: Protect the production environment with required reviewers and a deployment branch rule limited to `main`, so only an approved run from `main` can produce a token with `environment` `production`; and grant `id-token: write` only to the release job.

### 5. Write the tests

Describe how to test the policy without deploying.

Worked answer: Evaluate the trust policy against the four claim sets with IAM's exact-match semantics, for example with the IAM policy simulator or a unit test that applies the `StringEquals` conditions; then run a real pull request workflow and confirm `AssumeRoleWithWebIdentity` is denied. Alert on any successful assumption whose session tags or CloudTrail record show a subject other than the production environment.

Deliverable: The trust policy JSON, a decision table for J1 to J4, the effect of the wildcard proposal, and the GitHub environment settings.

Scope: The claim sets are abridged and the IDs invented. Check the claims your own workflows emit by printing the decoded token in a test run with the token itself redacted.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [SPIFFE concepts](https://spiffe.io/docs/latest/spiffe-about/spiffe-concepts/)
- [SPIFFE Workload API](https://spiffe.io/docs/latest/spiffe-specs/spiffe_workload_api/)
- [RFC 8693: OAuth token exchange](https://www.rfc-editor.org/rfc/rfc8693.html). January 2020.
- [GitHub: OpenID Connect reference for GitHub Actions](https://docs.github.com/en/actions/reference/security/oidc)
- [AWS IAM: create a role for OpenID Connect federation (GitHub)](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html)
- [Google Cloud: workload identity federation with deployment pipelines](https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines)
- [Microsoft Entra: workload identity federation](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation)