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:
- 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.
- 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.
- 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
- func Deploy(ctx context.Context, cs kubernetes.Interface, o Options) error
- func ForwardArgs(kubeconfigPath string, o Options) ([]string, error)
- func Remove(ctx context.Context, cs kubernetes.Interface, o Options) error
- func RenderJSON(o Options) ([]byte, error)
- func ValidateListenAddr(addr string) error
- type Options
- type Posture
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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.