Skip to content
Go back

Headlamp: K8s UI Without the License Drama

By SumGuy 11 min read
Headlamp: K8s UI Without the License Drama

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:

Terminal window
chmod +x Headlamp-0.26.0-linux-x64.AppImage
./Headlamp-0.26.0-linux-x64.AppImage

It 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.

Terminal window
helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
helm repo update
helm 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:

Terminal window
kubectl -n headlamp get pods
kubectl -n headlamp get svc

Plugin Dev CLI

If you’re building or testing plugins locally (covered later), install the CLI:

Terminal window
npx @kinvolk/headlamp-plugin create my-plugin

It 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:

Terminal window
kubectl create serviceaccount headlamp-admin -n headlamp
kubectl create clusterrolebinding headlamp-admin \
--clusterrole=cluster-admin \
--serviceaccount=headlamp:headlamp-admin
kubectl create token headlamp-admin -n headlamp --duration=8760h

Copy 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:

headlamp-values.yaml
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.internal

Create the secret separately (don’t inline secrets in values files that end up in git):

Terminal window
kubectl create secret generic headlamp-oidc-secret \
--from-literal=clientSecret='your-client-secret-here' \
-n headlamp

Install with the values file:

Terminal window
helm upgrade --install headlamp headlamp/headlamp \
-n headlamp \
-f headlamp-values.yaml

In Authentik (or Keycloak), create an OIDC provider with:

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):

headlamp-values.yaml
volumeMounts:
- name: kubeconfig
mountPath: /etc/headlamp/
readOnly: true
volumes:
- name: kubeconfig
secret:
secretName: my-kubeconfig-secret

Multi-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/:

headlamp-values.yaml
volumes:
- name: kubeconfigs
projected:
sources:
- secret:
name: prod-kubeconfig
- secret:
name: staging-kubeconfig
volumeMounts:
- name: kubeconfigs
mountPath: /etc/headlamp/
readOnly: true

Each 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:

headlamp-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: headlamp
namespace: headlamp
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-internal"
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
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: 80

Apply it:

Terminal window
kubectl apply -f headlamp-ingress.yaml

cert-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:

headlamp-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: headlamp-viewer
rules:
- 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/v1
kind: ClusterRoleBinding
metadata:
name: headlamp-viewers
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: headlamp-viewer
subjects:
- kind: Group
name: "headlamp-viewers" # maps to your OIDC group
apiGroup: rbac.authorization.k8s.io

Apply 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

Terminal window
# From the Headlamp UI: Settings → Plugins → Browse Plugins
# Or install via Helm values:
headlamp-values.yaml
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/plugins

The 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:

src/index.tsx
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 item
registerSidebarEntry({
parent: null,
name: "my-plugin",
label: "My Resources",
url: "/my-plugin",
icon: "mdi:kubernetes",
});
// Register the route
registerRoute({
path: "/my-plugin",
exact: true,
name: "My Plugin",
component: () => <MyResourceView />,
});
// The view component
function 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:

dist/main.js
npx @kinvolk/headlamp-plugin build

For local testing with the desktop app, drop the dist/ folder into:

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:

headlamp-values.yaml
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:

headlamp-values.yaml
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.

headlamp-values.yaml
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:

headlamp-values.yaml
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.png

Create the ConfigMap from your logo file:

Terminal window
kubectl create configmap headlamp-logo \
--from-file=logo.png=./your-logo.png \
-n headlamp

It’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:

Skip Headlamp when:


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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Jellyseerr Tagging Workflows for Real Libraries

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts