Skip to main content

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

Manual Steps
  1. 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)"'
  2. 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, or watch on secrets.

  3. Inspect the Role rules before changing them (any kubectl machine)

    kubectl get role <ROLE_NAME> -n <NAMESPACE> -o yaml

    Confirm which rules grant access to secrets and whether they are needed (for example, an in‑namespace controller vs. general users).

  4. Edit the Role to remove unnecessary secret privileges (any kubectl machine)
    For each Role where secret access is not strictly required, remove get, list, and/or watch on secrets:

    kubectl edit role <ROLE_NAME> -n <NAMESPACE>

    In the editor:

    • Locate any rules entry where resources includes secrets.
    • Remove get, list, and watch from verbs (or, if no verbs remain, delete that entire rules item).
    • Save and exit to apply.
  5. (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.name to something like <ROLE_NAME>-secrets.
      • Remove all non‑secret rules.
      kubectl apply -f /tmp/<ROLE_NAME>-with-secrets.yaml
    • 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>
  6. 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:

  1. 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}'
  1. Back up each affected Role (example for namespace app-namespace, role app-role)
kubectl get role app-role -n app-namespace -o yaml > app-role-backup.yaml
  1. Edit the Role to remove get, list, watch on secrets
    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"].

  1. Declarative update via manifest (alternative to step 3)
    Edit the backed‑up manifest file app-role-backup.yaml, adjust the rules section as above, then apply:
kubectl apply -f app-role-backup.yaml
  1. Repeat steps 2–4 for every Role flagged in step 1, ensuring only workloads that truly require secret read access retain get/list/watch on secrets.

  2. 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