Skip to main content

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

Manual Steps
  1. List all Roles/ClusterRoles that grant create on pods/exec
    Run on: any machine with kubectl access

    kubectl 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}
    ]'
  2. Identify “broad” bindings to those Roles/ClusterRoles
    Run on: any machine with kubectl access

    { 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)))
    | { bindingKind: $kind,
    namespace: ($m.namespace // ""),
    bindingName: $m.name,
    roleRefKind: $ref.kind,
    roleRefName: $ref.name,
    subjectKind: .kind,
    subjectName: .name }
    ]'
  3. 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/exec and document that set.
    • Plan to delete the broad binding and create new bindings only to those named identities.
  4. 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>
  5. 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/exec only 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
  6. Verify that no broad subjects are granted create on pods/exec
    Run on: any machine with kubectl access

    { 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'

    Confirm the output is is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. 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"
  1. 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
  1. 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.

  1. If needed, remove pods/exec from 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
  1. 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'