Skip to main content

Minimize Access To Create Persistent Volumes

More Info:

Creating PersistentVolumes can enable privilege escalation via hostPath volumes. This permission should be limited to trusted administrators only.

Risk Level

High

Address

Security

Compliance Standards

  • CIS AKS

Triage and Remediation

Remediation

Manual Steps
  1. On any machine with kubectl access, list all ClusterRoles and Roles that can create PersistentVolumes:

    kubectl get clusterrole -o json | jq -r '
    .items[]
    | select(.rules[]? | any(.apiGroups[]? == "" and (.resources[]? | IN("persistentvolumes","persistentvolume")) and (.verbs[]? | IN("create","*"))))
    | .metadata.name' | sort -u
    kubectl get role -A -o json | jq -r '
    .items[]
    | select(.rules[]? | any(.apiGroups[]? == "" and (.resources[]? | IN("persistentvolumes","persistentvolume")) and (.verbs[]? | IN("create","*"))))
    | [.metadata.namespace, .metadata.name] | @tsv' | sort -u
  2. For each identified ClusterRole/Role, list the subjects bound to it to understand who can create PersistentVolumes:

    # ClusterRoleBindings
    kubectl get clusterrolebinding -o json | jq -r '
    .items[]
    | select(.roleRef.kind=="ClusterRole" and .roleRef.name==("NAME_OF_CLUSTERROLE_HERE"))
    | .metadata.name as $b
    | .subjects[]? | [$b,.kind,.namespace//"",.name] | @tsv'
    # RoleBindings (namespaced)
    kubectl get rolebinding -A -o json | jq -r '
    .items[]
    | select(.roleRef.kind=="Role" and .roleRef.name==("NAME_OF_ROLE_HERE"))
    | .metadata.namespace as $ns
    | .metadata.name as $b
    | .subjects[]? | [$ns,$b,.kind,.namespace//"",.name] | @tsv'
  3. For each Role/ClusterRole and its bound subjects, classify whether they are trusted administrators (e.g., dedicated admin groups, platform SRE service accounts) or non‑administrative users/workloads by reviewing your access model and group mappings (for example via your IdP or kubectl describe on ServiceAccounts):

    kubectl describe clusterrole NAME_OF_CLUSTERROLE_HERE
    kubectl describe role -n NAMESPACE NAME_OF_ROLE_HERE
    kubectl describe sa -n NAMESPACE SERVICEACCOUNT_NAME
  4. For any non‑administrative subject that should not create PersistentVolumes, edit the associated Role/ClusterRole to remove the create verb on persistentvolumes while preserving other needed permissions:

    kubectl edit clusterrole NAME_OF_CLUSTERROLE_HERE
    # or
    kubectl edit role -n NAMESPACE NAME_OF_ROLE_HERE

    In the editor, locate rules where resources includes persistentvolumes (or persistentvolume) and remove create (and * if overly broad) from verbs for that resource, or split the rule so PersistentVolumes no longer have create granted.

  5. Where a Role/ClusterRole exists solely to grant PV creation to non‑admins and is no longer required, remove the bindings or the role itself (after confirming no legitimate dependency):

    # Remove bindings first
    kubectl delete clusterrolebinding NAME_OF_BINDING
    kubectl delete rolebinding -n NAMESPACE NAME_OF_BINDING

    # Optionally delete the unused role
    kubectl delete clusterrole NAME_OF_CLUSTERROLE_HERE
    kubectl delete role -n NAMESPACE NAME_OF_ROLE_HERE
  6. Verify that PV creation is now limited to the intended admin roles only by re-running the evidence collection and confirming that only trusted admin roles retain create on PersistentVolumes:

    kubectl get clusterrole -o json | jq -r '
    .items[]
    | select(.rules[]? | any(.apiGroups[]? == "" and (.resources[]? | IN("persistentvolumes","persistentvolume")) and (.verbs[]? | IN("create","*"))))
    | .metadata.name' | sort -u
    kubectl get role -A -o json | jq -r '
    .items[]
    | select(.rules[]? | any(.apiGroups[]? == "" and (.resources[]? | IN("persistentvolumes","persistentvolume")) and (.verbs[]? | IN("create","*"))))
    | [.metadata.namespace, .metadata.name] | @tsv' | sort -u
Using kubectl
# 1) List all ClusterRoles and Roles that can create PersistentVolumes
# Run on: any machine with kubectl access

kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
.rules[]
| (.resources[]? | select(. == "persistentvolumes"))
and
(.verbs[]? | select(. == "create" or . == "*"))
)
| .metadata.name
' | sort -u

kubectl get role --all-namespaces -o json \
| jq -r '
.items[]
| select(
.rules[]
| (.resources[]? | select(. == "persistentvolumes"))
and
(.verbs[]? | select(. == "create" or . == "*"))
)
| [.metadata.namespace, .metadata.name] | @tsv
' | sort -u

Problem indication:

  • Any listed ClusterRole or Role is capable of creating PersistentVolumes.
  • Pay particular attention to:
    • Broadly named roles such as edit, developer, default, or app-specific roles not intended for cluster administration.
    • Roles in application namespaces (e.g., team-a/*, prod-app/*) that include create or * on persistentvolumes.
# 2) Inspect the detailed rules of each suspicious ClusterRole
# Replace <clusterrole-name> with a name from the list above

kubectl get clusterrole <clusterrole-name> -o yaml

Problem indication:

  • Under rules: you see entries like:
    • resources: ["persistentvolumes"] with verbs: containing create or *.
  • If the role name/purpose suggests non-admin usage, this is likely excessive.
# 3) Inspect the detailed rules of each suspicious Role
# Replace <namespace> and <role-name> with values from the roles list

kubectl get role -n <namespace> <role-name> -o yaml

Problem indication:

  • Same as for ClusterRole: any rule granting create or * on persistentvolumes to non-admin roles is suspect.
# 4) See who is bound to a suspicious ClusterRole
# Replace <clusterrole-name> with a candidate from step 1

kubectl get clusterrolebinding -o json \
| jq -r --arg CR "<clusterrole-name>" '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == $CR)
| .metadata.name
'

# For each binding:
kubectl get clusterrolebinding <binding-name> -o yaml

Problem indication:

  • subjects: includes:
    • Broad groups (e.g., system:authenticated, developers, ci-users).
    • Service accounts used by applications rather than administrators.
  • If such subjects are bound to a role that can create PersistentVolumes, this is a likely violation.
# 5) See who is bound to a suspicious Role
# Replace <namespace> and <role-name>

kubectl get rolebinding -n <namespace> -o json \
| jq -r --arg RB "<role-name>" '
.items[]
| select(.roleRef.kind == "Role" and .roleRef.name == $RB)
| .metadata.name
'

# For each binding:
kubectl get rolebinding -n <namespace> <binding-name> -o yaml

Problem indication:

  • Same as for ClusterRoleBinding: if non-admin users, groups, or service accounts are subjects, and the role grants create on persistentvolumes, this needs review.
# 6) (Optional) Spot-check who can create PersistentVolumes (simulation)
# Replace <user-or-sa> with an identity you want to test, e.g.:
# --user alice
# --serviceaccount app-namespace:app-sa

kubectl auth can-i create persistentvolumes --user <user-or-sa>

Problem indication:

  • Output yes for identities that are not trusted administrators suggests privileges that should be reviewed and likely reduced.
Automation
#!/usr/bin/env bash
# Report ClusterRoles/Roles that can create PersistentVolumes (pv) for review.
# Run on: any machine with kubectl access and current context set to the target cluster.

set -euo pipefail

echo "=== Checking ClusterRoles with create access on persistentvolumes ==="
kubectl get clusterrole -o json \
| jq -r '
.items[]
| {
name: .metadata.name,
rules: (
.rules[]
| select(.resources[]? == "persistentvolumes")
| select(.verbs[]? == "create")
)
}
| select(.rules != null)
| "CLUSTERROLE: \(.name)\n RESOURCES: \(.rules.resources | join(","))\n VERBS: \(.rules.verbs | join(","))\n"
' | sed '/^$/d' || true

echo
echo "=== Checking Roles with create access on persistentvolumes (all namespaces) ==="
kubectl get role -A -o json \
| jq -r '
.items[]
| {
namespace: .metadata.namespace,
name: .metadata.name,
rules: (
.rules[]
| select(.resources[]? == "persistentvolumes")
| select(.verbs[]? == "create")
)
}
| select(.rules != null)
| "ROLE: \(.namespace)/\(.name)\n RESOURCES: \(.rules.resources | join(","))\n VERBS: \(.rules.verbs | join(","))\n"
' | sed '/^$/d' || true

echo
echo "=== Showing RoleBindings/ClusterRoleBindings that reference the above (for context) ==="
echo "This helps identify which subjects (users/groups/serviceaccounts) receive PV create permissions."

echo
echo "--- ClusterRoleBindings (may grant PV create via ClusterRoles above) ---"
kubectl get clusterrolebinding -o wide

echo
echo "--- RoleBindings (may grant PV create via Roles above, per-namespace) ---"
kubectl get rolebinding -A -o wide

Explanation of output that indicates a problem:

  • Any CLUSTERROLE: or ROLE: entry listed by this script that includes:
    • RESOURCES: persistentvolumes (or includes persistentvolumes in a list), and
    • VERBS: containing create
  • These roles grant the ability to create PersistentVolume objects.
  • For each such role, review who receives it using the clusterrolebinding and rolebinding listings:
    • If bound to non-administrative users, groups, or service accounts, this is a potential problem per CISAKS 4.1.8 and should be reviewed and likely removed or restricted.