Skip to main content
Version: next
Prime feature only
This feature is only available with a Prime subscription. See plans or contact sales.

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

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-config flag.

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:

  1. Open Backstage → Create → Get kubectl OIDC kubeconfig
  2. Select your cluster from the list
  3. 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 groupKubernetes bindingScope
adminscluster-adminCluster-wide
editorseditCluster-wide
viewersviewCluster-wide

Team members

Keycloak groupKubernetes bindingScope
<team>-adminsadminadn-<team> namespace (hub)
<team>-editorseditadn-<team> namespace (hub)
<team>-viewersviewadn-<team> namespace (hub)
<team>-admins/editors/viewersRoleBinding via KyvernoAll <team>-* namespaces (hub + spokes)

All group names are prefixed with kubrixoidc: in Kubernetes subjects (e.g. kubrixoidc:uniweb-admins).

Cluster-wide grants

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>
Best practice: why kubectl get pods -A does not work

Team 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:

CRDtype annotationBackstage entityUsed by
KubernetesClusterMarkerkubernetes-clusterGeneral catalog entryKubernetes plugin, cluster overview
KubernetesClusterAccessMarkerkubectl-oidc-cluster-accessPer-role access entryGet 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
Prerequisite: OpenBao must be populated first

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

SituationBehaviour
Token still validCached token returned silently - no browser
Token expired, refresh token validNew token fetched silently via refresh token - no browser
Refresh token expiredBrowser 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:adminscluster-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

GapSeverityMitigation
Control-plane AuthConfig not appliedCriticalCoordinate with whoever manages the cluster's control plane; add to runbook
Kyverno down on namespace createLowKyverno replays on restart; acceptable
Token revocation delay (1h)MediumLower accessTokenLifespan to 300 if needed
Platform editors cluster-wideLowConscious decision; document it
cluster-admin gate is Keycloak-onlyMediumEnforce MFA on Keycloak admins group
adn-<team> namespace orderingLowArgo CD sync waves handle this in normal flow
kubelogin not auto-distributedLowAdd to onboarding docs
AuthConfig apiVersion unverifiedMediumConfirm with control-plane owner before rollout