Skip to main content

Directory sync

Suggested reading

Read the Identity overview first for the IdentityProvider resource, userInfoPrefix, and claim mappings. A directory is an optional addition to a provider you already have working for login.

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.

A directory also rescues the cases where the token can't carry the answer:

  • Entra ID drops the groups claim entirely for users in more than 200 groups — its JWT overage limit — substituting pointers Hub doesn't follow.
  • Okta only emits the groups its claim filter matches.

In both cases the directory reads membership from the provider's API, which isn't subject to token size limits.

Enable a directory

Add a directory block to the IdentityProvider. Hub queries the provider's own API with credentials you supply:

kind: IdentityProvider
spec:
directory:
groupClaimType: DirectoryID # or DisplayName
userClaimType: DirectoryID # or Email
config:
apiVersion: directory.hub.upbound.io/v1alpha1
kind: EntraConfiguration
# ...provider-specific fields

Both claim types default to DirectoryID when omitted.

The kind selects the provider:

kindQueriesAuthenticates with
EntraConfigurationMicrosoft GraphClient secret (setup)
OktaConfigurationOkta Management APISigned JWT assertion (setup)
KeycloakConfigurationKeycloak Admin REST APIClient secret

Directory credentials are encrypted at rest and read back as ***. Send *** in a PUT to keep the stored value, or a real value to rotate it. Provider URLs must use HTTPS.

Matching directory subjects to role bindings

This is the part that most often goes wrong. Two names have to agree.

Everything the directory returns — groups and users alike — is named <identityprovider-name>:<claim value>. The claim-type fields pick which directory field becomes that value:

SubjectFieldDirectoryID (default)Alternative
GroupgroupClaimTypeThe provider's internal group IDDisplayName — the group's name
UseruserClaimTypeThe provider's internal user IDEmail — the user's email address

So a directory group is <identityprovider-name>:<group name or ID>, and a directory user is <identityprovider-name>:<email or ID> — depending on which side of that switch you're on.

A signed-in caller, meanwhile, is named <userInfoPrefix><claim value> — the username and groups Hub read out of their token.

Role bindings match on the string, so a binding written against a subject you found in the directory only applies to real callers when both halves line up. In practice that means:

  • Set userInfoPrefix to <identityprovider-name>: so the two prefixes agree.
  • Set each claim type to match what your tokens actually carry. Entra ID sends group object IDs, so it wants groupClaimType: DirectoryID; Okta sends group names, so it wants DisplayName. For users, match userClaimType to whatever claimMappings.username maps — Email if you mapped the email claim, DirectoryID if you left the username on sub.
warning

Get this wrong and the binding is silently inert — it names a subject nobody resolves to. Confirm a real caller's identity with kubectl auth whoami (see Verifying your identity) and check it matches the names the directory returns. This restriction is planned to be lifted in future versions of the Hub.

A name-carrying claim type (DisplayName, Email) makes bindings readable but depends on that field being populated: Hub rejects a group with no display name when groupClaimType: DisplayName, and a user with no email when userClaimType: Email. DirectoryID always resolves, at the cost of opaque bindings.

Query the directory

GET /apis/authentication.hub.upbound.io/v1beta1/groups?identityProvider=<name>
GET /apis/authentication.hub.upbound.io/v1beta1/groups?identityProvider=<name>&displayNameQuery=<prefix>
GET /apis/authentication.hub.upbound.io/v1beta1/users?identityProvider=<name>&group=<group-resource-name>

identityProvider is required on both endpoints. Listing users additionally requires group — there's no "every user in the directory" query, only "members of this group". Pass the group's full resource name, prefix included, as the groups endpoint returned it.

Behaviors to expect:

  • displayNameQuery is a prefix match, not a substring search. Query the start of a group's name, not a word in the middle.
  • Group search considers at most 200 matches per query — Hub's own cap, not the Entra overage limit above. A group missing from a broad search usually falls outside that set, so narrow the query rather than paging.
  • Group results are cached for five minutes per provider and query, so a group you just created in the IdP may take that long to appear. User lists share that cache when they resolve the group.
  • User lists page at 50 by default, up to 200 per page.