headlamp

package
v0.14.34-dev Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package headlamp deploys and audits a Headlamp operating UI on a peer-hosted DKS control plane.

WHY. cloudbox's Cluster page is a cross-host INVENTORY only — it structurally cannot operate a peer-hosted plane (its endpoint is not even registered when cloudbox runs no cluster). Operating a cluster belongs where the kubeconfig is: the control-plane host. Headlamp is that operating UI, and it is also a full cluster-admin surface, so the auth boundary is designed before the deployment shape.

SECURITY MODEL — three layers, none optional:

  1. The Headlamp pod's own ServiceAccount receives ZERO RBAC grants. Headlamp runs with token login: every apiserver request is authenticated with the token the operator pastes into the UI for that session. A compromised Headlamp pod therefore holds only an unprivileged SA token (system:basic-user discovery), never cluster-admin. The token mount stays on solely so Headlamp's in-cluster config (apiserver address + CA) resolves.
  2. The Service is ClusterIP only — never NodePort, never LoadBalancer, never hostNetwork/hostPort. The only way off the node is a port-forward pinned to 127.0.0.1 (ForwardArgs), which follows outpost's standing rule that loopback + the tunnel/mesh is the only ingress. Remote reach rides the EXISTING authenticated surfaces — the mesh forwarder (allowlisted service name MeshServiceName) or a matrix-tunnel app with RequireLogin — not a new port. No LAN bind is offered here; a follow-up that offers one must force the auth gate per the admin_addr precedent.
  3. An OPTIONAL read-only viewer ServiceAccount (ViewerServiceAccount) is bound to the built-in aggregate ClusterRole "view" (namespaced reads, excludes Secrets and RBAC objects) plus a dedicated nodes-get/list/watch ClusterRole (nodes are cluster-scoped, so "view" alone cannot show them, and a plane UI without node visibility is pointless). It exists only so the operator can mint a scoped audit token to paste at login; no pod mounts it. That token is a cluster credential — per the tenancy model it must never reach a guest or non-owner (guests reach shared apps via Periscope); see Options.DisableViewerRBAC for the full caveat. Set Options.DisableViewerRBAC to converge it away. Admin access is always the operator pasting an admin token themselves — this package never mints or persists one.

Posture drift (a Service flipped to NodePort, a binding granted to the pod SA, a hostNetwork pod) is detected by Inspect and repaired by re-running Deploy.

WIRING IS DEFERRED. No four-surface toggle exists yet; the headlamp_enabled / headlamp_port follow-up (loom/zot builtin pattern) is described in docs/peer-dks-headlamp.md. Until then the operator path is: Deploy (or `kubectl apply` of RenderJSON output), ForwardArgs for the loopback forward, and `outpost mesh service add headlamp 127.0.0.1:<port>` for authenticated remote reach.

Headlamp against a real peer-hosted plane is HARDWARE-UNPROVEN — see docs/peer-dks-headlamp.md for the evidence boundary.

Index

Constants

View Source
const (
	// DefaultNamespace is the dedicated namespace everything lives in.
	// Dedicated so Remove can delete it without collateral damage.
	DefaultNamespace = "headlamp"

	// DefaultName names the ServiceAccount, Deployment, and Service.
	// The app.kubernetes.io/name label matches what the merged
	// acceptance harness (script/dks-peer-acceptance.sh check 9)
	// already selects on.
	DefaultName = "headlamp"

	// DefaultImage pins the upstream Headlamp release this package was
	// written against. Override via Options.Image; the pin has NOT been
	// validated against a live peer plane (hardware-unproven).
	DefaultImage = "ghcr.io/headlamp-k8s/headlamp:v0.27.0"

	// DefaultLocalPort is the loopback port ForwardArgs publishes on.
	// script/dks-headlamp-verify.sh probes the same default.
	DefaultLocalPort = 18466

	// ServicePort is the ClusterIP Service port; containerPort is what
	// headlamp-server itself listens on inside the pod.
	ServicePort = 80

	// MeshServiceName is the forwarder service name the follow-up
	// wiring (and today's manual `outpost mesh service add`) exposes
	// the loopback forward under. The mesh forwarder only reaches
	// allowlisted names — that boundary is the remote auth gate.
	MeshServiceName = "headlamp"

	// ViewerServiceAccount is the optional read-only token-minting SA.
	ViewerServiceAccount = "headlamp-viewer"

	// NodeViewClusterRole grants get/list/watch on nodes only — the one
	// cluster-scoped read "view" lacks that a plane UI needs.
	NodeViewClusterRole = "outpost-headlamp-nodeview"
)

Variables

This section is empty.

Functions

func Deploy

func Deploy(ctx context.Context, cs kubernetes.Interface, o Options) error

Deploy creates or converges the Headlamp deployment on the plane the client points at. It is idempotent and self-repairing: a drifted Service type is forced back to ClusterIP, and when DisableViewerRBAC is set any previously created viewer artifacts are removed. It never binds anything to the pod ServiceAccount.

func ForwardArgs

func ForwardArgs(kubeconfigPath string, o Options) ([]string, error)

ForwardArgs returns the argv of the kubectl port-forward that publishes the ClusterIP Service on 127.0.0.1:<LocalPort>. The --address pin is the load-bearing part: the forward must never bind a non-loopback interface. kubeconfigPath may be empty to use kubectl's own resolution.

func Remove

func Remove(ctx context.Context, cs kubernetes.Interface, o Options) error

Remove deletes everything Deploy created, viewer RBAC included. The namespace itself is deleted only when it carries the outpost managed-by label — a namespace this package did not create is never destroyed. NotFound at any step is fine (idempotent).

func RenderJSON

func RenderJSON(o Options) ([]byte, error)

RenderJSON renders the full manifest as a v1 List in JSON — kubectl applies JSON exactly like YAML (`kubectl apply -f headlamp.json`), and JSON needs no extra dependency. Output is deterministic for a given Options value. The viewer RBAC objects are included unless DisableViewerRBAC is set.

func ValidateListenAddr

func ValidateListenAddr(addr string) error

ValidateListenAddr enforces the loopback-only bind rule on a host:port the follow-up wiring (or any caller) intends to listen on. It rejects empty hosts (an empty host means all interfaces) and any non-loopback address. There is deliberately no override parameter: a future LAN bind must be a conscious follow-up that forces an auth gate on every request (the admin_addr precedent), not a flag here.

Types

type Options

type Options struct {
	Namespace string // "" => DefaultNamespace
	Name      string // "" => DefaultName
	Image     string // "" => DefaultImage
	LocalPort int    // 0 => DefaultLocalPort (loopback forward port)

	// DisableViewerRBAC skips (and on Deploy, removes) the read-only
	// viewer SA and its two bindings. Inverted so the zero value keeps
	// the documented default: viewer RBAC on.
	//
	// OPERATOR CAUTION — a token minted from the viewer SA is a CLUSTER
	// CREDENTIAL: read-only, but cluster-wide. The tenancy model
	// (docs/dks-tenancy-model.md) is absolute here: non-owners get no
	// cluster credential; guests reach shared apps via Periscope. A
	// minted viewer token must therefore NEVER reach a guest or
	// non-owner — never pasted into a Headlamp session anyone but the
	// owner can drive, never handed out, never persisted. Mint it
	// short-lived (`kubectl -n headlamp create token headlamp-viewer
	// --duration=1h`), use it, let it expire.
	//
	// Default evaluated: ON remains the default because creating the SA
	// and its bindings confers no credential by itself — a credential
	// exists only once someone already holding cluster-admin mints a
	// token, which is an owner-only act. The residual risk is token
	// distribution, governed by the caution above and by
	// docs/peer-dks-headlamp.md; operators who want the minting target
	// itself gone set this flag.
	DisableViewerRBAC bool
}

Options parameterizes a Headlamp deployment. The zero value is valid and maps to the secure defaults above.

func (Options) Normalize

func (o Options) Normalize() (Options, error)

Normalize fills defaults and validates. Every entry point (Deploy/Remove/Inspect/RenderJSON/ForwardArgs) goes through it.

type Posture

type Posture struct {
	DeploymentFound bool
	ReadyReplicas   int32
	ServiceFound    bool
	ServiceType     corev1.ServiceType

	// Violations lists every observed deviation from the security
	// model: an exposed Service (NodePort/LoadBalancer/externalIPs),
	// host namespaces or hostPorts, a privileged container, a pod not
	// running as the zero-grant SA, or ANY RBAC binding attached to
	// the pod SA (it must have none) or unexpected binding on the
	// viewer SA.
	Violations []string
}

Posture is the audited state of a Headlamp deployment. It is FAIL-CLOSED at the API layer: Inspect returns an error — not a Posture — whenever any query it needs could not be answered, so a flaky apiserver can never read as "secure".

func Inspect

func Inspect(ctx context.Context, cs kubernetes.Interface, o Options) (*Posture, error)

Inspect audits the live cluster state against the security model. Missing deployment/service is reported via the Found fields (that is an answer, not an error); any API failure is an error.

func (*Posture) Running

func (p *Posture) Running() bool

Running reports whether the deployment exists and has at least one ready replica.

func (*Posture) Secure

func (p *Posture) Secure() bool

Secure reports whether the audited surface matches the security model: both objects present and zero violations.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL