Skip to content

status-server

The status page backend: the embedded web dashboard over the Finding state machine and the FindingRollup statistics, the sign-in surface, and the three human actions (approve, suspend, resume). It is a server, not a controller — it runs no reconcilers and takes no leases; a controller-runtime cache gives it live watches, and an SSE stream tells open browsers to refetch when anything changes.

status-server serve --namespace patchy --auth-config /etc/patchy/auth/config.yaml

The exposure contract is deliberately asymmetric — see the status page for the UI tour and the RBAC grammar:

  • Rollup statistics are public. GET /api/rollups (and the SSE stream) serve without a session.
  • The findings surface always requires authentication. GET /api/findings (the trimmed list projection), GET /api/findings/{name} (one finding's full detail — description, alerts, enrichments, the phase log, and the run reports the list omits), and every action POST demand a signed-in identity whose RBAC passes the corresponding access review. With no auth config at all the server runs in the unconfigured posture: rollups only, and the page explains that sign-in is not configured.
  • Agent conversations ride the findings gate. GET /api/findings/{name}/runs/{investigation|remediation}/{attempt}/transcript is Server-Sent Events in both modes: a finished run replays its stored ConfigMap, a running one streams from the agent's pod log. Following a live run needs pods and pods/log read in --agent-namespace (through the API server — the server never dials an agent pod); without that grant, completed runs' conversations still serve. See Agent conversations.

Flags

The shared flags (--listen-addr is the page's own address here — there is no webhook), plus:

Flag Env Default Purpose
--namespace PATCHY_NAMESPACE POD_NAMESPACE Namespace the Findings and FindingRollups live in
--agent-namespace PATCHY_AGENT_NAMESPACE patchy-agents Namespace the agent Jobs run in; live conversations are followed from their pod logs
--kubeconfig PATCHY_KUBECONFIG in-cluster Kubeconfig path for running outside the cluster
--health-addr PATCHY_HEALTH_ADDR :8081 healthz/readyz probe listen address
--auth-config PATCHY_AUTH_CONFIG (unset) Mounted authentication config; absent ⇒ rollups-only (see below)

Authentication configuration

--auth-config points at a YAML file, conventionally a mounted Secret (patchy-status-auth, key config.yaml — the deployments mount it optional, so removing the Secret degrades to rollups-only rather than failing the pod). A present-but-invalid file is a startup error: a broken configuration never silently downgrades to no authentication.

mode: oidc # none | anonymous | oidc
sessionDuration: 168h # absolute session lifetime (default 7 days)
# insecure: true               # drop the cookies' Secure flag — plain-HTTP local dev ONLY
anonymous: # mode: anonymous only
  username: status-viewer
  groups: [patchy-viewers]
oidc: # mode: oidc only
  issuerURL: https://sso.example.com
  clientID: patchy-status
  clientSecret: "..." # or clientSecretFile: /path/to/projected/key
  # scopes: [openid, offline_access, profile, email, groups]
  # authURLParams: {}        # extra authorize-endpoint query parameters
  # autoLogin: false         # bounce straight to the provider instead of the sign-in panel
  # redirectURL: ""          # override the derived <scheme>://<host>/oauth2/callback
  claims: # claim NAMES, mapped onto the identity
    username: email # the subject access reviews run for
    groups: groups
    displayName: name

Modes

  • none — every request is a fixed development identity with authorization bypassed entirely. The dev overlay ships this; never expose it.
  • anonymous — every request is the one configured identity, but access reviews still run: cluster RBAC for that username/groups decides what every visitor may see and do.
  • oidc — the real SSO flow. The server itself is the OAuth2 client (authorization-code + PKCE); the SPA never sees a token.

Sessions and cookies (mode oidc)

There is no server-side session store. The ID token, refresh token, and session start are sealed with AES-256-GCM — the key is derived (HKDF-SHA256) from the OIDC client secret, so rotating the client secret signs everyone out — and stored in chunked HttpOnly cookies (patchy-auth, patchy-auth-1, …). Every request re-verifies the ID token; an expired token is renewed via the refresh token in place, but never past sessionDuration from the original sign-in. Three small SPA-readable cookies carry no secrets: patchy-auth-provider (how sign-in works), patchy-auth-error (the last failure), patchy-auth-logout (pauses autoLogin after an explicit sign-out).

The callback URL is derived from X-Forwarded-Proto / X-Forwarded-Host (or the Host header), which assumes a trusted fronting proxy — set oidc.redirectURL explicitly if yours cannot be trusted to strip those.

Authorization

Per-user grants are ordinary Kubernetes RBAC, resolved server-side with SubjectAccessReviews for the signed-in user (users need no kubeconfig and never talk to the API server):

  • native get on findings gates viewing the findings surface;
  • the custom verbs approve, suspend, resume on findings.patchy.bitwisemedia.uk gate the action buttons, one verb per button.

Grants are namespace-scoped and stamped into the payload as each finding's userActions; the client intersects them with the finding's own state machine, and every POST is re-checked server-side. See deploy/kustomize/base/rbac.users.example.yaml for ready-made viewer / approver / operator tiers.

The server's own ServiceAccount is deliberately narrow: findings read + spec write (approve records spec.approval; suspend/resume toggle spec.suspend — it never writes findings/status and never moves a phase; the owning controllers react to the spec change), findingrollups read, and cluster-scoped subjectaccessreviews create for the reviews.