The K8s Dashboard Problem Is Solved, You Just Haven’t Met the Answer Yet
Kubernetes dashboards have a complicated history. The official Dashboard was fine until it became a CVE magnet. Lens was great until it became a commercial product that wanted your email, your soul, and eventually a subscription. Octant got archived. K9s is excellent but it’s a TUI — your ops intern is not reading a terminal, and neither is your manager when they want to see “the thing running.”
Headlamp is the answer most people haven’t landed on yet. It’s a CNCF Sandbox project (not some random GitHub repo — actual governance, actual roadmap), it’s Apache 2.0 licensed with no enterprise gating, and it has a real plugin SDK instead of fake extensibility. You can run it as a desktop app, deploy it in-cluster as a web UI, or do both simultaneously with the same kubeconfig contexts.
Let’s install it, lock it down, wire up SSO, write a plugin, and get out before dinner.
Installation: Pick Your Poison
Headlamp ships in three modes. Pick based on your use case.
Desktop App (Electron)
If you want something local that reads your existing kubeconfig, the Electron app is zero-infrastructure. Download the .AppImage (Linux), .dmg (macOS), or .exe (Windows) from github.com/kubernetes-sigs/headlamp/releases.
On Linux:
chmod +x Headlamp-0.26.0-linux-x64.AppImage./Headlamp-0.26.0-linux-x64.AppImageIt picks up ~/.kube/config automatically. If you have multiple contexts, the cluster switcher in the top-left shows all of them. That’s it. No setup. No ingress. No helm.
Use this for local dev or when you don’t want to expose a UI to your home network.
In-Cluster Web UI (Helm)
For the home lab — where you want Headlamp available on your LAN without running the desktop app — the Helm chart is the standard path.
helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/helm repo updatehelm install headlamp headlamp/headlamp \ --namespace headlamp \ --create-namespace \ --set service.type=ClusterIP \ --set ingress.enabled=true \ --set "ingress.hosts[0].host=headlamp.yourdomain.internal" \ --set "ingress.hosts[0].paths[0].path=/" \ --set "ingress.hosts[0].paths[0].pathType=Prefix"That gets you a running deployment with a ClusterIP service. We’ll add TLS in a bit.
Check it’s alive:
kubectl -n headlamp get podskubectl -n headlamp get svcPlugin Dev CLI
If you’re building or testing plugins locally (covered later), install the CLI:
npx @kinvolk/headlamp-plugin create my-pluginIt scaffolds a TypeScript/React plugin project with hot reload. You don’t need this for basic deployment — it’s for the plugin section.
Authentication: Don’t Leave the Front Door Open
Headlamp supports three auth modes. The right one depends on how serious you are about this.
Mode 1: Bearer Token (Default)
When you first hit the Headlamp UI, it asks you to paste a token. This is the kubeconfig service account token flow. It works. It’s fine for a single user who knows what they’re doing.
Create a service account with cluster-admin for your own use:
kubectl create serviceaccount headlamp-admin -n headlampkubectl create clusterrolebinding headlamp-admin \ --clusterrole=cluster-admin \ --serviceaccount=headlamp:headlamp-adminkubectl create token headlamp-admin -n headlamp --duration=8760hCopy that token, paste it in the UI, done.
Don’t give that token to anyone else. Don’t commit it anywhere. Rotate it occasionally. You know the drill.
Mode 2: OIDC with Authentik/Keycloak/Authelia
This is the grown-up path. Wire up OIDC and every user logs in with their SSO credentials, gets RBAC based on group membership, and you have an audit trail.
Create a Helm values file:
config: oidc: clientID: "headlamp" clientSecret: "your-client-secret-here" issuerURL: "https://auth.yourdomain.internal/application/o/headlamp/" scopes: "openid,email,profile,groups"
env: - name: HEADLAMP_CONFIG_OIDC_CLIENT_SECRET valueFrom: secretKeyRef: name: headlamp-oidc-secret key: clientSecret
service: type: ClusterIP
ingress: enabled: true hosts: - host: headlamp.yourdomain.internal paths: - path: / pathType: Prefix tls: - secretName: headlamp-tls hosts: - headlamp.yourdomain.internalCreate the secret separately (don’t inline secrets in values files that end up in git):
kubectl create secret generic headlamp-oidc-secret \ --from-literal=clientSecret='your-client-secret-here' \ -n headlampInstall with the values file:
helm upgrade --install headlamp headlamp/headlamp \ -n headlamp \ -f headlamp-values.yamlIn Authentik (or Keycloak), create an OIDC provider with:
- Redirect URI:
https://headlamp.yourdomain.internal/oidc-callback - Scopes: openid, email, profile, groups
The groups scope is key — Kubernetes can map OIDC groups to RBAC roles, which means you can have read-only users and admin users without managing service account tokens for each person.
Mode 3: Custom kubeconfig Mount
If you want to mount a kubeconfig directly (useful for CI or automated contexts):
volumeMounts: - name: kubeconfig mountPath: /etc/headlamp/ readOnly: true
volumes: - name: kubeconfig secret: secretName: my-kubeconfig-secretMulti-Cluster: Because One k3s Node Is Never Enough
If you’re like most home lab people, you have at least two clusters — maybe a prod-ish k3s setup and a chaos-testing cluster. Headlamp handles multi-cluster cleanly.
For the desktop app: just add multiple contexts to ~/.kube/config. Headlamp discovers all of them and shows a cluster picker on load.
For in-cluster deployment, you can pass multiple kubeconfig files. The recommended pattern is to store each cluster’s kubeconfig as a separate secret and mount them under /etc/headlamp/:
volumes: - name: kubeconfigs projected: sources: - secret: name: prod-kubeconfig - secret: name: staging-kubeconfig
volumeMounts: - name: kubeconfigs mountPath: /etc/headlamp/ readOnly: trueEach file in that directory gets treated as a separate cluster context. The UI shows a dropdown at the top. Click. Switch. Done.
Reverse Proxy + TLS
You want TLS on this. Always. Even on a LAN you control.
Traefik Ingress (k3s default)
If you’re on k3s, Traefik is already there. Use an IngressRoute or a standard Ingress with cert-manager annotations:
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: headlamp namespace: headlamp annotations: cert-manager.io/cluster-issuer: "letsencrypt-internal" traefik.ingress.kubernetes.io/router.entrypoints: websecurespec: ingressClassName: traefik tls: - hosts: - headlamp.yourdomain.internal secretName: headlamp-tls rules: - host: headlamp.yourdomain.internal http: paths: - path: / pathType: Prefix backend: service: name: headlamp port: number: 80Apply it:
kubectl apply -f headlamp-ingress.yamlcert-manager handles the certificate. Traefik handles the termination. Headlamp gets plain HTTP from the ingress controller and the browser sees HTTPS. Standard stuff.
RBAC: Headlamp Stays in Its Lane
This is worth saying clearly because it matters: Headlamp never bypasses Kubernetes RBAC. Every API call Headlamp makes to the cluster uses the authenticated user’s credentials. If your OIDC user only has view on the default namespace, that’s exactly what Headlamp shows them — no workarounds, no admin backdoor in the UI.
Set up RBAC for a read-only viewer role:
apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: headlamp-viewerrules: - apiGroups: [""] resources: ["pods", "services", "configmaps", "namespaces", "events"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments", "replicasets", "statefulsets", "daemonsets"] verbs: ["get", "list", "watch"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: headlamp-viewersroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: headlamp-viewersubjects: - kind: Group name: "headlamp-viewers" # maps to your OIDC group apiGroup: rbac.authorization.k8s.ioApply it, and anyone in the headlamp-viewers OIDC group gets read-only access. They can watch logs, browse resources, and not accidentally delete your database StatefulSet at 2 AM. Everybody wins.
Plugins: This Is Where It Gets Fun
Headlamp’s plugin system is the real differentiator. It’s a proper TypeScript/React SDK, not “paste a ConfigMap and pray.” There’s an official plugin registry, community plugins for Flux, ArgoCD, cert-manager, and KubeVirt, and you can build your own.
Installing Community Plugins
# From the Headlamp UI: Settings → Plugins → Browse Plugins# Or install via Helm values:config: pluginsDir: /headlamp/plugins
initContainers: - name: install-flux-plugin image: ghcr.io/headlamp-k8s/headlamp-plugin-flux:latest command: ["cp", "-r", "/plugin/.", "/headlamp/plugins/flux"] volumeMounts: - name: plugins mountPath: /headlamp/plugins
volumes: - name: plugins emptyDir: {}
volumeMounts: - name: plugins mountPath: /headlamp/pluginsThe Flux plugin gives you GitRepository, Kustomization, and HelmRelease resources surfaced natively in Headlamp’s sidebar. Same pattern for ArgoCD, cert-manager, KubeVirt.
Writing Your Own Plugin
Here’s a minimal plugin that adds a sidebar entry and a custom resource view. This is the scaffold you get from npx @kinvolk/headlamp-plugin create:
import { registerSidebarEntry, registerRoute,} from "@kinvolk/headlamp-plugin/lib";import { SectionBox, SectionHeader } from "@kinvolk/headlamp-plugin/lib/components/common";import { useResources } from "@kinvolk/headlamp-plugin/lib/hooks";
// Register a sidebar nav itemregisterSidebarEntry({ parent: null, name: "my-plugin", label: "My Resources", url: "/my-plugin", icon: "mdi:kubernetes",});
// Register the routeregisterRoute({ path: "/my-plugin", exact: true, name: "My Plugin", component: () => <MyResourceView />,});
// The view componentfunction MyResourceView() { const [items, error] = useResources("mycrd.example.com", "v1", "myresources");
if (error) return <div>Error: {error.message}</div>; if (!items) return <div>Loading...</div>;
return ( <SectionBox> <SectionHeader title="My Custom Resources" /> <ul> {items.map((item: any) => ( <li key={item.metadata.name}> {item.metadata.namespace}/{item.metadata.name} </li> ))} </ul> </SectionBox> );}Build and package it:
npx @kinvolk/headlamp-plugin buildFor local testing with the desktop app, drop the dist/ folder into:
- Linux:
~/.config/Headlamp/plugins/my-plugin/ - macOS:
~/Library/Application Support/Headlamp/plugins/my-plugin/
Restart Headlamp. Your sidebar entry appears.
For in-cluster deployment, serve the dist/main.js from any HTTP endpoint and add it to your Helm values:
config: pluginURLs: - "https://plugins.yourdomain.internal/my-plugin/main.js"Telemetry Opt-Out
By default, Headlamp collects anonymized usage telemetry. It’s not nefarious, but if you’re privacy-minded or running in an air-gapped environment, disable it:
env: - name: HEADLAMP_BACKEND_TELEMETRY value: "off"Or set it in a deployment patch if you’re managing values with Kustomize or ArgoCD.
Customization: Make It Yours
Headlamp supports some basic branding. Useful if multiple people use the same instance and you want it to feel like something your team actually built.
config: # Show a custom app name appName: "SumGuy Lab Cluster" # Default to dark theme theme: "dark" # Default cluster to load on login defaultCluster: "prod"For a custom logo, you mount a PNG and set the path:
config: logoPath: "/etc/headlamp/logo.png"
volumes: - name: custom-logo configMap: name: headlamp-logo
volumeMounts: - name: custom-logo mountPath: /etc/headlamp/logo.png subPath: logo.pngCreate the ConfigMap from your logo file:
kubectl create configmap headlamp-logo \ --from-file=logo.png=./your-logo.png \ -n headlampIt’s not deep theming — just the logo and the app name. Enough to distinguish “this is the prod UI” from “this is the lab UI” when you have multiple instances.
What It Actually Looks Like
Since you can’t see a screenshot here: the Headlamp UI is clean, dark-by-default, and organized around the standard Kubernetes resource hierarchy.
The workload list gives you Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs with real-time replica counts and status badges. Click a Deployment and you get the full spec, the ReplicaSet lineage, and the pods it manages — all on one page, no hunting through kubectl output.
Pod details include resource requests/limits, environment variables (with secret masking — values shown as *** unless you explicitly reveal them), volume mounts, and the conditions timeline. There’s a live log stream with namespace-scoped and container-scoped filtering — much closer to kubectl logs -f than the old dashboard’s “refresh” button. The terminal-in-pod button opens an actual exec shell in the browser. No kubectl exec needed. Your junior team members will love this. Use it responsibly.
The resource graph view shows relationships between objects — which Service points to which Pods, which Ingress routes to which Service. It’s not as deep as some commercial tools, but it’s genuinely useful for onboarding people who don’t yet have the mental model.
When To Use It
Use Headlamp when:
- You want a real web UI (not a TUI) that multiple people can access without distributing kubeconfigs
- You’re running k3s or any standard Kubernetes and want something CNCF-backed with a real release cadence
- You want OIDC SSO so your users log in with your existing identity provider
- You’re building internal tooling and the plugin SDK is a better fit than a separate app
- You care about licensing — Apache 2.0, full stop, no enterprise tier
Skip Headlamp when:
- You’re a solo operator who lives in the terminal — K9s does everything faster for that workflow
- You need deep Helm release management UI (Headlamp’s plugin helps but it’s not Helm Dashboard)
- You want vendor-backed support SLAs — then you want a commercial product, and that’s fine
The Bottom Line
Headlamp is what the Kubernetes dashboard ecosystem should have looked like from the start: Apache 2.0, CNCF-governed, plugin SDK that’s actually usable, OIDC auth that respects RBAC, and a UI that doesn’t look like it was designed in 2014. The desktop app is genuinely convenient for local clusters. The Helm deployment is clean. The plugin system is real extensibility, not theater.
If you’re still pasting kubeconfig tokens into the official dashboard or paying for Lens, give Headlamp an afternoon. Helm install, wire up Traefik, configure OIDC, and you’ve got a proper cluster UI that’ll still be free and open source in five years.
Your future self — the one who just handed a junior engineer a read-only login instead of a raw kubeconfig — will appreciate it.