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
-
Identify the offending ClusterRoleBindings (any machine with kubectl access)
{ kubectl get clusterroles,roles --all-namespaces -o jsonkubectl get clusterrolebindings,rolebindings --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, namespace: ($m.namespace // ""), name: $m.name, roleRef: $ref, subject: . }]'Review the JSON output and list all bindings where
subject.nameis one of the “broad” subjects. Decide which bindings should be changed, and who the legitimate human operators are. -
Inspect each flagged binding to understand its scope and usage (any machine with kubectl access)
Replace the names below with each binding from step 1:# For a ClusterRoleBindingkubectl get clusterrolebinding <binding-name> -o yaml# For a RoleBinding (if any are flagged)kubectl get rolebinding <binding-name> -n <namespace> -o yamlConfirm that the
roleRefindeed needspods/execand determine the specific users or service accounts that should retain this ability. -
Remove broad subjects from the binding (any machine with kubectl access)
For each flagged binding, edit it and remove anysubjectsentries withname: system:authenticated,system:unauthenticated,system:anonymous, orsystem:serviceaccounts. Keep or add only specific named users or a single, purpose-built ServiceAccount.# ClusterRoleBindingkubectl edit clusterrolebinding <binding-name># RoleBinding (if present)kubectl edit rolebinding <binding-name> -n <namespace>In the editor, modify the
subjects:list. Example before:subjects:- kind: Groupname: system:authenticatedapiGroup: rbac.authorization.k8s.ioExample after (replace with your real identities):
subjects:- kind: Username: alice@example.comapiGroup: rbac.authorization.k8s.ioSave and exit to apply changes.
-
(Optional) Split shared bindings into dedicated ones (any machine with kubectl access)
If a binding currently mixes broad subjects and specific operators, it may be clearer to:- Remove the broad subject from the existing binding (as in step 3), and
- Create a dedicated binding for the specific operator(s), if one does not already exist:
kubectl create clusterrolebinding <new-binding-name> \--clusterrole=<exec-role-name> \--user=<human-operator-identity>Adjust
--useror use--serviceaccount=<namespace>:<sa-name>as appropriate. -
(Optional) Tighten the ClusterRole itself if it is overly broad (any machine with kubectl access)
If a ClusterRole grantscreateonpods/exec(or*resources/verbs) more broadly than required, refine it:kubectl edit clusterrole <clusterrole-name>In the
rules:section, restrictresourcesandverbsto the minimum necessary. For example, avoidresources: ["*"]orverbs: ["*"]if not strictly required. If possible, movepods/execinto a separate, tightly bound ClusterRole used only by the small set of human operators. -
Verify remediation (any machine with kubectl access)
Re-run the audit and ensure it reportsis_compliant=trueor no offending rows:{ 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'
Using kubectl
On any machine with kubectl access:
- Identify the offending binding and role
kubectl get clusterrolebindings.rbac.authorization.k8s.io \
-o wide | grep -i pods-exec || true
kubectl describe clusterrolebindings.rbac.authorization.k8s.io <BINDING_NAME>
kubectl describe clusterroles.rbac.authorization.k8s.io <CLUSTERROLE_NAME>
- Remove broad subjects from the binding
Edit the binding to remove any of these subjects:
- system:authenticated
- system:unauthenticated
- system:anonymous
- system:serviceaccounts
kubectl edit clusterrolebinding.rbac.authorization.k8s.io <BINDING_NAME>
In the subjects: list, delete any entries whose name is one of the above. Save and exit.
Alternatively, patch to remove a specific subject (example for system:authenticated):
kubectl get clusterrolebinding <BINDING_NAME> -o yaml > /tmp/crb.yaml
# edit /tmp/crb.yaml and remove undesired subjects from spec.subjects
kubectl apply -f /tmp/crb.yaml
- Optionally, create a restricted ClusterRoleBinding for named human operators only
Prepare a manifest like:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pods-exec-operators
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: <EXISTING_POD_EXEC_CLUSTERROLE_NAME>
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.io
- kind: User
name: bob@example.com
apiGroup: rbac.authorization.k8s.io
Apply it:
kubectl apply -f pods-exec-operators.yaml
- Verification (same logic as the audit)
{ 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
# Purpose: Review and narrow broad pods/exec RBAC grants on OKE using kubectl.
# Scope: Run on any machine with kubectl access and current-context set.
# Impact: Changes to RoleBinding/ClusterRoleBinding take effect immediately.
set -euo pipefail
# --- Configuration ----------------------------------------------------------
# Namespace where privileged human operators live (adjust if needed).
PRIV_NS="kube-system"
# Name of the ServiceAccount to which narrowed exec rights will be granted.
# This SA will be created in $PRIV_NS if it does not exist.
PRIV_SA="human-operators-exec"
# Label to mark bindings that this script has already processed.
MANAGED_LABEL_KEY="security.cloudanix.com/exec-rbac-managed"
MANAGED_LABEL_VAL="true"
# --- Preconditions ----------------------------------------------------------
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl not found in PATH" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found in PATH; install jq to use this script" >&2
exit 1
fi
echo "Using kubectl context: $(kubectl config current-context)"
# --- Ensure privileged ServiceAccount exists -------------------------------
echo "Ensuring privileged ServiceAccount ${PRIV_SA} exists in namespace ${PRIV_NS}..."
if ! kubectl get namespace "${PRIV_NS}" >/dev/null 2>&1; then
echo "Namespace ${PRIV_NS} does not exist; creating..."
kubectl create namespace "${PRIV_NS}"
fi
if ! kubectl get sa "${PRIV_SA}" -n "${PRIV_NS}" >/dev/null 2>&1; then
echo "Creating ServiceAccount ${PRIV_SA} in namespace ${PRIV_NS}..."
kubectl create sa "${PRIV_SA}" -n "${PRIV_NS}"
else
echo "ServiceAccount ${PRIV_SA} already exists in ${PRIV_NS}."
fi
# --- Find broad exec-granting bindings -------------------------------------
echo "Discovering RoleBindings/ClusterRoleBindings that grant pods/exec to broad subjects..."
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT
# Get all roles/clusterroles and bindings JSON
kubectl get roles,clusterroles --all-namespaces -o json > "${TMP_DIR}/roles.json"
kubectl get rolebindings,clusterrolebindings --all-namespaces -o json > "${TMP_DIR}/bindings.json"
# Produce JSON list of offending bindings with enough info to patch
jq -n -f /dev/stdin --slurpfile roles "${TMP_DIR}/roles.json" --slurpfile binds "${TMP_DIR}/bindings.json" <<'JQ' > "${TMP_DIR}/offenders.json"
($roles[0]) as $roles | ($binds[0]) as $b |
def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];
# First, roles/clusterroles that can create pods/exec (or *)
[ $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
|
# Now, bindings that reference those roles and include broad subjects
[ $b.items[]
| .kind as $kind | .metadata as $m | .roleRef as $ref
| .subjects as $subjects
| select(any($execRoles[];
.name == $ref.name and .kind == $ref.kind
and (.ns == "" or .ns == ($m.namespace // ""))))
| ($subjects // []) as $subjs
| [ $subjs[] | select(.name as $n | broad | index($n)) ] as $broadSubs
| select($broadSubs | length > 0)
| {
kind: $kind,
name: $m.name,
namespace: ($m.namespace // null),
apiVersion: $m.apiVersion,
uid: $m.uid,
roleRef: $ref,
broadSubjects: $broadSubs,
allSubjects: $subjs
}
]
JQ
if [[ ! -s "${TMP_DIR}/offenders.json" || "$(jq '. | length' "${TMP_DIR}/offenders.json")" -eq 0 ]]; then
echo "No broad pods/exec grants detected; cluster appears compliant."
echo "Re-running verification command from control:"
{ 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'
exit 0
fi
echo "Found $(jq '. | length' "${TMP_DIR}/offenders.json") offending bindings."
# --- Process each offending binding ----------------------------------------
jq -c '.[]' "${TMP_DIR}/offenders.json" | while read -r item; do
KIND="$(echo "${item}" | jq -r '.kind')"
NAME="$(echo "${item}" | jq -r '.name')"
NS="$(echo "${item}" | jq -r '.namespace // ""')"
if [[ -n "${NS}" ]]; then
FULL="${KIND}/${NS}/${NAME}"
else
FULL="${KIND}/${NAME}"
fi
echo "Processing ${FULL}..."
# Skip if already labeled as managed
if [[ -n "${NS}" ]]; then
MANAGED_VAL="$(kubectl get "${KIND}" "${NAME}" -n "${NS}" -o jsonpath="{.metadata.labels.${MANAGED_LABEL_KEY}}" 2>/dev/null || true)"
else
MANAGED_VAL="$(kubectl get "${KIND}" "${NAME}" -o jsonpath="{.metadata.labels.${MANAGED_LABEL_KEY}}" 2>/dev/null || true)"
fi
if [[ "${MANAGED_VAL}" == "${MANAGED_LABEL_VAL}" ]]; then
echo " Already managed by this script; skipping."
continue
fi
# Build new subjects: remove broad subjects, add the privileged SA if not present
ALL_SUBJECTS="$(echo "${item}" | jq '.allSubjects')"
if [[ "${ALL_SUBJECTS}" == "null" ]]; then
ALL_SUBJECTS="[]"
fi
NEW_SUBJECTS="$(echo "${ALL_SUBJECTS}" | jq --arg ns "${PRIV_NS}" --arg sa "${PRIV_SA}" '
def broad: ["system:authenticated","system:unauthenticated","system:anonymous","system:serviceaccounts"];
# Filter out broad subjects
[ .[] | select(.name as $n | (broad | index($n) | not)) ] as $filtered
|
# Ensure the privileged SA is present
if any($filtered[]?; .kind == "ServiceAccount" and .name == $sa and (.namespace // "") == $ns) then
$filtered
else
$filtered + [{ "kind": "ServiceAccount", "name": $sa, "namespace": $ns }]
end
')"
# Prepare a patch file
PATCH_FILE="${TMP_DIR}/patch-${KIND}-${NS}-${NAME}.yaml"
cat > "${PATCH_FILE}" <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ${KIND}
metadata:
name: ${NAME}
$( [[ -n "${NS}" ]] && echo " namespace: ${NS}" )
labels:
${MANAGED_LABEL_KEY}: "${MANAGED_LABEL_VAL}"
subjects: $(echo "${NEW_SUBJECTS}" | jq -c '.')
EOF
echo " Applying narrowed-subjects patch..."
if [[ -n "${NS}" ]]; then
kubectl apply -f "${PATCH_FILE}" -n "${NS}"
else
kubectl apply -f "${PATCH_FILE}"
fi
done
# --- Verification -----------------------------------------------------------
echo "Re-running verification command to confirm no broad pods/exec grants remain..."
{ 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'
echo "Review the output above. 'is_compliant=true' indicates the broad pods/exec grants have been removed."