AWS Minimize Access To Secrets
More Info:
Roles that grant get, list, or watch access to secrets expose sensitive data to a wider set of subjects. Restrict such access to only those workloads that require it.
Risk Level
High
Address
Security
Compliance Standards
- CIS EKS
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
List Roles with 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)"' -
Review which subjects use each Role (any kubectl machine)
For each<NAMESPACE> <ROLE_NAME>from step 1, list RoleBindings:kubectl get rolebindings -n <NAMESPACE> -o json | jq -r '.items[]| select(.roleRef.kind=="Role" and .roleRef.name=="<ROLE_NAME>")| .metadata.name as $rb| .subjects[]| "\($rb) \(.kind) \(.namespace // "-") \(.name)"'Decide which workloads or users truly require
get,list, orwatchonsecrets. -
Inspect the Role rules before changing them (any kubectl machine)
kubectl get role <ROLE_NAME> -n <NAMESPACE> -o yamlConfirm which rules grant access to
secretsand whether they are needed (for example, an in‑namespace controller vs. general users). -
Edit the Role to remove unnecessary secret privileges (any kubectl machine)
For each Role where secret access is not strictly required, removeget,list, and/orwatchonsecrets:kubectl edit role <ROLE_NAME> -n <NAMESPACE>In the editor:
- Locate any
rulesentry whereresourcesincludessecrets. - Remove
get,list, andwatchfromverbs(or, if no verbs remain, delete that entirerulesitem). - Save and exit to apply.
- Locate any
-
(Optional) Split roles for least privilege (any kubectl machine)
If only some subjects need secret access:- Clone the original Role, keeping only secret‑related rules:
kubectl get role <ROLE_NAME> -n <NAMESPACE> -o yaml > /tmp/<ROLE_NAME>-with-secrets.yaml
- Edit
/tmp/<ROLE_NAME>-with-secrets.yaml:- Change
metadata.nameto something like<ROLE_NAME>-secrets. - Remove all non‑secret
rules.
kubectl apply -f /tmp/<ROLE_NAME>-with-secrets.yaml - Change
- Update RoleBindings so only the specific workloads/users that truly require secret access bind to
<ROLE_NAME>-secrets, and other subjects bind to a role without secret verbs:kubectl edit rolebinding <ROLEBINDING_NAME> -n <NAMESPACE>
- Clone the original Role, keeping only secret‑related rules:
-
Verify no unintended Roles retain secrets access (any kubectl machine)
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)echo "SECRETS_ACCESS_FOUND_COUNT=$count"Review any remaining Roles in this count to ensure their access is justified and documented.
Using kubectl
On any machine with kubectl access:
- Identify roles with secret read access
kubectl get roles --all-namespaces -o wide
kubectl get roles --all-namespaces -o yaml \
| yq 'select(.rules[]? | (.resources[]? == "secrets") and ((.verbs[]? == "get") or (.verbs[]? == "list") or (.verbs[]? == "watch"))) | {ns: .metadata.namespace, name: .metadata.name, rules: .rules}'
- Back up each affected Role (example for namespace
app-namespace, roleapp-role)
kubectl get role app-role -n app-namespace -o yaml > app-role-backup.yaml
- Edit the Role to remove
get,list,watchonsecrets
Interactive edit (recommended when changes are small):
kubectl edit role app-role -n app-namespace
In the editor, find any rule like:
rules:
- apiGroups: [""]
resources:
- secrets
verbs:
- get
- list
- watch
- create
- update
Change it to remove read verbs while keeping only what is required. For example:
rules:
- apiGroups: [""]
resources:
- secrets
verbs:
- create
- update
or, if the Role does not need any secret access, remove the entire rule block that has resources: ["secrets"].
- Declarative update via manifest (alternative to step 3)
Edit the backed‑up manifest fileapp-role-backup.yaml, adjust therulessection as above, then apply:
kubectl apply -f app-role-backup.yaml
-
Repeat steps 2–4 for every Role flagged in step 1, ensuring only workloads that truly require secret read access retain
get/list/watchonsecrets. -
Verification
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)
echo "ROLES_WITH_SECRET_READ_VERBS=$count"
Automation
#!/usr/bin/env bash
set -euo pipefail
# Automation: Minimize access to secrets in Roles cluster-wide
# Applies to: any machine with kubectl access to the cluster
# Requirement: kubectl, jq, and a kubeconfig with sufficient RBAC rights
# This script:
# - Finds Roles that grant get/list/watch on "secrets"
# - Removes those verbs from the Role rules (only for resource "secrets")
# - Leaves other resources/verbs untouched
# - Is idempotent (safe to re-run)
# - Verifies the result using the benchmark audit logic
# ---------- configuration ----------
# Backup directory for original Role definitions
BACKUP_DIR="./role-secrets-backups-$(date +%Y%m%d%H%M%S)"
mkdir -p "${BACKUP_DIR}"
echo "Backing up and patching Roles that grant get/list/watch on secrets..."
echo "Backups stored in: ${BACKUP_DIR}"
# ---------- function to patch a single Role ----------
patch_role() {
local ns="$1"
local name="$2"
echo "Processing Role ${ns}/${name}..."
# Backup original Role manifest
kubectl get role "${name}" -n "${ns}" -o yaml > "${BACKUP_DIR}/${ns}__${name}.yaml"
# Fetch Role as JSON
role_json="$(kubectl get role "${name}" -n "${ns}" -o json)"
# Transform rules:
# - For rules that include "secrets" in resources:
# * remove get/list/watch from verbs
# * if verbs becomes empty, drop that rule entirely
# - All other rules are preserved
patched_json="$(jq '
.rules |= [
.[] |
if (.resources // [] | index("secrets")) != null then
# This rule touches secrets; clean its verbs
.verbs |= [ .[] | select(. != "get" and . != "list" and . != "watch") ]
| if (.verbs | length) == 0 then
empty
else
.
end
else
.
end
]
' <<< "${role_json}")"
# Apply the patched Role
# Use kubectl replace to fully update the Role definition
tmpfile="$(mktemp)"
printf '%s\n' "${patched_json}" > "${tmpfile}"
kubectl replace -f "${tmpfile}" >/dev/null
rm -f "${tmpfile}"
echo "Patched Role ${ns}/${name}."
}
# ---------- discover affected Roles ----------
echo "Discovering Roles with get/list/watch access to secrets..."
mapfile -t affected_roles < <(
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
)
if [ "${#affected_roles[@]}" -eq 0 ]; then
echo "No Roles with get/list/watch access to secrets found. Nothing to change."
else
echo "Found ${#affected_roles[@]} affected Roles:"
printf ' %s\n' "${affected_roles[@]}"
echo
# ---------- patch each affected Role ----------
for entry in "${affected_roles[@]}"; do
ns="$(cut -d' ' -f1 <<< "${entry}")"
name="$(cut -d' ' -f2- <<< "${entry}")"
patch_role "${ns}" "${name}"
done
fi
# ---------- verification ----------
echo
echo "Verifying that no Roles 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 | tr -d " ")"
if [ "${count}" -gt 0 ]; then
echo "SECRETS_ACCESS_FOUND"
echo "Verification FAILED: ${count} Roles still grant get/list/watch on secrets."
echo "Review remaining Roles manually; some may be system or intentionally privileged."
exit 1
else
echo "Verification PASSED: no Roles grant get/list/watch on secrets."
fi