pods/exec Should Not Be Granted To Broad Subjects
More Info:
Advisory: review Roles/ClusterRoles that grant create on pods/exec. Exec into a running pod bypasses image immutability and admission controls.
Risk Level
High
Address
Security
Compliance Standards
- Cloudanix Best Practice
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
List all Roles/ClusterRoles that grant
createonpods/exec
Run on: any machine with kubectl accesskubectl get roles,clusterroles --all-namespaces -o json \| jq '[ .items[]| select(any(.rules[]?;(any(.resources[]?; . == "pods/exec" or . == "*"))and (any(.verbs[]?; . == "create" or . == "*"))))| {kind, namespace: (.metadata.namespace // ""), name: .metadata.name}]' -
Identify “broad” bindings to those Roles/ClusterRoles
Run on: any machine with kubectl access{ kubectl get roles,clusterroles --all-namespaces -o jsonkubectl get rolebindings,clusterrolebindings --all-namespaces -o json} | jq -rs '.[0] as $roles | .[1] |def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];[ $roles.items[]| select(any(.rules[]?;(any(.resources[]?; . == "pods/exec" or . == "*"))and (any(.verbs[]?; . == "create" or . == "*"))))| { kind: .kind, ns: (.metadata.namespace // ""), name: .metadata.name } ] as $execRoles| [ .items[]| .kind as $kind | .metadata as $m | .roleRef as $ref| select(any($execRoles[];.name == $ref.name and .kind == $ref.kindand (.ns == "" or .ns == ($m.namespace // ""))))| ((.subjects // [])[] | select(.name as $n | broad | index($n)))| { bindingKind: $kind,namespace: ($m.namespace // ""),bindingName: $m.name,roleRefKind: $ref.kind,roleRefName: $ref.name,subjectKind: .kind,subjectName: .name }]' -
Review and decide which broad bindings must be removed or replaced
Run on: any machine with kubectl access- For each listed binding, determine which specific human users or service accounts truly need
pods/execand document that set. - Plan to delete the broad binding and create new bindings only to those named identities.
- For each listed binding, determine which specific human users or service accounts truly need
-
Remove a non‑justified broad binding to
pods/exec
Run on: any machine with kubectl access- For a ClusterRoleBinding:
kubectl delete clusterrolebinding <BINDING_NAME>
- For a RoleBinding (namespace‑scoped):
kubectl delete rolebinding <BINDING_NAME> -n <NAMESPACE>
- For a ClusterRoleBinding:
-
Create least‑privilege bindings for named subjects that actually need exec
Run on: any machine with kubectl access- Example: bind an existing ClusterRole that includes
pods/execonly to a specific user:kubectl create clusterrolebinding allow-exec-to-alice \--clusterrole=<EXEC_CLUSTERROLE_NAME> \--user=alice@example.com - Example: bind to a single service account in a namespace:
kubectl create rolebinding allow-exec-to-deployer \--role=<EXEC_ROLE_NAME> \--serviceaccount=app-namespace:deployer-sa \-n app-namespace
- Example: bind an existing ClusterRole that includes
-
Verify that no broad subjects are granted
createonpods/exec
Run on: any machine with kubectl access{ kubectl get roles,clusterroles --all-namespaces -o jsonkubectl get rolebindings,clusterrolebindings --all-namespaces -o json} | jq -rs '.[0] as $roles | .[1] |def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];[ $roles.items[]| select(any(.rules[]?;(any(.resources[]?; . == "pods/exec" or . == "*"))and (any(.verbs[]?; . == "create" or . == "*"))))| { kind: .kind, ns: (.metadata.namespace // ""), name: .metadata.name } ] as $execRoles| [ .items[]| .kind as $kind | .metadata as $m | .roleRef as $ref| select(any($execRoles[];.name == $ref.name and .kind == $ref.kindand (.ns == "" or .ns == ($m.namespace // ""))))| ((.subjects // [])[] | select(.name as $n | broad | index($n)))| "kind=\($kind)"+ (if ($m.namespace // "") == "" then "" else " ns=\($m.namespace)" end)+ " name=\($m.name) uid=\($m.uid) apiVersion=rbac.authorization.k8s.io/v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ " subject=\(.name) roleRef=\($ref.kind)/\($ref.name) grants=pods/exec is_compliant=false"] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'Confirm the output is
is_compliant=true.
Using kubectl
On any machine with kubectl access:
- Identify the offending bindings and roles
kubectl get rolebindings,clusterrolebindings --all-namespaces -o wide
kubectl get roles,clusterroles --all-namespaces -o yaml | grep -A10 "pods/exec"
- Inspect a specific flagged binding and its role
Replace the names from the audit output accordingly:
# Example: inspect a ClusterRoleBinding
kubectl get clusterrolebinding exec-access-binding -o yaml
# Example: inspect the referenced ClusterRole
kubectl get clusterrole exec-access-role -o yaml
- Remove broad subjects from the binding
Edit the binding so that subjects like system:authenticated, system:unauthenticated, system:anonymous, and system:serviceaccounts are removed and replaced with a small set of named human users or dedicated groups.
# ClusterRoleBinding
kubectl edit clusterrolebinding exec-access-binding
# RoleBinding (namespace-scoped)
kubectl edit rolebinding exec-access-binding -n <namespace>
In the editor, adjust subjects from something like:
subjects:
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
to something like:
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.io
- kind: User
name: bob@example.com
apiGroup: rbac.authorization.k8s.io
Make sure no remaining subject has name equal to any of:
system:authenticated, system:unauthenticated, system:anonymous, system:serviceaccounts.
- If needed, remove
pods/execfrom a shared ClusterRole
If a ClusterRole is broadly bound and you cannot safely constrain the subjects, move pods/exec into a dedicated, tightly bound role.
a) Remove pods/exec from the shared ClusterRole:
kubectl edit clusterrole shared-role-with-exec
In the editor, delete or narrow any rule that includes pods/exec or resources: ["*"] with verbs: ["create"] (or "*"), unless it is strictly needed for that broad audience.
b) Create a dedicated ClusterRole and bind only specific operators:
cat <<'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pods-exec-operators
rules:
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pods-exec-operators-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pods-exec-operators
subjects:
- kind: User
apiGroup: rbac.authorization.k8s.io
name: alice@example.com
- kind: User
apiGroup: rbac.authorization.k8s.io
name: bob@example.com
EOF
- Verification
Run the same style of check to confirm no broad subjects are granted create on pods/exec:
{ kubectl get roles,clusterroles --all-namespaces -o json \
kubectl get rolebindings,clusterrolebindings --all-namespaces -o json; } | jq -rs '
.[0] as $roles | .[1] |
def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];
[ $roles.items[]
| select(any(.rules[]?;
(any(.resources[]?; . == "pods/exec" or . == "*"))
and (any(.verbs[]?; . == "create" or . == "*"))))
| { kind: .kind, ns: (.metadata.namespace // ""), name: .metadata.name } ] as $execRoles
| [ .items[]
| .kind as $kind | .metadata as $m | .roleRef as $ref
| select(any($execRoles[];
.name == $ref.name and .kind == $ref.kind
and (.ns == "" or .ns == ($m.namespace // ""))))
| ((.subjects // [])[] | select(.name as $n | broad | index($n)))
| "kind=\($kind)"
+ (if ($m.namespace // "") == "" then "" else " ns=\($m.namespace)" end)
+ " name=\($m.name) uid=\($m.uid) apiVersion=rbac.authorization.k8s.io/v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ " subject=\(.name) roleRef=\($ref.kind)/\($ref.name) grants=pods/exec is_compliant=false"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Automation
#!/usr/bin/env bash
# Fix CBP C2.3: tighten ClusterRoleBindings/RoleBindings that grant create on pods/exec
# Scope: run from any machine with kubectl access and current-context set to target GKE cluster.
set -euo pipefail
# --- Configurable allowlist of subjects that ARE allowed to have pods/exec ---
# Format: kind:name (kind is one of: User, Group, ServiceAccount)
# Example entries:
# User:alice@example.com
# Group:devops
# ServiceAccount:exec-operator
ALLOWED_SUBJECTS=(
# Add your approved human operators or specific SAs here
# Example:
# "User:alice@example.com"
)
allowed_subject() {
local kind="$1" name="$2" s
for s in "${ALLOWED_SUBJECTS[@]}"; do
if [[ "$s" == "${kind}:${name}" ]]; then
return 0
fi
done
return 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }
}
require_cmd kubectl
require_cmd jq
echo "Discovering Roles/ClusterRoles that grant create on pods/exec ..."
# Collect roles/clusterroles and bindings once
roles_json="$(kubectl get roles,clusterroles --all-namespaces -o json)"
bindings_json="$(kubectl get rolebindings,clusterrolebindings --all-namespaces -o json)"
# Compute execRoles (namespaced and cluster-wide)
exec_roles_json="$(
jq -r '
[ .items[]
| select(any(.rules[]?;
(any(.resources[]?; . == "pods/exec" or . == "*"))
and (any(.verbs[]?; . == "create" or . == "*"))))
| { kind: .kind, ns: (.metadata.namespace // ""), name: .metadata.name }
]' <<<"$roles_json"
)"
if [[ "$(jq 'length' <<<"$exec_roles_json")" -eq 0 ]]; then
echo "No Roles/ClusterRoles grant create on pods/exec; nothing to change."
else
echo "Found $(jq 'length' <<<"$exec_roles_json") Roles/ClusterRoles granting pods/exec."
fi
# Extract violating bindings: those that bind execRoles to "broad" subjects
violating_bindings_json="$(
jq -r --argjson execRoles "$exec_roles_json" '
def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];
[ .items[]
| .kind as $kind | .metadata as $m | .roleRef as $ref
| select(any($execRoles[];
.name == $ref.name and .kind == $ref.kind
and (.ns == "" or .ns == ($m.namespace // ""))))
| . as $b
| ((.subjects // [])[]
| select(.name as $n | broad | index($n))
| { bindingKind: $kind,
bindingNS: ($m.namespace // ""),
bindingName: $m.name,
subjectKind: .kind,
subjectName: .name })
]' <<<"$bindings_json"
)"
if [[ "$(jq 'length' <<<"$violating_bindings_json")" -eq 0 ]]; then
echo "No bindings grant pods/exec to broad subjects; cluster is already compliant."
else
echo "Processing $(jq 'length' <<<"$violating_bindings_json") violating subject bindings..."
fi
# Group by binding (kind/ns/name) and patch them
mapfile -t bindings < <(jq -r '
map(.bindingKind + "|" + .bindingNS + "|" + .bindingName) | unique[]' \
<<<"$violating_bindings_json")
for b in "${bindings[@]}"; do
IFS='|' read -r bKind bNS bName <<<"$b"
ns_arg=()
[[ -n "$bNS" ]] && ns_arg=( -n "$bNS" )
echo "Inspecting $bKind $([[ -n "$bNS" ]] && echo "$bNS/")$bName ..."
# Get full binding
b_json="$(kubectl get "$bKind" "${ns_arg[@]}" "$bName" -o json)"
# Build new subjects array:
# - drop broad subjects
# - keep any non-broad subjects that are in ALLOWED_SUBJECTS
new_subjects="$(
jq --argjson vb "$violating_bindings_json" '
def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];
. as $orig
| .subjects // []
| map(
if (.name as $n | broad | index($n)) then
empty # drop all broad subjects
else
# only keep explicitly allowed non-broad subjects (optional gate)
(if (.kind and .name) then
.kind + ":" + .name
else
""
end) as $id
| if $id == "" then empty
else $id
end
end
)
' <<<"$b_json"
)"
# Reconstruct subjects list from allowed IDs
if [[ -z "$new_subjects" ]]; then
# Could happen if there were only broad subjects and no allowed ones
subject_array="[]"
else
subject_array="$(
jq -n --argjson ids "$new_subjects" '
[ $ids[]
| capture("(?<kind>[^:]+):(?<name>.+)")
| {kind: .kind, name: .name}
]'
)"
fi
# Additionally, preserve any existing specific subjects that were not broad,
# regardless of ALLOWED_SUBJECTS, to avoid over-revocation.
preserved_specific="$(
jq '
def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];
(.subjects // [])
| map(select(.name as $n | (broad | index($n)) | not))
' <<<"$b_json"
)"
# Merge: preserved_specific ∪ subject_array, keyed by kind+name (idempotent)
merged_subjects="$(
jq -n --argjson a "$preserved_specific" --argjson b "$subject_array" '
( ($a + $b)
| (map({ key: (.kind + ":" + .name), value: . }) | from_entries) )
| [.[]]
'
)"
# If merged_subjects is empty, we effectively remove all subjects from the binding,
# which strips access but keeps the object; this is safe and idempotent.
tmp_patch="$(mktemp)"
cat >"$tmp_patch" <<EOF
{"subjects": $merged_subjects}
EOF
echo "Patching $bKind $([[ -n "$bNS" ]] && echo "$bNS/")$bName to remove broad subjects..."
kubectl patch "$bKind" "${ns_arg[@]}" "$bName" --type=merge -p "$(cat "$tmp_patch")" >/dev/null
rm -f "$tmp_patch"
done
echo "Re-running compliance check..."
{ kubectl get roles,clusterroles --all-namespaces -o json
kubectl get rolebindings,clusterrolebindings --all-namespaces -o json
} | jq -rs '
.[0] as $roles | .[1] |
def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];
[ $roles.items[]
| select(any(.rules[]?;
(any(.resources[]?; . == "pods/exec" or . == "*"))
and (any(.verbs[]?; . == "create" or . == "*"))))
| { kind: .kind, ns: (.metadata.namespace // ""), name: .metadata.name } ] as $execRoles
| [ .items[]
| .kind as $kind | .metadata as $m | .roleRef as $ref
| select(any($execRoles[];
.name == $ref.name and .kind == $ref.kind
and (.ns == "" or .ns == ($m.namespace // ""))))
| ((.subjects // [])[] | select(.name as $n | broad | index($n)))
| "kind=\($kind)"
+ (if ($m.namespace // "") == "" then "" else " ns=\($m.namespace)" end)
+ " name=\($m.name) uid=\($m.uid) apiVersion=rbac.authorization.k8s.io/v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ " subject=\(.name) roleRef=\($ref.kind)/\($ref.name) grants=pods/exec is_compliant=false"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'