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
userInfoPrefixa 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.
| Flow | Used by | What happens |
|---|---|---|
| OIDC browser redirect | Humans logging into the Hub UI | Click 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 grant | CLIs and terminal-only clients | The 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 exchange | Machines that already hold an IdP token | The 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 reservedsystem:orupbound:namespaces — which is what stops a provider from claimingsystem:masters.claimMappings.usernameandclaimMappings.groups. Select which claims in the token become the Hub username and group list.usernamedefaults to thesubclaim; set it explicitly when you want a readable username such asemail.groupsis optional but recommended — without it you can only bind roles to individual users.claimValidationRulesanduserValidationRules. CEL expressions Hub evaluates on every token; failed rules reject the token. Common uses: requireemail_verified == true, restrict to a single email domain, or block reserved group prefixes.redirect.browserLogin. At most oneIdentityProvidermay set this totrue. 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 theissclaim 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.scopesdefaults to["openid", "email", "profile"]and must includeopenid; Hub rejects a provider without it.- A
subclaim is always required. Hub prepends a claim-validation rule of its own demanding a non-empty stringsub, 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 calling | How it logs in |
|---|---|
| The Hub UI in a browser | Hub redirects you to whichever IdentityProvider sets redirect.browserLogin: true, then sets a session cookie when the provider sends you back. |
kubectl against the Hub API | kubectl login |
A human principal through CLI, curl, or an AI agent | CLI and AI agent login |
| A CI job, a pipeline, or a ServiceAccount | Workload 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=offlinewithprompt=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
OrganizationRoleBindingorRealmRoleBindingtakes 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.
Related resources
How-to guides
- Installing Hub — register a provider and turn its
values into a running
IdentityProvider. - Multiple providers — uniqueness rules and moving browser login between providers.
- Workload identities — how CI jobs and ServiceAccounts get a Hub identity and a token.
- kubectl login and CLI and AI agent login — the two human-facing login paths.
Feature pages
- Access management — bind roles to the identities defined here.
Reference