Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Harden Kubernetes Role-Based Access Control by implementing least-privilege policies, auditing role bindings, eliminating cluster-admin sprawl, and integrating external identity providers.
.claude/skills/implementing-rbac-hardening-for-kubernetes/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-18 | ✗→✗ | = Same ✗ | — | — |
Kubernetes RBAC regulates access to cluster resources based on roles assigned to users, groups, and service accounts. Default configurations often grant excessive permissions, and without active hardening, RBAC becomes a primary attack vector for privilege escalation, lateral movement, and data exfiltration. Hardening requires implementing least-privilege principles, eliminating unnecessary ClusterRole bindings, separating service accounts, integrating external identity providers, and continuous auditing.
Audit and remove unnecessary cluster-admin bindings:
bash# List all cluster-admin bindings kubectl get clusterrolebindings -o json | jq -r ' .items[] | select(.roleRef.name == "cluster-admin") | "\(.metadata.name) -> \(.subjects[]? | "\(.kind)/\(.name) (\(.namespace // "cluster"))")" '
Use Role and RoleBinding instead of ClusterRole and ClusterRoleBinding:
yaml# Good: Namespace-scoped role apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: application name: app-developer rules: - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch"] - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: application name: app-developer-binding subjects: - kind: Group name: dev-team apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: app-developer apiGroup: rbac.authorization.k8s.io
yamlapiVersion: v1 kind: ServiceAccount metadata: name: payment-processor namespace: payments automountServiceAccountToken: false # Disable auto-mount --- apiVersion: apps/v1 kind: Deployment metadata: name: payment-processor namespace: payments spec: template: spec: serviceAccountName: payment-processor automountServiceAccountToken: true # Only mount when explicitly needed containers: - name: processor image: payments/processor:v2.1@sha256:abc...
Block permissions that enable privilege escalation:
yaml# Dangerous verbs/resources to restrict: # - secrets: get, list, watch (exposes all secrets in namespace) # - pods/exec: create (enables command execution in pods) # - pods: create with privileged securityContext # - serviceaccounts/token: create (generates new tokens) # - clusterroles/clusterrolebindings: create, update (self-escalation) # - nodes/proxy: create (bypasses API server authorization) # Safe read-only role example apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: security-viewer rules: - apiGroups: [""] resources: ["pods", "services", "namespaces", "nodes"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments", "daemonsets", "statefulsets"] verbs: ["get", "list", "watch"] - apiGroups: ["networking.k8s.io"] resources: ["networkpolicies"] verbs: ["get", "list", "watch"]
yaml# API server flags for OIDC integration apiVersion: v1 kind: Pod metadata: name: kube-apiserver spec: containers: - name: kube-apiserver command: - kube-apiserver - --oidc-issuer-url=https://idp.company.com - --oidc-client-id=kubernetes - --oidc-username-claim=email - --oidc-groups-claim=groups - --oidc-ca-file=/etc/kubernetes/pki/oidc-ca.crt
bash# All ClusterRoleBindings with subjects kubectl get clusterrolebindings -o json | jq -r ' .items[] | select(.subjects != null) | .subjects[] as $s | "\(.metadata.name) | \(.roleRef.name) | \($s.kind)/\($s.name)" ' | sort | column -t -s '|' # All RoleBindings across namespaces kubectl get rolebindings --all-namespaces -o json | jq -r ' .items[] | select(.subjects != null) | .subjects[] as $s | "\(.metadata.namespace) | \(.metadata.name) | \(.roleRef.name) | \($s.kind)/\($s.name)" ' | sort | column -t -s '|'
bash# Find service accounts with cluster-admin or admin roles kubectl get clusterrolebindings -o json | jq -r ' .items[] | select(.roleRef.name == "cluster-admin" or .roleRef.name == "admin") | select(.subjects[]?.kind == "ServiceAccount") | "\(.subjects[] | select(.kind == "ServiceAccount") | "\(.namespace)/\(.name)")" '
bash# Find pods using the default service account kubectl get pods --all-namespaces -o json | jq -r ' .items[] | select(.spec.serviceAccountName == "default" or .spec.serviceAccountName == null) | "\(.metadata.namespace)/\(.metadata.name)" '
bash# Find pods with auto-mounted service account tokens kubectl get pods --all-namespaces -o json | jq -r ' .items[] | select(.spec.automountServiceAccountToken != false) | "\(.metadata.namespace)/\(.metadata.name) sa=\(.spec.serviceAccountName // "default")" '
bash# Install rbac-lookup kubectl krew install rbac-lookup # View RBAC for a specific user kubectl rbac-lookup developer@company.com # View all RBAC bindings wide format kubectl rbac-lookup --kind user -o wide
bash# Install rakkess kubectl krew install access-matrix # Show access matrix for current user kubectl access-matrix # Show access for a specific service account kubectl access-matrix --sa payments:payment-processor
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.