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 ClusterRoleBindings (any machine with kubectl access)

    { kubectl get clusterroles,roles --all-namespaces -o json
    kubectl 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.kind
    and (.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.name is one of the “broad” subjects. Decide which bindings should be changed, and who the legitimate human operators are.

  2. 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 ClusterRoleBinding
    kubectl get clusterrolebinding <binding-name> -o yaml

    # For a RoleBinding (if any are flagged)
    kubectl get rolebinding <binding-name> -n <namespace> -o yaml

    Confirm that the roleRef indeed needs pods/exec and determine the specific users or service accounts that should retain this ability.

  3. Remove broad subjects from the binding (any machine with kubectl access)
    For each flagged binding, edit it and remove any subjects entries with name: system:authenticated, system:unauthenticated, system:anonymous, or system:serviceaccounts. Keep or add only specific named users or a single, purpose-built ServiceAccount.

    # ClusterRoleBinding
    kubectl 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: Group
    name: system:authenticated
    apiGroup: rbac.authorization.k8s.io

    Example after (replace with your real identities):

    subjects:
    - kind: User
    name: alice@example.com
    apiGroup: rbac.authorization.k8s.io

    Save and exit to apply changes.

  4. (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 --user or use --serviceaccount=<namespace>:<sa-name> as appropriate.

  5. (Optional) Tighten the ClusterRole itself if it is overly broad (any machine with kubectl access)
    If a ClusterRole grants create on pods/exec (or * resources/verbs) more broadly than required, refine it:

    kubectl edit clusterrole <clusterrole-name>

    In the rules: section, restrict resources and verbs to the minimum necessary. For example, avoid resources: ["*"] or verbs: ["*"] if not strictly required. If possible, move pods/exec into a separate, tightly bound ClusterRole used only by the small set of human operators.

  6. Verify remediation (any machine with kubectl access)
    Re-run the audit and ensure it reports is_compliant=true or no offending rows:

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

On any machine with kubectl access:

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