Skip to content
Hexel StudioDocs

Workload identity

Register external identity providers for your workloads and trust them in role trust policies, ahead of token exchange.

On this page

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

FieldTypeDescription
idstringProvider ID, wip_.... Trust policies refer to it.
organization_idstringYour organization.
namestringUp to 128 characters. Unique in your organization.
descriptionstringOptional.
typestringoidc, aws or spiffe. Cannot be changed later.
configobjectSettings for the type, below. Returned as stored, with defaults filled in.
match_keystringSet by Hexel Studio: the OIDC issuer, the AWS account ID or the SPIFFE trust domain. Unique per type in your organization.
is_enabledbooleantrue on create. A disabled provider matches no identity.
created_by_user_idstringWho created the provider.
created_at, updated_atstringTimestamps.

oidc config

FieldTypeDescription
issuerstringRequired. An https URL without query, fragment or credentials. A trailing / is removed.
audiencesarray of stringsRequired. At least one non-empty value.
jwksobjectOptional JWK set with at least one key, up to 64 KB.
allowed_algsarray of stringsRS256 and/or ES256. Default: both.
max_token_age_secondsinteger60 to 86400. Default 3600.

aws config

FieldTypeDescription
account_idstringRequired. 12-digit AWS account ID.
allowed_regionsarray of stringsOptional AWS region names, such as us-east-1. Stored sorted.

spiffe config

FieldTypeDescription
trust_domainstringRequired. A valid trust domain. A spiffe:// prefix is removed and the value is lower-cased.
bundle_pemstringPEM certificates of the trust bundle.
bundle_endpointstringhttps 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"
}
StatusWhen
400The name, type or config is invalid. The error says which rule failed.
409Another provider in your organization has the same name or the same issuer, account ID or trust domain.

List, read, update and delete

CallPermissionResponse
GET /v1/workload-identity-providersiam:workload_identity_provider:readArray of your organization's providers
GET /v1/workload-identity-providers/{provider_id}iam:workload_identity_provider:readThe provider, or 404
PATCH /v1/workload-identity-providers/{provider_id}iam:workload_identity_provider:writeThe updated provider
DELETE /v1/workload-identity-providers/{provider_id}iam:workload_identity_provider:write204 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 for the rest of the trust policy language.

Provider typeActionKey that must be pinned
oidcsts:role:assume_with_web_identityoidc:sub
awssts:role:assume_with_aws_identityaws:arn
spiffests:role:assume_with_x509spiffe: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 typeKeys
oidcoidc:iss, oidc:sub, oidc:aud, and oidc:claim/<name> for any string or string-array claim
awsaws:account, aws:arn, aws:principal_type, aws:user_id
spiffespiffe:id, spiffe:trust_domain, spiffe:path
Anyciam: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.

FieldTypeDescription
identity.sourcestringoidc, aws or spiffe.
identity.issuerstringThe OIDC issuer, the AWS account ID or the SPIFFE trust domain. It selects your enabled provider with that match_key.
identity.subjectstringThe OIDC sub, the AWS role ARN or the SPIFFE ID.
identity.attributesobjectOptional. String arrays by name: OIDC claims (aud feeds oidc:aud); for AWS principal_type and user_id; for SPIFFE path.
policyobjectOptional draft trust policy. Without it, the role's saved trust policy is used.
session_namestringOptional. 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:

reasonMeaning
provider_not_trustedNo enabled provider of your organization has that type and issuer.
trust_no_matchNo Allow statement matched the provider, action and conditions.
trust_explicit_denyA Deny statement matched. matched_statement names it.
trust_condition_errorA Deny statement's condition could not be evaluated, so the request is refused.
role_disabledThe 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.