GCP Minimize Access To Secrets
More Info:
Roles that grant get, list or watch on Secret objects expose sensitive credentials. Access to Secrets should be minimized across the cluster.
Risk Level
High
Address
Security
Compliance Standards
- CIS GKE
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify all Roles with sensitive Secret access
- Run on: any machine with kubectl access
kubectl get roles --all-namespaces -o json | jq '.items[]| select(.rules[]?| (.resources[]? == "secrets")and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch")))| {namespace: .metadata.namespace, name: .metadata.name, rules: .rules}' | less- Review each Role to understand which subjects (RoleBindings) depend on it and what applications/functions they support:
kubectl get rolebindings --all-namespaces -o wide | grep '<role-name>' -
Decide which Roles truly need Secret access
- For each Role found, determine:
- Does the workload actually need to read Secret contents (e.g., a controller that manages Secrets)?
- Or is Secret reference in Pod specs (which does not require RBAC get/list/watch) sufficient?
- Mark Roles as:
- “Keep read to secrets” (strictly required), or
- “Can be removed or narrowed” (preferred).
- For each Role found, determine:
-
Remove unnecessary Secret rules from a Role
- Run on: any machine with kubectl access
- Export the Role manifest, edit locally, and remove
secretsfromresourcesand/or removeget,list,watchfromverbswhere not strictly needed:
kubectl get role -n <namespace> <role-name> -o yaml > /tmp/role-<namespace>-<role-name>.yaml- Edit
/tmp/role-<namespace>-<role-name>.yaml:- In
.rules[], deletesecretsfromresources:or delete the entire rule if only for secrets. - If some verbs are truly required, keep only the minimal needed subset.
- In
- Apply the updated Role:
kubectl apply -f /tmp/role-<namespace>-<role-name>.yaml -
Split roles when only some subjects need Secret access
- If some workloads bound to the Role require Secret access and others do not:
- Create a new, narrow Role only for those workloads:
cat <<'EOF' > /tmp/role-secrets-<namespace>-<new-name>.yamlapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:name: <new-name>namespace: <namespace>rules:- apiGroups: [""]resources: ["secrets"]verbs: ["get"] # or minimal required verbsEOFkubectl apply -f /tmp/role-secrets-<namespace>-<new-name>.yaml
- Update RoleBindings so only the specific service accounts/users that truly need Secret access bind to
<new-name>, and others bind to a Role without Secret permissions:Adjustkubectl edit rolebinding -n <namespace> <rolebinding-name>.roleRef.nameand/or split into multiple RoleBindings as needed.
- Create a new, narrow Role only for those workloads:
- If some workloads bound to the Role require Secret access and others do not:
-
Re-test workloads depending on modified Roles
- Run on: any machine with kubectl access
- For each namespace where Roles were changed, check Pods for errors indicating missing Secret access:
kubectl get pods -n <namespace>kubectl logs -n <namespace> <pod-name> --tail=100- If a workload fails due to required Secret reads, minimally re-add the specific verbs it needs (e.g., only
geton specific secrets viaresourceNamesif appropriate).
-
Verification (no Roles with unnecessary Secret read access)
- Run on: any machine with kubectl access
count=$(kubectl get roles --all-namespaces -o json | jq '.items[]| select(.rules[]?| (.resources[]? == "secrets")and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch")))' | wc -l)if [ "$count" -gt 0 ]; thenecho "SECRETS_ACCESS_FOUND"elseecho "NO_EXCESS_SECRETS_ACCESS_FOUND"fi
Using kubectl
# 1) Discover Roles with secret read access
# Run on: any machine with kubectl access
kubectl get roles --all-namespaces -o json | jq '
.items[]
| select(.rules[]?
| (.resources[]? == "secrets")
and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
)
| {namespace: .metadata.namespace, name: .metadata.name, rules: .rules}
'
# 2) For each offending Role, export its manifest for review/edit
# Replace <namespace> and <role-name> with actual values from the command above.
kubectl get role <role-name> -n <namespace> -o yaml > /tmp/role-<namespace>-<role-name>.yaml
# 3) Edit the Role manifest locally to remove or narrow secret access
# - Remove "secrets" from resources where not strictly required
# - Or remove "get", "list", "watch" from verbs for rules that reference "secrets"
#
# Example BEFORE (snippet in the exported YAML):
# rules:
# - apiGroups: [""]
# resources: ["pods", "secrets"]
# verbs: ["get", "list", "watch"]
#
# Example AFTER (if secrets access not needed):
# rules:
# - apiGroups: [""]
# resources: ["pods"]
# verbs: ["get", "list", "watch"]
#
# Example AFTER (if write-only access to secrets is enough):
# rules:
# - apiGroups: [""]
# resources: ["secrets"]
# verbs: ["create", "update", "patch"]
# Edit the file with your editor of choice:
vi /tmp/role-<namespace>-<role-name>.yaml
# 4) Apply the updated Role
kubectl apply -f /tmp/role-<namespace>-<role-name>.yaml
Verification:
# Re-run the audit logic to confirm no Roles grant get/list/watch on secrets
# Run on: any machine with kubectl access
count=$(kubectl get roles --all-namespaces -o json | jq '
.items[]
| select(.rules[]?
| (.resources[]? == "secrets")
and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
)' | wc -l)
if [ "$count" -gt 0 ]; then
echo "SECRETS_ACCESS_FOUND"
else
echo "NO_SECRETS_READ_ACCESS_IN_ROLES"
fi
Automation
#!/usr/bin/env bash
#
# Minimize Access To Secrets (CIS GKE 4.1.2) - Automation
#
# Scope: any machine with kubectl access to the cluster
#
# Behavior:
# - Scans all Roles that grant get/list/watch on "secrets"
# - For each such Role, interactively asks whether to remove those verbs for "secrets"
# - Creates a timestamped backup of each modified Role manifest in ./role-backups/
# - Applies the sanitized Role back to the cluster
# - Re-runs the audit check at the end to verify
#
# NOTE: This is a MANUAL benchmark control. There is no one-size-fits-all fix.
# This script helps you review and selectively remove secret access; it
# will NOT change anything without your confirmation for each Role.
#
set -euo pipefail
# --- Pre-req checks ---------------------------------------------------------
if ! command -v kubectl >/dev/null 2>&1; then
echo "ERROR: kubectl not found in PATH. Install/configure kubectl and re-run." >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq not found in PATH. Install jq and re-run." >&2
exit 1
fi
echo "Using kubectl context:"
kubectl config current-context || {
echo "ERROR: No current kubectl context configured." >&2
exit 1
}
# --- Find Roles with get/list/watch on secrets ------------------------------
echo
echo "Scanning Roles for get/list/watch access to \"secrets\"..."
roles_json=$(kubectl get roles --all-namespaces -o json)
# Count roles matching the condition
count=$(echo "${roles_json}" | jq '
.items[]
| select(.rules[]?
| (.resources[]? == "secrets")
and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
)' | wc -l | tr -d ' ')
if [ "${count}" -eq 0 ]; then
echo "No Roles with get/list/watch access to secrets found. Nothing to change."
exit 0
fi
echo "Found ${count} Role(s) with get/list/watch access to secrets."
# --- Prepare backup directory -----------------------------------------------
backup_dir="./role-backups"
mkdir -p "${backup_dir}"
timestamp=$(date +"%Y%m%d-%H%M%S")
# --- Iterate over affected Roles -------------------------------------------
echo
echo "Reviewing each affected Role. For each Role you can choose whether to"
echo "remove get/list/watch verbs from rules that apply to the \"secrets\" resource."
echo
# Extract namespace and name for each affected Role
echo "${roles_json}" | jq -r '
.items[]
| select(.rules[]?
| (.resources[]? == "secrets")
and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
)
| [.metadata.namespace, .metadata.name]
| @tsv
' | while IFS=$'\t' read -r ns name; do
echo "-----------------------------------------------------------------"
echo "Role: ${name}"
echo "Namespace: ${ns}"
echo
# Show current Role (rules only) for review
kubectl get role "${name}" -n "${ns}" -o json | jq '{metadata: {name: .metadata.name, namespace: .metadata.namespace}, rules: .rules}' | sed 's/^/ /'
echo
read -r -p "Remove get/list/watch on \"secrets\" from this Role? [y/N]: " answer
answer=${answer:-N}
case "${answer}" in
[yY][eE][sS]|[yY])
echo "Processing Role ${ns}/${name}..."
# Backup full manifest
backup_file="${backup_dir}/${ns}__${name}__${timestamp}.yaml"
kubectl get role "${name}" -n "${ns}" -o yaml > "${backup_file}"
echo " Backup saved to ${backup_file}"
# Generate sanitized Role JSON:
# - For each rule:
# * If "secrets" is among resources:
# - remove get/list/watch from verbs
# - keep other verbs intact
# * Keep rules even if secrets was the only resource; verbs for non-secret
# resources are unchanged by this script.
sanitized_json=$(kubectl get role "${name}" -n "${ns}" -o json | jq '
. as $role
| $role
| .rules |= (
map(
if (.resources? | index("secrets")) then
.verbs |= (map(select(. != "get" and . != "list" and . != "watch")))
else
.
end
)
)
')
# Apply the sanitized Role
echo "${sanitized_json}" | kubectl apply -f -
echo " Updated Role ${ns}/${name} applied."
;;
*)
echo "Skipping Role ${ns}/${name} (no changes made)."
;;
esac
echo
done
# --- Verification (re-run audit command) ------------------------------------
echo "-----------------------------------------------------------------"
echo "Re-running audit to verify remaining Roles with get/list/watch on secrets..."
verify_count=$(kubectl get roles --all-namespaces -o json | jq '
.items[]
| select(.rules[]?
| (.resources[]? == "secrets")
and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
)' | wc -l | tr -d ' ')
if [ "${verify_count}" -gt 0 ]; then
echo "SECRETS_ACCESS_FOUND"
echo "Remaining Roles with get/list/watch on secrets: ${verify_count}"
echo
echo "You should now:"
echo " - Review the remaining Roles and decide if they truly need secret access."
echo " - Re-run this script to iteratively reduce access where appropriate."
else
echo "No Roles with get/list/watch access to secrets remain."
echo "Verification PASSED."
fi