CKS — GUI Access Control

8 minute read

Exam Context

Property Value
Domain Domain 1 — Cluster Setup
Weight 10%
Objective Minimize use of, and access to, GUI elements

The Kubernetes Dashboard is the primary GUI element in a cluster. When misconfigured, it becomes one of the most dangerous attack vectors — providing unauthenticated access to create, delete, and modify any resource in the cluster.

The Threat: Why GUI Access Must Be Controlled

The Tesla Cryptomining Incident

In 2018, attackers discovered Tesla’s Kubernetes Dashboard was publicly accessible without authentication. They:

  1. Accessed the Dashboard over the internet (exposed via LoadBalancer with no auth)
  2. Used the Dashboard’s built-in terminal to deploy cryptomining containers
  3. Found AWS credentials in the Pod environment variables
  4. Accessed Tesla’s AWS S3 buckets containing proprietary telemetry data
  5. Operated undetected for months, consuming GPU resources for mining

Root cause: The Dashboard was deployed with a ClusterAdmin ServiceAccount and exposed without authentication.

Dashboard Attack Surface

Risk Description
Unauthenticated access Default Dashboard install allows skip-login in older versions
ClusterAdmin binding Dashboard ServiceAccount bound to cluster-admin ClusterRole
External exposure Dashboard exposed via NodePort or LoadBalancer without authentication
RBAC bypass Dashboard running with overprivileged ServiceAccount bypasses intended RBAC
Exec into Pods Dashboard provides a built-in terminal to exec into any Pod
Secret viewing Dashboard can display Secrets in plaintext

Kubernetes Dashboard Security Configuration

Default Dashboard Installation

The standard Dashboard install:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml

This installs the Dashboard in the kubernetes-dashboard namespace with a minimal ServiceAccount — but older versions and common tutorials bind it to cluster-admin, creating a massive security hole.

Check Current Dashboard Access

# Check if Dashboard is installed
kubectl get pods -n kubernetes-dashboard

# Check the ServiceAccount
kubectl get serviceaccount -n kubernetes-dashboard

# Check RBAC bindings for the Dashboard
kubectl get clusterrolebinding | grep dashboard
kubectl get rolebinding -n kubernetes-dashboard

Verify Dashboard Exposure

# Check how the Dashboard Service is exposed
kubectl get svc -n kubernetes-dashboard

Expected (secure) output:

NAME                        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
kubernetes-dashboard        ClusterIP   10.96.45.123   <none>        443/TCP   30m

ClusterIP is secure — it’s only accessible from within the cluster or via kubectl proxy.

Dangerous outputs:

# NodePort — accessible on every node's IP
kubernetes-dashboard   NodePort   10.96.45.123   <none>   443:30443/TCP   30m

# LoadBalancer — accessible from the internet
kubernetes-dashboard   LoadBalancer   10.96.45.123   203.0.113.50   443:30443/TCP   30m

Restricting Dashboard Access with RBAC

Step 1: Create a Read-Only ClusterRole

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: dashboard-viewer
rules:
  - apiGroups: [""]
    resources: ["pods", "services", "namespaces", "events"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets", "daemonsets", "statefulsets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["batch"]
    resources: ["jobs", "cronjobs"]
    verbs: ["get", "list", "watch"]

What’s NOT included:

  • No create, update, delete, patch verbs — read only
  • No secrets resource — cannot view Secret contents
  • No exec sub-resource — cannot exec into Pods
  • No nodes resource — cannot view node details

Step 2: Create a Dedicated ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  name: dashboard-readonly
  namespace: kubernetes-dashboard

Step 3: Bind the ClusterRole to the ServiceAccount

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: dashboard-readonly-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: dashboard-viewer
subjects:
  - kind: ServiceAccount
    name: dashboard-readonly
    namespace: kubernetes-dashboard

Step 4: Generate a Token for the ServiceAccount

# Create a token (Kubernetes 1.24+)
kubectl create token dashboard-readonly -n kubernetes-dashboard --duration=8h

This token is used to log into the Dashboard. It expires after 8 hours and has only read-only permissions.

Apply All RBAC Resources

kubectl apply -f dashboard-viewer-role.yaml
kubectl apply -f dashboard-readonly-sa.yaml
kubectl apply -f dashboard-readonly-binding.yaml

# Verify
kubectl auth can-i list pods --as=system:serviceaccount:kubernetes-dashboard:dashboard-readonly
# Expected: yes

kubectl auth can-i delete pods --as=system:serviceaccount:kubernetes-dashboard:dashboard-readonly
# Expected: no

kubectl auth can-i get secrets --as=system:serviceaccount:kubernetes-dashboard:dashboard-readonly
# Expected: no

kubectl auth can-i create pods/exec --as=system:serviceaccount:kubernetes-dashboard:dashboard-readonly
# Expected: no

Removing Dangerous Bindings

If the Dashboard was installed with excessive permissions, fix it:

Find and Remove cluster-admin Bindings

# Find any ClusterRoleBinding giving Dashboard cluster-admin
kubectl get clusterrolebinding -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | 
  select(.subjects[]?.name | contains("dashboard")) | .metadata.name'
# Delete the dangerous binding
kubectl delete clusterrolebinding kubernetes-dashboard-admin

Verify No Excessive Permissions Remain

# List all bindings for the Dashboard ServiceAccount
kubectl get clusterrolebinding -o json | \
  jq '.items[] | select(.subjects[]?.namespace=="kubernetes-dashboard") | 
  {name: .metadata.name, role: .roleRef.name}'

Disabling Public Dashboard Exposure

Convert LoadBalancer/NodePort to ClusterIP

# Patch the service to ClusterIP
kubectl patch svc kubernetes-dashboard -n kubernetes-dashboard \
  -p '{"spec": {"type": "ClusterIP"}}'

Restrict with NetworkPolicy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: restrict-dashboard-access
  namespace: kubernetes-dashboard
spec:
  podSelector:
    matchLabels:
      k8s-app: kubernetes-dashboard
  policyTypes:
    - Ingress
  ingress:
    # Only allow access from specific admin namespace
    - from:
        - namespaceSelector:
            matchLabels:
              purpose: admin
      ports:
        - protocol: TCP
          port: 8443

This NetworkPolicy restricts Dashboard access to only Pods in namespaces labeled purpose: admin.

Access Dashboard Securely via kubectl proxy

# Start kubectl proxy (local access only)
kubectl proxy --port=8001

# Access the Dashboard at:
# http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/

kubectl proxy authenticates through your kubeconfig and only listens on localhost. This is the safest way to access the Dashboard.

Access via Port-Forward

# Port-forward the Dashboard
kubectl port-forward -n kubernetes-dashboard svc/kubernetes-dashboard 8443:443

# Access at https://localhost:8443

Secure Alternatives to the Kubernetes Dashboard

Tool Type Security Advantage
kubectl CLI No GUI attack surface; uses kubeconfig authentication
k9s Terminal UI Local terminal, reads kubeconfig, no network exposure
Lens Desktop app Local application, RBAC-aware, no cluster-side deployment
Headlamp Web UI OIDC/token auth built-in, more secure default configuration
Octant Desktop app Local rendering, no cluster-side components

CKS recommendation: For production clusters, use kubectl or k9s over the Dashboard. The Dashboard adds attack surface without significant operational benefit for experienced operators.

When to Completely Remove the Dashboard

Remove the Dashboard entirely if:

  • Your organization uses kubectl or CLI tools exclusively
  • The Dashboard has been involved in a security incident
  • You can’t justify the attack surface for the limited convenience it provides
  • Compliance requirements restrict GUI-based cluster management

Removing the Dashboard

# Delete all Dashboard resources
kubectl delete namespace kubernetes-dashboard

# Verify removal
kubectl get namespace kubernetes-dashboard
# Expected: Error from server (NotFound): namespaces "kubernetes-dashboard" not found

# Check for lingering ClusterRoles and ClusterRoleBindings
kubectl get clusterrole | grep dashboard
kubectl get clusterrolebinding | grep dashboard

# Delete any remaining cluster-scoped resources
kubectl delete clusterrole kubernetes-dashboard 2>/dev/null
kubectl delete clusterrolebinding kubernetes-dashboard 2>/dev/null

Security Hardening Checklist

  • Dashboard ServiceAccount uses minimal RBAC (never cluster-admin)
  • Dashboard Service type is ClusterIP (not NodePort or LoadBalancer)
  • Skip-login option is disabled
  • Token authentication is enforced
  • Tokens are short-lived (8h or less)
  • NetworkPolicy restricts which Pods can access the Dashboard
  • cluster-admin ClusterRoleBindings for Dashboard are removed
  • Dashboard namespace is labeled for NetworkPolicy selectors
  • Regular audits check for Dashboard permission drift

Exam Tips

Tip Details
Know the risks Be able to explain why an exposed Dashboard is dangerous
RBAC is the key control Bind Dashboard SA to minimal ClusterRole, never cluster-admin
kubectl auth can-i Use this to verify RBAC restrictions quickly
Service type matters ClusterIP = safe, NodePort/LoadBalancer = exposed
kubectl proxy The secure way to access ClusterIP services from your workstation
Don’t forget NetworkPolicy Restrict ingress to Dashboard Pods from admin namespaces only

Practice Questions


Q1: The Kubernetes Dashboard is deployed with a ClusterRoleBinding to cluster-admin. Explain the security risk and fix it.

Show Answer

Risk: A Dashboard bound to cluster-admin can:

  • Create, modify, or delete any resource in any namespace
  • Read all Secrets (including ServiceAccount tokens and TLS keys)
  • Exec into any Pod (arbitrary code execution)
  • Create Pods with hostPID, hostNetwork, or privileged security context
  • Effectively grants anyone with Dashboard access full cluster control

If the Dashboard is exposed without authentication, this is equivalent to giving anonymous users root access to the entire cluster.

Fix:

# 1. Find and delete the dangerous binding
kubectl get clusterrolebinding | grep dashboard
kubectl delete clusterrolebinding kubernetes-dashboard-admin

# 2. Create a restricted ClusterRole
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: dashboard-restricted
rules:
  - apiGroups: [""]
    resources: ["pods", "services", "namespaces"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch"]
EOF

# 3. Bind it to the Dashboard ServiceAccount
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: dashboard-restricted-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: dashboard-restricted
subjects:
  - kind: ServiceAccount
    name: kubernetes-dashboard
    namespace: kubernetes-dashboard
EOF

Q2: How would you securely access the Kubernetes Dashboard without exposing it outside the cluster?

Show Answer

Method 1: kubectl proxy

kubectl proxy --port=8001
# Access: http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/
  • Authenticates via kubeconfig
  • Only listens on localhost
  • No cluster-side network exposure

Method 2: kubectl port-forward

kubectl port-forward -n kubernetes-dashboard svc/kubernetes-dashboard 8443:443
# Access: https://localhost:8443
  • Direct tunnel to the Dashboard service
  • Only accessible locally
  • Requires a valid kubeconfig

Both methods:

  • Ensure the Service remains ClusterIP
  • No Ingress, NodePort, or LoadBalancer needed
  • Traffic is authenticated by kubectl before reaching the Dashboard

Q3: Write a NetworkPolicy that restricts Kubernetes Dashboard access to only Pods with label role: admin.

Show Answer
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: dashboard-admin-only
  namespace: kubernetes-dashboard
spec:
  podSelector:
    matchLabels:
      k8s-app: kubernetes-dashboard
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              role: admin
          namespaceSelector: {}
      ports:
        - protocol: TCP
          port: 8443

Notes:

  • podSelector targets the Dashboard Pod (label k8s-app: kubernetes-dashboard)
  • ingress.from allows only Pods labeled role: admin from any namespace
  • Port 8443 is the default Dashboard HTTPS port
  • All other ingress to the Dashboard is denied
  • namespaceSelector: {} combined with podSelector means “Pods with role: admin in any namespace”

Q4: What is the difference between exposing the Dashboard via NodePort vs. ClusterIP + kubectl proxy?

Show Answer
Aspect NodePort ClusterIP + kubectl proxy
Network exposure Accessible on every node’s IP at port 30000-32767 Only accessible via localhost
Authentication Depends on Dashboard config (may allow skip-login) kubectl authenticates via kubeconfig first
Firewall Must be blocked by external firewall No external port opened
Attack surface Anyone who can reach node IP:port Requires kubectl access + valid kubeconfig
Network scanning Discoverable via port scan Invisible to external scanners
Production use Never appropriate Recommended for ad-hoc access

NodePort risks:

  • Every node exposes the same port → larger attack surface
  • Network scanners find it easily
  • If no authentication is configured, it’s wide open
  • Even with firewall rules, accidental misconfiguration can expose it

Q5: An organization uses the Kubernetes Dashboard in production. What security controls should be in place?

Show Answer

Minimum required controls:

  1. RBAC: Bind the Dashboard ServiceAccount to a read-only ClusterRole. Never use cluster-admin.

  2. Service Type: Use ClusterIP only. Never NodePort or LoadBalancer.

  3. Authentication: Require token-based login. Disable skip-login: ```yaml

    In the Dashboard Deployment args

    args:

    • –enable-skip-login=false
    • –token-ttl=28800 # 8 hours ```
  4. NetworkPolicy: Restrict ingress to Dashboard Pods from authorized sources only.

  5. Short-lived tokens: Generate tokens with --duration flag:
    kubectl create token dashboard-user -n kubernetes-dashboard --duration=8h
    
  6. Audit logging: Ensure API audit policy captures Dashboard ServiceAccount requests.

  7. Regular review: Periodically audit Dashboard RBAC bindings:
    kubectl auth can-i --list --as=system:serviceaccount:kubernetes-dashboard:kubernetes-dashboard
    
  8. Consider removal: If the Dashboard isn’t actively used, remove it entirely — it’s attack surface with no benefit.

← Node Metadata Protection Binary Verification →

Updated: