AWS Minimize Access To Secrets
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)
- AWS Startup Security Baseline
- BSI C5 (Germany)
- Brazil LGPD
- CCPA / CPRA (California)
- CIS Critical Security Controls v8
- CIS EKS
- 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
-
List all Roles that grant get/list/watch on secrets
- 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 -
Inspect each identified Role to understand who uses it and why
- For each
<NAMESPACE> <ROLE_NAME>from step 1, run:
kubectl -n <NAMESPACE> get role <ROLE_NAME> -o yamlkubectl -n <NAMESPACE> get rolebinding -o yaml \| yq '.items[] | select(.roleRef.kind=="Role" and .roleRef.name=="<ROLE_NAME>")'- Review whether subjects (users/groups/serviceaccounts) truly require get/list/watch on secrets.
- For each
-
Edit Roles to remove unnecessary get/list/watch on secrets
- For each Role where secret access is not strictly required, edit it:
kubectl -n <NAMESPACE> edit role <ROLE_NAME>- In the opened YAML, locate any rule with
resources: ["secrets"](or containingsecrets) and removeget,list, andwatchfrom theverbslist, keeping only the minimal verbs truly needed (often none). Save and exit.
-
Split mixed-purpose Roles if only some subjects need secret access
- If a Role includes both secret permissions and other needed permissions for many subjects, create a dedicated minimal-secret Role and narrow its binding:
# Export the existing Rolekubectl -n <NAMESPACE> get role <ROLE_NAME> -o yaml > /tmp/<ROLE_NAME>.yaml- Edit
/tmp/<ROLE_NAME>.yamllocally to:- Remove all rules except the minimal
secretsrule for specific subjects that truly need it. - Change
metadata.nameto a new name, e.g.<ROLE_NAME>-secrets-minimal.
- Remove all rules except the minimal
- Apply the new Role:
kubectl -n <NAMESPACE> apply -f /tmp/<ROLE_NAME>.yaml- Create or update RoleBindings so that only the specific user/group/serviceaccount that truly needs secret access is bound to
<ROLE_NAME>-secrets-minimal, and other subjects are bound to a Role withoutsecretsaccess.
-
Repeat for all namespaces and avoid granting cluster-wide secret access via Roles
- Ensure you do not introduce new
ClusterRolepermissions on secrets while adjusting Roles. - For workloads that previously relied on broad secret access, verify and, if necessary, adjust their service accounts and secret mounting to use only the specific secrets they need.
- Ensure you do not introduce new
-
Verification
- 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" -eq 0 ]; thenecho "SECRETS_ACCESS_NOT_FOUND"elseecho "SECRETS_ACCESS_FOUND: $count roles still grant get/list/watch on secrets"fi
Using kubectl
On any machine with kubectl access:
- Identify roles with get/list/watch on 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)"'
- For each listed role, inspect current rules
Replace NAMESPACE and ROLE_NAME with values from the previous output:
kubectl -n NAMESPACE get role ROLE_NAME -o yaml > ROLE_NAME-role.yaml
- Edit the Role manifest locally (ROLE_NAME-role.yaml) and remove
get,list, andwatchfrom any rule that includesresources: ["secrets"], or remove thesecretsresource from rules that do not need it. Save the file.
Example before:
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create"]
Example after (if only create is required):
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create"]
- Apply the updated Role
kubectl apply -f ROLE_NAME-role.yaml
-
Repeat steps 2–4 for every Role that was identified in step 1.
-
Verify that no Roles still grant 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)
if [ "$count" -eq 0 ]; then
echo "NO_SECRETS_GET_LIST_WATCH_IN_ROLES"
else
echo "SECRETS_ACCESS_FOUND_IN_ROLES: $count"
fi
Automation
#!/usr/bin/env bash
set -euo pipefail
# Automation to minimize get/list/watch access to Secret objects in Roles.
# Runs from any machine with kubectl + jq + sed + mktemp and cluster access.
# CONFIGURABLE: set a backup directory for modified Roles
BACKUP_DIR="${BACKUP_DIR:-./role-secret-backups}"
mkdir -p "${BACKUP_DIR}"
echo "Discovering Roles that grant get/list/watch on secrets..."
# Get all Roles that reference 'secrets' with get/list/watch
role_list_json="$(kubectl get roles --all-namespaces -o json)"
mapfile -t TARGETS < <(
echo "${role_list_json}" | jq -r '
.items[]
| select(
.rules[]? as $r
| ($r.resources[]? == "secrets")
and (
($r.verbs[]? == "get") or
($r.verbs[]? == "list") or
($r.verbs[]? == "watch")
)
)
| .metadata.namespace + ":" + .metadata.name
' | sort -u
)
if [ "${#TARGETS[@]}" -eq 0 ]; then
echo "No Roles with get/list/watch on secrets found. Nothing to change."
else
echo "Found ${#TARGETS[@]} Role(s) with get/list/watch on secrets."
fi
for ns_name in "${TARGETS[@]}"; do
ns="${ns_name%%:*}"
name="${ns_name##*:}"
echo "Processing Role ${ns}/${name}..."
# Backup original Role manifest (idempotent: overwrite backup each run)
backup_file="${BACKUP_DIR}/${ns}__${name}.yaml"
kubectl get role "${name}" -n "${ns}" -o yaml > "${backup_file}"
echo " Backup saved to ${backup_file}"
# Work on a temp file
tmpfile="$(mktemp)"
cp "${backup_file}" "${tmpfile}"
# Use jq to surgically remove get/list/watch verbs only for rules affecting secrets
# Strategy:
# - For each rule that includes "secrets" in resources, remove get/list/watch from verbs.
# - If a rule's verbs become empty, we keep the rule (no verbs) to avoid changing indices.
# Kubernetes will reject a rule with no verbs, so instead we drop such rules.
#
# Implementation detail:
# 1) Convert to JSON
# 2) Transform .rules
# 3) Convert back to yaml
json_transformed="$(
kubectl get role "${name}" -n "${ns}" -o json | jq '
.rules |= map(
if (.resources // [] | index("secrets")) != null then
# This rule touches secrets; strip get/list/watch verbs
.verbs |= map(select(. != "get" and . != "list" and . != "watch")) |
# Drop rule if verbs is now empty
if (.verbs | length) == 0 then
empty
else
.
end
else
.
end
)
'
)"
# Skip apply if there is no change (idempotent)
if diff -q <(echo "${json_transformed}" | jq -S .) <(kubectl get role "${name}" -n "${ns}" -o json | jq -S .) >/dev/null 2>&1; then
echo " No changes needed for ${ns}/${name}."
rm -f "${tmpfile}"
continue
fi
# Convert transformed JSON to YAML for kubectl apply
echo "${json_transformed}" | kubectl apply -f -
echo " Updated Role ${ns}/${name} to remove get/list/watch on secrets."
rm -f "${tmpfile}"
done
echo "Re-running verification..."
# Verification: re-run the benchmark-style check
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"
echo "WARNING: Some Roles still grant get/list/watch on secrets."
echo "They may be system or critical Roles that should be reviewed manually."
else
echo "No Roles with get/list/watch on secrets remain."
fi