Skip to main content

Minimize Access To Create Pods

More Info:​

The ability to create pods can be abused to run arbitrary workloads and escalate privileges. Restrict create access on pod objects to trusted subjects only.

Risk Level​

High

Address​

Security

Compliance Standards​

  • CIS EKS

Triage and Remediation​

Remediation​

Manual Steps
  1. List all Roles/ClusterRoles that can create pods

    • Run on: any machine with kubectl access
    kubectl get roles,clusterroles -A -o json | jq '
    [.items[] |
    select(
    .rules[]? |
    (.resources[]? == "pods" and .verbs[]? == "create")
    )
    ] | .[].metadata | {kind: .ownerReferences[0].kind, name: .name, namespace: .namespace}'

    Review each listed Role/ClusterRole to determine which subjects (users/groups/serviceaccounts) really need to create pods.

  2. Inspect a specific ClusterRole’s permissions

    • Run on: any machine with kubectl access
      Replace CLUSTERROLE_NAME with the actual name:
    kubectl get clusterrole CLUSTERROLE_NAME -o yaml

    Identify rules where resources includes pods and verbs includes create.

  3. Remove create on pods from a ClusterRole that should not have it

    • Run on: any machine with kubectl access
      Edit the ClusterRole:
    kubectl edit clusterrole CLUSTERROLE_NAME

    In each rules entry that has resources: ["pods", ...] (or includes pods), remove create from the verbs list (keep other verbs like get, list, watch if still required), then save and exit.

  4. Adjust namespace-scoped Roles similarly

    • Run on: any machine with kubectl access
      For each namespace where Roles were identified:
    kubectl get role ROLE_NAME -n NAMESPACE -o yaml
    kubectl edit role ROLE_NAME -n NAMESPACE

    In rules, remove create from verbs wherever resources includes pods. Save when done.

  5. Re-verify there are no unintended subjects that still require pod creation

    • Run on: any machine with kubectl access
      For any remaining Roles/ClusterRoles that must retain pods create access (e.g., system components or trusted CI/CD), document the justification and ensure their RoleBindings/ClusterRoleBindings are scoped only to the required subjects:
    kubectl get rolebinding,clusterrolebinding -A -o yaml | grep -E "name: CLUSTERROLE_NAME|subjects:"

    Tighten bindings as needed (edit or delete) so only trusted subjects retain this access.

  6. Verification

    • Run on: any machine with kubectl access
    access=$(kubectl get roles,clusterroles -N kube-system -A -o json | jq '
    [.items[] |
    select(
    .rules[]? |
    (.resources[]? == "pods" and .verbs[]? == "create")
    )
    ] | length')

    if [ "$access" -gt 0 ]; then
    echo "pods_create_access"
    else
    echo "no_pods_create_access_or_only_approved_remaining"
    fi

    Confirm that any remaining pods create permissions correspond only to explicitly approved, trusted Roles/ClusterRoles.

Using kubectl

On any machine with kubectl access:

  1. Identify Roles/ClusterRoles that can create pods
kubectl get roles,clusterroles -A -o json | jq -r '
.items[]
| select(
.rules[]? |
(.resources[]? == "pods" and .verbs[]? == "create")
)
| "\(.kind)/\(.metadata.name) [ns=\(.metadata.namespace // "cluster-scope")]"
'
  1. For each offending Role/ClusterRole, fetch its manifest
  • Namespaced Role (replace NAMESPACE and ROLE_NAME):
kubectl get role ROLE_NAME -n NAMESPACE -o yaml > role-ROLE_NAME.yaml
  • ClusterRole (replace CLUSTERROLE_NAME):
kubectl get clusterrole CLUSTERROLE_NAME -o yaml > clusterrole-CLUSTERROLE_NAME.yaml
  1. Edit the manifest(s) locally to remove create from any rule that grants it on pods.

Example before:

rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "create"]

Example after (remove create):

rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]

If a rule is only for create on pods, delete that rule block entirely.

  1. Apply the updated manifest(s)
kubectl apply -f role-ROLE_NAME.yaml
kubectl apply -f clusterrole-CLUSTERROLE_NAME.yaml

(Repeat for each edited file.)

  1. Re-run the verification
access=$(kubectl get roles,clusterroles -N -o json | jq '
[.items[] |
select(
.rules[]? |
(.resources[]? == "pods" and .verbs[]? == "create")
)
] | length')

if [ "$access" -gt 0 ]; then
echo "pods_create_access"
else
echo "no_pods_create_access"
fi
Automation
#!/usr/bin/env bash
set -euo pipefail

# Minimize access to create pods on Roles and ClusterRoles
# Runs on: any machine with kubectl access and current-context pointing to target cluster

# REQUIREMENTS:
# - kubectl installed and configured
# - jq installed

echo "[+] Discovering Roles/ClusterRoles with 'create' on pods..."

tmpdir="$(mktemp -d)"
trap 'rm -rf "${tmpdir}"' EXIT

# Get all roles and clusterroles in JSON
kubectl get roles,clusterroles -A -o json > "${tmpdir}/all-roles.json"

# Extract all affected Role/ClusterRole names and kinds
jq -r '
.items[]
| select(.rules[]? | (.resources[]? == "pods" and .verbs[]? == "create"))
| [.kind, .metadata.name, (.metadata.namespace // "")] | @tsv
' "${tmpdir}/all-roles.json" > "${tmpdir}/affected.tsv" || true

if [ ! -s "${tmpdir}/affected.tsv" ]; then
echo "[+] No Roles or ClusterRoles grant create on pods. Nothing to change."
else
echo "[+] The following objects grant create on pods:"
column -t "${tmpdir}/affected.tsv"

echo
echo "[!] This script will REMOVE the 'create' verb from any rule that includes 'pods'."
echo " It does NOT modify RoleBindings/ClusterRoleBindings, nor does it change"
echo " rules granting other verbs on pods."
echo

while IFS=$'\t' read -r kind name namespace; do
ns_arg=()
if [ -n "${namespace}" ] && [ "${kind}" = "Role" ]; then
ns_arg=(--namespace "${namespace}")
fi

echo "[+] Processing ${kind}/${name}${namespace:+ in namespace ${namespace}}"

# Fetch current YAML
kubectl get "${kind}" "${name}" "${ns_arg[@]}" -o yaml > "${tmpdir}/${kind}_${namespace}_${name}.yaml"

# Use yq if available for safer YAML manipulation; otherwise use jq+yaml round-trip through kubectl
if command -v yq >/dev/null 2>&1; then
# Remove 'create' verb from any rule on 'pods'
yq '
(.rules // [])
|= map(
if (.resources // [] | any(. == "pods")) and (.verbs // [] | any(. == "create")) then
.verbs |= map(select(. != "create"))
else
.
end
)
' "${tmpdir}/${kind}_${namespace}_${name}.yaml" > "${tmpdir}/${kind}_${namespace}_${name}.patched.yaml"
else
# Fallback: convert to JSON, edit with jq, apply as YAML via kubectl.
# This will preserve semantics but may reorder fields.
kubectl get "${kind}" "${name}" "${ns_arg[@]}" -o json \
| jq '
.rules |=
( . // [] | map(
if (.resources // [] | any(. == "pods")) and (.verbs // [] | any(. == "create")) then
.verbs |= map(select(. != "create"))
else
.
end
)
)
' > "${tmpdir}/${kind}_${namespace}_${name}.patched.json"
# Convert back to YAML via kubectl apply -f - directly from JSON
fi

# Apply the patched object
if command -v yq >/dev/null 2>&1; then
kubectl apply -f "${tmpdir}/${kind}_${namespace}_${name}.patched.yaml"
else
kubectl apply -f "${tmpdir}/${kind}_${namespace}_${name}.patched.json"
fi

done < "${tmpdir}/affected.tsv"
fi

echo
echo "[+] Verifying that no Roles or ClusterRoles grant 'create' on pods..."

access=$(kubectl get roles,clusterroles -A -o json | jq '
[.items[] |
select(
.rules[]? |
(.resources[]? == "pods" and .verbs[]? == "create")
)
] | length')

if [ "${access}" -gt 0 ]; then
echo "[-] Verification FAILED: ${access} Role(s)/ClusterRole(s) still grant 'create' on pods."
echo " Inspect with:"
echo " kubectl get roles,clusterroles -A -o json | jq -r \"
.items[]
| select(.rules[]? | (.resources[]? == \\\"pods\\\" and .verbs[]? == \\\"create\\\"))
| {kind, name: .metadata.name, namespace: .metadata.namespace}
\""
exit 1
else
echo "[+] Verification PASSED: no Roles or ClusterRoles grant 'create' on pods."
fi