#!/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."