You are viewing documentation for Cozystack next, which is currently in beta. For the latest stable version, see the v1.5 documentation.
OIDC authentication for kubectl
Tenant Kubernetes clusters can authenticate kubectl users through OIDC instead of the shared static admin kubeconfig. Each user then has their own identity, per-user audit, and RBAC that can be revoked by disabling the account — not by rotating a shared certificate.
The identity model is deliberately per-cluster rather than per-tenant: each tenant Kubernetes cluster gets its own OIDC audience, and a token minted for cluster A is rejected by cluster B’s apiserver. That gives you cross-cluster isolation without provisioning a Keycloak realm per tenant. The full rationale (why per-cluster audience and not per-tenant realm; how it relates to Keycloak Organizations; what BYO-OIDC looks like) is in the design proposal.
kubernetes-<cluster>-admin-kubeconfig Secret in the tenant namespace stays available as a break-glass path regardless of whether OIDC is enabled.Modes
spec.oidc.mode picks the identity source:
None— the default. No OIDC; only the static admin kubeconfig works. Existing clusters render identically to before.System— trust the platformcozyKeycloak realm via a per-cluster public client and audience binding. Users are the ones a Cozystack platform admin already provisioned incozy; the tenant does not manage a directory of its own.CustomConfig— trust a tenant-supplied issuer directly (BYO IdP: Okta, Auth0, a customer’s own Keycloak).cozyis not in the path.
Enable OIDC — System mode
apiVersion: apps.cozystack.io/v1alpha1
kind: Kubernetes
metadata:
name: prod
namespace: tenant-acme
spec:
oidc:
mode: System
users:
- email: alice@acme.example
role: admin # binds to ClusterRole/cluster-admin
- email: bob@acme.example
role: view # binds to ClusterRole/view
# ...
Cozystack provisions:
- A per-cluster
KeycloakClientin thecozyrealm, withclientIdset to<namespace>-kubernetes-<cluster-name>(for the CR above:tenant-acme-kubernetes-prod).public: true, PKCE required, redirect URIs locked tolocalhost:8000andlocalhost:18000(thekubectl oidc-logindefaults). - A per-cluster
KeycloakClientScopewhose audience mapper pins the token’saudclaim to that sameclientId. - A structured
AuthenticationConfiguration(apiserver.config.k8s.io/v1beta1) on the tenant kube-apiserver, pointing at thecozyissuer and the per-cluster audience. - One
ClusterRoleBindinginside the tenant cluster for eachusers[]entry —admin→cluster-admin,view→view. The chart uses yourusers[].emailvalue as theUser:subject and matches it against the token’semailclaim.
Removing a user from users[] prunes their ClusterRoleBinding on the next reconcile.
Prerequisite
System mode requires the platform-level OIDC feature (authentication.oidc.enabled at the Cozystack platform values). If the flag is off, the chart hard-fails the render with a clear message. Ask a Cozystack platform admin to enable it, or use CustomConfig.
Enable OIDC — CustomConfig mode
Bring your own issuer. Two supply paths, mutually exclusive:
spec:
oidc:
mode: CustomConfig
customConfig:
config: |
apiVersion: apiserver.config.k8s.io/v1beta1
kind: AuthenticationConfiguration
jwt:
- issuer:
url: https://idp.acme.example
certificateAuthority: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
audiences:
- cozystack-prod
claimMappings:
username:
claim: email
prefix: ""
groups:
claim: groups
prefix: ""
users:
- email: alice@acme.example
role: admin
…or via a pre-existing Secret in the tenant namespace (you create it separately, e.g. under your own GitOps repository, so the AuthenticationConfiguration does not live inside the Kubernetes CR):
spec:
oidc:
mode: CustomConfig
customConfig:
secretRef:
name: acme-byo-authn-config # Secret with a `config.yaml` key holding the AuthenticationConfiguration
Setting both config and secretRef.name (or neither) fails the render. In CustomConfig mode no Keycloak objects are provisioned in cozy; the tenant apiserver trusts the operator-supplied issuer directly.
Ensure your BYO issuer emits the email claim in the JWT. Every conformant OIDC provider does when the client requests the email scope. If you distribute a hand-crafted kubeconfig instead of using the chart-generated one, remember to include --oidc-extra-scope=email in the kubectl oidc-login exec block.
Get the kubeconfig
In System mode, Cozystack writes a ready-to-use kubeconfig into a kubernetes-<cluster>-oidc-kubeconfig Secret in the tenant namespace (the same namespace where the Kubernetes resource lives). It’s exposed to the dashboard alongside the admin kubeconfig, and you can also fetch it directly:
kubectl --namespace tenant-acme get secret kubernetes-prod-oidc-kubeconfig \
--output=jsonpath='{.data.kubeconfig}' | base64 -d > prod.kubeconfig
The file contains the tenant CA (extracted from the Kamaji-issued admin kubeconfig at reconcile time), the external apiserver URL, and a kubectl oidc-login exec block wired to your per-cluster client.
In CustomConfig mode no kubeconfig Secret is generated — you distribute the OIDC kubeconfig out-of-band from your own IdP configuration.
Sign in
Install the oidc-login kubectl plugin once:
kubectl krew install oidc-login
Then use the OIDC kubeconfig — the first request triggers the browser flow:
kubectl --kubeconfig prod.kubeconfig get pods --all-namespaces
kubectl oidc-login opens Keycloak’s login page on localhost:8000 (falling back to localhost:18000), captures the token, and caches it locally. Subsequent calls are silent until the token expires.
Toggling OIDC off
Setting spec.oidc.mode back to None — or deleting the Kubernetes resource entirely — reconciles a cleanup pass that removes the tenant apiserver’s --authentication-config flag, deletes the chart-owned OIDC Secrets, and drops the per-cluster Keycloak client and audience scope. ClusterRoleBindings labelled by the release are also removed from the tenant cluster (best-effort during pre-delete, since the tenant apiserver may already be tearing down).
Prerequisites and gotchas
- Don’t mix with legacy
--oidc-*flags. The tenant kube-apiserver refuses to boot if both--authentication-config(injected byspec.oidc) and any legacy--oidc-*flag are set. If you previously wired OIDC by hand throughcontrolPlane.apiServer.extraArgs, remove those flags before enablingspec.oidc. The chart fails the render with a pointer to this migration. oidc-loginplugin required. Withoutkubectl krew install oidc-loginthe exec block errors out client-side. The plugin is a documented prerequisite.emailVerified: truewhen provisioning Keycloak users. Phase 1 does not add aclaimValidationRulesentry to the renderedAuthenticationConfiguration— soemail_verifiedis not chart-enforced. SetemailVerified: trueon theKeycloakRealmUser(or complete the email-verify flow through the Keycloak UI) so the identity holding a givenusers[].emailis guaranteed authentic. Thecozyrealm’s defaultduplicateEmails: falseprevents a second account from claiming an already-registered address. If the issuer explicitly emitsemail_verified: falseon a token the apiserver rejects it (k8s upstream behaviour); a missing claim is treated as verified. CELclaimValidationRulesto make this a hard gate is a follow-up hardening path.- Custom issuer with a self-signed CA. In
CustomConfigmode you can supply the CA inline underissuer.certificateAuthority. The legacy--oidc-*flag path could not.
What’s out of scope for this feature
- Per-tenant Keycloak realms. Managed multi-tenant identity (a hosted directory the tenant self-administers) is a separate proposal, evaluated against Keycloak Organizations. Track it in the community proposal.
- Federating an external IdP into the platform
cozyrealm. BYO-for-Cozystack-itself is a distinct problem — this feature is BYO-for-a-managed-service. - Cross-cluster SSO inside one tenant. By design: each cluster has its own audience, which is the per-cluster isolation primitive.
- RFC 8693 token exchange. Possible future optimisation; not required for the per-cluster client + audience model.