Skip to main content

Upbound Identity

Upbound Identity is how the Hub knows who is calling any of its APIs. Every request to Hub carries a bearer token; Identity turns the claims in that token into a Hub identity that authorization can then reason about.

This page is the concepts entry point. If you want to deploy your first identity provider end-to-end, see Installing Hub, which walks the provider registration and the Helm values it produces. If you want to authorize what each identity can do, see Access management — Identity is a prerequisite for that page.

What Hub identifies

Hub recognizes three kinds of callers:

  • Human users. Authenticated through an OIDC identity provider you register. After signing in with your IdP (Entra ID, Google Workspace, Keycloak, Okta, Amazon Cognito, or any OIDC-compliant issuer), humans carry a Hub-issued bearer token whose username and group claims come from that IdP.
  • OIDC workload identities. CI jobs, scripts, and agents that already hold an OIDC token from a trusted IdP — a GitHub Actions job, a GitLab CI pipeline, a cloud workload-identity-federated service. They trade that token for a Hub one through token exchange, and their Hub username comes from the same claim mappings and userInfoPrefix a human's would.
  • Control plane workloads. Every control plane connected to Hub has its own credentials and therefore its own Hub identity. When a Kubernetes ServiceAccount inside a managed control plane calls Hub, its Hub username has the shape upbound:hub:controlplane:<realm>:<controlplane>:<kubernetes-subject>.

Role bindings can target both kinds of workload identity directly. See Workload identities.

Once a request is authenticated, Hub knows three things about the caller: a username, a list of groups, and a set of extras (provider-supplied attributes like the verified email address). All three are visible through the SelfSubjectReview endpoint or kubectl auth whoami (see Verifying your identity).

How tokens reach Hub

There are three ways to obtain a Hub bearer token. All three end in the same place — a JWT signed by Hub — and every subsequent authorization decision is identical regardless of how the token was obtained.

FlowUsed byWhat happens
OIDC browser redirectHumans logging into the Hub UIClick Log in, Hub bounces you to your IdP, you authenticate, Hub sets a session cookie. Refresh is automatic while the IdP session is alive.
Device authorization grantCLIs and terminal-only clientsThe CLI asks Hub for a user code and prints a short code plus a URL. You approve in a browser, and the CLI receives a Hub access token plus a long-lived refresh token. See CLI and AI agent login.
Token exchangeMachines that already hold an IdP tokenThe client POSTs its IdP-issued JWT to Hub's token exchange endpoint (RFC 8693). Hub validates the token against the registered IdP and returns a Hub access token. Used by CI/CD systems and any service integrated with SSO.

You never send an IdP token directly to Hub's data APIs; you always trade it in for a Hub token first, either explicitly (token exchange) or implicitly (the browser redirect does it for you behind the cookie).

The IdentityProvider resource

Each OIDC issuer you trust is represented by an IdentityProvider resource in the authentication.hub.upbound.io API group. It carries both the OAuth2 redirect parameters for the login flow and the token-validation rules Hub applies to every bearer token.

A minimal IdentityProvider for a standards-compliant provider that is used for humans to log in through looks like this:

apiVersion: authentication.hub.upbound.io/v1beta1
kind: IdentityProvider
metadata:
name: entra
spec:
redirect:
browserLogin: true
clientSecret: "<client-secret>" # encrypted at rest; reads return "***"
scopes:
- openid
- email
- profile
validation:
# Prefix every username, group, and extra with this string. Prevents
# collisions between IdPs and stops "system:masters" impersonation.
# Immutable after creation — choose it carefully.
userInfoPrefix: "entra:"
issuer:
url: https://login.microsoftonline.com/<tenant-id>/v2.0
audiences:
- <client-id>
claimMappings:
username:
# A user with email alice@example.com becomes "entra:alice@example.com".
claim: email
groups:
# A group value "platform-eng" becomes the Hub group "entra:platform-eng".
claim: groups
extra:
- key: authentication.hub.upbound.io/email
valueExpression: claims.email
claimValidationRules:
- expression: "claims.email_verified == true"
message: "email must be verified"
userValidationRules:
- expression: "user.username.endsWith('@example.com')"
message: "email domain not permitted"

The four key knobs:

  • userInfoPrefix. Prepended to every value Hub reads from the username, group, and extras claims. Pick something short and provider-specific (entra:, okta:, keycloak:). It may contain only alphanumerics, hyphens, underscores, dots, forward slashes, and colons, and may not overlap the reserved system: or upbound: namespaces — which is what stops a provider from claiming system:masters.
  • claimMappings.username and claimMappings.groups. Select which claims in the token become the Hub username and group list. username defaults to the sub claim; set it explicitly when you want a readable username such as email. groups is optional but recommended — without it you can only bind roles to individual users.
  • claimValidationRules and userValidationRules. CEL expressions Hub evaluates on every token; failed rules reject the token. Common uses: require email_verified == true, restrict to a single email domain, or block reserved group prefixes.
  • redirect.browserLogin. At most one IdentityProvider may set this to true. That provider drives the Hub UI login redirect when multiple IdPs are registered.

Fields you can't change later

Two fields are immutable, so getting them wrong means deleting and recreating the provider:

  • validation.userInfoPrefix. Every role binding written against the old prefix stops matching, so a change is really a migration. See Multiple providers.
  • validation.issuer.url. Must equal the iss claim in issued tokens exactly — scheme, host, path, no trailing slash.

metadata.name is also worth getting right up front, because it's how the directory names groups. The names upbound, ctp, and space, and anything starting with upbound-, ctp-, or space-, are reserved for providers Hub creates itself. Unrelated names that merely begin with the same letters, such as spacex, are fine.

Defaults worth knowing

  • redirect.scopes defaults to ["openid", "email", "profile"] and must include openid; Hub rejects a provider without it.
  • A sub claim is always required. Hub prepends a claim-validation rule of its own demanding a non-empty string sub, regardless of what you map the username to. A token without one is rejected before your own rules run.

The IdentityProvider doesn't carry a redirect URI. Hub composes the callback from the externally reachable base URL of hub-core<base-url>/oidc/callback — so the URI you register with your provider has to match that host exactly. On a Helm install the base URL is hub-core.api.externalURL; see Installing Hub.

Multiple providers

You can register any number of IdentityProvider resources. Every provider's tokens are accepted at the token-exchange endpoint, so a second provider is the normal way to admit CI and workload identities alongside your human IdP.

Hub enforces uniqueness rules across providers — one browser-login holder, no shared issuer URLs, no overlapping userInfoPrefix values — and moving browser login between providers takes a specific sequence. See Multiple providers.

Per-provider examples

The fields above mean the same thing for every provider. What differs is how each one emits group claims and what its issuer URL looks like, so a complete worked IdentityProvider — app registration, issuer URL format, group-claim setup, and directory configuration where it applies — lives on its own page:

A provider that isn't listed still works as long as it's OIDC-compliant: a stable issuer URL publishing .well-known/openid-configuration, the authorization code flow, and a claim carrying group membership. The Keycloak and Okta pages are the closest templates to start from.

Logging in

Which login path you take depends on what's calling. They all end in the same place — a Hub-issued JWT — and every authorization decision downstream is identical regardless of how you got it.

What's callingHow it logs in
The Hub UI in a browserHub redirects you to whichever IdentityProvider sets redirect.browserLogin: true, then sets a session cookie when the provider sends you back.
kubectl against the Hub APIkubectl login
A human principal through CLI, curl, or an AI agentCLI and AI agent login
A CI job, a pipeline, or a ServiceAccountWorkload identities

Browser login needs exactly one provider holding browserLogin: true. If none does, the UI shows an error; if you're moving the flag between providers, see Multiple providers.

Verifying your identity

The settings above decide what Hub resolves a caller to. Read the result back rather than deriving it — role bindings match on exact strings, and a subject name that's off by one character grants nothing:

$ kubectl auth whoami
ATTRIBUTE VALUE
Username entra:alice@example.com
Groups [entra:platform-eng system:authenticated]

That posts a SelfSubjectReview, which also returns the provider, issuer, and other attributes Hub derived while minting the token. See Verifying your identity.

Directory sync

JWT claims tell Hub who a caller is. A directory lets Hub ask the IdP what groups and users exist, so you can search for a subject when writing a role binding instead of copying strings out of a decoded token — and it reads group membership that a token can't carry, such as an Entra ID user in more than 200 groups.

Add a directory block to an IdentityProvider to enable it. See Directory sync.

Token lifecycle

  • Hub refreshes your session against the IdP. At login Hub asks your provider for an OIDC refresh token (access_type=offline with prompt=consent, so providers that only issue one on first authorization still return it) and stores it server-side on the session row. It never reaches the browser, which holds only an opaque session cookie. Once the cached ID token expires, Hub exchanges that refresh token at the provider's token endpoint for a fresh ID token, re-derives your identity from it, and updates the session — rotating the stored refresh token whenever the provider returns a replacement. If the exchange fails, Hub deletes the session and you sign in again.
  • Role-binding changes apply immediately. Hub resolves role bindings from the database on every request, so granting or revoking an OrganizationRoleBinding or RealmRoleBinding takes effect on the caller's next request. They don't need a new token.
  • Identity changes need a new token. The username and groups Hub matches bindings against travel inside the token. Changing a user's group membership in your IdP therefore has no effect until they obtain a fresh Hub token.
  • CLI sessions are backed by a server-side record. A device-flow refresh token is only usable while Hub still holds the matching session record, so a refresh token copied off a client's disk is inert on its own.

How-to guides

Feature pages

Reference