0058. OIDC login and Kubernetes impersonation for ichor-ui

Status: Accepted · Date: 2026-10-26 · Design section: Trust; CLI and UI Decided: with ichorUI.oidc.enabled (default off) every ichor-ui RPC needs an OIDC ID token that github.com/coreos/go-oidc/v3 verifies (signature, issuer, audience = client ID, expiry), the backend calls the Kubernetes API as that user with Impersonate-User/Impersonate-Group so the API server alone decides what they may do, and AuditRecord.actor and the PR's Requested by: are that Kubernetes user name; the UI is a public client doing authorization code + PKCE in the browser (no client secret exists); the impersonate grant is on users and groups only, rendered only when OIDC is on and narrowable by name; with OIDC off nothing changes except a startup warning and a UI banner; the CLI does not forward credentials

Context

architecture.md "Trust" says the UI backend holds a read-only Talos identity and the browser holds nothing, and that "OIDC login with Kubernetes impersonation comes later so that RBAC and audit apply per user". The design decides nothing more: not the flow, the library, the impersonation scope, the off-state, or the CLI. Until now AuditRecord.actor was the fixed string ichor-ui/ichor-cli, and anyone who could reach ichor-ui wrote with its ServiceAccount's permissions (ADR 0030's port-forward is the boundary).

Decisions

1. Design gives: OIDC login, Kubernetes impersonation, RBAC and audit per user, browser holds no kube/Talos credential. Everything below is what the design left open.

2. Impersonation. uiapi.Server shadows Get/List/Create/Update/Delete so a call made under a context that carries an Identity goes through a client with Impersonate-User and one Impersonate-Group per group; no handler changed. Kubernetes then enforces RBAC, and the API server's own audit log names the user. Ichor writes no authorization logic. Reads that are the backend's own business and whose result is not returned to the user (secrets bundle, argocd-cm, the repository token, the Application that planWrite inspects) stay on the ServiceAccount via s.Client: users are not meant to read the secrets bundle. A read whose result reaches the user (the tracking Application's status, the Kubernetes Node's status, TalosStorage) is made as the user; ADR 0062's amendment lists every s.Client read and which side it is on. Without an identity (point 4) the ServiceAccount is used for all of them. The Talos read-only identity is unchanged; RPCs that reach a node first read the TalosMachine as the user, so a user who cannot get it cannot reach its node data.

Scope of impersonate. One rule: verb impersonate on users and groups in the core group, cluster scoped (impersonation is a cluster-scoped verb). Not serviceaccounts, not userextras, not uids. It is rendered only when ichorUI.oidc.enabled, so the default install gains nothing. Names cannot be enumerated for a real IdP, so by default it allows any name; ichorUI.oidc.impersonate.users/.groups turn into resourceNames for sites that can list them (a user or group outside the list gets a 403). Compensating controls in the backend: names get a configurable prefix (default oidc:) so an IdP value cannot be a built-in, and a name that still starts with system: is refused. The ServiceAccount token remains the thing to protect; this grant is the reason it matters more once OIDC is on.

3. Token verification. github.com/coreos/go-oidc/v3 v3.21.0 is added. go mod graph showed only coreos/go-oidc v2.5.0+incompatible (an old indirect dep of k8s.io/apiserver, unusable for a v3 import path) and no JWKS-verifying code we could reuse without pulling in the apiserver authenticator machinery. It brings go-jose/v4, already in the module graph. The verifier does discovery, JWKS rotation, and checks signature, issuer, audience and expiry; we add: username claim present (default email, which must be email_verified), groups claim optional. Errors say "invalid ID token" and never include the token or library detail. Discovery failing at startup is fatal, never a silent fall back to open.

4. OIDC not configured. Behaviour is exactly as before: writes use the ServiceAccount, the actor is ichor-ui. It is not made stricter, so nobody is locked out on upgrade. What changes is that it is no longer silent: ichor-ui logs a WARNING at start and GET /auth/config reports enabled:false, which the UI shows as a banner. /auth/config (issuer and client ID only), the SPA assets and /metrics are open; every Connect RPC needs a token when OIDC is on.

5. CLI. The CLI runs on the user's kubeconfig (ADR 0049) and does not forward its credentials to ichor-ui: sending a kubeconfig token to a server that then acts with it is a credential-forwarding path we do not want, it would need tokenreviews to verify, and a client-certificate kubeconfig has no token to send. With OIDC on the CLI therefore uses its in-process fallback (the same uiapi.Server on the user's own RBAC; actor stays ichor-cli for now). Known consequence, not done in this change: the CLI does not yet detect an OIDC-enabled backend, so its calls through the port-forward get Unauthenticated instead of falling back; ichor ... --local works, and commands that need the backend (node) will need an ID token flag later. A follow-up should make the CLI read /auth/config, fall back automatically, and take its actor name from a SelfSubjectReview.

5a. Amendment: the CLI detects OIDC (point 5's follow-up, done). Right after the port-forward is up and the probe answers, the CLI reads GET /auth/config. enabled:true makes it drop the tunnel and use the in-process uiapi.Server on the user's own kubeconfig and RBAC, printing one line to stderr saying so. Any other answer (non-200, unparseable body, an older backend) counts as not enabled, so the call goes to the backend, which rejects it itself if it does require a token: detection never opens anything up. node get / node logs need the backend's read-only Talos identity, so with OIDC on they fail with a reason (the backend requires OIDC, the CLI cannot present a token and never forwards kubeconfig credentials) instead of Unauthenticated; the flag that would carry an ID token is still later. The in-process actor (AuditRecord.actor, Requested by:, the approval decision) is the username from a SelfSubjectReview made with the user's kubeconfig; if that call fails the actor stays ichor-cli. Only the username is read; no credential is logged or stored.

6. Login in the UI. Authorization code + PKCE (S256), state and nonce, in the browser, as a public client: there is no client secret to store, mount or leak, and the backend stays stateless (no sessions or cookies; it verifies the bearer ID token on each call). The ID token lives in sessionStorage and is sent as Authorization: Bearer; a rejected token restarts login. It is a credential for Ichor only, not for Kubernetes or Talos. Cost: the IdP must allow CORS for browser discovery and token requests and have the UI's origin as a redirect URI, and there is no refresh token, so the user logs in again when the ID token expires. A backend-held session was rejected as more code (cookie signing, CSRF, secret handling) for the same result.

7. Audit. AuditRecord.actor is Identity.User, the exact string given to Impersonate-User, so it matches Kubernetes' audit log; prmode.SubmitPRRequest.Requester receives the same value for Requested by:.

8. Secrets. The ID token and (nonexistent) client secret are never logged, put in errors or written to audit; tests assert the verifier's errors do not echo the token. Nothing logs a request header.

Amendment: what an impersonated user needs for the effective config

GetEffectiveConfig lists TalosConfigPatch and TalosStorage as the user (ADR 0062), so a user needs get/list on talosclusters, talosmachines, talosconfigpatches and talosstorages in the cluster's namespace; the chart's per-CRD *-viewer-role ClusterRoles grant exactly that. Without talosstorages list the view fails with Forbidden for that user. The ServiceAccount's own list on talosstorages (ADR 0018 amendment) is separate: it serves ResolveSystemInputs and the OIDC-off case.

Amendment: Ichor's own users and issuer (ADR 0094, Proposed)

ADR 0094 adds IchorUser accounts and makes ichor-ui an OIDC issuer of its own that runs beside the external IdP described here. Nothing in this ADR changes while ichorUI.users.enabled stays false. When it is on, the authenticator accepts tokens from both sources, the kube-apiserver trusts both through Talos's KubeAuthenticationConfig, and the CLI can present an Ichor-issued token to the backend (point 5a's "later").

Search the docs

Search pages and headings