Skip to main content

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

Manual Steps
  1. 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>'
  2. 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).
  3. Remove unnecessary Secret rules from a Role

    • Run on: any machine with kubectl access
    • Export the Role manifest, edit locally, and remove secrets from resources and/or remove get, list, watch from verbs where 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[], delete secrets from resources: or delete the entire rule if only for secrets.
      • If some verbs are truly required, keep only the minimal needed subset.
    • Apply the updated Role:
    kubectl apply -f /tmp/role-<namespace>-<role-name>.yaml
  4. 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>.yaml
        apiVersion: rbac.authorization.k8s.io/v1
        kind: Role
        metadata:
        name: <new-name>
        namespace: <namespace>
        rules:
        - apiGroups: [""]
        resources: ["secrets"]
        verbs: ["get"] # or minimal required verbs
        EOF

        kubectl 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:
        kubectl edit rolebinding -n <namespace> <rolebinding-name>
        Adjust .roleRef.name and/or split into multiple RoleBindings as needed.
  5. 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 get on specific secrets via resourceNames if appropriate).
  6. 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 ]; then
    echo "SECRETS_ACCESS_FOUND"
    else
    echo "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