feat: pluggable authentication — static keys + OIDC + sidecar broker (closes SKA-275)#2
Open
feat: pluggable authentication — static keys + OIDC + sidecar broker (closes SKA-275)#2
Conversation
Closes the security gap that left the API server accepting unauthenticated requests after K8sLeaseStore made it deployable. The operator already plumbed --berth-api-key as a Bearer token; this wires the server side to actually validate it. internal/auth: - StaticAuthenticator now stores SHA-256 hashes of tokens internally. NewStaticAuthenticator(rawTokens) hashes on construction (test convenience). NewStaticAuthenticatorFromKeysFile(path) loads the on-disk format documented in the README — '<key-id>:<sha256-hex>', with comments and blank lines ignored. Authenticate hashes the presented token and looks up by hash. - Reload() re-reads the file atomically; on parse error the previous key set is preserved, so a malformed update can't lock the system out. - Error messages are deliberately generic and never echo the rejected token or known key ids, removing the obvious enumeration oracle. - NoOpAuthenticator added for tests / future composition (chained authenticators). Production no-auth mode uses nil authn instead. internal/api: - AuthMiddleware reads Authorization: Bearer, calls the Authenticator, attaches *auth.Identity to the request context (retrievable via IdentityFromContext), and returns a 401 JSON envelope on any failure. - NewMux signature is NewMux(mgr, authn). When authn is nil the lease routes are unauthenticated (dev mode); when non-nil they are wrapped by AuthMiddleware. /healthz is always unauthenticated. cmd/apiserver: - Adds --auth-mode (none|static-keys) and --api-keys-file flags. Default is 'static-keys' when --coordination-namespace is set (production), 'none' otherwise (dev). 'none' mode logs a loud warning at startup. - SIGHUP triggers Reload() on the configured authenticator so keys can be rotated without restarting the server. - Exit-code refactor: main() is now a thin wrapper around run() that returns int, so config-error exits are testable in principle. Tests cover the static authenticator hashing, file format, error messages, reload behavior (including preserve-on-error); the middleware (success / missing header / wrong scheme / authenticator error / oracle-resistant body); and end-to-end roundtrips against a real StaticAuthenticator both via raw HTTP and via the Go client's WithAPIKey. README documents the flags, the keys-file format, and the suggested key-generation + rotation workflow. Refs: SKA-275
…KA-279)
Adds the server-side OIDC Authenticator that validates JWTs from any
OIDC provider — Okta, PingFederate, Entra/Azure AD, Auth0, GitHub OIDC,
GitLab OIDC, AWS Cognito, Dex, Keycloak. Selected via --auth-mode=oidc.
internal/auth.OIDCAuthenticator wraps github.com/coreos/go-oidc/v3 and
validates signature against the issuer's JWKS (cached and refreshed
on kid-miss by the underlying lib), the standard JWT claims (iss, aud,
exp, nbf), and any caller-supplied required claims. Returns a
deliberately generic error on every failure so a 401 response can't be
used as an oracle to enumerate valid kids/audiences.
cmd/apiserver gains:
--oidc-issuer-url OIDC issuer (drives discovery)
--oidc-audience expected aud claim
--oidc-required-claim repeatable key=value (matches string-valued
and array-valued claims)
--oidc-username-claim JWT claim copied into Identity.Holder (default sub)
--oidc-tenant-claim JWT claim copied into Identity.Tenant (default sub)
--oidc-jwks-url optional override of discovered JWKS
The buildAuthenticator switch grows an oidc case alongside none /
static-keys; switching backends is config-only, no code change.
Tests cover happy path, bad signature (token signed by a key not in
the JWKS), expired/wrong-audience/wrong-issuer tokens, required-claim
enforcement (string and array forms), custom username/tenant claim
mapping, generic-error oracle resistance, and a smoke test that
discovery against an unreachable issuer fails fast.
The operator-side companion — short-lived token acquisition via a
sidecar broker — is the next commit (SKA-283).
Refs: SKA-275
… (SKA-283) The operator-side companion to SKA-279: enables short-lived, externally-refreshing JWTs in place of the static API key, closing the "long-lived secret" gap for the OIDC story. Three layers: 1. pkg/client refactor (back-compat): WithAPIKey is now a closure over a static string under the hood, and a new WithAPIKeyFunc accepts a getter the client invokes per request. Existing WithAPIKey callers see no behavior change. This is what lets the operator pick up rotated tokens without restarting. 2. Operator file token source: internal/operator.FileTokenSource reads a bearer token from a path with a small TTL cache (default 1s). Fails fast at startup if the file is missing/empty/unreadable; on later read errors returns the previously-cached value so a transient sidecar restart doesn't kill the reconcile loop. cmd/operator gains --berth-api-key-file, mutually exclusive with --berth-api-key. When set, the operator uses WithAPIKeyFunc(ts.Get). Tests cover happy path, missing file, empty file, whitespace trimming, rotation pickup after TTL, and read-error fallback. 3. Reference broker (cmd/berth-oidc-broker): a small (~250 LOC) token broker intended to run as a sidecar to the operator. Does OAuth2 client-credentials against the configured IdP (token endpoint discovered via OIDC), writes the access token atomically (temp file + rename, mode 0600) to the shared volume, and refreshes well before expiry. Supports --oidc-audience as a token-request parameter (Auth0, some Okta authorization servers) and --oidc-scopes. On failure, retries on a min-refresh cadence so a transient IdP outage doesn't tear down the broker. Tests cover arg validation, secret loading from literal vs file, token-URL discovery vs override, refresh-interval math, atomic writes (no leftover temp files), scope parsing, and an end-to-end run-loop test against a fake token endpoint. Plumbing: - Makefile builds the new berth-oidc-broker binary alongside the others. - README documents --berth-api-key-file on the operator and shows the full sidecar pod sketch (broker + operator + Memory-backed emptyDir). Includes guidance for Entra/Cognito/Auth0 audience handling and notes that users wanting exotic flows (token exchange, MTLS-bound tokens) can substitute their own broker — the operator only cares that the file at --berth-api-key-file contains a valid bearer token. With this commit and SKA-279, the operator can authenticate to the API server with no long-lived secret on its side beyond the IdP client- credentials Secret, satisfying SKA-275's pluggable-authentication goal. Refs: SKA-275
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the auth gap that left the API server accepting unauthenticated requests after
K8sLeaseStoremade it deployable. Lands all three pieces of the auth story together so the operational picture is reviewable end-to-end.Authenticatorinterface used by everything below.--berth-api-key-filewith reload +cmd/berth-oidc-brokerreference token broker for the sidecar pattern.Together these close SKA-275 (the parent — pluggable authentication for the Berth API server). SKA-280 (SPIFFE/SPIRE) remains explicitly long-term-deferred.
End-to-end flow this enables
Result: cross-cluster operator auth with no long-lived secret on the operator side beyond the IdP's client-credentials Secret. Token rotation is non-disruptive (the operator's
FileTokenSourcere-reads on a 1-second cache TTL; the broker's atomic write means readers never see partial files).Auth modes available
--auth-mode=selects between three implementations of theAuthenticatorinterface ininternal/auth:none— accepts every request. Logs a loudWARNat startup. Default only when--coordination-namespaceis empty (i.e., dev mode using the in-memory store).static-keys— file of<key-id>:<sha256-hex>entries, hashed at rest. SIGHUP reloads in place; preserve-on-error means a botched edit can't lock you out.oidc— JWT validation against an OIDC provider's JWKS. Supports--oidc-required-claim=key=valuefor coarse authorization, custom username/tenant claim mapping, and JWKS URL override.Switching backends is config-only — no code change required.
Operator changes
--berth-api-key-file <path>flag, mutually exclusive with--berth-api-key. Existing static-key callers see no behavior change.internal/operator.FileTokenSourcereads the file with a small (1s) cache and returns the previously-cached value on transient read errors so a sidecar restart doesn't take down the reconcile loop.Reference broker
cmd/berth-oidc-broker(~250 LOC) is a small purpose-built sidecar:audienceform parameter for Auth0/some Okta setups, scopes for everything else).For exotic flows (token exchange, mTLS-bound tokens) users can substitute their own broker — the operator only cares that the file at
--berth-api-key-filecontains a valid bearer token.Test plan
go test ./... -race -count=1— all packages greenmake lint— golangci-lint + go vet cleanmake build— all four binaries (apiserver,operator,berth,berth-oidc-broker) buildOut of scope
Files of note
internal/auth/static.go— hash-backed static key authenticatorinternal/auth/oidc.go— go-oidc-backed JWT validatorinternal/auth/noauth.go—NoOpAuthenticatorfor tests/compositioninternal/api/middleware.go—AuthMiddleware,IdentityFromContextinternal/operator/tokensource.go—FileTokenSourcefor the operatorpkg/client/options.go—WithAPIKey(back-compat) + newWithAPIKeyFunccmd/berth-oidc-broker/main.go— reference sidecar brokercmd/apiserver/main.go—--auth-mode+--api-keys-file+ all--oidc-*flagscmd/operator/main.go—--berth-api-key-file+ mutual-exclusion checkREADME.md— Authentication section with Okta/PingFederate/Entra walkthroughs and the sidecar pod sketch