Keycloak setup for the operator
Two independent pieces have to exist in Keycloak before the operator is useful:
- An admin credential for the operator itself — so it can create groups and manage memberships via the Keycloak admin REST API.
- A Rancher OIDC client with a
groupsclaim — so that the groups the operator creates actually translate into Rancher access when a user logs in. Without this, the operator happily creates groups and bindings that grant nobody anything.
scripts/keycloak-bootstrap.sh performs both idempotently. The sections
below explain what it does and why, so you can also do it by hand in the
admin console.
1. Operator admin credential
The operator authenticates with the password grant against
admin-cli (keycloak_client.py). It does not support client
credentials / service accounts — the client ID is hardcoded to
admin-cli and there is no env var to override it. So you need a real
user with a password.
Create a dedicated user (do not reuse the realm super-admin):
| Field | Value |
|---|---|
| Realm | the realm holding the managed groups (→ KEYCLOAK_USER_REALM = KEYCLOAK_REALM), or master |
| Username | e.g. rancher-operator |
| Password | non-temporary — a temporary password makes the token request fail |
required — set one (e.g. rancher-operator@noreply.invalid) and mark it verified |
|
| Required actions | none |
The email is not cosmetic. Keycloak's declarative user profile marks email required, so a user without one trips the
VERIFY_PROFILErequired action at authentication time and the password grant fails with"error_description": "Account is not fully set up"— even though the user'srequiredActionslist reads as empty in the admin API. This is the single most likely reason a freshly created operator user can't log in.
Required roles
Assign these client roles of the realm-management client in the
target realm:
| Role | Needed for |
|---|---|
manage-users |
create/delete groups, add/remove users from groups (Keycloak files group writes under the users permission set) |
view-users |
read users, groups and group members (composite: includes query-users, query-groups) |
That is the minimal set for every call the operator makes:
GET /users/{id}, GET /users?username=&exact=true,
GET /users?q=<attr>:<value>&exact=true (attribute-mode members, see §4), GET|POST /groups,
POST /groups/{id}/children, DELETE /groups/{id},
GET /groups/{id}/members, PUT|DELETE /users/{id}/groups/{gid}.
No manage-realm, no admin role.
If the user lives in
masterinstead of the target realm, assign the same two roles from the<realm>-realmclient inmaster(e.g.riigipilv-realm) rather than fromrealm-management.
Wiring it into the operator
1 2 3 4 5 | |
KEYCLOAK_URL must be the base URL. The client builds
/admin/realms/... and /realms/{userRealm}/protocol/openid-connect/token
directly onto it — for Keycloak < 17 (legacy WildFly distribution) you
must include the /auth suffix in the URL.
Verify
1 2 3 4 5 6 7 8 | |
2. Rancher OIDC client (the part that makes groups mean something)
The operator writes bindings whose principal is:
1 | |
Both binding scopes use that same principal format, so nothing here is
scope-specific: project-scope spec.keycloak.roleBindings produce
ProjectRoleTemplateBindings, and (since 0.4.0) cluster-scope
spec.cluster.keycloak.roleBindings produce ClusterRoleTemplateBindings.
Cluster-scope groups are created under the same Keycloak parent group
as the project-scope ones.
Three things must line up for that principal to resolve at login time:
- Rancher must use the "Keycloak (OIDC)" auth provider — not
Keycloak SAML, not the generic OIDC provider. The provider name is
baked into the principal prefix (
keycloakoidc_); a generic OIDC config producesoidc_group://and the bindings will never match. - The token must carry a
groupsclaim. - The claim must contain the bare group name, not the full path.
The operator uses
groupNamealone, so a mapper with Full group path enabled emits/parent/childand nothing matches.
Client configuration
| Setting | Value |
|---|---|
| Client ID | e.g. rancher |
| Client authentication | On (confidential) |
| Standard flow | Enabled |
| Direct access grants | Optional |
| Valid redirect URIs | https://<rancher-host>/verify-auth |
| Web origins | https://<rancher-host> |
Group membership mapper
Add a Group Membership mapper on the client (Client → Client scopes →
rancher-dedicated → Add mapper → By configuration → Group Membership):
| Setting | Value |
|---|---|
| Name | groups |
| Token Claim Name | groups |
| Full group path | Off ← critical |
| Add to ID token | On |
| Add to access token | On |
| Add to userinfo | On |
Keycloak ships a built-in
groupsclient scope. It is not in the default scopes, and in several versions its mapper has Full group path set to true. Prefer a dedicated client mapper as above; if you use the built-in scope instead, open its mapper and turn full path off.
Rancher side
In Rancher: Users & Authentication → Auth Provider → Keycloak (OIDC)
| Field | Value |
|---|---|
| Client ID | rancher |
| Client Secret | from Keycloak → Client → Credentials |
| Issuer | https://keycloak.example.com/realms/myrealm |
| Rancher URL | https://<rancher-host> |
Enable it before the operator reconciles its first CR — Rancher can
reject a groupPrincipalId for an auth provider that is not configured.
Verify the claim end to end
1 2 3 4 5 | |
Expect bare names — ["p-abc-admin"], not ["/parent/p-abc-admin"].
3. Group naming constraints
- Names must be unique realm-wide.
get_group_by_namesearches top-level groups and one level of subgroups and returns the first exact name match (keycloak_client.py:107-116), and Rancher principals are name-only. Two same-named groups under different parents will collide. Namespace them, e.g.<project-slug>-admin. - Only one level of nesting is resolvable. The CR's
keycloak.parentGroupName→roleBindings[].groupNameshape (parent → child) is exactly what the lookup supports. Deeper hierarchies created by hand won't be found. - The operator tags what it creates with the group attribute
managed_by=rancher-keycloak-operator. Handy for auditing; it is not used for filtering.
4. Looking members up by a user attribute
Each CR member names a Keycloak user in one of three ways:
| Member fields | userIdentifier is |
|---|---|
lookupByID: true (default) |
the Keycloak user UUID |
lookupByID: false |
the Keycloak username |
lookupAttribute: <name> |
the value of user attribute <name>; lookupByID is ignored |
The attribute form exists for deployments where Waldur and Rancher authenticate against different Keycloaks, so neither the UUID nor necessarily the username is shared. The identifier both sides do share (typically a national civil code) is then sent by the site agent, and the Rancher-side Keycloak may hold it as a user attribute instead of the username:
1 2 3 | |
If the Rancher Keycloak stores the civil code as the username, use
lookupByID: false instead — no attribute setup is needed.
What has to hold for attribute lookup to work:
- Only administrators may write the attribute. Attribute lookup
gives the grant to whoever holds the value, so a user who could edit
the attribute in the Account Console, at registration or on the
first-broker-login profile review could copy someone else's civil
code into their own profile and receive that person's access. On
Keycloak 24+ (declarative user profile always on), declare the
attribute in Realm settings → User profile with edit permission
adminonly (viewadmin), or fill it from an identity-provider or LDAP mapper. Leaving it undeclared works only with Unmanaged attributes set to Only administrators can write (ADMIN_EDIT). Never use Enabled, which lets users edit it. The operator enforces this: before matching by an attribute it readsGET /users/profile. If users can edit the attribute, or the profile can't be read, it logs an error, adds nobody by that attribute, and stops keeping current members on the strength of it. The check is cached for five minutes, so fixing the realm needs no restart. - The attribute must be visible to the admin API. An attribute that
is neither declared nor allowed as unmanaged is neither searchable
with
q=nor returned in group member listings, so every lookup silently finds nobody and the user is never added. - The operator credential needs
view-users(already part of the minimal role set in §1); the search isGET /users?q=<name>:<value>. - Values must match exactly what the site agent sends: same attribute name, same value, no extra whitespace or prefix such as a country code. The operator re-checks every search hit against the returned attributes and keeps only exact matches, so a substring match in Keycloak's search can never resolve to the wrong user.
- Values must be unique. Keycloak does not enforce uniqueness of
attributes. If a value matches more than one user, the operator adds
nobody and logs an error with the attribute name and match count
(values are masked in logs, e.g.
3800…01).
Status syncedMembers echoes the attribute value for members matched
by attribute (as it echoes the username for username-mode members), so
the site agent can confirm each grant against what it submitted.
5. Cleanup semantics to be aware of
On CR deletion the operator removes members (by the UUIDs Keycloak lists for each group, whatever form the spec used) and deletes the CR's project groups. Cluster-scope groups and the per-cluster parent group are shared by every CR on the same cluster (one per ResourceProject of a Waldur Resource), so they stay while any other CR for that cluster exists. The last CR removes the cluster groups as well.
The parent group is deleted only if it has no child groups left.
Keycloak deletes a group's subgroups along with it, so this check is what
keeps one CR's deletion from taking its siblings' groups. It reads
subGroupCount from GET /groups/{id}: Keycloak 26 leaves subGroups
empty there and lists children only under /groups/{id}/children.
Pre-existing groups adopted by name are deleted the same way as ones it
created — don't point a CR at a group you also use for something else.