> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reducto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Workload identity federation

> Call the Reducto API from CI jobs, pods, VMs and functions with the identity your platform already issues, instead of a stored API key.

Workload identity federation lets a workload call the Reducto API without a long-lived API key. The workload sends a short-lived token from its platform (GitHub Actions, Kubernetes, AWS, Azure, Google Cloud and others) to Reducto. Reducto verifies the token and returns a short-lived Reducto access token. The workload uses that access token in place of an API key.

Use workload identity federation when:

* A CI workflow calls Reducto, and you do not want a Reducto API key in your repository secrets.
* A service runs on Kubernetes, a cloud VM or a serverless runtime that already has a platform identity.
* Your security team requires that no long-lived credentials sit in secret stores or environment variables.
* You want to limit Reducto access to one repository, branch, service account or cloud role.

## Concepts

| Term | Meaning |
| - | - |
| **Identity provider** | The issuer of the workload's token, for example GitHub Actions or an EKS cluster. In Reducto, an identity provider record (`wip_...`) says which issuer and audience to trust and where to get the issuer's public signing keys. |
| **Subject token** | The short-lived JWT that your platform issues to the workload. Reducto accepts OIDC JWTs and SPIFFE JWT-SVIDs. |
| **Mapping** | A record under an identity provider (`wim_...`) that says which tokens may get access. A mapping is a set of conditions on token claims, for example `repository` equals `acme/payments`. |
| **Access token** | The short-lived Reducto bearer token that the exchange returns. It starts with `rdat_` and works anywhere an API key works. |

## How it works

An Owner or Admin of your Reducto organization configures two things before the workload runs:

1. An **identity provider** with the issuer URL, the audience, and the key source that Reducto uses to verify signatures.
2. One or more **mappings** with the claim values that a token must have.

At runtime:

1. The workload gets a subject token from its platform, with the audience you configured.
2. The workload sends the subject token and the identity provider ID to `POST https://platform.reducto.ai/oauth/token`.
3. Reducto verifies the signature, issuer, audience and expiry, then evaluates the provider's mappings. Exactly one enabled mapping must match.
4. Reducto returns an access token for your organization. It expires in one hour or less.
5. The workload calls the Reducto API with `Authorization: Bearer <access token>`. Before the token expires, the workload exchanges a fresh subject token again.

The Reducto Python and Node.js SDKs do steps 2, 4 and 5 for you. You give the SDK the identity provider ID and a function that returns a subject token.

> [!NOTE]
>
> X.509 certificate federation (mutual TLS client certificates) is not supported yet. Use an OIDC JWT or a SPIFFE JWT-SVID.

## Choose a setup guide

Each guide shows how to get a token on that platform, what its claims look like, which provider and mapping values to set, and the SDK code.

> [card] [GitHub Actions](https://docs.reducto.ai/security/workload-identity-federation/github-actions.md)
>
> OIDC tokens for CI and deploy workflows.

> [card] [Kubernetes](https://docs.reducto.ai/security/workload-identity-federation/kubernetes.md)
>
> Projected service account tokens in self-managed clusters.

> [card] [AWS](https://docs.reducto.ai/security/workload-identity-federation/aws.md)
>
> Outbound identity federation and Amazon EKS.

> [card] [Microsoft Azure](https://docs.reducto.ai/security/workload-identity-federation/azure.md)
>
> Managed identity tokens and AKS.

> [card] [Google Cloud](https://docs.reducto.ai/security/workload-identity-federation/google-cloud.md)
>
> Metadata server identity tokens and GKE.

> [card] [Oracle Cloud Infrastructure](https://docs.reducto.ai/security/workload-identity-federation/oracle-cloud.md)
>
> Instance principal tokens from an identity domain.

> [card] [SPIFFE](https://docs.reducto.ai/security/workload-identity-federation/spiffe.md)
>
> JWT-SVIDs from SPIRE or another SPIFFE provider.

> [card] [Idira](https://docs.reducto.ai/security/workload-identity-federation/idira.md)
>
> JWT-SVIDs from Idira Secure Workload Access.

Any other issuer works if it signs standard JWTs with a JWKS that it publishes or that you upload. Use the fields below with the issuer's values.

## Set up workload identity federation in Studio

You must be an **Owner** or **Admin** of the organization. See [Roles & Permissions](https://docs.reducto.ai/studio-roles-permissions.md).

**1. Open the workload identity settings**

In [Studio](https://studio.reducto.ai), open **Settings > Workload identity**.

**2. Create an identity provider**

Click **Add provider**. Choose a preset for your platform to fill the issuer and audience, or enter them yourself. Save the provider and copy its ID (`wip_...`).

**3. Add a mapping**

Open the provider and add a mapping with the claim conditions that a trusted token must have. See [Configure mappings](#configure-mappings).

**4. Test a token**

Use **Test a token** to paste a sample subject token. Studio shows the verified claims, the derived attributes, and which mappings match. The test does not issue an access token.

**5. Connect the workload**

Give the workload the provider ID and update its code. See [Connect the workload](#connect-the-workload).

### Identity provider options

| Option | Description |
| - | - |
| **Name** | 1 to 80 characters. Lowercase letters, digits and hyphens, starting with a letter or digit, for example `github-actions-prod`. |
| **Description** | Optional. Which workloads use this provider. |
| **Issuer** | The exact `iss` claim of your tokens, for example `https://token.actions.githubusercontent.com`. Reducto ignores a trailing slash. You cannot change the issuer after you create the provider. |
| **Audience** | The audience your workload requests. The token's `aud` claim must contain this value. We recommend `https://platform.reducto.ai`. |
| **Key source** | Where Reducto gets the public keys to verify signatures. Defaults to OIDC discovery. See [Key sources](#key-sources). |
| **Attribute transformations** | Optional. Up to 20 transformations that copy a nested claim into a `reducto.*` attribute. Defaults to none. See [Copy nested claims](#copy-nested-claims). |
| **Enabled** | Defaults to on. A disabled provider rejects all exchanges, and access tokens it already issued stop working within 60 seconds. |

The audience is an exact-match string. It does not have to be a URL that receives traffic. Use a value that only Reducto accepts, so a token minted for Reducto cannot be replayed to another service, and the reverse.

### Key sources

Each provider uses exactly one key source.

| Key source | When to use it | Behavior |
| - | - | - |
| **OIDC discovery** (default) | The issuer publishes `/.well-known/openid-configuration` on the public internet. GitHub Actions, EKS, GKE, AKS, Azure, Google and AWS outbound federation do. | Reducto fetches `<issuer>/.well-known/openid-configuration`, then the `jwks_uri` it lists. |
| **Custom discovery URL** | The discovery document is on a different host than the issuer. Oracle identity domains need this. | Reducto fetches discovery from the URL you give. The token's `iss` must still equal the provider issuer. |
| **Uploaded JWKS** | The issuer is not reachable from the internet, for example a private Kubernetes cluster. | Reducto verifies with the public JWKS you paste. It makes no outbound request. |

Discovery and JWKS URLs must use `https` on port 443 with a public DNS name. Reducto rejects IP addresses, credentials, query strings and fragments in these URLs. It also rejects private host names, such as names without a dot or names that end in `.svc`, `.internal`, `.local` or `.lan`. An issuer such as `https://kubernetes.default.svc` needs an uploaded JWKS.

Reducto caches discovery documents and JWKS for 600 seconds. If a token has a `kid` that is not in the cached JWKS, Reducto fetches the JWKS once more before it rejects the token. It fetches the JWKS again at most once every 30 seconds per issuer. When your issuer rotates signing keys, publish the new key before you sign tokens with it.

An uploaded JWKS must contain a non-empty `keys` array of 1 to 20 public keys, each with a unique `kid` and a `kty` of `RSA`, `EC` or `OKP`. It must not contain private key material. Update the provider before your issuer signs with a new key. Reducto rejects tokens signed by a key that is not in the uploaded JWKS.

### Copy nested claims

An attribute transformation copies one claim into a `reducto.*` attribute. Use one when the claim you want to match is inside a nested object or a list, such as AWS principal tags or the Kubernetes namespace. Top-level claims need no transformation, because conditions match them directly.

Each transformation has two fields:

* `attribute`: the attribute name. It is `reducto.` followed by 1 to 64 lowercase letters, digits and underscores, starting with a letter or underscore. For example, `reducto.namespace`.
* `claim`: the path to the claim, as a list of 1 to 10 segments. The path starts at the top level of the token claims. A string segment is an object key of 1 to 256 characters and can contain dots and slashes. An integer segment of 0 or more is a list index.

```json theme={null}
[
  {
    "attribute": "reducto.namespace",
    "claim": ["kubernetes.io", "namespace"]
  },
  {
    "attribute": "reducto.aws_environment",
    "claim": ["https://sts.amazonaws.com/", "principal_tags", "environment"]
  },
  {
    "attribute": "reducto.first_group",
    "claim": ["groups", 0]
  }
]
```

The value at the path must be a string, an integer or a boolean. Reducto compares it to a mapping condition as a string: `true` becomes `"true"` and `7` becomes `"7"`. If the path does not exist in the token, or the value is a list, an object or `null`, the exchange fails at the transform stage.

Mapping keys that start with `reducto.` resolve only from transformations. A raw claim named `reducto.something` in the token has no effect.

## Configure mappings

A mapping decides which verified tokens get an access token for your organization.

| Option | Description |
| - | - |
| **Name** | A name for the mapping, for example `payments-main`. |
| **Description** | Optional. |
| **Conditions** | 1 to 32 key and value pairs. A key is a top-level claim (`sub`, `repository`, `ref`) or an attribute from a transformation (`reducto.namespace`). A value is a string, an integer or a boolean. |
| **Token lifetime** | Lifetime of access tokens issued through this mapping, from 60 to 3600 seconds. Defaults to 3600. |
| **Enabled** | Defaults to on. Disabling a mapping stops new exchanges, and access tokens it already issued stop working within 60 seconds. |

All conditions in a mapping must match. Values match exactly, with one exception: a string value can end with one `*` after a non-empty prefix. The value then matches any claim that starts with that prefix.

| Value | Valid | Matches |
| - | - | - |
| `repo:acme/payments:*` | Yes | `repo:acme/payments:ref:refs/heads/main`, `repo:acme/payments:environment:production` |
| `system:serviceaccount:payments:*` | Yes | Every service account in the `payments` namespace |
| `*` | No | A wildcard needs a prefix |
| `repo:*:main` | No | A wildcard must be the last character |

If a claim is a list, such as an `aud` with several values, the condition matches when any item matches. To match a value inside a nested object, copy it to an attribute with a transformation and match on that attribute.

### Mapping resolution

Reducto evaluates every enabled mapping under the requested provider. **Exactly one** mapping must match:

* If no mapping matches, the exchange fails.
* If two or more mappings match, the exchange fails. Reducto does not pick one.

Write mappings that do not overlap. For example, do not create both `sub: repo:acme/*` and `sub: repo:acme/payments:*` under one provider, because a token from `acme/payments` matches both. Use a separate provider, or tighter conditions, for each trust boundary.

### Example: GitHub Actions deploys from main

A GitHub Actions job in `acme/payments` gets a token with these claims:

```json theme={null}
{
  "iss": "https://token.actions.githubusercontent.com",
  "aud": "https://platform.reducto.ai",
  "sub": "repo:acme/payments:ref:refs/heads/main",
  "repository": "acme/payments",
  "repository_owner": "acme",
  "ref": "refs/heads/main",
  "workflow_ref": "acme/payments/.github/workflows/deploy.yml@refs/heads/main",
  "iat": 1793322245,
  "exp": 1793322545
}
```

The mapping `payments-main` has two conditions:

| Key | Value |
| - | - |
| `sub` | `repo:acme/payments:*` |
| `ref` | `refs/heads/main` |

Both conditions match, so Reducto issues an access token. A job on a feature branch of the same repository matches `sub` but not `ref`, so its exchange fails. A job in `acme/web` matches neither condition.

## Connect the workload

The Reducto SDKs exchange the subject token, cache the access token, and exchange again before it expires. Pass `workload_identity` (Python) or `workloadIdentity` (Node.js) with the provider ID and a token provider. The SDKs include token providers for common platforms in `reducto.auth` and `reductoai/auth`.

```python Python theme={null}
import os

from reducto import Reducto
from reducto.auth import k8s_service_account_token_provider

client = Reducto(
    workload_identity={
        "identity_provider_id": os.environ["REDUCTO_IDENTITY_PROVIDER_ID"],
        "provider": k8s_service_account_token_provider("/var/run/secrets/reducto/token"),
    }
)

result = client.parse.run(input="https://cdn.reducto.ai/samples/fidelity-example.pdf")
print(result.result.chunks[0].content[:500])
```

```javascript Node.js theme={null}
import Reducto from "reductoai";
import { k8sServiceAccountTokenProvider } from "reductoai/auth";

const client = new Reducto({
  workloadIdentity: {
    identityProviderId: process.env.REDUCTO_IDENTITY_PROVIDER_ID,
    provider: k8sServiceAccountTokenProvider("/var/run/secrets/reducto/token"),
  },
});

const result = await client.parse.run({
  input: "https://cdn.reducto.ai/samples/fidelity-example.pdf",
});
console.log(result.result.chunks[0].content.slice(0, 500));
```

```bash cURL theme={null}
SUBJECT_TOKEN=$(cat /var/run/secrets/reducto/token)

ACCESS_TOKEN=$(jq -n \
  --arg subject_token "$SUBJECT_TOKEN" \
  --arg provider_id "$REDUCTO_IDENTITY_PROVIDER_ID" \
  '{
    grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
    subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
    subject_token: $subject_token,
    identity_provider_id: $provider_id
  }' \
  | curl -sS --fail-with-body -X POST "https://platform.reducto.ai/oauth/token" \
      -H "Content-Type: application/json" -d @- \
  | jq -r .access_token)

curl -X POST "https://platform.reducto.ai/parse" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"input":"https://cdn.reducto.ai/samples/fidelity-example.pdf"}'
```

### Token providers

| Platform | Python (`reducto.auth`) | Node.js (`reductoai/auth`) | Token source |
| - | - | - | - |
| GitHub Actions | `github_actions_token_provider(audience)` | `githubActionsTokenProvider({ audience })` | `ACTIONS_ID_TOKEN_REQUEST_URL` |
| Kubernetes (EKS, GKE, AKS, self-managed) | `k8s_service_account_token_provider(path)` | `k8sServiceAccountTokenProvider(path)` | Projected service account token file |
| AWS outbound identity federation | `aws_web_identity_token_provider(audience, signing_algorithm=..., duration_seconds=...)` | `awsWebIdentityTokenProvider({ audience, signingAlgorithm, durationSeconds })` | `sts:GetWebIdentityToken` (needs `boto3`, or the optional peer dependency `@aws-sdk/client-sts`) |
| Azure managed identity | `azure_managed_identity_token_provider(resource, client_id=...)` | `azureManagedIdentityTokenProvider({ resource, clientId })` | Azure Instance Metadata Service |
| Google Cloud | `gcp_id_token_provider(audience)` | `gcpIdTokenProvider({ audience })` | Metadata server identity token |
| Any JWT file (SPIFFE, Idira, OCI) | `identity_token_file(path)` | `identityTokenFile(path)` | A file that holds a JWT, read again on every exchange |

The GitHub Actions, AWS and Google Cloud helpers take an `audience` option that defaults to `https://platform.reducto.ai`. The Kubernetes helper defaults to `/var/run/secrets/kubernetes.io/serviceaccount/token`. For Azure, `resource` defaults to `https://management.azure.com/`, so set it to your Reducto audience.

For any other source, pass your own token provider: an object with `token_type` (`"jwt"` or `"id"`) and a `get_token` function in Python, or `tokenType` and an async `getToken` function in Node.js. The SDK calls it each time it needs a fresh subject token. The [Oracle Cloud](https://docs.reducto.ai/security/workload-identity-federation/oracle-cloud.md) guide shows an example.

A failed exchange raises `OAuthError` with the `error` and `error_description` from Reducto. A token provider that cannot return a subject token raises `SubjectTokenProviderError`. In Python, import both from `reducto`. In Node.js, import both from `reductoai/auth`.

### Configure with environment variables

The SDKs also read workload identity settings from the environment. With these variables set, `Reducto()` and `new Reducto()` need no arguments.

| Variable | Description |
| - | - |
| `REDUCTO_IDENTITY_PROVIDER_ID` | The identity provider ID, `wip_...`. Not a secret. |
| `REDUCTO_IDENTITY_TOKEN_FILE` | Path to a file that holds the subject token. The SDK reads the file again on every exchange, so it picks up rotated tokens. |
| `REDUCTO_IDENTITY_TOKEN` | The subject token itself. Use it only for short-lived processes, because the SDK cannot refresh it. |

Set `REDUCTO_IDENTITY_PROVIDER_ID` with one of `REDUCTO_IDENTITY_TOKEN_FILE` or `REDUCTO_IDENTITY_TOKEN`.

```yaml theme={null}
env:
  - name: REDUCTO_IDENTITY_PROVIDER_ID
    value: wip_8f2k3m9q1r5t7v0x2z4b6d8f
  - name: REDUCTO_IDENTITY_TOKEN_FILE
    value: /var/run/secrets/reducto/token
```

### Credential precedence

The SDKs use the first credential they find, in this order:

1. The `api_key` / `apiKey` argument.
2. The `workload_identity` / `workloadIdentity` argument.
3. The `REDUCTO_API_KEY` environment variable.
4. The workload identity environment variables.

Passing both an API key argument and a workload identity argument raises an error.

> [!WARNING]
>
> `REDUCTO_API_KEY` wins over the workload identity environment variables. When you move a workload from an API key to federation, remove `REDUCTO_API_KEY` from its container environment, CI secrets and shell profiles. Then delete the old key in Studio.

## Token lifetime and renewal

An access token expires at the earliest of:

* The token lifetime of the mapping (default 3600 seconds).
* 3600 seconds after issue.
* The subject token's `exp`.

Reducto does not issue refresh tokens. To renew, exchange a fresh subject token.

The SDKs renew automatically. They exchange again before expiry, with a buffer of 20 minutes or half the token lifetime, whichever is shorter. Concurrent requests share one exchange. If the API returns HTTP 401, the SDK exchanges once more and retries the request.

If you call the exchange endpoint yourself, store `access_token` with `expires_at` (a Unix timestamp in seconds) and exchange again a few minutes before that time. `expires_in` counts from the moment of issue, so network time does not extend it. See the [token exchange reference](https://docs.reducto.ai/security/workload-identity-federation/reference.md).

Cache the access token and use it for every request until shortly before it expires. Do not exchange a token for each API request. The token endpoint has a rate limit per IP address, see [Rate limits](https://docs.reducto.ai/security/workload-identity-federation/reference.md#rate-limits).

Access tokens are revocable. If you disable or delete the provider or the mapping that issued a token, the token stops working within 60 seconds.

## Regions

Each Reducto region has its own identity providers, mappings and access tokens.

| Region | Exchange endpoint | API base URL |
| - | - | - |
| US | `https://platform.reducto.ai/oauth/token` | `https://platform.reducto.ai` |
| EU | `https://eu.platform.reducto.ai/oauth/token` | `https://eu.platform.reducto.ai` |
| AU | `https://au.platform.reducto.ai/oauth/token` | `https://au.platform.reducto.ai` |

Create the provider in the region your workload calls. A provider ID from one region does not exist in another, and an access token works only in the region that issued it. The SDKs send the exchange to the client's `base_url` (`baseURL` in Node.js), so a client configured for the EU region exchanges in the EU region.

The audience is independent of the region. You can use `https://platform.reducto.ai` as the audience in every region.

## Limits

| Limit | Value |
| - | - |
| Identity providers per organization | 50 |
| Mappings per identity provider | 50 |
| Attribute transformations per provider | 20 |
| Conditions per mapping | 32 |
| Access token lifetime | 60 to 3600 seconds |
| Subject token size | 16 KiB |
| Token exchange requests per IP address | 300 per 5 minutes (default) |
| Exchange history retention | 30 days |

## Troubleshooting

Studio lists recent exchanges under **Settings > Workload identity > Exchange history**, with the outcome and, for failures, the stage and the reason. The token endpoint's `error_description` names the failed stage but does not include your configuration values.

To inspect a token, decode its payload on the machine that has it. Do not paste production tokens into third-party JWT websites.

```bash theme={null}
cut -d. -f2 <<< "$SUBJECT_TOKEN" | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
```

### HTTP 429 from the token endpoint

**Problem**: `POST /oauth/token` returns HTTP 429.

**Solution**: Too many exchanges came from one IP address. Cache each access token until shortly before it expires, and share one client per process. Workloads behind one NAT gateway share the limit. Retry with exponential backoff.

### Signature or key errors

**Problem**: The exchange fails with a signature or key error.

**Solution**: Check that the token header has a `kid` and an asymmetric `alg` (RS, PS or ES family). For OIDC discovery, open `<issuer>/.well-known/openid-configuration` from a public network and confirm that its `jwks_uri` lists the `kid`. For an uploaded JWKS, upload the current key set, including any new key.

### Issuer or audience mismatch

**Problem**: The exchange fails at the claims stage.

**Solution**: Compare the token's `iss` with the provider issuer character by character (only a trailing slash is ignored). Confirm that the token's `aud` contains the provider audience exactly. Most platforms set `aud` from the audience you request, so request the same value you configured.

### Token expired or not yet valid

**Problem**: Reducto rejects a token that looks valid.

**Solution**: Reducto allows 30 seconds of clock skew. Check the workload's clock with NTP, and check that the token's `exp` is in the future. For tokens on disk, confirm that the file is refreshed.

### No mapping matches

**Problem**: The exchange fails at the mapping stage.

**Solution**: Run **Test a token** in Studio. It lists each mapping and the conditions that failed. Check for a disabled mapping, a typo in a claim value, or a wildcard in the wrong place.

### More than one mapping matches

**Problem**: The exchange fails although your mapping matches.

**Solution**: Another enabled mapping under the same provider also matches. **Test a token** lists every match. Tighten or disable one of them so that exactly one matches.

### The SDK uses an API key instead

**Problem**: Requests succeed, but the exchange history is empty.

**Solution**: `REDUCTO_API_KEY` is set in the environment and takes precedence. Remove it. See [Credential precedence](#credential-precedence).

### The API rejects the access token

**Problem**: The exchange succeeds, but API calls return HTTP 401.

**Solution**: Confirm that the workload calls the same region that issued the token, and that the provider and mapping are still enabled. The same billing and organization checks that apply to API keys also apply to access tokens.

## Security recommendations

* Use a dedicated audience for Reducto, such as `https://platform.reducto.ai`, and no other service.
* Create one mapping per workload. Match exact subjects (`sub`, role ARNs, SPIFFE IDs) instead of broad patterns.
* Keep production and non-production workloads on separate mappings, or separate providers.
* Use the shortest token lifetime that fits the workload.
* Never log subject tokens or access tokens.
* Review the exchange history for unexpected subjects and repeated failures.
* Disable and delete providers and mappings that you no longer use.
* After you move a workload to federation, delete its old API key.

## Related pages

* [Token exchange reference](https://docs.reducto.ai/security/workload-identity-federation/reference.md)
* [Roles & Permissions](https://docs.reducto.ai/studio-roles-permissions.md)
* [Studio account management](https://docs.reducto.ai/studio-account.md)
