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 ClusterRoleBindings that grant
pods/execto broad subjects- Run on: any machine with kubectl access
# Identify bindings flagged by the audit (copy relevant lines or rerun the jq if needed)kubectl get clusterrolebindings -o yaml | grep -n "system:authenticated\|system:unauthenticated\|system:anonymous\|system:serviceaccounts" -n -
Inspect each implicated ClusterRole and its bindings
- Run on: any machine with kubectl access
Replace<clusterrole-name>with the name from the audit output.
# See which permissions include pods/execkubectl get clusterrole <clusterrole-name> -o yaml# See which ClusterRoleBindings use this ClusterRolekubectl get clusterrolebindings \-o jsonpath='{range .items[?(@.roleRef.kind=="ClusterRole" && @.roleRef.name=="<clusterrole-name>")]}{@.metadata.name}{"\n"}{end}' - Run on: any machine with kubectl access
-
Remove broad subjects from each non-essential binding and replace with named users/groups
- Run on: any machine with kubectl access
For each implicated ClusterRoleBinding<binding-name>:
# Edit the binding to remove broad subjects and add specific subjectskubectl edit clusterrolebinding <binding-name>In the editor:
- Under
subjects:, remove any entries with:name: system:authenticatedname: system:unauthenticatedname: system:anonymouskind: Groupandname: system:serviceaccounts(or similar broad SA groups)
- Optionally add explicit human operators or specific groups, for example:
subjects:- kind: Username: alice@example.comapiGroup: rbac.authorization.k8s.io- kind: Groupname: ops-teamapiGroup: rbac.authorization.k8s.io
- Run on: any machine with kubectl access
-
If needed, split shared ClusterRoles to avoid giving
pods/execbroadly- Run on: any machine with kubectl access
When a ClusterRole is used by both broad and restricted bindings and only some needpods/exec:
# Export the existing ClusterRolekubectl get clusterrole <clusterrole-name> -o yaml > /tmp/<clusterrole-name>.yaml# Create a copy for operators-only that still has pods/execcp /tmp/<clusterrole-name>.yaml /tmp/<clusterrole-name>-operators.yamlsed -i 's/name: <clusterrole-name>/name: <clusterrole-name>-operators/' /tmp/<clusterrole-name>-operators.yaml# In the ORIGINAL file, remove pods/exec (and any '*' that implies it) from rules:# - Edit /tmp/<clusterrole-name>.yaml manually to delete "pods/exec" from resources# and avoid "*" covering it.# Apply both roles:kubectl apply -f /tmp/<clusterrole-name>.yamlkubectl apply -f /tmp/<clusterrole-name>-operators.yaml - Run on: any machine with kubectl access
-
Point restricted operators at the new operators-only ClusterRole
- Run on: any machine with kubectl access
For bindings that should retainpods/exec, updateroleRef.name:
kubectl edit clusterrolebinding <binding-name>In the editor, change:
roleRef:kind: ClusterRolename: <clusterrole-name>to:
roleRef:kind: ClusterRolename: <clusterrole-name>-operators - Run on: any machine with kubectl access
-
Verification: confirm no broad subjects have
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)))] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'Ensure the output is
is_compliant=true.
Using kubectl
On any machine with kubectl access:
- Identify the offending bindings and their ClusterRoles (from the audit output)
Use the audit output to note:
- The binding kind/name/namespace
- The roleRef (ClusterRole name)
- The broad subject (e.g.
system:authenticated,system:serviceaccounts)
- Inspect the ClusterRole rules granting
pods/exec
kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml
Look for rules like:
rules:
- apiGroups: [""]
resources: ["pods/exec"] # or ["*"]
verbs: ["create"] # or ["*"]
- Restrict
pods/execto a small set of named human operators
a) Create a dedicated ClusterRole that only grants pods/exec:
cat << 'EOF' > exec-into-pods-clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: exec-into-pods
rules:
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
EOF
kubectl apply -f exec-into-pods-clusterrole.yaml
b) Bind that role only to specific named users or a single dedicated SA (example with two Azure AD users):
cat << 'EOF' > exec-into-pods-clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: exec-into-pods-operators
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: exec-into-pods
subjects:
- kind: User
name: "alice@contoso.com"
- kind: User
name: "bob@contoso.com"
EOF
kubectl apply -f exec-into-pods-clusterrolebinding.yaml
Adjust subjects to your actual Azure AD user principals or a single service account as appropriate.
- Remove broad subjects from existing offending bindings
For each binding reported by the audit as non‑compliant, edit it to remove broad subjects such as system:authenticated, system:unauthenticated, system:anonymous, system:serviceaccounts, and (if present) any Group that represents all users.
ClusterRoleBinding (cluster‑scoped):
kubectl edit clusterrolebinding <BINDING_NAME>
RoleBinding (namespaced):
kubectl edit rolebinding <BINDING_NAME> -n <NAMESPACE>
In the opened YAML, under subjects:, delete entries like:
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: system:serviceaccounts
apiGroup: rbac.authorization.k8s.io
- kind: User
name: system:anonymous
If the binding is only used to grant pods/exec broadly and you have replaced it with the new, restricted binding, you can optionally delete the old binding entirely:
kubectl delete clusterrolebinding <BINDING_NAME>
# or
kubectl delete rolebinding <BINDING_NAME> -n <NAMESPACE>
- (Optional) Narrow the original ClusterRole if it is too broad
If the original ClusterRole grants pods/exec alongside other permissions and is bound to many subjects, you can remove pods/exec from it and rely on the new exec-into-pods ClusterRole for exec access:
kubectl edit clusterrole <CLUSTERROLE_NAME>
Under rules:, either:
- Remove
"pods/exec"fromresources, or - Split rules so that non‑exec permissions stay in this ClusterRole, and exec is granted only by the new dedicated ClusterRole.
- Verification
Run the same logic used by the audit to confirm there are no bindings granting create on pods/exec to broad subjects:
{ 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 ClusterRoles that grant create on pods/exec
# Platform: Azure AKS
# Runs on: any machine with kubectl access and jq installed
#
# Behavior:
# - Detects ClusterRoleBindings that:
# * bind a ClusterRole/Role that allows create (or *) on pods/exec (or *)
# * and have at least one subject in:
# system:authenticated, system:unauthenticated,
# system:anonymous, system:serviceaccounts
# - For each such binding:
# * removes the broad subjects from the binding
# * leaves any named users, groups, or individual service accounts intact
# - Safe to re-run: when no broad subjects remain, no changes are made
# - Verification: re-runs the benchmark jq audit at the end
set -euo pipefail
# -------- Preconditions --------
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl not found in PATH. Install kubectl and ensure KUBECONFIG is set." >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found in PATH. Install jq." >&2
exit 1
fi
echo "Fetching current Roles/ClusterRoles and Bindings..."
TMPDIR="$(mktemp -d)"
trap 'rm -rf "'"$TMPDIR"'"' EXIT
ROLES_JSON="${TMPDIR}/roles.json"
BINDINGS_JSON="${TMPDIR}/bindings.json"
kubectl get roles,clusterroles --all-namespaces -o json > "${ROLES_JSON}"
kubectl get rolebindings,clusterrolebindings --all-namespaces -o json > "${BINDINGS_JSON}"
# -------- Identify exec-capable roles (pods/exec create or *) --------
echo "Identifying Roles/ClusterRoles that can create pods/exec..."
EXEC_ROLES_JSON="${TMPDIR}/exec_roles.json"
jq '
[ .items[]
| select(any(.rules[]?;
(any(.resources[]?; . == "pods/exec" or . == "*"))
and (any(.verbs[]?; . == "create" or . == "*"))))
| { kind: .kind, ns: (.metadata.namespace // ""), name: .metadata.name }
]
' "${ROLES_JSON}" > "${EXEC_ROLES_JSON}"
if [[ "$(jq 'length' "${EXEC_ROLES_JSON}")" -eq 0 ]]; then
echo "No Roles or ClusterRoles grant create on pods/exec. Nothing to do."
exit 0
fi
# -------- Process RoleBindings and ClusterRoleBindings --------
echo "Scanning RoleBindings and ClusterRoleBindings for broad subjects..."
BROAD_SUBJECTS='["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"]'
# Function to patch a single binding (RoleBinding or ClusterRoleBinding)
patch_binding() {
local ns kind name
ns="$1" # can be empty for ClusterRoleBinding
kind="$2" # RoleBinding or ClusterRoleBinding
name="$3"
echo "Processing ${kind}/${name}${ns:+ in namespace ${ns}} ..."
# Get current YAML
if [[ "${kind}" == "ClusterRoleBinding" ]]; then
kubectl get clusterrolebinding "${name}" -o yaml > "${TMPDIR}/binding.yaml"
else
kubectl -n "${ns}" get rolebinding "${name}" -o yaml > "${TMPDIR}/binding.yaml"
fi
# Use yq if available; otherwise fall back to kubectl+jsonpatch
if command -v yq >/dev/null 2>&1; then
# Remove broad subjects from subjects list, if any
yq eval '
.subjects |= (
. // [] |
map(select(
(.kind == "Group" and .name == "system:authenticated" ) | not and
(.kind == "Group" and .name == "system:unauthenticated" ) | not and
(.kind == "User" and .name == "system:anonymous" ) | not and
(.kind == "Group" and .name == "system:serviceaccounts" ) | not
))
)
' "${TMPDIR}/binding.yaml" > "${TMPDIR}/binding_patched.yaml"
# Apply only if changed
if ! diff -q "${TMPDIR}/binding.yaml" "${TMPDIR}/binding_patched.yaml" >/dev/null 2>&1; then
echo " Removing broad subjects from ${kind}/${name}..."
kubectl apply -f "${TMPDIR}/binding_patched.yaml"
else
echo " No broad subjects present; no change."
fi
else
# yq not available: do JSON patch via kubectl
echo " yq not found; using jsonpatch via kubectl."
# Build JSON representation of subjects without broad subjects
local subj_json filtered_json
if [[ "${kind}" == "ClusterRoleBinding" ]]; then
subj_json="$(kubectl get clusterrolebinding "${name}" -o json | jq '.subjects // []')"
else
subj_json="$(kubectl -n "${ns}" get rolebinding "${name}" -o json | jq '.subjects // []')"
fi
filtered_json="$(jq --argjson broad "${BROAD_SUBJECTS}" '
[ .[]
| select(
(.kind == "Group" and (.name | IN($broad[]))) | not and
(.kind == "User" and (.name == "system:anonymous")) | not
)
]
' <<< "${subj_json}")"
if [[ "${subj_json}" == "${filtered_json}" ]]; then
echo " No broad subjects present; no change."
return
fi
# Construct and apply JSON patch
local patch
patch="$(jq -n --argjson subs "${filtered_json}" '[{"op":"replace","path":"/subjects","value":$subs}]')"
echo " Removing broad subjects from ${kind}/${name} via jsonpatch..."
if [[ "${kind}" == "ClusterRoleBinding" ]]; then
kubectl patch clusterrolebinding "${name}" --type=json -p "${patch}"
else
kubectl -n "${ns}" patch rolebinding "${name}" --type=json -p "${patch}"
fi
fi
}
export TMPDIR BROAD_SUBJECTS
export -f patch_binding
# Build list of offending bindings and invoke patch_binding for each
jq -rs --argjson broad "${BROAD_SUBJECTS}" '
.[0] as $roles | .[1] |
[ $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 // ""))))
| { kind: $kind, ns: ($m.namespace // ""), name: .metadata.name,
subjects: (.subjects // []) }
| select(any(.subjects[];
(.kind == "Group" and .name as $n | $broad | index($n))
or (.kind == "User" and .name == "system:anonymous")))
]
' "${ROLES_JSON}" "${BINDINGS_JSON}" > "${TMPDIR}/offending_bindings.json"
COUNT="$(jq 'length' "${TMPDIR}/offending_bindings.json")"
if [[ "${COUNT}" -eq 0 ]]; then
echo "No RoleBindings or ClusterRoleBindings with broad subjects and pods/exec create found. Nothing to change."
else
echo "Found ${COUNT} offending bindings. Updating..."
# Iterate in bash to keep compatibility
for i in $(seq 0 $((COUNT - 1))); do
ns="$(jq -r ".[$i].ns" "${TMPDIR}/offending_bindings.json")"
kind="$(jq -r ".[$i].kind" "${TMPDIR}/offending_bindings.json")"
name="$(jq -r ".[$i].name" "${TMPDIR}/offending_bindings.json")"
patch_binding "${ns}" "${kind}" "${name}"
done
fi
# -------- Verification (re-run audit) --------
echo
echo "Re-running audit to verify that no broad subjects can exec pods..."
{
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
'