---
title: Workload identity
description: Register external identity providers for your workloads and trust
  them in role trust policies, ahead of token exchange.
url: https://hexelstudio.com/docs/identity/workload-identity
---

<Warning>
A workload cannot use its own identity to call Hexel Studio yet. You can register workload identity providers and name them as `Federated` principals in role trust policies, but the token service does not accept OIDC, AWS or SPIFFE credentials: a token exchange with a JWT or ID token answers `unsupported_token_type` ("workload identity federation is not enabled yet"), and there is no exchange at all for AWS or SPIFFE identities. Provider settings such as `jwks`, `allowed_algs` and bundles are validated and stored but not used to verify anything. What works today is testing a trust policy against a sample workload identity. Until exchange is available, give workloads a [service user](https://hexelstudio.com/docs/identity/service-users) with an API key or OAuth client. This API may change.
</Warning>

## How it is designed to work

A workload identity provider is an external issuer that your organization trusts: an OIDC issuer (for example a Kubernetes cluster or a CI system), an AWS account, or a SPIFFE trust domain. A role's trust policy names the provider as a `Federated` principal and pins which workloads of that provider may assume the role. When exchange is available, a workload will present the credential it already has and receive a session of the role, with no Hexel Studio secret to store.

Today you can do every step except the exchange:

1. Register a provider.
2. Name it in a role's trust policy.
3. Test the trust policy with a simulated workload identity.

## Providers

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Provider ID, `wip_...`. Trust policies refer to it. |
| `organization_id` | string | Your organization. |
| `name` | string | Up to 128 characters. Unique in your organization. |
| `description` | string | Optional. |
| `type` | string | `oidc`, `aws` or `spiffe`. Cannot be changed later. |
| `config` | object | Settings for the type, below. Returned as stored, with defaults filled in. |
| `match_key` | string | Set by Hexel Studio: the OIDC issuer, the AWS account ID or the SPIFFE trust domain. Unique per type in your organization. |
| `is_enabled` | boolean | `true` on create. A disabled provider matches no identity. |
| `created_by_user_id` | string | Who created the provider. |
| `created_at`, `updated_at` | string | Timestamps. |

### `oidc` config

| Field | Type | Description |
| --- | --- | --- |
| `issuer` | string | **Required.** An `https` URL without query, fragment or credentials. A trailing `/` is removed. |
| `audiences` | array of strings | **Required.** At least one non-empty value. |
| `jwks` | object | Optional JWK set with at least one key, up to 64 KB. |
| `allowed_algs` | array of strings | `RS256` and/or `ES256`. Default: both. |
| `max_token_age_seconds` | integer | 60 to 86400. Default `3600`. |

### `aws` config

| Field | Type | Description |
| --- | --- | --- |
| `account_id` | string | **Required.** 12-digit AWS account ID. |
| `allowed_regions` | array of strings | Optional AWS region names, such as `us-east-1`. Stored sorted. |

### `spiffe` config

| Field | Type | Description |
| --- | --- | --- |
| `trust_domain` | string | **Required.** A valid trust domain. A `spiffe://` prefix is removed and the value is lower-cased. |
| `bundle_pem` | string | PEM certificates of the trust bundle. |
| `bundle_endpoint` | string | `https` URL of the trust bundle. |

Set exactly one of `bundle_pem` and `bundle_endpoint`.

### Register a provider

`POST /v1/workload-identity-providers` needs `iam:workload_identity_provider:write`.

```bash
curl -X POST https://api.hexelstudio.com/v1/workload-identity-providers \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "github-actions",
    "type": "oidc",
    "config": {
      "issuer": "https://token.actions.githubusercontent.com",
      "audiences": ["https://sts.hexelstudio.com"]
    }
  }'
```

The response is `201 Created` with the provider:

```json
{
  "id": "wip_...",
  "organization_id": "<org-id>",
  "name": "github-actions",
  "type": "oidc",
  "match_key": "https://token.actions.githubusercontent.com",
  "config": {
    "issuer": "https://token.actions.githubusercontent.com",
    "audiences": ["https://sts.hexelstudio.com"],
    "allowed_algs": ["RS256", "ES256"],
    "max_token_age_seconds": 3600
  },
  "is_enabled": true,
  "created_by_user_id": "<user-id>",
  "created_at": "2026-10-20T09:00:00Z",
  "updated_at": "2026-10-20T09:00:00Z"
}
```

| Status | When |
| --- | --- |
| `400` | The name, type or config is invalid. The `error` says which rule failed. |
| `409` | Another provider in your organization has the same name or the same issuer, account ID or trust domain. |

### List, read, update and delete

| Call | Permission | Response |
| --- | --- | --- |
| `GET /v1/workload-identity-providers` | `iam:workload_identity_provider:read` | Array of your organization's providers |
| `GET /v1/workload-identity-providers/{provider_id}` | `iam:workload_identity_provider:read` | The provider, or `404` |
| `PATCH /v1/workload-identity-providers/{provider_id}` | `iam:workload_identity_provider:write` | The updated provider |
| `DELETE /v1/workload-identity-providers/{provider_id}` | `iam:workload_identity_provider:write` | `204 No Content` |

`PATCH` takes `name`, `description`, `config` and `is_enabled`. A new `config` replaces the old one and is validated again. Setting `is_enabled` to `false` also revokes the existing sessions of every role whose trust policy names the provider.

```bash
curl -X PATCH https://api.hexelstudio.com/v1/workload-identity-providers/<provider-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_enabled": false}'
```

You cannot delete a provider while a role's trust policy names it. The call answers `409` with the roles to change first:

```json
{
  "error": "workload identity provider is referenced by role trust policies",
  "roles": ["<role-id>"]
}
```

## Trust a provider in a role's trust policy

A trust policy statement for workloads uses a federated action, a `Federated` principal with provider IDs, and a condition that pins the workload. Set the trust policy with `PUT /v1/roles/{role_id}/trust-policy`, which needs `iam:role:write_trust`. See [Roles](https://hexelstudio.com/docs/identity/roles) for the rest of the trust policy language.

| Provider type | Action | Key that must be pinned |
| --- | --- | --- |
| `oidc` | `sts:role:assume_with_web_identity` | `oidc:sub` |
| `aws` | `sts:role:assume_with_aws_identity` | `aws:arn` |
| `spiffe` | `sts:role:assume_with_x509` | `spiffe:id` |

```bash
curl -X PUT https://api.hexelstudio.com/v1/roles/<role-id>/trust-policy \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "Version": "2026-02-17",
    "Statement": [
      {
        "Sid": "DeployFromMain",
        "Effect": "Allow",
        "Action": "sts:role:assume_with_web_identity",
        "Principal": {"Federated": ["<provider-id>"]},
        "Condition": {
          "StringEquals": {
            "oidc:sub": "repo:acme/api:ref:refs/heads/main",
            "oidc:aud": "https://sts.hexelstudio.com"
          }
        }
      }
    ]
  }'
```

The trust policy is rejected with `400` (`"error": "invalid trust policy"` and a `problems` list) when:

- an Allow statement with a federated action does not pin the key in the table with `StringEquals`, or with `StringLike` and a pattern that does not start with `*` or `?`;
- a statement mixes `sts:role:assume` with a federated action, or a federated action with a `User`, `Group`, `ServiceUser`, `Role` or `Org` principal;
- `Federated` is used without a federated action;
- a provider is not in your organization, or its type does not match the action;
- a principal value contains `*`.

### Condition keys

| Provider type | Keys |
| --- | --- |
| `oidc` | `oidc:iss`, `oidc:sub`, `oidc:aud`, and `oidc:claim/<name>` for any string or string-array claim |
| `aws` | `aws:account`, `aws:arn`, `aws:principal_type`, `aws:user_id` |
| `spiffe` | `spiffe:id`, `spiffe:trust_domain`, `spiffe:path` |
| Any | `ciam:principal_type` (the provider type), `ciam:source_type`, `ciam:source_id`, `ciam:session_name` |

Operators are `StringEquals`, `StringNotEquals`, `StringLike`, `StringNotLike`, `Bool`, `ForAnyValue:StringEquals` and `ForAnyValue:StringLike`. In `StringLike`, `*` matches any run of characters and `?` exactly one. All conditions of a statement must hold; any of the listed values may match. A Deny statement that matches wins over every Allow.

## Test a trust policy

`POST /v1/roles/{role_id}/trust-policy/simulate` runs the same trust decision that role assumption will use, for a workload identity you describe. Nothing is issued or stored. It needs `iam:role:read`. Add `policy` to test a draft trust policy before you save it.

| Field | Type | Description |
| --- | --- | --- |
| `identity.source` | string | `oidc`, `aws` or `spiffe`. |
| `identity.issuer` | string | The OIDC issuer, the AWS account ID or the SPIFFE trust domain. It selects your enabled provider with that `match_key`. |
| `identity.subject` | string | The OIDC `sub`, the AWS role ARN or the SPIFFE ID. |
| `identity.attributes` | object | Optional. String arrays by name: OIDC claims (`aud` feeds `oidc:aud`); for AWS `principal_type` and `user_id`; for SPIFFE `path`. |
| `policy` | object | Optional draft trust policy. Without it, the role's saved trust policy is used. |
| `session_name` | string | Optional. Feeds `ciam:session_name`. |

```bash
curl -X POST https://api.hexelstudio.com/v1/roles/<role-id>/trust-policy/simulate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "identity": {
      "source": "oidc",
      "issuer": "https://token.actions.githubusercontent.com",
      "subject": "repo:acme/api:ref:refs/heads/main",
      "attributes": {"aud": ["https://sts.hexelstudio.com"]}
    }
  }'
```

```json
{
  "allowed": true,
  "reason": "trust_allow",
  "role_id": "<role-id>",
  "organization_id": "<org-id>",
  "scope": {"workspace_id": null, "environment_id": null},
  "duration": 3600,
  "matched_statement": "DeployFromMain",
  "provider_id": "<provider-id>",
  "source": {"type": "oidc", "id": "repo:acme/api:ref:refs/heads/main"}
}
```

When the answer is `"allowed": false`, `reason` says why:

| `reason` | Meaning |
| --- | --- |
| `provider_not_trusted` | No enabled provider of your organization has that type and issuer. |
| `trust_no_match` | No Allow statement matched the provider, action and conditions. |
| `trust_explicit_deny` | A Deny statement matched. `matched_statement` names it. |
| `trust_condition_error` | A Deny statement's condition could not be evaluated, so the request is refused. |
| `role_disabled` | The role is disabled. |

The simulation does not check the credential itself: it trusts the `identity` you send. When exchange is available, the token service will build that identity only after verifying the workload's credential.

## What happens if a workload tries to exchange today

The token service at `https://sts.hexelstudio.com/token` refuses workload credentials:

```bash
curl -X POST https://sts.hexelstudio.com/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
    "subject_token": "<workload-oidc-token>",
    "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
    "resource": "<role-id>"
  }'
```

```json
{
  "error": "unsupported_token_type",
  "error_description": "workload identity federation is not enabled yet"
}
```

The status is `400`. The same answer comes back for `urn:ietf:params:oauth:token-type:id_token`. Token exchange works only with a Hexel Studio access token as `subject_token`, to assume a role: see [Roles](https://hexelstudio.com/docs/identity/roles).
