Skip to main content

AWS Minimize Access Create Pods

More Info:​

The ability to create pods in a namespace can provide a number of opportunities for privilege escalation, such as assigning privileged service accounts to these pods or mounting hostPaths with access to sensitive data (unless Pod Security Policies are implemented to restrict this access)

Risk Level​

Low

Address​

Security

Compliance Standards​

  • APRA CPS 234 (Australia)
  • BSI C5 (Germany)
  • Brazil LGPD
  • CCPA / CPRA (California)
  • CIS Critical Security Controls v8
  • CIS EKS
  • CMMC 2.0
  • CSA Cloud Controls Matrix v4
  • DPDPA
  • Digital Operational Resilience Act (EU)
  • Essential 8
  • ISO/IEC 27017
  • ISO/IEC 27018
  • ISO/IEC 27701
  • KSA PDPL
  • MAS Technology Risk Management (Singapore)
  • MITRE ATT&CK (Cloud)
  • NIST SP 800-171
  • NYDFS 23 NYCRR 500
  • SWIFT Customer Security Controls Framework
  • Sarbanes-Oxley IT General Controls
  • UK NCSC Cyber Assessment Framework

Triage and Remediation​

Remediation​

Manual Steps
  1. List all roles/clusterroles that grant create on pods (run on any machine with kubectl access):

    kubectl get roles,clusterroles -A -o json | jq '
    .items[]
    | select(.rules[]? | (.resources[]? == "pods" and .verbs[]? == "create"))
    | {kind, name:.metadata.name, namespace:.metadata.namespace, rules:.rules}'
  2. For each Role that should not allow pod creation, edit it to remove create from any rule that includes pods (run on any machine with kubectl):

    # Example for a namespaced Role
    kubectl -n <NAMESPACE> edit role <ROLE_NAME>

    In the editor, find any rule like:

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

    Change to:

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

    or remove the entire rule if pods access is not needed, then save and exit.

  3. For each ClusterRole that should not allow pod creation, edit it similarly (run on any machine with kubectl):

    kubectl edit clusterrole <CLUSTERROLE_NAME>

    In the editor, remove create from any verbs list that applies to pods, or remove the pods rule entirely, then save and exit.

  4. For any Role/ClusterRole that must still allow some users to create pods, consider creating a separate, narrowly-scoped role for them and binding it only to the required subjects (run on any machine with kubectl):

    # Example: create a dedicated role allowing pod create only in one namespace
    kubectl -n <NAMESPACE> apply -f - <<'EOF'
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
    name: pod-creator-limited
    rules:
    - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create"]
    EOF

    # Bind it to a specific user/group/serviceaccount
    kubectl -n <NAMESPACE> apply -f - <<'EOF'
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
    name: pod-creator-limited-binding
    subjects:
    - kind: User
    name: example-user
    roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: Role
    name: pod-creator-limited
    EOF
  5. Review RoleBindings and ClusterRoleBindings to ensure only intended subjects are linked to any remaining roles/clusterroles that have pods create (run on any machine with kubectl):

    kubectl get rolebindings,clusterrolebindings -A -o yaml | less

    Adjust bindings as needed:

    kubectl -n <NAMESPACE> edit rolebinding <ROLEBINDING_NAME>
    kubectl edit clusterrolebinding <CLUSTERROLEBINDING_NAME>
  6. Verification (run on any machine with kubectl access):

    access=$(kubectl get roles,clusterroles -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_remaining"
    fi
Using kubectl

On any machine with kubectl access:

  1. List all 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.namespace + ":" + .metadata.name)
'
  1. For each Role in a namespace where pod creation should be removed, edit it to drop create on pods:

Example: remove create for pods from a specific Role

# View the Role YAML
kubectl -n <NAMESPACE> get role <ROLE_NAME> -o yaml > /tmp/role-<ROLE_NAME>.yaml

Edit /tmp/role-<ROLE_NAME>.yaml:

  • In every .rules[] where resources includes pods, remove create from the verbs list.
  • If verbs becomes empty, remove that rule entry entirely.

Apply the updated Role:

kubectl apply -f /tmp/role-<ROLE_NAME>.yaml
  1. For each ClusterRole where pod creation should be removed, do the same:
kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml > /tmp/clusterrole-<CLUSTERROLE_NAME>.yaml

Edit /tmp/clusterrole-<CLUSTERROLE_NAME>.yaml:

  • In every .rules[] where resources includes pods, remove create from verbs, or delete the rule if it only provided create on pods.

Apply the updated ClusterRole:

kubectl apply -f /tmp/clusterrole-<CLUSTERROLE_NAME>.yaml
  1. (Optional) If a role exists only to permit pod creation and is no longer needed, delete it:
kubectl -n <NAMESPACE> delete role <ROLE_NAME>
kubectl delete clusterrole <CLUSTERROLE_NAME>
  1. Verify that no Roles/ClusterRoles retain create on pods:
access=$(kubectl get roles,clusterroles -N -o json | jq '
[.items[] |
select(
.rules[]? |
(.resources[]? == "pods" and .verbs[]? == "create")
)
] | length')

if [ "$access" -eq 0 ]; then
echo "OK: no pods create access in Roles/ClusterRoles"
else
echo "pods_create_access still present in $access role(s)"
fi
Automation
#!/usr/bin/env bash
#
# Minimize access to create pods (CISEKS 4.1.4)
#
# This script:
# - Lists all Roles/ClusterRoles that have "create" on "pods"
# - Interactively removes that specific permission from each role
# - Verifies that no roles remain with pods/create
#
# REQUIREMENTS:
# - Run on any machine with kubectl access and cluster-admin privileges
# - jq must be installed
#

set -euo pipefail

# Namespace to store backups of modified roles
BACKUP_NS="rbac-backup"

echo "==> Ensuring backup namespace exists: ${BACKUP_NS}"
kubectl get ns "${BACKUP_NS}" >/dev/null 2>&1 || kubectl create namespace "${BACKUP_NS}" >/dev/null

echo "==> Detecting Roles and ClusterRoles with pods/create access"

# Get all roles/clusterroles that match the condition
matches_json=$(kubectl get roles,clusterroles -A -o json | jq '
.items
| map(
select(
.rules[]? as $r
| ($r.resources[]? == "pods" and $r.verbs[]? == "create")
)
)')

count=$(echo "${matches_json}" | jq 'length')

if [ "${count}" -eq 0 ]; then
echo "No Roles or ClusterRoles grant create on pods. Nothing to do."
else
echo "Found ${count} Roles/ClusterRoles with pods/create:"
echo "${matches_json}" | jq -r '.[] | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'
fi

# Function to strip only "create" verb from rules that target "pods"
strip_pods_create() {
jq '
.rules |= map(
if (.resources? // [] | index("pods")) != null and (.verbs? // [] | index("create")) != null then
.verbs |= (map(select(. != "create")))
else
.
end
)
# Drop any rules that end up with no verbs
| .rules |= map(select((.verbs // []) | length > 0))
'
}

# Process each matching role/clusterrole
echo
echo "==> Reviewing and updating roles"

for i in $(seq 0 $((count - 1))); do
item=$(echo "${matches_json}" | jq ".[$i]")
kind=$(echo "${item}" | jq -r '.kind')
name=$(echo "${item}" | jq -r '.metadata.name')
ns=$(echo "${item}" | jq -r '.metadata.namespace // empty')

if [ "${kind}" = "Role" ]; then
fqname="Role/${ns}/${name}"
else
fqname="ClusterRole/${name}"
fi

echo
echo "------------------------------------------------------------"
echo "Processing ${fqname}"

# Show current rules that include pods/create
echo "Current rules including pods/create:"
echo "${item}" | jq '.rules[] | select(.resources? // [] | index("pods")) | select(.verbs? // [] | index("create"))'

# Skip system and well-known controller roles by default; these often need pods/create
# You may adjust this list based on your environment.
if [[ "${name}" =~ ^system: ]] || [[ "${name}" =~ controller ]] || [[ "${name}" =~ kubernetes-dashboard ]]; then
echo "NOTE: ${fqname} appears to be a system or controller role."
read -r -p "Skip modifying this role? [Y/n]: " ans
ans=${ans:-Y}
if [[ "${ans}" =~ ^[Yy]$ ]]; then
echo "Skipping ${fqname}"
continue
fi
fi

read -r -p "Remove 'create' on 'pods' from this role? [y/N]: " confirm
confirm=${confirm:-N}
if [[ ! "${confirm}" =~ ^[Yy]$ ]]; then
echo "User chose not to modify ${fqname}"
continue
fi

# Backup current definition
backup_name="${name}-$(date +%Y%m%d%H%M%S)"
if [ "${kind}" = "Role" ]; then
echo "Backing up ${fqname} to namespace ${BACKUP_NS} as role ${backup_name}"
kubectl get role "${name}" -n "${ns}" -o yaml \
| sed "s/^ namespace: .*/ namespace: ${BACKUP_NS}/" \
| sed "s/^metadata:\$/metadata:\n name: ${backup_name}/" \
| kubectl apply -n "${BACKUP_NS}" -f -
else
echo "Backing up ${fqname} to namespace ${BACKUP_NS} as configmap ${backup_name}"
kubectl get clusterrole "${name}" -o yaml \
| kubectl create configmap "${backup_name}" -n "${BACKUP_NS}" --from-file=role.yaml=/dev/stdin --dry-run=client -o yaml \
| kubectl apply -f -
fi

echo "Stripping pods/create from ${fqname} and applying updated role"

if [ "${kind}" = "Role" ]; then
kubectl get role "${name}" -n "${ns}" -o json \
| strip_pods_create \
| kubectl apply -n "${ns}" -f -
else
kubectl get clusterrole "${name}" -o json \
| strip_pods_create \
| kubectl apply -f -
fi

echo "Updated ${fqname}"
done

echo
echo "==> Verification: re-running pods/create access check"

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

if [ "${access}" -gt 0 ]; then
echo "pods_create_access still present in ${access} role(s). Review remaining roles manually."
kubectl get roles,clusterroles -A -o json | jq -r '
.items[]
| select(
.rules[]? |
(.resources[]? == "pods" and .verbs[]? == "create")
)
| "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'
exit 1
else
echo "SUCCESS: No Roles or ClusterRoles grant create on pods."
fi