Skip to main content

Minimize Access To Create PersistentVolume Objects

More Info:

Create access to PersistentVolume objects can be used to mount host paths and access node data. Restrict this permission to trusted administrators only.

Risk Level

Medium

Address

Security

Compliance Standards

  • CIS EKS

Triage and Remediation

Remediation

Manual Steps
  1. List all principals with PV create rights

    • Run on: any machine with kubectl access
    • Command:
      kubectl get clusterrole -o json \
      | jq -r '
      .items[]
      | select(
      .rules[]
      | select(
      ((.apiGroups // []) | index("") != null)
      and ((.resources // []) | index("persistentvolumes") != null)
      and ((.verbs // []) | index("create") != null)
      )
      )
      | .metadata.name
      ' | sort -u
    • Save the resulting ClusterRole names for review.
  2. Review each ClusterRole’s permissions and intended use

    • Run for each ClusterRole name from step 1:
      kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml
    • Manually confirm:
      • Why this role needs to create persistentvolumes (if at all).
      • Whether it is intended only for cluster administrators or for broader use.
  3. Identify which subjects are bound to those ClusterRoles

    • Run:
      kubectl get clusterrolebindings -o json \
      | jq -r '
      .items[]
      | select(.roleRef.kind=="ClusterRole"
      and (.roleRef.name | IN({"'$(kubectl get clusterrole -o json | jq -r ".items[].metadata.name" | paste -sd'","' -)'" })))
      | .metadata.name + " " + .roleRef.name
      '
    • Or, per role for clarity:
      kubectl get clusterrolebinding -o json \
      | jq -r '
      .items[]
      | select(.roleRef.kind=="ClusterRole" and .roleRef.name=="<CLUSTERROLE_NAME>")
      | .metadata.name + " " + (.subjects // [] | map(.kind+":"+(.namespace // "-")+":"+.name) | join(","))
      '
    • Manually assess whether each user/group/service account truly requires PV creation.
  4. Decide and implement RBAC tightening
    For each ClusterRole / subject combination where PV creation is not strictly required:

    • Option A – Remove the binding for non-admin subjects (preferred when only some subjects should lose access):
      • Edit binding:
        kubectl edit clusterrolebinding <BINDING_NAME>
      • Remove untrusted subjects from .subjects.
    • Option B – Remove PV create from the ClusterRole (only if no remaining subject needs it):
      • Edit role:
        kubectl edit clusterrole <CLUSTERROLE_NAME>
      • In rules, remove persistentvolumes from resources or create from verbs, as appropriate.
  5. Optionally create a dedicated admin-only PV role

    • If PV creation is needed only for cluster admins, define a narrowly scoped role and bind it only to trusted administrators:
      cat <<'EOF' | kubectl apply -f -
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRole
      metadata:
      name: pv-admin
      rules:
      - apiGroups: [""]
      resources: ["persistentvolumes"]
      verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
      EOF
    • Bind it only to your administrator group or user:
      cat <<'EOF' | kubectl apply -f -
      apiVersion: rbac.authorization.k8s.io/v1
      kind: ClusterRoleBinding
      metadata:
      name: pv-admin-binding
      roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: pv-admin
      subjects:
      - kind: User
      name: admin@example.com
      EOF
  6. Verify current PV create access after changes

    • Re-run the discovery from step 1 to ensure only intended ClusterRoles retain PV create:
      kubectl get clusterrole -o json \
      | jq -r '
      .items[]
      | select(
      .rules[]
      | select(
      ((.apiGroups // []) | index("") != null)
      and ((.resources // []) | index("persistentvolumes") != null)
      and ((.verbs // []) | index("create") != null)
      )
      )
      | .metadata.name
      ' | sort -u
    • Confirm that every remaining role with PV create is explicitly justified and bound only to trusted administrators.
Using kubectl
# 1) List all roles/clusterroles that can create PersistentVolumes
# Run on: any machine with kubectl access

kubectl get clusterrole -ojson \
| jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // []) | index("persistentvolumes")
and
(.verbs // []) | (index("create") or index("*"))
)
| any
)
| .metadata.name' | sort

kubectl get role -A -ojson \
| jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // []) | index("persistentvolumes")
and
(.verbs // []) | (index("create") or index("*"))
)
| any
)
| (.metadata.namespace + "/" + .metadata.name)' | sort

Problem indication: Any ClusterRole or namespaced Role in this list grants create (or *) on persistentvolumes. These are candidates for review; broad or non-admin sounding names (e.g. developer, ci, default, edit, view) are usually problematic.

# 2) Inspect each identified role/clusterrole in detail
# Replace <clusterrole-name> and <namespace>/<role-name> from step 1

# ClusterRole detail (rules, who should have this?)
kubectl get clusterrole <clusterrole-name> -o yaml

# Role detail
kubectl get role -n <namespace> <role-name> -o yaml

Problem indication: In the rules section, look for:

  • resources: ["persistentvolumes"] with verbs including create or *.
  • Very wide resources/verbs (e.g. resources: ["*"], verbs: ["*"]) assigned to non-admin use cases.
# 3) Find who is bound to these roles/clusterroles
# For each role/clusterrole from step 1

# ClusterRoleBindings for a given ClusterRole
kubectl get clusterrolebindings.rbac.authorization.k8s.io -ojson \
| jq -r '
.items[]
| select(.roleRef.kind=="ClusterRole" and .roleRef.name=="<clusterrole-name>")
| .metadata.name' | sort

# RoleBindings for a given Role (namespaced)
kubectl get rolebindings.rbac.authorization.k8s.io -n <namespace> -ojson \
| jq -r '
.items[]
| select(.roleRef.kind=="Role" and .roleRef.name=="<role-name>")
| .metadata.name' | sort

# Inspect each binding to see subjects (users, groups, service accounts)
kubectl get clusterrolebinding <binding-name> -o yaml
kubectl get rolebinding -n <namespace> <binding-name> -o yaml

Problem indication: In each binding’s subjects:

  • Non-admin users/groups (e.g. generic app teams, CI service accounts, default service accounts) bound to roles that can create persistentvolumes.
  • Wildcard or overly broad groups (e.g. system:authenticated) having such roles.
# 4) Cluster-wide review snapshot (optional quick overview)
# Shows all create-PV permissions and their subjects in one place (read-only)
kubectl get clusterrole,role -A -ojson \
| jq -r '
.items[]
| . as $role
| (.rules // [])
| map(
select(
(.resources // []) | index("persistentvolumes")
and
(.verbs // []) | (index("create") or index("*"))
)
)
| select(length>0)
| $role.kind + " " +
(if $role.kind=="ClusterRole" then $role.metadata.name
else ($role.metadata.namespace + "/" + $role.metadata.name)
end)' | while read kind name; do
echo "=== $kind $name ==="
if [ "$kind" = "ClusterRole" ]; then
kubectl get clusterrolebinding -ojson \
| jq -r --arg r "$name" '
.items[]
| select(.roleRef.kind=="ClusterRole" and .roleRef.name==$r)
| "CRB: " + .metadata.name + " subjects=" + (.subjects // [] | map(.kind+":"+(.namespace // "")+":"+.name) | join(","))'
else
ns="${name%%/*}"; rn="${name##*/}"
kubectl get rolebinding -n "$ns" -ojson \
| jq -r --arg r "$rn" '
.items[]
| select(.roleRef.kind=="Role" and .roleRef.name==$r)
| "RB: " + .metadata.name + " subjects=" + (.subjects // [] | map(.kind+":"+(.namespace // "")+":"+.name) | join(","))'
fi
echo
done

Problem indication: Any role/clusterrole that allows create on persistentvolumes and is bound to non-trusted administrator identities should be flagged for human review and potential restriction.

Automation
#!/usr/bin/env bash
#
# Report all subjects that can create PersistentVolumes (pv) cluster-wide.
# Run on: any machine with kubectl access and current kube-context set to the target cluster.

set -euo pipefail

# 1. Show all ClusterRoles/Roles that grant create on persistentvolumes
echo "=== RBAC rules granting create on persistentvolumes ==="
kubectl get clusterrole,role -A -o json | \
jq -r '
.items[]
| {
kind: .kind,
namespace: (.metadata.namespace // ""),
name: .metadata.name,
rules: (.rules // [])
}
| select(
.rules[]
| select(
((.resources // []) | index("persistentvolumes")) and
((.verbs // []) | index("create"))
)
)
| "\(.kind)\t\(.namespace)\t\(.name)"
' | sort -u | column -t

echo
echo "=== ClusterRole/Role details for those entries ==="
kubectl get clusterrole,role -A -o json | \
jq -r '
.items[]
| {
kind: .kind,
namespace: (.metadata.namespace // ""),
name: .metadata.name,
rules: (.rules // [])
} as $r
| select(
$r.rules[]
| select(
((.resources // []) | index("persistentvolumes")) and
((.verbs // []) | index("create"))
)
)
| (
"-----",
"KIND: \($r.kind)",
"NAMESPACE: \($r.namespace)",
"NAME: \($r.name)",
"RULES:",
(
$r.rules[]
| select(
((.resources // []) | index("persistentvolumes")) and
((.verbs // []) | index("create"))
)
| " apiGroups: \(.apiGroups // [])",
" resources: \(.resources // [])",
" verbs: \(.verbs // [])"
)
)
'

# 2. List all bindings that reference those ClusterRoles/Roles
echo
echo "=== RoleBindings/ClusterRoleBindings that use those roles ==="
# First, collect role names into a temp file for easy re-use.
TMP_ROLES="$(mktemp)"
kubectl get clusterrole,role -A -o json | \
jq -r '
.items[]
| {
kind: .kind,
namespace: (.metadata.namespace // ""),
name: .metadata.name,
rules: (.rules // [])
} as $r
| select(
$r.rules[]
| select(
((.resources // []) | index("persistentvolumes")) and
((.verbs // []) | index("create"))
)
)
| "\($r.kind),\($r.namespace),\($r.name)"
' | sort -u > "${TMP_ROLES}"

if [[ ! -s "${TMP_ROLES}" ]]; then
echo "No roles or clusterroles grant create on persistentvolumes."
rm -f "${TMP_ROLES}"
exit 0
fi

echo "Roles that can create persistentvolumes:"
cat "${TMP_ROLES}"
echo

echo "Bindings:"
kubectl get clusterrolebinding,rolebinding -A -o json | \
jq -r --argfile roles "${TMP_ROLES}" '
def role_match($ref):
($roles[] | split(",") ) as $r
| ($r[0] == ($ref.kind) and $r[2] == ($ref.name)
and ( ( ($ref.kind == "Role") and ($ref.namespace == $r[1]) )
or ($ref.kind == "ClusterRole") ) );

.items[]
| {
kind: .kind,
namespace: (.metadata.namespace // ""),
name: .metadata.name,
roleRef: .roleRef,
subjects: (.subjects // [])
} as $b
| select(
($b.kind == "ClusterRoleBinding" and
role_match({kind:"ClusterRole", name:$b.roleRef.name, namespace:""}))
or
($b.kind == "RoleBinding" and
role_match({kind:"Role", name:$b.roleRef.name, namespace:$b.namespace}))
)
| (
"-----",
"BINDING KIND: \($b.kind)",
"BINDING NAMESPACE: \($b.namespace)",
"BINDING NAME: \($b.name)",
" roleRef: \($b.roleRef.kind)/\($b.roleRef.name)",
" subjects:",
(
if ($b.subjects | length) == 0 then
" (none)"
else
($b.subjects[] |
" - kind: \(.kind)",
" name: \(.name)",
(if .namespace then " namespace: \(.namespace)" else empty end)
)
end
)
)
'

rm -f "${TMP_ROLES}"

echo
echo "=== INTERPRETATION ==="
echo "Any ClusterRole/Role listed above that is bound to broad subjects"
echo "(e.g. system:authenticated, system:serviceaccounts, default service accounts,"
echo "or non-admin users/groups) represents a potential problem."
echo
echo "Manually review:"
echo " - Whether each subject truly needs to create PersistentVolumes."
echo " - Whether the binding scope can be narrowed or the permission removed."

How to use and what indicates a problem

  • Run the script on any machine with kubectl and access to the cluster.
  • Focus on:
    • ClusterRoles/Rules that include resources: ["persistentvolumes"] and verbs including create.
    • Bindings where those roles are granted to:
      • Wide groups like system:authenticated, system:serviceaccounts, or system:unauthenticated.
      • Namespace default service accounts (e.g. kind: ServiceAccount, name: default).
      • Regular users/groups that are not trusted cluster administrators.

Any such combination (privileged role + broad/non-admin subject) is the condition that needs manual review and likely restriction, per the benchmark.