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
Remediation
Manual Steps
-
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.
-
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.
- Why this role needs to create
- Run for each ClusterRole name from step 1:
-
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.
- Run:
-
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.
- Edit binding:
- Option B – Remove PV create from the ClusterRole (only if no remaining subject needs it):
- Edit role:
kubectl edit clusterrole <CLUSTERROLE_NAME>
- In
rules, removepersistentvolumesfromresourcesorcreatefromverbs, as appropriate.
- Edit role:
- Option A – Remove the binding for non-admin subjects (preferred when only some subjects should lose access):
-
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/v1kind: ClusterRolemetadata:name: pv-adminrules:- 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/v1kind: ClusterRoleBindingmetadata:name: pv-admin-bindingroleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: pv-adminsubjects:- kind: Username: admin@example.comEOF
- If PV creation is needed only for cluster admins, define a narrowly scoped role and bind it only to trusted administrators:
-
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.
- Re-run the discovery from step 1 to ensure only intended ClusterRoles retain PV
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"]withverbsincludingcreateor*.- 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
kubectland access to the cluster. - Focus on:
- ClusterRoles/Rules that include
resources: ["persistentvolumes"]andverbsincludingcreate. - Bindings where those roles are granted to:
- Wide groups like
system:authenticated,system:serviceaccounts, orsystem:unauthenticated. - Namespace default service accounts (e.g.
kind: ServiceAccount,name: default). - Regular users/groups that are not trusted cluster administrators.
- Wide groups like
- ClusterRoles/Rules that include
Any such combination (privileged role + broad/non-admin subject) is the condition that needs manual review and likely restriction, per the benchmark.