rancher-keycloak-operator
A Kubernetes operator that manages Rancher projects and Keycloak OIDC group bindings via Custom Resource Definitions.
What it does
When you create a ManagedRancherProject CR, the operator:
- Creates a Rancher project (or adopts an existing one) in the cluster specified by
spec.clusterId - Sets the project-wide
spec.resourceQuota.limitcap fromspec.resourceQuotas(CPU / memory / storage) - Creates Keycloak groups (parent + child per project-scope role)
- Binds project-scope Keycloak groups to the Rancher project via
ProjectRoleTemplateBinding(PRTB) - Optionally (if
spec.cluster.keycloak.roleBindingsis set, since 0.4.0) creates additional cluster-scope Keycloak groups under the same parent and binds them cluster-wide viaClusterRoleTemplateBinding(CRTB) - Syncs user membership in all Keycloak groups (project + cluster scope)
The operator does NOT create or own namespaces — namespace lifecycle (and per-namespace ResourceQuotas under the project cap) is left to users via Rancher.
When you update the CR (add/remove users, change quotas), the operator reconciles the diff. When you delete the CR, the operator removes its Rancher project, project bindings and project groups. Cluster-scope bindings and groups, and the per-cluster parent group, are shared by every CR on the cluster, so they are removed only with the last one.
Multi-cluster architecture
One operator instance connects to a Rancher management server and can manage projects across multiple downstream clusters. Each CR specifies its target cluster via spec.clusterId.
graph LR
A[Site Agent / kubectl] -->|create/patch/delete| B[ManagedRancherProject CR]
B -->|spec.clusterId| C[Operator - Reconciler]
C --> D[RancherClient]
C --> E[KeycloakClient]
D -->|v3 API| F[Rancher Mgmt Server\nserves N clusters]
E -->|Admin API| G[Keycloak]
style B fill:#326ce5,color:#fff
style F fill:#0075a8,color:#fff
style G fill:#4d4d4d,color:#fff
The operator also periodically scans for unmanaged Rancher projects per cluster via the RancherProjectInventory CRD.
CRDs
ManagedRancherProject (waldur.io/v1alpha1)
Declares the desired state of a Rancher project with Keycloak OIDC access.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | |
The cluster block is independent of the project block. Cluster groups
nest under the same Keycloak parent (status.keycloakParentGroupId),
so a single c_<cluster> parent group contains both per-project
(..._<rp>_<role>) and cluster-scope (..._cluster_<role>) children.
Multiple CRs targeting the same cluster typically all carry the same
set of cluster bindings (Resource-scope role assignments are duplicated
across every CR born from that Resource); the operator de-duplicates
the underlying CRTBs by (clusterId, groupPrincipalId, roleTemplateId).
Status (set by operator):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
RancherProjectInventory (waldur.io/v1alpha1)
Read-only singleton per cluster. Updated by the operator's discovery scan.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Quick start
Prerequisites
- Python 3.10+
- Access to a Kubernetes cluster (for CRDs)
- Rancher API access (bearer token)
- Keycloak admin access
Deploy with Helm (recommended)
Add the Helm repository and install:
1 2 3 4 5 6 7 8 9 10 11 | |
Or install from a local checkout:
1 2 3 4 5 6 7 8 | |
The Helm chart installs:
- CRDs (ManagedRancherProject, RancherProjectInventory)
- ServiceAccount + ClusterRole + ClusterRoleBinding
- Secret with Rancher and Keycloak credentials
- Deployment running the kopf operator
Helm values
| Value | Default | Description |
|---|---|---|
image.repository |
opennode/rancher-keycloak-operator |
Docker image |
image.tag |
Chart appVersion | Image tag |
config.rancher.url |
(required) | Rancher management server URL |
config.rancher.bearerToken |
(required) | Rancher API bearer token |
config.rancher.verifySsl |
false |
Verify Rancher TLS |
config.keycloak.url |
(optional) | Keycloak URL (omit to disable) |
config.keycloak.realm |
Keycloak realm | |
config.keycloak.username |
Keycloak admin username | |
config.keycloak.password |
Keycloak admin password | |
config.keycloak.userRealm |
master |
Realm for admin auth |
config.keycloak.verifySsl |
false |
Verify Keycloak TLS |
config.crNamespace |
waldur-system |
Namespace for CRs |
existingSecret |
Use existing secret instead of creating one | |
installCRDs |
true |
Install CRDs with the chart |
resources.requests.cpu |
50m |
CPU request |
resources.requests.memory |
128Mi |
Memory request |
Upgrade
1 2 | |
Uninstall
1 2 3 | |
Deploy manually (without Helm)
1 2 3 4 5 | |
Run locally (development)
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Run tests
1 2 3 | |
Reconciliation sequence
Each reconciliation is idempotent ("ensure exists", not "create"):
flowchart TD
A[CR Created/Updated] --> B[Ensure Rancher Project]
B --> D[Set Project resourceQuota.limit\ncpu / memory / storage]
D --> E{Keycloak enabled?}
E -->|Yes| F[Ensure Parent Group]
E -->|No| K
F --> G[Ensure Project Child Groups\none per spec.keycloak.roleBindings entry]
G --> H[Ensure Rancher PRTBs\nper project group]
H --> I[Sync Project Group Members\ndiff desired vs actual]
I --> CG{spec.cluster.keycloak\nconfigured?}
CG -->|Yes| CH[GC orphan CRTBs\nfrom prior status]
CH --> CI[Ensure Cluster Child Groups\nunder same parent]
CI --> CJ[Ensure Rancher CRTBs\non spec.clusterId]
CJ --> CK[Sync Cluster Group Members]
CK --> J
CG -->|No| J
J{All conditions True?}
J -->|Yes| K[Phase: Ready]
J -->|No| L[Phase: Updating]
M[CR Deleted] --> N[Remove PRTBs]
N --> O[Empty + Delete Project Keycloak Groups]
O --> S{Other CRs on\nthe cluster?}
S -->|No| T[Remove CRTBs,\nEmpty + Delete Cluster Groups]
S -->|Yes| U[Keep shared cluster\nCRTBs + groups]
T --> V[Delete Parent Group\nif no child groups]
U --> V
V --> Q[Delete Rancher Project]
style K fill:#198754,color:#fff
style L fill:#ffc107,color:#000
style M fill:#dc3545,color:#fff
A periodic resync (every 5 minutes) re-runs the reconciliation for drift detection. Concurrent reconciles for the same CR (create + timer + retries) are serialised through a per-CR asyncio.Lock so they can't race each other into duplicate Rancher projects.
Cluster-scope orphan-GC: when a cluster roleBinding disappears from spec (e.g. the last user of a cluster role is revoked in Waldur), the operator deletes the previously-tracked CRTB so cluster access is actually withdrawn. The shared cluster Keycloak group is left in place — multiple CRs (one per Resource × ResourceProject) typically share it, so a per-CR delete must not yank the rug out from siblings. Tolerable cost: an empty group can linger until the last CR for that cluster is deleted, which removes the cluster CRTBs and groups. Whether other CRs remain is checked at deletion time; CRs that are themselves being deleted don't count, and if the CRs can't be listed the cluster is treated as shared.
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
RANCHER_URL |
Yes | — | Rancher management server URL |
RANCHER_BEARER_TOKEN |
Yes | — | Server-level API bearer token |
RANCHER_VERIFY_SSL |
No | true |
Verify Rancher TLS |
KEYCLOAK_URL |
No | — | Keycloak base URL (omit to disable) |
KEYCLOAK_REALM |
If KC | — | Keycloak realm for managed groups |
KEYCLOAK_USERNAME |
If KC | — | Keycloak admin username |
KEYCLOAK_PASSWORD |
If KC | — | Keycloak admin password |
KEYCLOAK_USER_REALM |
No | master |
Realm for admin authentication |
KEYCLOAK_VERIFY_SSL |
No | true |
Verify Keycloak TLS |
CR_NAMESPACE |
No | waldur-system |
Namespace for CRs |
Note: There is no RANCHER_CLUSTER_ID env var. Each CR specifies its target cluster via spec.clusterId. One operator instance serves all clusters managed by the Rancher server.
Keycloak prerequisites: the operator needs a dedicated admin user (manage-users + view-users on realm-management) and — so the groups it creates actually grant Rancher access — a Rancher OIDC client emitting a groups claim with full group path off. See docs/keycloak-setup.md; scripts/keycloak-bootstrap.sh creates both idempotently.
Attribute-mode members (lookupAttribute, 0.5.0+) need the attribute declared in the realm's user profile (Keycloak 24+) with edit permission admin only. The operator refuses attribute lookups that users could satisfy by editing their own profile. An undeclared attribute is never found, and a value matching more than one user adds nobody. See docs/keycloak-setup.md §4.
Duplicate handling
- Same user twice in one roleBinding: automatically deduplicated (set-based)
- Member identity is
(lookupAttribute, userIdentifier): a username and an attribute value that happen to be the same string are kept as two members - Same groupName across roleBindings: member lists are merged into a union before syncing, so no roleBinding accidentally removes another's members. A warning is logged.