Kubernetes Dashboard: Web UI Setup and Security
The Kubernetes Dashboard is a general-purpose web-based UI for Kubernetes clusters that lets you manage applications, troubleshoot running workloads, and inspect cluster resources without writing kubectl commands. While powerful and useful for development and operations teams, the Dashboard has a history of being misconfigured in ways that expose cluster admin access to the internet — the 2018 Tesla cryptojacking incident famously exploited an unsecured Dashboard. This guide covers proper installation, RBAC-scoped access, and secure ingress exposure.
Table of Contents
Installing Kubernetes Dashboard
The Kubernetes Dashboard project publishes official manifests and a Helm chart. Dashboard v3 (released 2024) introduced a significant architecture change — the backend now runs as a separate service from the frontend, and authentication is handled by an OIDC-compatible auth proxy rather than via token pasting.
# Method 1: Official manifests (Dashboard v3)
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v3.0.0/charts/kubernetes-dashboard.yaml
# Method 2: Helm chart (recommended for production)
helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/
helm repo update
helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard \
--namespace kubernetes-dashboard \
--create-namespace \
--set app.ingress.enabled=false # we'll configure ingress separately
# Verify all pods are running
kubectl get pods -n kubernetes-dashboard
After installation, the Dashboard namespace contains:
kubernetes-dashboard-web— the React frontend servicekubernetes-dashboard-api— the backend API service that talks to the Kubernetes APIkubernetes-dashboard-auth— the authentication/session management servicekubernetes-dashboard-metrics-scraper— collects resource usage metrics from the Metrics Server
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml.
Access Methods: Port-Forward vs Ingress
For development clusters or occasional administrative access, kubectl proxy or port-forward is the safest option — access is authenticated via your kubeconfig and never exposed beyond your local machine.
# Method 1: kubectl proxy (recommended for developers)
# Starts a proxy server on localhost:8001 that uses your kubeconfig credentials
kubectl proxy
# Access at:
# http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard-web:443/proxy/
# Method 2: Port-forward directly to the Dashboard service
kubectl port-forward -n kubernetes-dashboard svc/kubernetes-dashboard-web 8443:443
# Access at https://localhost:8443
# Browser will warn about self-signed certificate — this is expected
For teams that need persistent access without running kubectl locally, expose the Dashboard via an ingress controller with HTTPS and authentication middleware. See the Secure Ingress Exposure section below.
RBAC: Admin and Read-Only Access
The Dashboard operates with the permissions of the authenticated user. Never create a ClusterRoleBinding that grants cluster-admin to the Dashboard's ServiceAccount — this was the root cause of the historical security incidents. Instead, create granular roles matching what each user or team actually needs.
# Read-only ClusterRole — view all resources but cannot modify anything
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: dashboard-viewer
rules:
- apiGroups: [""]
resources:
- pods
- pods/log
- services
- configmaps
- namespaces
- nodes
- persistentvolumeclaims
- persistentvolumes
- events
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch"]
---
# Namespace-scoped developer role — full access within their namespace only
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dashboard-developer
namespace: team-payments
rules:
- apiGroups: ["", "apps", "batch", "networking.k8s.io"]
resources: ["*"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods/exec", "pods/portforward"]
verbs: ["create"]
# Create a ServiceAccount for the Dashboard admin user
apiVersion: v1
kind: ServiceAccount
metadata:
name: dashboard-admin
namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dashboard-admin-binding
subjects:
- kind: ServiceAccount
name: dashboard-admin
namespace: kubernetes-dashboard
roleRef:
kind: ClusterRole
name: dashboard-viewer # read-only — change to cluster-admin only if truly needed
apiGroup: rbac.authorization.k8s.io
Service Account Token Authentication
Dashboard v2 and earlier accepted a bearer token that users would paste into a login screen. In Kubernetes 1.24+, tokens are no longer automatically created for ServiceAccounts — you must create them explicitly as Secrets.
# Create a long-lived token for the dashboard admin ServiceAccount
apiVersion: v1
kind: Secret
metadata:
name: dashboard-admin-token
namespace: kubernetes-dashboard
annotations:
kubernetes.io/service-account.name: dashboard-admin
type: kubernetes.io/service-account-token
# Retrieve the token value
kubectl get secret dashboard-admin-token \
-n kubernetes-dashboard \
-o jsonpath='{.data.token}' | base64 --decode
# For temporary access, create a bound token with a short TTL
kubectl create token dashboard-admin \
-n kubernetes-dashboard \
--duration=1h
kubectl create token for interactive access.
OIDC Integration for Team Access
For teams of more than a few people, individual token management is impractical. Integrate the Dashboard with your organisation's OIDC provider (Okta, Google Workspace, Azure AD, Keycloak) so that users log in with their existing SSO credentials.
# Configure the Kubernetes API server for OIDC
# Add these flags to kube-apiserver (kubeadm: edit /etc/kubernetes/manifests/kube-apiserver.yaml)
- --oidc-issuer-url=https://accounts.google.com
- --oidc-client-id=kubernetes-dashboard
- --oidc-username-claim=email
- --oidc-groups-claim=groups
# For EKS, configure OIDC via aws-auth ConfigMap or access entries
# For GKE, OIDC is configured at the cluster level
# Deploy oauth2-proxy in front of the Dashboard for SSO
helm upgrade --install oauth2-proxy oauth2-proxy/oauth2-proxy \
--namespace kubernetes-dashboard \
--set config.clientID=YOUR_OIDC_CLIENT_ID \
--set config.clientSecret=YOUR_OIDC_CLIENT_SECRET \
--set config.cookieSecret=$(openssl rand -base64 32) \
--set config.upstreamUrl=https://kubernetes-dashboard-web.kubernetes-dashboard.svc.cluster.local:443 \
--set extraArgs.provider=oidc \
--set extraArgs.oidc-issuer-url=https://accounts.google.com \
--set extraArgs.email-domain=yourcompany.com
Secure Ingress Exposure
Exposing the Dashboard via ingress requires HTTPS and strong authentication. Never expose the Dashboard on HTTP or without authentication — a publicly accessible Dashboard without auth is a critical security vulnerability.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kubernetes-dashboard
namespace: kubernetes-dashboard
annotations:
# Require HTTPS (Traefik example)
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
# Forward auth to oauth2-proxy for SSO
traefik.ingress.kubernetes.io/router.middlewares: kubernetes-dashboard-oauth2proxy@kubernetescrd
# Restrict access to internal IPs only
traefik.ingress.kubernetes.io/router.middlewares: ip-allowlist@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- dashboard.internal.example.com
secretName: dashboard-tls
rules:
- host: dashboard.internal.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kubernetes-dashboard-web
port:
number: 443
*.internal.example.com resolves only on VPN). Add an IP allowlist middleware as a second layer — block all source IPs except your VPN gateway and office ranges.
Dashboard Features and Use Cases
The Kubernetes Dashboard provides a visual interface for tasks that are cumbersome in kubectl:
- Pod logs: Stream logs from any container in any pod, with the ability to filter by time range and keyword
- Resource viewer: Browse all Kubernetes resources with their current state, labels, annotations, and events
- Exec shell: Open an interactive shell inside a running container (subject to RBAC — requires pods/exec permission)
- Scale deployments: Increase or decrease replica count with a single click
- Rolling updates: Update container image tags via the Dashboard and observe the rollout progress
- Resource usage graphs: CPU and memory usage trends per pod and namespace (requires Metrics Server)
- YAML editor: View and edit raw YAML for any resource with syntax highlighting
The Dashboard is particularly valuable for on-call engineers who need to quickly investigate incidents without context-switching to a terminal, and for application developers who want to inspect their pods without learning advanced kubectl commands.
Dashboard Alternatives: Lens, Headlamp, Octant
The official Kubernetes Dashboard is not the only option for a cluster web UI. Several mature alternatives offer different trade-offs:
- Lens: A desktop application (Electron) that connects to multiple clusters simultaneously. Rich UI, built-in metrics, and extension marketplace. Free for personal use; Lens Pro adds team features. No in-cluster component needed — runs entirely on your laptop using kubeconfig.
- Headlamp: An open-source, extensible Kubernetes UI that can run as a web app (in-cluster) or as a desktop application. CNCF sandbox project. Designed for plugin extensibility.
- k9s: Not a web UI but a terminal-based cluster navigator that provides a Dashboard-like experience in the terminal. Extremely fast, keyboard-driven, and loved by experienced operators.
# Install Headlamp as an in-cluster web UI
helm repo add headlamp https://headlamp-k8s.github.io/headlamp/
helm upgrade --install headlamp headlamp/headlamp \
--namespace headlamp \
--create-namespace \
--set ingress.enabled=true \
--set ingress.hosts[0].host=headlamp.internal.example.com