Restricting Access to GUI Elements
What Are "GUI Elements" in Kubernetes?
In the Kubernetes context, "GUI elements" refers to any web-based management interface that is deployed within or provides access to a cluster. These interfaces present a significant attack surface because they often provide visual, easy-to-use access to sensitive cluster operations -- meaning a compromised GUI can lead to full cluster compromise with minimal attacker effort.
Common GUI elements include:
| GUI Element | Purpose | Risk Level |
|---|---|---|
| Kubernetes Dashboard | Cluster management web UI | Critical -- can create/delete any resource |
| Grafana | Metrics visualization | High -- exposes infrastructure metrics, may have admin APIs |
| Prometheus UI | Metrics querying | Medium -- exposes internal service discovery and metrics |
| Kibana / OpenSearch Dashboards | Log analysis | High -- exposes application and system logs |
| ArgoCD UI | GitOps CD management | Critical -- can deploy arbitrary workloads |
| Jenkins | CI/CD pipeline execution | Critical -- can execute arbitrary code |
| Rancher / Lens | Cluster management | Critical -- full cluster control |
| Weave Scope | Cluster visualization | High -- exposes topology and can exec into containers |
CKS Exam Relevance
The CKS exam may ask you to:
- Identify and remove an insecurely deployed Kubernetes Dashboard
- Restrict access to a web UI using NetworkPolicies
- Audit services exposed via NodePort or LoadBalancer
- Configure RBAC for dashboard service accounts with least privilege
- Use
kubectl proxyorport-forwardfor secure access to internal UIs
Why GUI Elements Are Dangerous
Real-World Incident: Tesla Cryptojacking (2018)
Attackers discovered an unprotected Kubernetes Dashboard exposed to the internet on Tesla's infrastructure. The dashboard had no authentication, allowing attackers to deploy cryptocurrency mining workloads and access AWS credentials stored in environment variables. This is one of the most cited examples of why GUI security matters.
Kubernetes Dashboard Deep Dive
The Kubernetes Dashboard is the most commonly encountered GUI element and the one most likely to appear on the CKS exam.
Default Dashboard Deployment Risks
When the Kubernetes Dashboard is deployed with default settings, several risks arise:
| Risk | Description |
|---|---|
| Overly permissive ServiceAccount | Default install may bind to cluster-admin or overly broad ClusterRole |
| Skip login enabled | Some configurations allow bypassing authentication entirely |
| Exposed via NodePort/LoadBalancer | Makes the dashboard accessible from outside the cluster |
| No NetworkPolicy | Any pod in the cluster can access the dashboard service |
| Token-based auth without expiry | Long-lived tokens can be stolen and reused |
Auditing the Current Dashboard Deployment
# Check if the dashboard is deployed
kubectl get deployments -n kubernetes-dashboard
kubectl get deployments -A | grep -i dashboard
# Check the dashboard service type
kubectl get svc -n kubernetes-dashboard
# Look for Type: NodePort or LoadBalancer -- these are dangerous
# Check dashboard ServiceAccount and its bindings
kubectl get sa -n kubernetes-dashboard
kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(.subjects[]? | .namespace == "kubernetes-dashboard") |
"\(.metadata.name) -> \(.roleRef.name)"'
# Check what permissions the dashboard service account has
kubectl auth can-i --list \
--as=system:serviceaccount:kubernetes-dashboard:kubernetes-dashboard \
-n kubernetes-dashboard
# Check for skip-login argument in the dashboard deployment
kubectl get deploy kubernetes-dashboard -n kubernetes-dashboard -o yaml | \
grep -A5 "args"Why --enable-skip-login Is Dangerous
The --enable-skip-login flag allows users to bypass the login screen entirely. When enabled, anyone who can reach the dashboard can use it with whatever permissions the dashboard's own service account has.
# Check if skip-login is enabled
kubectl get deploy kubernetes-dashboard -n kubernetes-dashboard -o yaml | \
grep "enable-skip-login"
# If found, remove it by editing the deployment
kubectl edit deploy kubernetes-dashboard -n kubernetes-dashboard
# Remove the --enable-skip-login argument from the container argsNever Use Skip Login
The --enable-skip-login flag should never be used in any environment. If an exam question involves a dashboard with this flag, removing it is likely part of the answer.
Dashboard RBAC Configuration
Step 1: Create a Read-Only ClusterRole for Dashboard
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: dashboard-read-only
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "namespaces", "nodes", "events",
"persistentvolumeclaims", "replicationcontrollers", "resourcequotas"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets", "statefulsets", "replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "networkpolicies"]
verbs: ["get", "list", "watch"]
# NOTE: Secrets are deliberately excluded
# NOTE: No create, update, delete, or patch verbsStep 2: Create a Dedicated Service Account
apiVersion: v1
kind: ServiceAccount
metadata:
name: dashboard-viewer
namespace: kubernetes-dashboardStep 3: Bind the ClusterRole to the Service Account
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dashboard-viewer-binding
subjects:
- kind: ServiceAccount
name: dashboard-viewer
namespace: kubernetes-dashboard
roleRef:
kind: ClusterRole
name: dashboard-read-only
apiGroup: rbac.authorization.k8s.ioStep 4: Create a Token for the Service Account
# Create a time-limited token (recommended)
kubectl create token dashboard-viewer \
-n kubernetes-dashboard \
--duration=8h
# Or create a long-lived token secret (less recommended)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: dashboard-viewer-token
namespace: kubernetes-dashboard
annotations:
kubernetes.io/service-account.name: dashboard-viewer
type: kubernetes.io/service-account-token
EOF
# Retrieve the token
kubectl get secret dashboard-viewer-token \
-n kubernetes-dashboard \
-o jsonpath='{.data.token}' | base64 -dExam Tip
Use kubectl create token for time-limited tokens. This is the preferred method since Kubernetes 1.24+ as tokens are bound and expire automatically.
Step 5: Verify the Permissions
# Confirm read-only access
kubectl auth can-i get pods \
--as=system:serviceaccount:kubernetes-dashboard:dashboard-viewer
# yes
kubectl auth can-i delete pods \
--as=system:serviceaccount:kubernetes-dashboard:dashboard-viewer
# no
kubectl auth can-i get secrets \
--as=system:serviceaccount:kubernetes-dashboard:dashboard-viewer
# no
kubectl auth can-i create deployments \
--as=system:serviceaccount:kubernetes-dashboard:dashboard-viewer
# no
# Full permission list
kubectl auth can-i --list \
--as=system:serviceaccount:kubernetes-dashboard:dashboard-viewerRemoving Dangerous Dashboard RBAC
If the dashboard was deployed with overly broad permissions, you need to identify and fix them:
# Find any ClusterRoleBindings granting cluster-admin to dashboard service accounts
kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(
(.roleRef.name == "cluster-admin") and
(.subjects[]? | .namespace == "kubernetes-dashboard")
) | .metadata.name'
# Delete the dangerous binding
kubectl delete clusterrolebinding <binding-name>
# Replace with the read-only binding created above
kubectl apply -f dashboard-viewer-binding.yamlSecure Access Patterns
Access Methods Comparison
| Access Method | Security Level | Authentication | Use Case |
|---|---|---|---|
kubectl proxy | High | Uses kubeconfig credentials | Admin access from local machine |
kubectl port-forward | High | Uses kubeconfig credentials | Accessing a specific service temporarily |
| Ingress + OAuth2/OIDC | Medium-High | External identity provider | Team access with SSO |
| Ingress + mTLS | Medium-High | Client certificate | Service-to-service or trusted clients |
| Ingress (no auth) | Low | None | Never recommended |
| NodePort | Very Low | None (unless app enforces) | Never use for management UIs |
| LoadBalancer | Very Low | None (unless app enforces) | Never use for management UIs |
Method 1: kubectl proxy (Recommended for Admin Access)
kubectl proxy creates a secure tunnel between your local machine and the API server. It uses your kubeconfig credentials, so authentication is already handled.
# Start the proxy
kubectl proxy
# Starting to serve on 127.0.0.1:8001
# Access the dashboard at:
# http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/
# The proxy:
# - Listens ONLY on localhost (127.0.0.1)
# - Uses your kubeconfig for authentication
# - Proxies requests to the API server
# - No ports are exposed to the networkMethod 2: kubectl port-forward (For Specific Services)
# Forward the dashboard service to a local port
kubectl port-forward svc/kubernetes-dashboard -n kubernetes-dashboard 8443:443
# Access at: https://localhost:8443
# Forward Grafana
kubectl port-forward svc/grafana -n monitoring 3000:3000
# Forward Prometheus UI
kubectl port-forward svc/prometheus-server -n monitoring 9090:9090
# Forward ArgoCD
kubectl port-forward svc/argocd-server -n argocd 8080:443When to Use Each
kubectl proxy: Best for the Kubernetes Dashboard specifically, as it handles the API path automaticallykubectl port-forward: Best for any specific service (Grafana, Prometheus, ArgoCD, etc.)- Both methods require valid kubeconfig credentials and never expose ports beyond localhost
Method 3: Ingress with Authentication (For Team Access)
If a GUI must be accessible to a team (not just a single admin), use an Ingress with authentication:
# Example: Ingress with OAuth2 proxy for Grafana
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
namespace: monitoring
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://oauth2-proxy.example.com/oauth2/start?rd=$scheme://$host$request_uri"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- grafana.internal.example.com
secretName: grafana-tls
rules:
- host: grafana.internal.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 3000NetworkPolicies for GUI Services
NetworkPolicies add a layer of defense by controlling which pods and IP addresses can reach GUI services.
Restrict Dashboard Access to Specific Namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: restrict-dashboard-access
namespace: kubernetes-dashboard
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: kubernetes-dashboard
policyTypes:
- Ingress
ingress:
# Only allow traffic from the monitoring namespace
- from:
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- protocol: TCP
port: 8443Restrict Dashboard Access to Specific CIDR (Admin Network Only)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: dashboard-admin-only
namespace: kubernetes-dashboard
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: kubernetes-dashboard
policyTypes:
- Ingress
ingress:
# Only allow traffic from the admin network CIDR
- from:
- ipBlock:
cidr: 10.0.100.0/24
ports:
- protocol: TCP
port: 8443Default Deny All Ingress for Dashboard Namespace
# Start with a default deny, then add specific allow rules
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: kubernetes-dashboard
spec:
podSelector: {}
policyTypes:
- Ingress
---
# Then allow only kubectl proxy / API server traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-apiserver-only
namespace: kubernetes-dashboard
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: kubernetes-dashboard
policyTypes:
- Ingress
ingress:
- from:
- ipBlock:
cidr: 10.96.0.1/32 # API server ClusterIP (adjust for your cluster)
ports:
- protocol: TCP
port: 8443Securing Other GUI Elements
Grafana
# Check if Grafana is deployed
kubectl get deploy -A | grep -i grafana
# Check the service type
kubectl get svc -A | grep -i grafana
# Ensure it is ClusterIP, NOT NodePort or LoadBalancer
# Change from NodePort/LoadBalancer to ClusterIP
kubectl patch svc grafana -n monitoring -p '{"spec": {"type": "ClusterIP"}}'Key hardening steps for Grafana:
- Disable anonymous access in
grafana.ini:[auth.anonymous] enabled = false - Change the default admin password from
admin - Use OAuth2 or LDAP for authentication
- Set the service type to
ClusterIP - Apply a NetworkPolicy restricting ingress
Prometheus UI
# Check Prometheus service
kubectl get svc -A | grep -i prometheus
# Prometheus UI has no built-in authentication
# It should NEVER be exposed externally
# Access via port-forward only:
kubectl port-forward svc/prometheus-server -n monitoring 9090:9090ArgoCD
# Check ArgoCD service
kubectl get svc -n argocd
# Ensure argocd-server is ClusterIP
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "ClusterIP"}}'
# ArgoCD has built-in authentication
# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d
# Change the default password immediately after first login
# Access via port-forward
kubectl port-forward svc/argocd-server -n argocd 8080:443General Principles for Any Web UI
Hardening Checklist for Any Web UI
- Service type: Always use
ClusterIP-- neverNodePortorLoadBalancer - Authentication: Ensure login is required; disable anonymous/guest access
- Default credentials: Change all default passwords immediately
- RBAC: Apply least-privilege permissions to the UI's service account
- NetworkPolicy: Restrict which pods/networks can reach the UI
- TLS: Ensure the UI uses HTTPS, not plain HTTP
- Access method: Use
kubectl proxyorport-forwardfor admin access - Audit: Regularly check for newly deployed web UIs
Removing Unnecessary GUI Components
If a GUI component is not needed, the most secure option is to remove it entirely.
Audit for Web UIs in the Cluster
# Find all services exposed as NodePort or LoadBalancer
kubectl get svc -A -o wide | grep -E 'NodePort|LoadBalancer'
# Find dashboard-related resources
kubectl get all -n kubernetes-dashboard
# Find deployments that might be web UIs (common names)
kubectl get deploy -A | grep -iE 'dashboard|grafana|prometheus|kibana|jenkins|argocd|weave|rancher'
# Find services on common web ports
kubectl get svc -A -o json | \
jq -r '.items[] | select(.spec.ports[]? | .port == 80 or .port == 443 or .port == 8080 or .port == 8443 or .port == 3000 or .port == 9090) | "\(.metadata.namespace)/\(.metadata.name) port:\(.spec.ports[].port) type:\(.spec.type)"'
# Find ingresses that might expose web UIs
kubectl get ingress -ARemove the Kubernetes Dashboard
# Delete all dashboard resources
kubectl delete namespace kubernetes-dashboard
# Or if installed via Helm
helm uninstall kubernetes-dashboard -n kubernetes-dashboard
# Verify removal
kubectl get all -n kubernetes-dashboard
# No resources found
# Also check for any lingering ClusterRoles and ClusterRoleBindings
kubectl get clusterroles | grep -i dashboard
kubectl get clusterrolebindings | grep -i dashboard
# Delete any remaining cluster-scoped resources
kubectl delete clusterrole kubernetes-dashboard
kubectl delete clusterrolebinding kubernetes-dashboardConvert NodePort/LoadBalancer Services to ClusterIP
# Find all NodePort services
kubectl get svc -A --field-selector spec.type=NodePort
# Patch a service to ClusterIP
kubectl patch svc <service-name> -n <namespace> \
-p '{"spec": {"type": "ClusterIP"}}'
# Find all LoadBalancer services
kubectl get svc -A --field-selector spec.type=LoadBalancer
# Patch to ClusterIP
kubectl patch svc <service-name> -n <namespace> \
-p '{"spec": {"type": "ClusterIP"}}'Exam Tip
If an exam question asks you to secure a dashboard or web UI, the steps are usually:
- Check the service type and change to
ClusterIPif needed - Check the RBAC bindings and remove
cluster-adminaccess - Remove
--enable-skip-loginif present - Apply a NetworkPolicy to restrict access
- If the question says "remove" the dashboard, delete the namespace and cluster-scoped resources
Full Hardening Example
Here is a complete example of taking a poorly configured dashboard and securing it:
Before: Insecure Dashboard
# Dashboard exposed as NodePort with cluster-admin access
kubectl get svc -n kubernetes-dashboard
# NAME TYPE CLUSTER-IP PORT(S)
# kubernetes-dashboard NodePort 10.96.45.123 443:30443/TCP
kubectl get clusterrolebindings | grep dashboard
# dashboard-admin ClusterRole/cluster-admin ...
kubectl get deploy kubernetes-dashboard -n kubernetes-dashboard -o yaml | grep args -A5
# args:
# - --enable-skip-login # DANGEROUS
# - --namespace=kubernetes-dashboardAfter: Hardened Dashboard
# Step 1: Remove skip-login
kubectl patch deploy kubernetes-dashboard -n kubernetes-dashboard \
--type='json' \
-p='[{"op": "remove", "path": "/spec/template/spec/containers/0/args/0"}]'
# Verify the args list no longer contains --enable-skip-login
# Step 2: Change service type to ClusterIP
kubectl patch svc kubernetes-dashboard -n kubernetes-dashboard \
-p '{"spec": {"type": "ClusterIP"}}'
# Step 3: Remove the cluster-admin binding
kubectl delete clusterrolebinding dashboard-admin
# Step 4: Apply read-only RBAC (using the YAML from earlier sections)
kubectl apply -f dashboard-read-only-clusterrole.yaml
kubectl apply -f dashboard-viewer-sa.yaml
kubectl apply -f dashboard-viewer-binding.yaml
# Step 5: Apply NetworkPolicy
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: dashboard-restrict
namespace: kubernetes-dashboard
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: kubernetes-dashboard
policyTypes:
- Ingress
ingress: []
# Empty ingress = deny all ingress
# Access only via kubectl proxy (which goes through the API server)
EOF
# Step 6: Verify
kubectl get svc -n kubernetes-dashboard
# TYPE should be ClusterIP
kubectl auth can-i delete pods \
--as=system:serviceaccount:kubernetes-dashboard:dashboard-viewer
# no
kubectl get networkpolicy -n kubernetes-dashboard
# dashboard-restrictQuick Reference
Exam Speed Reference
# Find all web UIs and dashboards
kubectl get deploy -A | grep -iE 'dashboard|grafana|prometheus|kibana|jenkins|argocd'
# Find services exposed externally
kubectl get svc -A | grep -E 'NodePort|LoadBalancer'
# Check dashboard service account permissions
kubectl auth can-i --list \
--as=system:serviceaccount:kubernetes-dashboard:kubernetes-dashboard
# Change service to ClusterIP
kubectl patch svc <name> -n <ns> -p '{"spec": {"type": "ClusterIP"}}'
# Delete the dashboard entirely
kubectl delete ns kubernetes-dashboard
kubectl delete clusterrole kubernetes-dashboard
kubectl delete clusterrolebinding kubernetes-dashboard
# Secure access via proxy
kubectl proxy
# http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/
# Secure access via port-forward
kubectl port-forward svc/kubernetes-dashboard -n kubernetes-dashboard 8443:443
# Create a time-limited dashboard token
kubectl create token dashboard-viewer -n kubernetes-dashboard --duration=8h
# Check for skip-login flag
kubectl get deploy kubernetes-dashboard -n kubernetes-dashboard -o yaml | grep skip-loginKey Exam Takeaways
- Kubernetes Dashboard is the most exam-relevant GUI element -- know how to audit, secure, and remove it
- Never expose management UIs via
NodePortorLoadBalancer-- always useClusterIP - Never use
--enable-skip-login-- it bypasses all authentication - Remove
cluster-adminbindings from dashboard service accounts and replace with read-only roles - Use
kubectl proxyorkubectl port-forwardfor secure local access to any web UI - Apply NetworkPolicies to restrict which pods and networks can reach GUI services
- Audit regularly for web UIs exposed on NodePort, LoadBalancer, or unauthenticated Ingresses
- If a GUI component is not needed, the most secure action is to remove it entirely by deleting its namespace and cluster-scoped resources
- Always create dedicated service accounts with minimal RBAC for any dashboard that must remain deployed