Minimize Access To Create Persistent Volumes
More Info:
The ability to create PersistentVolumes can be used to mount hostPath volumes and access the underlying node. Limit who can create them.
Risk Level
Medium
Address
Security
Compliance Standards
- CIS Kubernetes
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify all subjects that can create PersistentVolumes
- Run on: any machine with kubectl access
kubectl get clusterrole -o json | jq -r '.items[]| select(.rules[]?| any(.apiGroups[]? == "" or .apiGroups[]? == "v1";.resources[]? == "persistentvolumes";(.verbs[]? == "create" or .verbs[]? == "*")))| .metadata.name' | sort -uSave this list of ClusterRoles for review.
-
Review the permissions in each identified ClusterRole
- Run on: any machine with kubectl access
for cr in $(kubectl get clusterrole -o name); dokubectl get "$cr" -o yaml | \yq '. | select(.rules[]?| any(.apiGroups[]? == "" or .apiGroups[]? == "v1";.resources[]? == "persistentvolumes";(.verbs[]? == "create" or .verbs[]? == "*")) )'doneFor each displayed ClusterRole, decide if the
createpermission onpersistentvolumesis truly required (e.g., storage admin vs general workloads). -
Map ClusterRoles with PV create permission to actual users/groups/service accounts
- Run on: any machine with kubectl access
# ClusterRoleBindingskubectl get clusterrolebinding -o yaml | \yq '.items[]| select(.roleRef.kind == "ClusterRole" and(.roleRef.name == "cluster-admin" or(.roleRef.name as $r |env.CR_LIST | split(" ") | any(. == $r))))| {name: .metadata.name, role: .roleRef.name, subjects: .subjects}' \CR_LIST="$(kubectl get clusterrole -o json | jq -r '.items[]| select(.rules[]?| any(.apiGroups[]? == "" or .apiGroups[]? == "v1";.resources[]? == "persistentvolumes";(.verbs[]? == "create" or .verbs[]? == "*")))| .metadata.name' | tr '\n' ' ')"Manually review each binding’s
subjectsto see which identities gain PV create capability. -
Decide and implement least-privilege changes
For each ClusterRole wherecreateonpersistentvolumesis NOT strictly needed:- Edit the ClusterRole to remove
create(and*if unused) forpersistentvolumes:- Run on: any machine with kubectl access
In thekubectl edit clusterrole <CLUSTERROLE_NAME>rulessection, removecreatefromverbsforresources: ["persistentvolumes"], or split out a dedicated rule if other verbs are still required.
If an identity needs only PV consumption, bind it instead to a narrower role that lacks PVcreate.
- Edit the ClusterRole to remove
-
If PV creation is needed, confine it to dedicated admin roles
- Ensure only a small, storage-admin–style ClusterRole retains
createonpersistentvolumes. - Rebind ClusterRoleBindings so that:
- General developers and application service accounts do NOT reference that ClusterRole.
- Only designated admin groups/users (e.g., an ops group in your IdP) are subjects of the storage admin ClusterRoleBinding:
Adjust thekubectl edit clusterrolebinding <BINDING_NAME>
subjectslist accordingly.
- Ensure only a small, storage-admin–style ClusterRole retains
-
Verify effective reduction of PV create access
- Re-run the permission discovery and confirm only the intended ClusterRoles retain PV
create:kubectl get clusterrole -o json | jq -r '.items[]| select(.rules[]?| any(.apiGroups[]? == "" or .apiGroups[]? == "v1";.resources[]? == "persistentvolumes";(.verbs[]? == "create" or .verbs[]? == "*")))| .metadata.name' | sort -u - Optionally, as a non-privileged user/service account, attempt to create a
PersistentVolumeand confirm it is rejected with aforbiddenerror.
- Re-run the permission discovery and confirm only the intended ClusterRoles retain PV
Using kubectl
Using kubectl
1. List all ClusterRoles that can create PersistentVolumes
Run on: any machine with kubectl access.
kubectl get clusterroles -o json | jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // []) as $r
| (.verbs // []) as $v
| select( ("persistentvolumes" | IN($r[]?)) and ("create" | IN($v[]?)) )
)
| length > 0
)
| .metadata.name
'
If you don’t have jq, use:
kubectl get clusterroles -o yaml | \
awk '
$0 ~ /^kind: ClusterRole/ { inrole=1; name="" }
inrole && $1 == "name:" { name=$2 }
/rules:/ { inrules=1 }
inrules && /resources:/ { inres=1 }
inrules && /verbs:/ { inverbs=1 }
inres && /persistentvolumes/ { gotpv=1 }
inverbs && /create/ { gotcreate=1 }
/^$/ {
if (inrole && gotpv && gotcreate) print name
inrole=inrules=inres=inverbs=gotpv=gotcreate=0
}
'
Output indicating a problem:
Any ClusterRole name returned here grants create on persistentvolumes to whoever is bound to it. These roles need review to see if that permission is justified.
2. Inspect each identified ClusterRole’s rules in detail
For each ClusterRole name from step 1:
kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml
Review the rules: section, focusing on entries where:
apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["create", ...]
Output indicating a problem:
- Roles that are broad (e.g.,
verbs: ["*"]orresources: ["*"]) and includepersistentvolumes. - Roles meant for general users, CI/CD, or application service accounts that also have
createonpersistentvolumeswithout a clear operational need.
3. Discover who actually gets these permissions (ClusterRoleBindings)
For each problematic ClusterRole from step 2:
kubectl get clusterrolebindings -o json | jq -r '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "<CLUSTERROLE_NAME>")
| .metadata.name
'
Without jq:
kubectl get clusterrolebindings -o yaml | \
awk -v target="<CLUSTERROLE_NAME>" '
$0 ~ /^kind: ClusterRoleBinding/ { inb=1; name="" }
inb && $1 == "name:" { name=$2 }
inb && $1 == "roleRef:" { inrole=1 }
inrole && $1 == "name:" && $2 == target { print name }
/^$/ { inb=inrole=0 }
'
Then inspect each binding:
kubectl get clusterrolebinding <BINDING_NAME> -o yaml
Output indicating a problem:
- Bindings that grant the ClusterRole to:
system:authenticated,system:unauthenticated, or large groups (e.g.,developers,ci-users) where most members do not need PV creation.- ServiceAccounts in namespaces unrelated to storage/cluster operations.
- Any binding where you cannot clearly justify why that subject must create PersistentVolumes.
4. Verification after manual changes
After you have manually updated or removed roles/bindings (via manifests or kubectl edit), re-run step 1:
kubectl get clusterroles -o json | jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // []) as $r
| (.verbs // []) as $v
| select( ("persistentvolumes" | IN($r[]?)) and ("create" | IN($v[]?)) )
)
| length > 0
)
| .metadata.name
'
Verification result interpretation:
- If no ClusterRole names are returned, then no ClusterRole currently grants
createonpersistentvolumes. - If only a small, well-justified set of highly privileged operational roles are returned, you must confirm they are appropriate; if not, further manual adjustment is required.
Automation
#!/usr/bin/env bash
# Report who can create PersistentVolumes (CIS Kubernetes 5.1.9)
set -euo pipefail
echo "=== ClusterRoles and ClusterRoleBindings with 'create' on persistentvolumes ==="
echo
# 1) List ClusterRoles that can create persistentvolumes
kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
.rules // []
| any(.resources[]? == "persistentvolumes"
and (.verbs[]? == "create" or .verbs[]? == "*"))
)
| .metadata.name
' | sort -u | while read -r cr; do
[ -z "$cr" ] && continue
echo "ClusterRole: ${cr}"
kubectl get clusterrole "$cr" -o json \
| jq -r '
.rules[]
| select(any(.resources[]? == "persistentvolumes"))
| " apiGroups: " + ( (.apiGroups // []) | join(",") )
+ "\n resources: " + ( (.resources // []) | join(",") )
+ "\n verbs: " + ( (.verbs // []) | join(",") )
'
echo
# Show who gets this ClusterRole via ClusterRoleBinding
echo " Bound via ClusterRoleBindings:"
kubectl get clusterrolebinding -o json \
| jq -r --arg cr "$cr" '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == $cr)
| " ClusterRoleBinding: " + .metadata.name
+ "\n subjects: "
+ ( if (.subjects // [] | length) == 0
then "NONE"
else
( .subjects[]
| " - kind: " + (.kind // "")
+ ", name: " + (.name // "")
+ (if .namespace then ", namespace: " + .namespace else "" end)
)
| join("\n")
end
)
'
echo
done
echo "=== Roles and RoleBindings with 'create' on persistentvolumes (namespace-scoped RBAC) ==="
echo
# 2) List namespace-scoped Roles that can create persistentvolumes
kubectl get role --all-namespaces -o json \
| jq -r '
.items[]
| select(
.rules // []
| any(.resources[]? == "persistentvolumes"
and (.verbs[]? == "create" or .verbs[]? == "*"))
)
| .metadata.namespace + " " + .metadata.name
' | sort -u | while read -r ns name; do
[ -z "$ns" ] && continue
echo "Role: ${name} (namespace: ${ns})"
kubectl get role "$name" -n "$ns" -o json \
| jq -r '
.rules[]
| select(any(.resources[]? == "persistentvolumes"))
| " apiGroups: " + ( (.apiGroups // []) | join(",") )
+ "\n resources: " + ( (.resources // []) | join(",") )
+ "\n verbs: " + ( (.verbs // []) | join(",") )
'
echo
# Show who gets this Role via RoleBinding
echo " Bound via RoleBindings:"
kubectl get rolebinding -n "$ns" -o json \
| jq -r --arg name "$name" '
.items[]
| select(.roleRef.kind == "Role" and .roleRef.name == $name)
| " RoleBinding: " + .metadata.name
+ "\n subjects: "
+ ( if (.subjects // [] | length) == 0
then "NONE"
else
( .subjects[]
| " - kind: " + (.kind // "")
+ ", name: " + (.name // "")
+ (if .namespace then ", namespace: " + .namespace else "" end)
)
| join("\n")
end
)
'
echo
done
echo "=== Summary: subjects that can create PersistentVolumes (cluster-scoped) ==="
echo
# 3) Compact subject summary for ClusterRoles
kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
.rules // []
| any(.resources[]? == "persistentvolumes"
and (.verbs[]? == "create" or .verbs[]? == "*"))
)
| .metadata.name
' | sort -u | while read -r cr; do
[ -z "$cr" ] && continue
kubectl get clusterrolebinding -o json \
| jq -r --arg cr "$cr" '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == $cr)
| .subjects[]?
| $cr + " -> " + (.kind // "") + "/" + (.name // "")
+ (if .namespace then " (ns:" + .namespace + ")" else "" end)
'
done | sort -u
How to run (any machine with kubectl access):
- Save as
report-pv-create-access.sh - Make executable:
chmod +x report-pv-create-access.sh - Run:
./report-pv-create-access.sh
What indicates a problem:
- Any
ClusterRoleorRolewhose rules include:resources: persistentvolumeswithverbscontainingcreateor*.
- Especially concerning:
- These roles bound to broad subjects, for example:
kind: Group, name: system:authenticatedkind: Group, name: system:masters(if that group is widely used)- ServiceAccounts used by application workloads rather than storage controllers.
- These roles bound to broad subjects, for example:
- For the summary section, any line like:
cluster-admin -> Group/system:authenticatedsome-pv-manager -> ServiceAccount/default (ns:default)should be reviewed to confirm that subject truly requires PV creation rights.