Skip to main content

The Cluster Admin Role Is Only Used Where Required

More Info:

The RBAC role cluster-admin provides wide-ranging powers over the environment and should be used only where and when needed.

Risk Level

Critical

Address

Security

Compliance Standards

  • APRA CPS 234 (Australia)
  • BSI C5 (Germany)
  • Brazil LGPD
  • CCPA / CPRA (California)
  • CIS Critical Security Controls v8
  • CIS GKE
  • CMMC 2.0
  • CSA Cloud Controls Matrix v4
  • DPDPA
  • Digital Operational Resilience Act (EU)
  • Essential 8
  • ISO/IEC 27017
  • ISO/IEC 27018
  • ISO/IEC 27701
  • KSA PDPL
  • MAS Technology Risk Management (Singapore)
  • MITRE ATT&CK (Cloud)
  • NIS2 Directive
  • NIST SP 800-171
  • NYDFS 23 NYCRR 500
  • SWIFT Customer Security Controls Framework
  • Sarbanes-Oxley IT General Controls
  • Securities and Exchange Board of India (SEBI) - Cloud Security Adoption Framework
  • UK NCSC Cyber Assessment Framework

Triage and Remediation

Remediation

Manual Steps
  1. On any machine with kubectl access, list all ClusterRoleBindings that grant cluster-admin and see who they bind to:

    kubectl get clusterrolebindings \
    -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS_KIND:.subjects[*].kind,SUBJECTS_NAME:.subjects[*].name \
    | grep 'cluster-admin'
  2. For each binding you found, display full details (to distinguish system vs non-system subjects):

    kubectl get clusterrolebinding <binding-name> -o yaml

    Manually review each subject; plan to keep bindings only if they are:

    • Group with name system:masters, or
    • other system: prefixed subjects that are required for core components.
  3. For any non-system user/group/service account that does not strictly require cluster-admin, design or select a least-privileged Role/ClusterRole that matches its actual needs. For example, to create a narrower ClusterRole for read-only access to pods and services:

    cat << 'EOF' > readonly-pods-services-clusterrole.yaml
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
    name: readonly-pods-services
    rules:
    - apiGroups: [""]
    resources: ["pods", "services"]
    verbs: ["get", "list", "watch"]
    EOF

    kubectl apply -f readonly-pods-services-clusterrole.yaml
  4. Bind the least-privileged role to the principal instead of cluster-admin, adjusting kind/name/namespace as appropriate:

    • Example for a user:
      cat << 'EOF' > readonly-binding-user.yaml
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRoleBinding
      metadata:
      name: readonly-pods-services-user-alice
      subjects:
      - kind: User
      name: alice@example.com
      roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: readonly-pods-services
      EOF

      kubectl apply -f readonly-binding-user.yaml
    • Example for a service account:
      cat << 'EOF' > readonly-binding-sa.yaml
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRoleBinding
      metadata:
      name: readonly-pods-services-sa-myapp
      subjects:
      - kind: ServiceAccount
      name: myapp
      namespace: default
      roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: readonly-pods-services
      EOF

      kubectl apply -f readonly-binding-sa.yaml
  5. After confirming that the replacement binding(s) exist and are correct, remove the excessive cluster-admin binding(s), being careful not to delete required system: bindings or the system:masters mapping:

    kubectl delete clusterrolebinding <binding-name>
  6. Verification (on any machine with kubectl access): rerun the audit, which should now return NO_CLUSTER_ADMIN_BINDINGS or only allowlist subjects:

    kubectl get clusterrolebindings -o json | jq -r '
    [
    .items[]
    | select(.roleRef.name == "cluster-admin")
    | .subjects[]?
    | select(.kind != "Group" or .name != "system:masters")
    ]
    | if length == 0
    then "NO_CLUSTER_ADMIN_BINDINGS"
    else "FOUND_CLUSTER_ADMIN_BINDING"
    end
    '
Using kubectl

On any machine with kubectl access:

  1. List all ClusterRoleBindings that grant cluster-admin and inspect subjects:
kubectl get clusterrolebindings -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name | grep cluster-admin

For deeper inspection of a specific binding:

kubectl get clusterrolebinding <binding-name> -o yaml
  1. For each non-system subject that does not truly require cluster-admin, create or select a least‑privileged (Cluster)Role and bind to that instead.

Example: create a narrowly scoped ClusterRole (edit as appropriate):

cat << 'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: app-ops-readonly
rules:
- apiGroups: [""]
resources: ["pods","services","configmaps"]
verbs: ["get","list","watch"]
EOF

Bind the principal to this least‑privileged ClusterRole:

cat << 'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: app-ops-readonly-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: app-ops-readonly
subjects:
- kind: User
name: user@example.com
apiGroup: rbac.authorization.k8s.io
EOF
  1. Remove excessive cluster-admin bindings for non-system principals (do NOT delete any system: bindings or the system:masters group):
kubectl delete clusterrolebinding <binding-name>

Repeat for each binding you have replaced with a least‑privileged alternative.

  1. Verification (on any machine with kubectl access):
kubectl get clusterrolebindings -o json | jq -r '
[
.items[]
| select(.roleRef.name == "cluster-admin")
| .subjects[]?
| select(.kind != "Group" or .name != "system:masters")
]
| if length == 0
then "NO_CLUSTER_ADMIN_BINDINGS"
else "FOUND_CLUSTER_ADMIN_BINDING"
end
'
Automation
#!/usr/bin/env bash
# Purpose: Assist review of cluster-admin bindings and optionally remove non-required ones.
# Scope: Run on any machine with kubectl access and correct context.
# Notes: This is a MANUAL control. This script does NOT auto-delete anything without confirmation.

set -euo pipefail

echo "=== Discovering all ClusterRoleBindings to role 'cluster-admin' ==="
kubectl get clusterrolebindings -o=custom-columns=NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name \
| grep 'cluster-admin' || true

echo
echo "=== Detailed JSON view of non-system 'cluster-admin' subjects (for review) ==="
kubectl get clusterrolebindings -o json | jq '
[
.items[]
| select(.roleRef.name == "cluster-admin")
| {
name: .metadata.name,
subjects: [
.subjects[]?
| select(.kind != "Group" or .name != "system:masters")
]
}
]
| map(select(.subjects | length > 0))
'

echo
echo "======================================================================"
echo "MANUAL REVIEW REQUIRED:"
echo " 1. For each binding above, decide if the subjects truly require full"
echo " cluster-admin. If not, design a least-privileged (Cluster)Role."
echo " 2. Create that Role/ClusterRole and bind subjects to it BEFORE removal."
echo " 3. Do NOT modify or delete any 'system:' prefixed bindings."
echo "======================================================================"
echo

read -r -p "List candidate ClusterRoleBindings (excluding 'system:' prefixes) for deletion? [y/N]: " LIST_CAND
if [[ "${LIST_CAND:-N}" =~ ^[Yy]$ ]]; then
echo
echo "=== Candidate ClusterRoleBindings (roleRef.name == 'cluster-admin', name does NOT start with 'system:') ==="
kubectl get clusterrolebindings -o json | jq -r '
.items[]
| select(.roleRef.name == "cluster-admin")
| select(.metadata.name | startswith("system:") | not)
| .metadata.name
'
fi

read -r -p "Enter a ClusterRoleBinding name to DELETE (or leave blank to skip deletion): " CRB_NAME
if [[ -n "${CRB_NAME}" ]]; then
echo
echo "You are about to DELETE ClusterRoleBinding: ${CRB_NAME}"
echo "Ensure you have already created replacement least-privileged bindings as needed."
read -r -p "Type the binding name again to confirm deletion: " CRB_CONFIRM
if [[ "${CRB_CONFIRM}" == "${CRB_NAME}" ]]; then
echo "Deleting ClusterRoleBinding '${CRB_NAME}'..."
kubectl delete clusterrolebinding "${CRB_NAME}"
else
echo "Confirmation did not match. Skipping deletion."
fi
else
echo "No deletion requested. Proceeding to verification."
fi

echo
echo "=== Verification: re-running benchmark audit logic ==="
kubectl get clusterrolebindings -o json | jq -r '
[
.items[]
| select(.roleRef.name == "cluster-admin")
| .subjects[]?
| select(.kind != "Group" or .name != "system:masters")
]
| if length == 0
then "NO_CLUSTER_ADMIN_BINDINGS"
else "FOUND_CLUSTER_ADMIN_BINDING"
end
'

Additional Reading: