Skip to content

OIDC Authentication

KSail provides native OIDC (OpenID Connect) authentication support. When configured, KSail:

  1. Injects OIDC flags into the Kubernetes API server at cluster creation time (per-distribution).
  2. Acts as an exec credential plugin (ksail cluster oidc get-token) — replacing the need for kubelogin.
  3. Auto-configures kubeconfig with an OIDC user and context after cluster creation.
  • KSail CLI installed
  • Docker running
  • An OIDC provider (e.g., Dex) — either already deployed or planned for deployment via GitOps
  1. Initialize a cluster with OIDC

    Terminal window
    ksail project init \
    --oidc-issuer-url https://dex.example.com \
    --oidc-client-id kubectl \
    --oidc-extra-scope email \
    --oidc-extra-scope groups

    Or add OIDC configuration to an existing ksail.yaml:

    spec:
    cluster:
    oidc:
    issuerURL: https://dex.example.com
    clientID: kubectl
    extraScopes:
    - email
    - profile
    - groups
  2. Create the cluster

    Terminal window
    ksail cluster create

    KSail automatically:

    • Configures the API server with --oidc-* flags
    • Adds an oidc-<cluster-name> user to your kubeconfig (exec-based, pointing to ksail cluster oidc get-token)
    • Adds an oidc@<cluster-name> context
  3. Deploy your OIDC provider

    Deploy your OIDC provider (e.g., Dex) as a workload. Once it’s running, the OIDC context becomes usable:

    Terminal window
    kubectl --context oidc@<cluster-name> get pods

    On first use, ksail cluster oidc get-token opens your browser for authentication.

All OIDC settings live under spec.cluster.oidc in ksail.yaml. See the Declarative Configuration reference for the full schema.

Field Type Default Description
issuerURL string OIDC provider issuer URL. Setting this enables OIDC.
clientID string OIDC client ID for kubectl authentication.
extraScopes []string Additional OIDC scopes beyond openid.
usernameClaim string email JWT claim for Kubernetes username.
usernamePrefix string oidc: Prefix prepended to OIDC usernames.
groupsClaim string groups JWT claim for Kubernetes group membership.
groupsPrefix string oidc: Prefix prepended to OIDC group names.
caFile string Path to CA certificate for self-signed OIDC providers.

OIDC can also be configured via CLI flags on ksail project init, ksail cluster create, and ksail cluster update:

--oidc-issuer-url OIDC provider issuer URL
--oidc-client-id OIDC client ID
--oidc-extra-scope Additional OIDC scopes (repeatable)
--oidc-username-claim JWT claim for Kubernetes username
--oidc-username-prefix Prefix for OIDC usernames
--oidc-groups-claim JWT claim for Kubernetes groups
--oidc-groups-prefix Prefix for OIDC groups
--oidc-ca-file Path to CA certificate

KSail injects the following flags into the Kubernetes API server based on spec.cluster.oidc:

--oidc-issuer-url=<issuerURL>
--oidc-client-id=<clientID>
--oidc-username-claim=<usernameClaim>
--oidc-username-prefix=<usernamePrefix>
--oidc-groups-claim=<groupsClaim>
--oidc-groups-prefix=<groupsPrefix>
--oidc-ca-file=<caFile> # only when caFile is set

Each distribution handles this differently:

Distribution Mechanism
Vanilla (Kind) kubeadm ClusterConfiguration patch with apiServer.extraArgs
K3s (K3d) --kube-apiserver-arg=--oidc-* flags
Talos cluster.apiServer.extraArgs machine config patch
VCluster controlPlane.distro.k8s.apiServer.extraArgs Helm values

ksail cluster oidc get-token implements the Kubernetes exec credential plugin protocol. The authentication flow:

  1. Check token cache (~/.ksail/oidc/cache/) — return if still valid
  2. If expired, attempt token refresh using the stored refresh token
  3. If no valid token, start authorization code flow with PKCE:
    • Start a local HTTP callback server
    • Open browser to the OIDC authorize endpoint
    • User authenticates (e.g., via GitHub, Google, LDAP — depends on provider configuration)
    • Receive callback with authorization code
    • Exchange code for tokens
  4. Cache tokens and output ExecCredential JSON to stdout

After ksail cluster create, your kubeconfig contains both admin and OIDC entries:

# Admin user (certificate-based, always works)
users:
- name: <cluster-name>
user:
client-certificate-data: ...
# OIDC user (exec-based, requires OIDC provider to be running)
- name: oidc-<cluster-name>
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: ksail
args:
- cluster
- oidc
- get-token
- --issuer-url=https://dex.example.com
- --client-id=kubectl
- --extra-scope=email
- --extra-scope=groups
interactiveMode: IfAvailable
contexts:
# Admin context (default)
- name: kind-<cluster-name>
context:
cluster: kind-<cluster-name>
user: <cluster-name>
# OIDC context
- name: oidc@<cluster-name>
context:
cluster: kind-<cluster-name>
user: oidc-<cluster-name>

The admin context remains the default. Switch to the OIDC context explicitly:

Terminal window
kubectl --context oidc@<cluster-name> get pods
# or
kubectl config use-context oidc@<cluster-name>

The API server OIDC flags reference an OIDC provider that is deployed after cluster creation. This is expected:

  • The admin context (certificate-based) always works, regardless of OIDC provider state.
  • The OIDC context becomes usable once the OIDC provider is running.
  • Deploy your OIDC provider via GitOps (ksail workload apply, Flux, or ArgoCD).

Self-Signed OIDC Providers (Local Development)

Section titled “Self-Signed OIDC Providers (Local Development)”

For local development with self-signed certificates (e.g., mkcert):

spec:
cluster:
oidc:
issuerURL: https://dex.platform.lan
clientID: kubectl
caFile: /path/to/rootCA.pem
extraScopes:
- email
- groups

The caFile field specifies a host path to the CA certificate. KSail automatically handles mounting it into the cluster:

  • Kind/Vanilla: Mounted as a read-only extraMount into all nodes at /etc/kubernetes/pki/oidc-ca.crt
  • K3d/K3s: Mounted as a volume into server nodes at /etc/kubernetes/pki/oidc-ca.crt
  • Talos: CA content is embedded via machine.files onto the node filesystem at /etc/kubernetes/pki/oidc-ca.crt
  • VCluster: Mounted via a hostPath volume from the host Kubernetes node into the VCluster control plane pod

The CA file path is also passed to ksail cluster oidc get-token via kubeconfig exec args for client-side token verification.

OIDC usernames and groups are available in Kubernetes RBAC. With the default prefixes:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
# Bind to a specific OIDC user
- kind: User
name: "oidc:user@example.com"
# Or bind to an OIDC group
- kind: Group
name: "oidc:platform-admins"

The OIDC story extends naturally to multiple clusters: authenticate once, then operate across every cluster that trusts the same identity provider — without managing per-cluster credentials.

KSail’s federation model is shared-issuer trust. Each cluster is configured independently, but when several clusters point at the same OIDC provider, they all accept tokens from a single identity:

  • Every cluster’s API server is configured with the same --oidc-issuer-url and --oidc-client-id, so each one independently verifies and trusts tokens minted by that provider.
  • Usernames and groups are derived from the same JWT claims, so a single OIDC identity (and its group memberships) maps to the same Kubernetes subject on every federated cluster — RBAC bindings are portable across the fleet.

There is no central control plane to manage: trust is established declaratively, per cluster, by pointing at a shared issuer.

Because tokens come from one provider, you authenticate once and the session is reused across all federated clusters. This works today with no extra configuration: ksail cluster oidc get-token caches tokens in ~/.ksail/oidc/cache/ keyed by the issuer URL, client ID, and scopesnot by cluster name. The first kubectl call against any federated cluster opens the browser; every subsequent call against any cluster sharing that OIDC configuration reuses the cached token (and silently refreshes it when it expires).

  1. Configure every cluster with the same OIDC provider

    Use identical issuerURL, clientID, and extraScopes in each cluster’s ksail.yaml:

    # cluster-a/ksail.yaml and cluster-b/ksail.yaml
    spec:
    cluster:
    oidc:
    issuerURL: https://dex.example.com
    clientID: kubectl
    extraScopes:
    - email
    - groups
  2. Apply portable RBAC

    Bind the same OIDC user or group on each cluster (see RBAC Integration). Because the username/group claims resolve identically everywhere, one set of bindings works fleet-wide.

  3. Authenticate once, use everywhere

    Terminal window
    kubectl --context oidc@cluster-a get pods # opens the browser on first use
    kubectl --context oidc@cluster-b get pods # reuses the cached token — no second login

The model so far covers identity federation — a single human identity, authenticated once and authorized independently on each cluster via portable RBAC. The complementary half is cross-cluster service-account projection: letting a workload in one cluster present a verifiable identity that another cluster (or an external service) trusts, without per-cluster static credentials.

Because KSail delegates here, no new ksail.yaml field is required — the existing OIDC configuration plus each distribution’s service-account issuer already supply the trust anchors. Pick the mechanism that matches your trust requirements:

Option A — Projected service-account tokens (lightweight, no mesh)

Section titled “Option A — Projected service-account tokens (lightweight, no mesh)”

This extends the shared-issuer model from user tokens to workload tokens. Every cluster’s API server is itself an OIDC issuer for the service-account tokens it mints (--service-account-issuer + a discoverable JWKS), so the same “verify against a trusted issuer” pattern applies:

  1. Project an audience-bound token in the source cluster

    Mount a projected service-account token whose audience names the receiving party. The token is a short-lived JWT signed by the source cluster’s service-account issuer, with claims identifying the namespace and service account:

    volumes:
    - name: federated-token
    projected:
    sources:
    - serviceAccountToken:
    audience: https://api.cluster-b.example.com # the receiver
    expirationSeconds: 3600
    path: token
  2. Trust the source cluster’s issuer at the receiver

    Configure the receiving cluster (or external service) to validate tokens against the source cluster’s service-account OIDC discovery document (<issuer>/.well-known/openid-configuration → JWKS). On the receiver this is the same API-server OIDC configuration KSail already understands, pointed at the source issuer; on a non-Kubernetes verifier it is standard OIDC discovery.

  3. Authorize the projected identity

    Bind RBAC (or the external service’s policy) to the token’s subject — system:serviceaccount:<namespace>:<name> — exactly as with portable RBAC.

The source cluster’s issuer URL must be resolvable by the verifier (a stable, externally reachable discovery endpoint), and audiences should be receiver-specific so a token cannot be replayed against an unintended cluster.

Option B — SPIFFE/SPIRE federation (mesh-grade, mutual)

Section titled “Option B — SPIFFE/SPIRE federation (mesh-grade, mutual)”

For mutual workload authentication across a fleet, use SPIFFE/SPIRE. Each cluster runs a SPIRE server owning a trust domain; clusters federate by exchanging trust bundles over the SPIFFE Federation endpoint API, after which a workload’s SVID — spiffe://<trust-domain>/ns/<namespace>/sa/<service-account> — is verifiable in every federated trust domain.

Option A — Projected tokens Option B — SPIFFE/SPIRE
Trust direction One-way (workload → receiver) Mutual (mTLS)
Extra infrastructure None (uses the cluster’s SA issuer) SPIRE servers + federation (may already exist via Cilium)
Best for A workload calling a specific cluster/service Service-to-service mesh across the fleet

Both keep the no-central-control-plane property: trust is declared per cluster against a shared/federated issuer. If a future need arises for KSail to manage this trust declaratively (e.g. a spec.cluster.federation surface), that can be added incrementally on top of these mechanisms — see the OIDC federation roadmap issue.