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. Identify the offending bindings and roles (any machine with kubectl access)

    kubectl get rolebindings,clusterrolebindings --all-namespaces -o wide
    kubectl get roles,clusterroles --all-namespaces -o yaml | grep -n "pods/exec" -n
  2. Inspect a flagged binding and its role (any machine with kubectl access)
    Replace the names from the finding/audit output.

    # Inspect the binding that uses broad subjects
    kubectl get clusterrolebinding <BINDING_NAME> -o yaml

    # Inspect the referenced ClusterRole to confirm pods/exec create permissions
    kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml
  3. Decide how pods/exec should be used (manual review)

    • Determine which specific human users or a small set of named service accounts truly need kubectl exec.
    • For any broad subject (system:authenticated, system:unauthenticated, system:anonymous, system:serviceaccounts), decide whether:
      • exec should be removed entirely for that binding, or
      • the binding should be replaced by a binding to specific users/groups/service accounts.
  4. Restrict or remove the broad binding (any machine with kubectl access)
    Example: edit an existing ClusterRoleBinding to remove broad subjects and add specific ones.

    kubectl edit clusterrolebinding <BINDING_NAME>

    In your editor:

    • Under subjects:, delete entries where name is any of:
      system:authenticated, system:unauthenticated, system:anonymous, system:serviceaccounts.
    • Optionally add tightly scoped subjects such as:
      subjects:
      - kind: User
      apiGroup: rbac.authorization.k8s.io
      name: alice@example.com
      - kind: ServiceAccount
      name: exec-operator
      namespace: ops
    • Save and exit to apply.
  5. Optionally narrow the ClusterRole itself (any machine with kubectl access)
    If the ClusterRole grants more than needed (e.g., "resources: ["*"] or verbs: ["*"]), edit it:

    kubectl edit clusterrole <CLUSTERROLE_NAME>

    In your editor, change the relevant rule to something like:

    rules:
    - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create"]

    or remove pods/exec from resources entirely if exec is no longer required.

  6. Verify the remediation (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 and no bindings with broad subjects remain for pods/exec.

Using kubectl

On any machine with kubectl access:

  1. Identify the offending bindings and roles
kubectl get clusterrolebindings,rolebindings --all-namespaces -o wide
kubectl get clusterroles,roles --all-namespaces -o yaml | grep -E "name: pods/exec|\- \\\*" -n

Focus on bindings where roleRef points to a Role/ClusterRole that allows create on pods/exec and subjects include any of:

  • system:authenticated
  • system:unauthenticated
  • system:anonymous
  • system:serviceaccounts
  1. Remove broad subjects from the binding

For each offending binding, edit it to remove the broad subject and (optionally) replace it with specific named users/groups.

ClusterRoleBinding example (cluster‑wide):

kubectl edit clusterrolebinding <binding-name>

In the editor, change e.g.:

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

or remove the subjects entry entirely if no one should have pods/exec via this binding.

RoleBinding example (namespace‑scoped):

kubectl edit rolebinding <binding-name> -n <namespace>

Adjust the subjects section the same way: eliminate system:authenticated, system:unauthenticated, system:anonymous, or system:serviceaccounts, keeping only specific users or a single, named service account if absolutely required.

  1. (Optional) Split out a least‑privilege exec role

If a broad ClusterRole currently contains pods/exec among many other permissions, consider:

kubectl get clusterrole <broad-role-name> -o yaml > /tmp/exec-role.yaml

Edit /tmp/exec-role.yaml to:

  • Change metadata.name to a new, restrictive name, e.g. exec-operators-only.
  • Remove all rules except the minimal exec rule, for example:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: exec-operators-only
rules:
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]

Apply it:

kubectl apply -f /tmp/exec-role.yaml

Then create a dedicated binding for named human operators only:

cat <<'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: exec-operators-only-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: exec-operators-only
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.io
- kind: User
name: bob@example.com
apiGroup: rbac.authorization.k8s.io
EOF
  1. Verification

Run the provided audit 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'
Automation
#!/usr/bin/env bash
#
# Restrict broad subjects from Roles/ClusterRoles that grant create on pods/exec.
# Platform: Amazon EKS
# Runs on: any machine with kubectl access and jq installed.
#
# Behavior:
# - Detects RoleBindings/ClusterRoleBindings whose role grants create on pods/exec
# and which are bound to any of:
# system:authenticated, system:unauthenticated, system:anonymous, system:serviceaccounts
# - Removes ONLY those broad subjects from the binding, leaving other subjects intact.
# - Safe to re-run: once subjects are removed, no further changes are made.
# - Prints a final compliance summary using the benchmark audit logic.

set -euo pipefail

# Ensure required tools
command -v kubectl >/dev/null 2>&1 || { echo "kubectl not found in PATH" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq not found in PATH" >&2; exit 1; }

BROAD_SUBJECTS='["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"]'

echo "Discovering Roles and ClusterRoles that grant create on pods/exec..."

# Build JSON with exec-capable roles/clusterroles and all bindings
COMBINED_JSON="$( { kubectl get roles,clusterroles --all-namespaces -o json ; kubectl get rolebindings,clusterrolebindings --all-namespaces -o json ; } )"

# Extract exec-capable roles/clusterroles
EXEC_ROLES_JSON="$(printf '%s\n' "$COMBINED_JSON" | jq -rs '
.[0].items[]
| select(any(.rules[]?;
(any(.resources[]?; . == "pods/exec" or . == "*"))
and (any(.verbs[]?; . == "create" or . == "*"))))
| { kind: .kind,
ns: (.metadata.namespace // ""),
name: .metadata.name }')"

if [ -z "$EXEC_ROLES_JSON" ] || [ "$EXEC_ROLES_JSON" = "null" ]; then
echo "No Roles/ClusterRoles grant create on pods/exec; nothing to change."
else
echo "Identifying RoleBindings/ClusterRoleBindings with broad subjects bound to exec-capable roles..."

BINDINGS_JSON="$(printf '%s\n' "$COMBINED_JSON" | jq -rs --argjson execRoles "$EXEC_ROLES_JSON" --argjson broad "$BROAD_SUBJECTS" '
.[1].items
| map(
. as $b
| .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 // [] as $subjects
| $subjects
| map(select(.name as $n | $broad | index($n))) as $broadSubjects
| select($broadSubjects | length > 0)
| {
kind: $kind,
namespace: ($m.namespace // ""),
name: $m.name,
apiVersion: "rbac.authorization.k8s.io/v1",
uid: $m.uid,
roleRefKind: $ref.kind,
roleRefName: $ref.name,
broadSubjects: $broadSubjects
}
)
')"

if [ -z "$BINDINGS_JSON" ] || [ "$BINDINGS_JSON" = "null" ] || [ "$(printf '%s' "$BINDINGS_JSON" | jq 'length')" -eq 0 ]; then
echo "No RoleBindings/ClusterRoleBindings bind broad subjects to exec-capable roles; nothing to change."
else
echo "The following bindings will have broad subjects removed:"
printf '%s\n' "$BINDINGS_JSON" | jq -r '
.[]
| "kind=\(.kind)"
+ (if .namespace == "" then "" else " ns=\(.namespace)" end)
+ " name=\(.name) roleRef=\(.roleRefKind)/\(.roleRefName)"
+ " removing_subjects=\([.broadSubjects[].name] | join(","))"
'

# Iterate and patch each binding
MAPFILE=()
while IFS= read -r line; do
MAPFILE+=("$line")
done < <(printf '%s\n' "$BINDINGS_JSON" | jq -c '.[]')

for item in "${MAPFILE[@]}"; do
kind=$(printf '%s\n' "$item" | jq -r '.kind')
ns=$(printf '%s\n' "$item" | jq -r '.namespace')
name=$(printf '%s\n' "$item" | jq -r '.name')
# Build a subjects list excluding broad subjects
patch_subjects=$(kubectl get "$kind" "$name" ${ns:+-n "$ns"} -o json \
| jq --argjson broad "$BROAD_SUBJECTS" '
.subjects // []
| map(select(.name as $n | $broad | index($n) | not))
')

# Construct full patch; if resulting subjects list is empty, set subjects: []
patch_json=$(jq -n --argjson subs "$patch_subjects" '{ "subjects": $subs }')

echo "Patching $kind ${ns:+namespace=$ns }name=$name to remove broad subjects..."
kubectl patch "$kind" "$name" ${ns:+-n "$ns"} --type=merge -p "$patch_json" 1>/dev/null
done
fi
fi

echo "Re-running compliance check..."

# Verification: run the benchmark audit logic
{ 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'