Skip to main content

Workload identities

Suggested reading

Read the Identity overview first for the IdentityProvider resource, userInfoPrefix, and claim mappings.

Not every caller of the Hub API is a human. A workload identity is a machine caller — a CI job, a pipeline, an agent, a ServiceAccount — that Hub authenticates the same way it authenticates a person, from claims in a JWT.

This page covers what a workload's Hub username looks like and how it trades its native credential for a Hub token. To grant it permissions once it has one, see Workload identities in access management.

Two kinds

Hub recognizes two kinds of workload identity. They differ only in where the identity comes from and what its username looks like.

Control plane ServiceAccounts

A Kubernetes ServiceAccount inside a managed control plane, authenticated through the control plane's own connection to Hub. Its Hub username has the shape:

upbound:hub:controlplane:<realm-name>:<control-plane-name>:system:serviceaccount:<namespace>:<sa-name>

The my-job ServiceAccount in the apps namespace of the prod control plane in the acme realm therefore reaches Hub as:

upbound:hub:controlplane:acme:prod:system:serviceaccount:apps:my-job

OIDC workload identities

Any workload holding a JWT from a registered IdentityProvider — a GitHub Actions job, a GitLab CI pipeline, a cloud workload-identity-federated service, or a ServiceAccount in a cluster Hub doesn't manage. These get a username from the provider's claim mappings exactly as a human does: the username comes from claimMappings.username.claim, groups from claimMappings.groups.claim, and both carry the provider's userInfoPrefix.

GitHub Actions is the common case. Register GitHub's OIDC issuer as an IdentityProvider and a workflow's token maps to whichever claim you point claimMappings.username.claim at. GitHub's sub claim identifies both the repository and the ref that triggered the run, so with userInfoPrefix: "github:" a job on the main branch of acme/infra reaches Hub as:

github:repo:acme/infra:ref:refs/heads/main
tip

Prefer the narrowest claim your provider offers. A sub that encodes both repository and ref lets you grant production access to a main-branch pipeline without granting it to every pull request build in the same repository.

Confirm the resolved username

Don't construct a workload's username by hand — read it back from Hub. Have the workload run:

kubectl auth whoami

See Verifying your identity for how to read the output. A role binding whose subject name is off by one character silently grants nothing, so this is worth doing before you write the binding.

Getting a Hub token

Neither kind of workload sends its native credential to Hub's data APIs. Both trade it for a Hub token first, through the token exchange endpoint.

Use hub-credential-helper rather than driving the exchange by hand. Point it at the file holding the workload's JWT — a projected ServiceAccount token, a GitHub Actions ID token, or any other IdP-issued JWT — and it exchanges, caches, and prints the Hub token:

hub-credential-helper get-token \
--hub-url=https://hub.example.com \
--token-file=/var/run/secrets/token

The helper also works as a kubectl credential plugin, so a job can run kubectl against Hub with no explicit token handling. See kubectl login for the kubeconfig wiring.

The raw exchange

If you can't run the helper, the endpoint accepts the standard RFC 8693 form directly:

POST /apis/tokenexchange.hub.upbound.io/v1alpha1/tokenexchangerequests
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<your IdP-issued JWT>
&subject_token_type=urn:ietf:params:oauth:token-type:jwt

The subject token must be issued by an IdentityProvider currently registered with Hub, and its aud must match one of that provider's configured issuer.audiences. The returned Hub token carries the same subject and group claims as the input, after userInfoPrefix is applied, but is signed by Hub's own key.