kubectl OIDC Access
High-Level Overview
kubriX Prime provides OIDC-based kubectl access using Keycloak as the identity provider. Users authenticate via browser login and receive scoped Kubernetes RBAC permissions derived from their team membership.
User (kubectl)
→ kubelogin plugin
→ Keycloak (kubrix realm, kubernetes-cli client, PKCE)
→ JWT token with groups claim
→ kube-apiserver (Structured Authentication, K8s 1.32+)
→ Kubernetes RBAC (ClusterRoleBindings + RoleBindings)
Required Kubernetes version: 1.32+ (Structured Authentication GA)
Prerequisites
Enable the Keycloak Kubernetes CLI client
kubrix:
keycloak:
kubernetesCli:
enabled: true
kube-apiserver AuthenticationConfiguration
The target cluster's kube-apiserver must be configured for Structured Authentication (K8s 1.32+, GA) with an AuthenticationConfiguration pointing at the Keycloak issuer. How this gets applied depends on who manages the control plane:
- Managed clusters (Gardener Shoots, EKS, AKS, GKE, etc.): typically applied by whoever owns the control plane config - often a separate team or GitOps repo outside kubriX's control.
- Self-managed clusters: set directly via the kube-apiserver's
--authentication-configflag.
This is a hard dependency - without it, or with a wrong issuer.url or audiences, every kubectl call fails with a silent 401.
Install kubelogin on the client
# macOS
brew install int128/kubelogin/kubelogin
# Linux
kubectl krew install oidc-login
Detailed Description
Getting a kubeconfig
Use the Get kubectl OIDC kubeconfig Backstage template:
- Open Backstage → Create → Get kubectl OIDC kubeconfig
- Select your cluster from the list
- Follow the step-by-step output to retrieve the CA cert and save the kubeconfig
The OIDC configuration (issuer URL, client ID, scopes) is pre-filled automatically. The template uses OwnedEntityPicker - each user only sees clusters for which their group owns an access marker. A platform admin (group: admins) sees platformteam-admins-<cluster>-kubectl-access markers; a team member in <team>-editors sees <team>-editors-<cluster>-kubectl-access markers.
RBAC Permissions
Permissions are derived from Keycloak group membership rendered by the team-onboarding chart. Teams can be defined either through the legacy .Values.teams list in values files or through one file per team in platform-apps/charts/team-onboarding/teams/<team>.yaml.
Platform team
| Keycloak group | Kubernetes binding | Scope |
|---|---|---|
admins | cluster-admin | Cluster-wide |
editors | edit | Cluster-wide |
viewers | view | Cluster-wide |
Team members
| Keycloak group | Kubernetes binding | Scope |
|---|---|---|
<team>-admins | admin | adn-<team> namespace (hub) |
<team>-editors | edit | adn-<team> namespace (hub) |
<team>-viewers | view | adn-<team> namespace (hub) |
<team>-admins/editors/viewers | RoleBinding via Kyverno | All <team>-* namespaces (hub + spokes) |
All group names are prefixed with kubrixoidc: in Kubernetes subjects (e.g. kubrixoidc:uniweb-admins).
The kubrixoidc:editors ClusterRoleBinding grants edit across every namespace - including system namespaces like kube-system, argocd, keycloak. The built-in edit role cannot modify RBAC, but it can create/delete pods, secrets, and configmaps everywhere. This is intentional for platform team editors but should be a conscious decision.
Anyone added to the Keycloak admins realm group gets cluster-admin with no secondary check - Keycloak group membership is the only guard. Enforce MFA on the admins group in Keycloak.
If these default admin/edit/view bindings are not granular enough for your requirements, you can define additional custom ClusterRoles/Roles and bind them to the relevant kubrixoidc:<group> subjects instead of - or alongside - the defaults.
Check current permissions
kubectl auth whoami
kubectl auth can-i --list -n <namespace>
kubectl auth can-i get pods -n adn-<team>
kubectl get pods -A does not workTeam RBAC is scoped to admin/edit/view on the team's own adn-<team> and <team>-* namespaces - not cluster-wide. kubectl get pods -A (or --all-namespaces) requires a cluster-wide list permission on pods, so it fails with a Forbidden error (or silently returns only the namespaces with access) for any group outside the platform admins/editors/viewers group.
Use namespace-scoped commands instead:
kubectl get pods -n adn-<team>
kubectl get pods -n <team>-<namespace>
When it's unclear which namespaces are accessible, kubectl auth can-i --list -n <namespace> (above) or checking the RBAC tables in this document is faster than guessing with -A.
How clusters appear in Backstage
There are two separate marker types, each with a different purpose:
| CRD | type annotation | Backstage entity | Used by |
|---|---|---|---|
KubernetesClusterMarker | kubernetes-cluster | General catalog entry | Kubernetes plugin, cluster overview |
KubernetesClusterAccessMarker | kubectl-oidc-cluster-access | Per-role access entry | Get kubectl OIDC kubeconfig template |
KubernetesClusterMarker
One marker per cluster, Helm-templated:
- Hub:
platform-apps/charts/backstage/templates/prime/kubernetes-cluster-marker-hub.yaml - Spoke:
platform-apps/charts/spoke-applications/templates/kubernetes-cluster-marker-spoke.yaml
Both are gated only by kubriXPlan: prime - independent of kubernetesRbac.enabled below. Every prime cluster gets a general catalog entry regardless of whether kubectl OIDC access is enabled for it.
cluster-kubeconfig ExternalSecret
Reads url/caCert from OpenBao and syncs them into a cluster-kubeconfig-<cluster> Secret in the backstage namespace, so the Backstage template can render a kubeconfig without users needing direct cluster access:
- Hub:
platform-apps/charts/backstage/templates/prime/es-hub-kubeconfig.yaml - Spoke:
platform-apps/charts/spoke-applications/templates/es-spoke-kubeconfig.yaml
The url and caCert values must exist at the expected OpenBao path before this ExternalSecret can sync - otherwise the sync fails and no cluster-kubeconfig-<cluster> Secret is created, so the Backstage template has nothing to render. Whichever process registers a cluster (registration script, GitOps pipeline, manual step, ...) needs to write its live connection data (API server URL + CA cert) to OpenBao at the path these ExternalSecrets expect.
Both are gated by kubriXPlan: prime and kubernetesRbac.enabled: true (top-level, on the chart that owns the ExternalSecret - the backstage chart for the hub, the spoke-applications chart for spokes). The flag defaults to false, so there is no need to declare kubernetesRbac.enabled: false explicitly in values files - omit the key entirely when a spoke should not get kubectl OIDC access.
kubernetesRbac:
enabled: true
KubernetesClusterAccessMarker
One marker per cluster per role (admins / editors / viewers), created by the team-onboarding Helm chart's templates/prime/backstage-kubectl-access-markers.yaml, gated by that chart's own top-level kubernetesRbac.enabled: true. These markers are what appear in the cluster selection list of the Get kubectl OIDC kubeconfig template.
The hub cluster (kubrix) gets its access markers from the same team-onboarding chart - in a separate block from the spoke loop, so regular teams do not get hub access markers.
Nested kubernetesRbac.spoke.enabled
Distinct from the top-level flag above: kubernetesRbac.spoke.enabled is an internal switch inside the team-onboarding chart itself, used only to tell templates like kubectl-rbac-spoke.yaml and grafana-alerting-secrets.yaml whether the chart is currently rendering as the Application deployed onto a spoke (as opposed to its normal hub rendering). It is not meant to be set directly in customer values - spoke-applications/templates/kubectl-rbac-app.yaml injects it automatically (valuesObject.kubernetesRbac.spoke.enabled: true) when it deploys team-onboarding onto a spoke cluster.
Token Caching (kubelogin)
kubelogin caches tokens locally at:
~/.kube/cache/oidc-login/
The cache file is named after a hash of the issuer URL + client ID. It contains the ID token, the refresh token, and expiry timestamps.
What happens on each kubectl call
| Situation | Behaviour |
|---|---|
| Token still valid | Cached token returned silently - no browser |
| Token expired, refresh token valid | New token fetched silently via refresh token - no browser |
| Refresh token expired | Browser opens for full re-login |
Force a fresh login (clear cache)
rm -rf ~/.kube/cache/oidc-login/
Do this when:
- Switching to a different user
- Group membership in Keycloak was changed and the new groups should take effect immediately (otherwise kubelogin reuses the cached token until it expires)
- Debugging auth issues
Known Limitations and Gaps
Hard dependencies outside this repo
1. kube-apiserver AuthenticationConfiguration must be applied first
Everything in this setup is dead without it. On managed clusters (Gardener Shoots, EKS, AKS, GKE, etc.) it typically lives outside kubriX's control - in a separate GitOps repo or console owned by the team/provider managing the control plane. If it is not applied, or applied with a wrong issuer.url or audiences, every kubectl call returns 401 - silently, from the user's perspective.
2. Kyverno must be running when a namespace is created
The ClusterPolicy generate rules fire on namespace creation events. If Kyverno is down at that moment, the RoleBinding is never created. Kyverno does replay missed events on restart, but there is a window.
Security gaps
3. No token revocation
If you remove a user from a Keycloak group, their existing access token is still valid until it expires. With accessTokenLifespan: "3600" that is up to 1 hour of lingering access. If near-instant revocation is needed, lower it to 300 (5 min) - kubelogin will silently refresh via the refresh token, so users will not notice unless you also terminate their Keycloak session.
4. Platform editors get cluster-wide edit
See the RBAC warning above - intentional, but should be a conscious decision.
5. kubrixoidc:admins → cluster-admin is a very wide gate
Anyone added to the Keycloak admins realm group gets full cluster control. There is no secondary check - Keycloak group membership is the only guard. Enforce MFA on the admins group in Keycloak.
Operational gaps
6. adn-<team> namespace must exist before RoleBindings apply
The static RoleBindings in kubectl-rolebindings.yaml reference namespace: adn-<team>. If team-onboarding has not fully reconciled yet, kubectl apply will fail for those resources. In normal Argo CD sync-wave flow this is handled automatically.
7. kubelogin is not distributed automatically
Users need to install it themselves (brew install / kubectl krew install oidc-login). There is no enforcement of a minimum version. Document this prominently in your onboarding runbook.
8. AuthenticationConfiguration API version needs verification
The reference patch uses apiserver.config.k8s.io/v1beta1. Confirm with whoever manages the target cluster's control plane which exact apiVersion their kube-apiserver expects before rollout - this can vary by Kubernetes distribution and version.
Summary
| Gap | Severity | Mitigation |
|---|---|---|
| Control-plane AuthConfig not applied | Critical | Coordinate with whoever manages the cluster's control plane; add to runbook |
| Kyverno down on namespace create | Low | Kyverno replays on restart; acceptable |
| Token revocation delay (1h) | Medium | Lower accessTokenLifespan to 300 if needed |
| Platform editors cluster-wide | Low | Conscious decision; document it |
cluster-admin gate is Keycloak-only | Medium | Enforce MFA on Keycloak admins group |
adn-<team> namespace ordering | Low | Argo CD sync waves handle this in normal flow |
| kubelogin not auto-distributed | Low | Add to onboarding docs |
AuthConfig apiVersion unverified | Medium | Confirm with control-plane owner before rollout |