#!/usr/bin/env bash
# Purpose: Summarize Kubernetes API audit policy coverage for key security concerns.
# Scope: Run on ANY MACHINE with:
# - kubectl access to the cluster
# - ssh access to EVERY CONTROL PLANE NODE (for policy files and flags)
#
# NOTE: This script DOES NOT fix anything. It only reports current state for review.
set -euo pipefail
# -----------------------------
# Helper: print section header
# -----------------------------
sec() {
printf '\n==== %s ====\n' "$*"
}
# ---------------------------------------------------
# 1. Discover control plane nodes via kubectl labels
# ---------------------------------------------------
sec "Discovering control plane nodes"
CONTROL_PLANE_NODES=$(kubectl get nodes -l node-role.kubernetes.io/control-plane= -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
if [ -z "$CONTROL_PLANE_NODES" ]; then
# Older clusters may use the master label
CONTROL_PLANE_NODES=$(kubectl get nodes -l node-role.kubernetes.io/master= -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
fi
if [ -z "$CONTROL_PLANE_NODES" ]; then
echo "No control plane nodes discovered via labels."
echo "You may need to specify them manually in this script."
exit 1
fi
echo "Control plane nodes:"
echo "$CONTROL_PLANE_NODES"
# ----------------------------------------------------
# 2. For each control plane node, inspect kube-apiserver
# ----------------------------------------------------
for NODE in $CONTROL_PLANE_NODES; do
sec "Node: $NODE"
# Assumes SSH access via the same name; adjust ssh target format if needed.
SSH_TARGET="$NODE"
# 2.1 Confirm kube-apiserver manifest path and audit flags
echo "-- kube-apiserver manifest and flags --"
ssh "$SSH_TARGET" 'sudo test -f /etc/kubernetes/manifests/kube-apiserver.yaml && echo "Found /etc/kubernetes/manifests/kube-apiserver.yaml" || echo "MISSING: /etc/kubernetes/manifests/kube-apiserver.yaml"' || true
ssh "$SSH_TARGET" '
if [ -f /etc/kubernetes/manifests/kube-apiserver.yaml ]; then
echo
echo "kube-apiserver command and audit-related flags:"
# Print container command and all args
yq e ".spec.containers[] | select(.name==\"kube-apiserver\") | .command, .args[]" /etc/kubernetes/manifests/kube-apiserver.yaml 2>/dev/null || \
python - <<PY 2>/dev/null
import yaml
from pathlib import Path
p = Path("/etc/kubernetes/manifests/kube-apiserver.yaml")
if p.is_file():
data = yaml.safe_load(p.read_text())
for c in data.get("spec", {}).get("containers", []):
if c.get("name") == "kube-apiserver":
for v in c.get("command", []):
print(v)
for v in c.get("args", []):
print(v)
PY
echo
echo "Filtered audit flags:"
yq e ".spec.containers[] | select(.name==\"kube-apiserver\") | .args[]" /etc/kubernetes/manifests/kube-apiserver.yaml 2>/dev/null \
| grep -E -- "--audit-(log-path|policy-file|maxage|maxbackup|maxsize|webhook-config-file|webhook-mode|webhook-batch-max-wait|webhook-batch-max-size)" || true
else
echo "kube-apiserver manifest not found; cannot detect audit configuration on this node."
fi
'
# 2.2 Extract audit-policy-file path from manifest
AUDIT_POLICY_PATH=$(
ssh "$SSH_TARGET" '
if [ -f /etc/kubernetes/manifests/kube-apiserver.yaml ]; then
yq e ".spec.containers[] | select(.name==\"kube-apiserver\") | .args[]" /etc/kubernetes/manifests/kube-apiserver.yaml 2>/dev/null \
| grep -E "^--audit-policy-file=" | head -n1 | sed "s/^--audit-policy-file=//"
fi
' 2>/dev/null || true
)
if [ -z "$AUDIT_POLICY_PATH" ]; then
echo
echo "WARNING: No --audit-policy-file flag detected on $NODE."
echo "This likely means audit is not governed by a custom policy file on this node."
continue
fi
echo
echo "Detected audit policy file on $NODE: $AUDIT_POLICY_PATH"
# 2.3 Show high-level policy info: existence and top-level rules count
ssh "$SSH_TARGET" "
if [ -f '$AUDIT_POLICY_PATH' ]; then
echo
echo 'Audit policy file exists. Top-level rules count:'
(yq e '.rules | length' '$AUDIT_POLICY_PATH' 2>/dev/null || python - <<PY 2>/dev/null
import yaml, sys
p = '$AUDIT_POLICY_PATH'
try:
with open(p) as f:
d = yaml.safe_load(f)
print(len(d.get('rules', []) or []))
except Exception as e:
print('ERROR parsing policy:', e, file=sys.stderr)
PY
) || true
else
echo
echo 'WARNING: Audit policy file path configured but file not found: $AUDIT_POLICY_PATH'
fi
"
# 2.4 Inspect coverage of key security concerns
if [ -n "$AUDIT_POLICY_PATH" ]; then
ssh "$SSH_TARGET" "
if [ -f '$AUDIT_POLICY_PATH' ]; then
echo
echo '--- Policy excerpts: SECRETS, CONFIGMAPS, TOKENREVIEWS (should log at least metadata, avoid full object) ---'
echo
echo 'Rules referencing secrets/configmaps/tokenreviews:'
yq e '.rules[] | select((.resources[]?.resources[]? == \"secrets\") or (.resources[]?.resources[]? == \"configmaps\") or (.resources[]?.resources[]? == \"tokenreviews\"))' '$AUDIT_POLICY_PATH' 2>/dev/null || python - <<PY 2>/dev/null
import yaml
from pprint import pprint
p = '$AUDIT_POLICY_PATH'
with open(p) as f:
d = yaml.safe_load(f)
for r in d.get('rules', []):
for rs in r.get('resources', []) or []:
for res in rs.get('resources', []) or []:
if res in ('secrets', 'configmaps', 'tokenreviews'):
pprint(r)
print('---')
break
PY
echo
echo '--- Policy excerpts: MODIFICATION of pods & deployments ---'
echo
echo 'Rules that match pods/deployments with verb create/update/patch/delete:'
yq e '.rules[] | select((.resources[]?.resources[]? == \"pods\" or .resources[]?.resources[]? == \"deployments\") and (.verbs[]? == \"create\" or .verbs[]? == \"update\" or .verbs[]? == \"patch\" or .verbs[]? == \"delete\"))' '$AUDIT_POLICY_PATH' 2>/dev/null || python - <<PY 2>/dev/null
import yaml
from pprint import pprint
p = '$AUDIT_POLICY_PATH'
with open(p) as f:
d = yaml.safe_load(f)
interesting = {'pods', 'deployments'}
verbs = {'create','update','patch','delete'}
for r in d.get('rules', []):
has_res = False
for rs in r.get('resources', []) or []:
if any(res in interesting for res in (rs.get('resources') or [])):
has_res = True
if not has_res:
continue
if r.get('verbs') and any(v in verbs for v in r['verbs']):
pprint(r)
print('---')
PY
echo
echo '--- Policy excerpts: pods/exec, pods/portforward, pods/proxy, services/proxy ---'
echo
echo 'Rules matching subresources exec/portforward/proxy:'
yq e '.rules[] | select(.resources[]?.resources[]? == \"pods\" or .resources[]?.resources[]? == \"services\") | select(.resources[]?.subresources[]? == \"exec\" or .resources[]?.subresources[]? == \"portforward\" or .resources[]?.subresources[]? == \"proxy\")' '$AUDIT_POLICY_PATH' 2>/dev/null || python - <<PY 2>/dev/null
import yaml
from pprint import pprint
p = '$AUDIT_POLICY_PATH'
with open(p) as f:
d = yaml.safe_load(f)
interesting_res = {'pods', 'services'}
interesting_sub = {'exec','portforward','proxy'}
for r in d.get('rules', []):
found = False
for rs in r.get('resources', []) or []:
if any(res in interesting_res for res in (rs.get('resources') or [])):
subs = set(rs.get('subresources') or [])
if subs & interesting_sub:
found = True
if found:
pprint(r)
print('---')
PY
else
echo
echo 'Cannot inspect policy; file missing: $AUDIT_POLICY_PATH'
fi
"
fi
done
sec "INTERPRETING THIS OUTPUT (what indicates a PROBLEM)"
cat <<'EOF'
For each control plane node:
1. Missing or misconfigured audit policy
- Problem if:
- kube-apiserver manifest is missing, OR
- There is NO --audit-policy-file flag, OR
- The configured audit policy file path does not exist.
- Impact: API requests may not be audited according to a policy.
2. Secrets / ConfigMaps / TokenReviews coverage
- Problem if:
- No rules are printed that mention resources: ["secrets"], ["configmaps"], or ["tokenreviews"], OR
- The matching rules log at Level: None (i.e., effectively not logged).
- Risk: Access to sensitive objects is not visible in audit logs.
- Additional risk: If Level is Request, RequestResponse, or includes ResponseBody, sensitive payloads may be logged rather than just metadata.
3. Modification of Pods and Deployments
- Problem if:
- No rules are printed that include resources: ["pods"] or ["deployments"] AND verbs including create, update, patch, or delete.
- Risk: Changes to workload definitions are not audited.
4. pods/exec, pods/portforward, pods/proxy, services/proxy
- Problem if:
- No rules are printed referencing:
- resource: pods with subresources: exec, portforward, proxy
- resource: services with subresource: proxy
- Risk: Interactive and lateral-movement style access paths are not recorded.
5. Logging level
- General recommendation from the benchmark:
- For most requests, minimally log at the Metadata level.
- Problems include:
- Level: None for the above key areas (no auditing).
- Very verbose levels capturing response bodies for Secrets/ConfigMaps/TokenReviews (potential exposure of sensitive data).
This script only surfaces the current configuration; you must manually review the printed rules to ensure they align with the benchmark guidance and your organization's requirements.
EOF