GCP Minimize Access Secrets - Security Rule
More Info:​
The Kubernetes API stores secrets, which may be service account tokens for the Kubernetes API or credentials used by workloads in the cluster. Access to these secrets should be restricted to the smallest possible group of users to reduce the risk of privilege escalation.
Risk Level​
High
Address​
Security
Compliance Standards​
- APRA CPS 234 (Australia)
- BSI C5 (Germany)
- Brazil LGPD
- CCPA / CPRA (California)
- CIS Critical Security Controls v8
- CIS GKE
- CMMC 2.0
- CSA Cloud Controls Matrix v4
- DPDPA
- Digital Operational Resilience Act (EU)
- Essential 8
- ISO/IEC 27017
- ISO/IEC 27018
- ISO/IEC 27701
- KSA PDPL
- MAS Technology Risk Management (Singapore)
- MITRE ATT&CK (Cloud)
- NIS2 Directive
- NIST SP 800-171
- NYDFS 23 NYCRR 500
- SWIFT Customer Security Controls Framework
- Sarbanes-Oxley IT General Controls
- UK NCSC Cyber Assessment Framework
Triage and Remediation​
- Remediation
Remediation​
Manual Steps
-
Identify roles with sensitive Secret access
- Run on: any machine with kubectl access
kubectl get roles --all-namespaces -o json \| jq -r '.items[]| select(.rules[]?| (.resources[]? == "secrets")and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch")))| "\(.metadata.namespace) \(.metadata.name)"' | sort -u -
Review who is bound to each identified Role
- Run on: any machine with kubectl access
# Replace <NAMESPACE> and <ROLE_NAME> from step 1kubectl get rolebindings -n <NAMESPACE> -o json \| jq -r '.items[]| select(.roleRef.kind == "Role" and .roleRef.name == "<ROLE_NAME>")| "RoleBinding: \(.metadata.name) | Subjects: \(.subjects[]? | "\(.kind)/\(.name)")"'- Manually decide which users/service accounts/groups truly require get/list/watch on secrets.
-
Inspect the exact Secret permissions in a Role
- Run on: any machine with kubectl access
# Replace <NAMESPACE> and <ROLE_NAME>kubectl get role -n <NAMESPACE> <ROLE_NAME> -o yaml- Manually determine which rules for
resources: ["secrets"]andverbs: ["get","list","watch"]can be removed or narrowed (e.g., by resourceNames or fewer verbs).
-
Edit Roles to minimize Secret access
- Run on: any machine with kubectl access
For each Role needing change:
kubectl edit role -n <NAMESPACE> <ROLE_NAME>In the editor:
- Locate
rules:entries whereresourcesincludessecrets. - Remove unneeded verbs
get,list,watchfromverbs:. - If Secret access is not required, delete the entire rule containing
secrets. Save and exit to apply changes.
- Run on: any machine with kubectl access
-
If necessary, adjust RoleBindings instead of Roles
- Run on: any machine with kubectl access
If some subjects no longer need Secret access but others still do: - Create a new, reduced-privilege Role without Secret access:
kubectl get role -n <NAMESPACE> <ROLE_NAME> -o yaml \| sed 's/name: <ROLE_NAME>/name: <NEW_ROLE_NAME>/' \| yq 'del(.rules[] | select(.resources[] == "secrets"))' \| kubectl apply -f -
- Rebind subjects that should not see secrets:
kubectl create rolebinding <NEW_RB_NAME> \-n <NAMESPACE> \--role=<NEW_ROLE_NAME> \--user=<USER_OR_GROUP_OR_SA>
- Then remove those subjects from the original RoleBinding:
kubectl edit rolebinding -n <NAMESPACE> <ROLEBINDING_NAME>
- Run on: any machine with kubectl access
-
Verify minimized Secret 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_SECRETS_ACCESS_WITH_GET_LIST_WATCH_IN_ROLES"fi
Using kubectl
On any machine with kubectl access:
-
Discover roles that can get/list/watch secrets
kubectl get roles --all-namespaces -o json \| jq -r '.items[]| select(.rules[]?| (.resources[]? == "secrets")and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch")))| "\(.metadata.namespace) \(.metadata.name)"' | sort -u -
For each role, review who uses it (RoleBindings)
# Replace <NAMESPACE> and <ROLE_NAME> from previous outputkubectl get rolebindings -n <NAMESPACE> -o yaml \| yq '.items[] | select(.roleRef.kind=="Role" and .roleRef.name=="<ROLE_NAME>")' -
Remove unnecessary get/list/watch verbs from a role (example)
Export the current role:kubectl get role -n <NAMESPACE> <ROLE_NAME> -o yaml > /tmp/role-<NAMESPACE>-<ROLE_NAME>.yamlEdit
/tmp/role-<NAMESPACE>-<ROLE_NAME>.yamland, in any rule that includesresources: ["secrets"], removeget,list, andwatchfrom theverbslist if not strictly required. For example, change:rules:- apiGroups: [""]resources: ["secrets"]verbs: ["get", "list", "watch", "create"]to:
rules:- apiGroups: [""]resources: ["secrets"]verbs: ["create"]Apply the updated role:
kubectl apply -f /tmp/role-<NAMESPACE>-<ROLE_NAME>.yaml -
If a role’s secret access is not needed at all, remove the rule entirely
In the same exported YAML, delete anyrulesentry whereresourcesincludessecrets, then apply:kubectl apply -f /tmp/role-<NAMESPACE>-<ROLE_NAME>.yaml -
If access is still needed by a smaller group, create a more scoped role and re-bind
Example minimal role allowing get on a specific secret only:# /tmp/role-secrets-reader-specific.yamlapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:name: secrets-reader-specificnamespace: <NAMESPACE>rules:- apiGroups: [""]resources: ["secrets"]resourceNames: ["<SECRET_NAME>"]verbs: ["get"]Apply it:
kubectl apply -f /tmp/role-secrets-reader-specific.yamlThen update or create a RoleBinding to use this scoped role instead of a broader one:
# /tmp/rb-secrets-reader-specific.yamlapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata:name: secrets-reader-specific-bindingnamespace: <NAMESPACE>subjects:- kind: Username: <USER_NAME>apiGroup: rbac.authorization.k8s.ioroleRef:kind: Rolename: secrets-reader-specificapiGroup: rbac.authorization.k8s.iokubectl apply -f /tmp/rb-secrets-reader-specific.yaml -
Verification (re-run the audit logic)
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_SECRETS_ACCESS_FOUND_FOR_GET_LIST_WATCH_IN_ROLES"fi
Automation
#!/usr/bin/env bash
#
# Minimize access to secrets (CIS GKE 4.1.2)
#
# Runs on: any machine with kubectl access and jq installed
#
# Behavior:
# - Enumerates all Roles that grant get/list/watch on "secrets"
# - Prints them for review
# - Interactively offers to remove get/list/watch on "secrets" from each matching rule
# - Applies changes via kubectl patch (idempotent)
# - Verifies that no Roles still grant get/list/watch on "secrets"
#
# NOTE: This is a MANUAL control; this script assists review but
# final decisions must be made by an administrator who understands
# which Roles truly need access to secrets (for example, controllers
# or operators that must read secrets to function).
set -euo pipefail
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "ERROR: '$1' is required but not installed or not in PATH." >&2
exit 1
fi
}
require_cmd kubectl
require_cmd jq
echo "=== Enumerating Roles with get/list/watch access to secrets ==="
# Collect Roles with get/list/watch on secrets
roles_json=$(kubectl get roles --all-namespaces -o json)
matches=$(echo "$roles_json" | jq -r '
.items[]
| select(
.rules[]?
| (.resources[]? == "secrets")
and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))
)
| "\(.metadata.namespace) \(.metadata.name)"
')
if [ -z "$matches" ]; then
echo "No Roles currently grant get/list/watch on secrets."
exit 0
fi
echo "The following Roles grant get/list/watch access to secrets:"
echo "$matches" | awk '{printf " - Namespace: %-30s Role: %s\n", $1, $2}'
echo
echo "For each Role, you must decide if it truly requires get/list/watch on secrets."
echo "This script can remove those verbs only from rules that target the \"secrets\" resource."
echo "Other resources and verbs in the same Role are preserved."
read -r -p "Proceed with interactive cleanup of these Roles? [y/N]: " PROCEED
PROCEED=${PROCEED:-n}
if [[ ! "$PROCEED" =~ ^[Yy]$ ]]; then
echo "Aborting. No changes made."
exit 0
fi
# Function to build a patched Role JSON with minimized secret access
build_patched_role() {
local ns="$1"
local name="$2"
# Extract the Role, then rewrite rules:
# - For each rule that includes "secrets":
# * remove get/list/watch from verbs for that rule
# * if no verbs remain, drop the rule entirely
# - Other rules untouched
kubectl get role "$name" -n "$ns" -o json | jq '
.rules |= (
map(
if (.resources // [] | index("secrets")) != null then
# rule targeting secrets: strip get/list/watch
.verbs |= map(select(. != "get" and . != "list" and . != "watch")) |
if (.verbs | length) == 0 then
# drop rule with no remaining verbs
empty
else
.
end
else
.
end
)
)
'
}
# Process each Role interactively
echo
echo "=== Processing Roles ==="
echo
while read -r ns name; do
[ -z "$ns" ] && continue
echo "Role: $name"
echo "Namespace: $ns"
echo
echo "Current rules for this Role:"
kubectl get role "$name" -n "$ns" -o json | jq '.rules' | sed 's/^/ /'
echo
read -r -p "Remove get/list/watch access on secrets for this Role? [y/N]: " ANSWER
ANSWER=${ANSWER:-n}
if [[ ! "$ANSWER" =~ ^[Yy]$ ]]; then
echo "Skipping $ns/$name"
echo
continue
fi
echo "Building patched Role without get/list/watch on secrets..."
patched_json=$(build_patched_role "$ns" "$name")
echo "Resulting rules will be:"
echo "$patched_json" | jq '.rules' | sed 's/^/ /'
echo
read -r -p "Apply this change to $ns/$name? [y/N]: " APPLY
APPLY=${APPLY:-n}
if [[ ! "$APPLY" =~ ^[Yy]$ ]]; then
echo "Skipping apply for $ns/$name"
echo
continue
fi
# Apply patch idempotently
echo "$patched_json" | kubectl apply -n "$ns" -f -
echo "Updated $ns/$name"
echo
done <<< "$matches"
echo "=== Verification ==="
echo "Re-running check for Roles with get/list/watch on secrets ..."
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 [ "$count" -gt 0 ]; then
echo "SECRETS_ACCESS_FOUND"
echo "There are still Roles with get/list/watch on secrets."
echo "Review them manually with:"
echo " kubectl get roles --all-namespaces -o json | jq -r '.items[] | select(.rules[]? | (.resources[]? == \"secrets\") and ((.verbs[]? == \"get\") or (.verbs[]? == \"list\") or (.verbs[]? == \"watch\"))) | \"Namespace: \(.metadata.namespace) Role: \(.metadata.name)\"'"
exit 1
else
echo "No Roles currently grant get/list/watch on secrets."
fi
echo "Done."