Skip to main content

Kubernetes 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

  • CIS Kubernetes

Triage and Remediation

Remediation

Manual Steps
  1. Identify which subjects currently have broad Secret access

    • Run on any machine with kubectl access:
      kubectl get clusterrole,role -A -o yaml | grep -nA5 -B3 "resources:.*secrets"
      kubectl get clusterrolebinding,rolebinding -A -o yaml | grep -nA8 -B3 "roleRef:"
  2. List the ClusterRoles/Roles that grant get, list, or watch on Secrets

    • Run:
      kubectl get clusterrole,role -A -o json \
      | jq -r '.items[]
      | select(.rules != null)
      | select([.rules[]
      | select(.resources != null)
      | select(.resources|index("secrets"))
      | select(.verbs|map(.=="get" or .=="list" or .=="watch")|any)
      ]|length>0)
      | (.kind + "/" + .metadata.name + " (ns: " + (.metadata.namespace // "cluster-scope") + ")")'
  3. Review and edit roles to remove or narrow Secret access

    • For each non-system role you decide should not have broad Secret access, edit it:
      kubectl edit clusterrole <clusterrole-name>
      or for a namespace-scoped role:
      kubectl edit role <role-name> -n <namespace>
    • In the opened YAML, locate rules: entries with resources: ["secrets"] (or including secrets) and:
      • Remove the get, list, and watch verbs from verbs:, or
      • Remove the entire rule if no longer needed.
    • Save and exit to apply.
  4. Adjust bindings so only intended subjects keep Secret access

    • For roles/clusterroles where some Secret access is still required but only for a limited group, edit bindings:
      kubectl edit clusterrolebinding <binding-name>
      or
      kubectl edit rolebinding <binding-name> -n <namespace>
    • Under subjects:, remove users/groups/serviceaccounts that should not be able to access Secrets.
    • Save and exit.
  5. Re-test access for specific identities before global verification

    • For high‑risk groups (for example system:authenticated or a CI user), explicitly test:
      kubectl auth can-i get,list,watch secrets --all-namespaces --as=<user-or-group>
    • Confirm it returns no for identities that should not have cluster‑wide Secret read access.
  6. Verification (derived from the audit command)

    • Run on any machine with kubectl access:
      echo "canGetListWatchSecretsAsSystemAuthenticated: $(kubectl auth can-i get,list,watch secrets --all-namespaces --as=system:authenticated)"
    • Confirm the output shows:
      canGetListWatchSecretsAsSystemAuthenticated: no
Using kubectl
# 1) Identify which ClusterRoles currently allow get/list/watch on secrets
# Run on: any machine with kubectl access
kubectl get clusterrole -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
(.resources // []) | index("secrets")
and ((.verbs // []) | (.[] | IN("get","list","watch")))
)
)
| .metadata.name
' | sort -u

# 2) Inspect each identified ClusterRole to decide if it truly needs secret read access
# Replace <clusterrole-name> with each name from the previous command.
kubectl get clusterrole <clusterrole-name> -o yaml

# 3) Edit ClusterRoles to remove get/list/watch on secrets where not required
# Example: remove only the secret-related verbs, keep others.
# Run once per ClusterRole you’ve decided should not read secrets.

# This opens your editor; adjust the 'rules' section to drop get/list/watch from
# any entry where 'resources: ["secrets"]' (or includes "secrets").
kubectl edit clusterrole <clusterrole-name>

# 4) For fine-grained control, split secret access into a dedicated ClusterRole (optional)
# Run on: any machine with kubectl access

# Create a narrow-scope ClusterRole that allows only what is actually needed;
# adjust namespaces and subjects before applying.
cat << 'EOF' > readonly-secrets-narrow-clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: readonly-secrets-narrow
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"] # Remove "list","watch" unless absolutely necessary
EOF

kubectl apply -f readonly-secrets-narrow-clusterrole.yaml

# Example: bind this narrowly to a specific service account instead of broad groups
cat << 'EOF' > readonly-secrets-narrow-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: readonly-secrets-narrow-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: readonly-secrets-narrow
subjects:
- kind: ServiceAccount
name: my-workload-sa
namespace: default
EOF

kubectl apply -f readonly-secrets-narrow-binding.yaml

# 5) Verification: confirm system:authenticated can no longer get/list/watch secrets cluster-wide
# Run on: any machine with kubectl access
kubectl auth can-i get,list,watch secrets --all-namespaces --as=system:authenticated
Automation
#!/usr/bin/env bash
#
# Minimize access to Secrets for system:authenticated
#
# Scope: any machine with kubectl access and current-context pointing to the target cluster.
# Requirements: kubectl, cluster-admin (or equivalent) privileges.

set -euo pipefail

echo "=== Checking current access for system:authenticated to secrets ==="
CURRENT_ACCESS=$(kubectl auth can-i get,list,watch secrets --all-namespaces --as=system:authenticated || echo "error")
echo "canGetListWatchSecretsAsSystemAuthenticated: ${CURRENT_ACCESS}"

echo "=== Enumerating RBAC bindings granting get/list/watch on secrets to system:authenticated ==="

# Temporary files
TMP_CLUSTERROLES=$(mktemp)
TMP_ROLES=$(mktemp)
trap 'rm -f "$TMP_CLUSTERROLES" "$TMP_ROLES"' EXIT

# 1) Find ClusterRoles that grant get/list/watch on secrets
kubectl get clusterroles -o json > "${TMP_CLUSTERROLES}"

echo "- ClusterRoles with get/list/watch on secrets:"
jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // [] | index("secrets")) and
(.verbs // [] | (index("get") or index("list") or index("watch")))
)
| any
)
| .metadata.name
' "${TMP_CLUSTERROLES}" | sort -u || true

CLUSTERROLES_WITH_SECRET_ACCESS=$(jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // [] | index("secrets")) and
(.verbs // [] | (index("get") or index("list") or index("watch")))
)
| any
)
| .metadata.name
' "${TMP_CLUSTERROLES}" | sort -u)

# 2) Find Roles that grant get/list/watch on secrets
kubectl get roles --all-namespaces -o json > "${TMP_ROLES}"

echo "- Namespaced Roles with get/list/watch on secrets:"
jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // [] | index("secrets")) and
(.verbs // [] | (index("get") or index("list") or index("watch")))
)
| any
)
| (.metadata.namespace + "/" + .metadata.name)
' "${TMP_ROLES}" | sort -u || true

ROLES_WITH_SECRET_ACCESS=$(jq -r '
.items[]
| select(
(.rules // [])
| map(
(.resources // [] | index("secrets")) and
(.verbs // [] | (index("get") or index("list") or index("watch")))
)
| any
)
| (.metadata.namespace + "/" + .metadata.name)
' "${TMP_ROLES}" | sort -u)

echo "=== Enumerating bindings that attach those roles to system:authenticated ==="

echo "- ClusterRoleBindings to system:authenticated that use affected ClusterRoles:"
kubectl get clusterrolebindings -o json \
| jq -r --argjson crn "[\"$(echo "${CLUSTERROLES_WITH_SECRET_ACCESS}" | paste -sd '","' -)\"]" '
.items[]
| select(
(.roleRef.kind == "ClusterRole")
and ((.roleRef.name) as $rname | ($crn | index($rname)))
and (
(.subjects // [])
| map(.kind == "Group" and .name == "system:authenticated")
| any
)
)
| .metadata.name
' | sort -u || true

CLUSTERROLEBINDINGS_TO_SYSTEM_AUTH=$(kubectl get clusterrolebindings -o json \
| jq -r --argjson crn "[\"$(echo "${CLUSTERROLES_WITH_SECRET_ACCESS}" | paste -sd '","' -)\"]" '
.items[]
| select(
(.roleRef.kind == "ClusterRole")
and ((.roleRef.name) as $rname | ($crn | index($rname)))
and (
(.subjects // [])
| map(.kind == "Group" and .name == "system:authenticated")
| any
)
)
| .metadata.name
' | sort -u)

echo "- RoleBindings to system:authenticated that use affected Roles:"
kubectl get rolebindings --all-namespaces -o json \
| jq -r --argjson rn "[\"$(echo "${ROLES_WITH_SECRET_ACCESS}" | paste -sd '","' -)\"]" '
.items[]
| select(
(.roleRef.kind == "Role")
and ((.metadata.namespace + "/" + .roleRef.name) as $rname | ($rn | index($rname)))
and (
(.subjects // [])
| map(.kind == "Group" and .name == "system:authenticated")
| any
)
)
| (.metadata.namespace + "/" + .metadata.name)
' | sort -u || true

ROLEBINDINGS_TO_SYSTEM_AUTH=$(kubectl get rolebindings --all-namespaces -o json \
| jq -r --argjson rn "[\"$(echo "${ROLES_WITH_SECRET_ACCESS}" | paste -sd '","' -)\"]" '
.items[]
| select(
(.roleRef.kind == "Role")
and ((.metadata.namespace + "/" + .roleRef.name) as $rname | ($rn | index($rname)))
and (
(.subjects // [])
| map(.kind == "Group" and .name == "system:authenticated")
| any
)
)
| (.metadata.namespace + "/" + .metadata.name)
' | sort -u)

echo "=== REVIEW REQUIRED (MANUAL CONTROL) ==="
echo "This control is MANUAL. There is no safe automatic edit of RBAC without human review."
echo
echo "1) Review the listed ClusterRoles and Roles and determine:"
echo " - Which uses of get/list/watch on secrets are strictly required."
echo " - Whether they must apply to the broad group system:authenticated."
echo
echo "2) For each binding below that is NOT required, you may remove system:authenticated"
echo " from subjects or replace it with a narrower group/service account."
echo
echo "ClusterRoleBindings to review (cluster-wide effect):"
echo "${CLUSTERROLEBINDINGS_TO_SYSTEM_AUTH:-<none>}"
echo
echo "RoleBindings to review (namespaced effect):"
echo "${ROLEBINDINGS_TO_SYSTEM_AUTH:-<none>}"
echo
echo "To edit a binding, run for each chosen binding:"
echo " # Example: edit ClusterRoleBinding"
echo " kubectl edit clusterrolebinding <clusterrolebinding-name>"
echo
echo " # Example: edit RoleBinding in a namespace"
echo " kubectl edit rolebinding <rolebinding-name> -n <namespace>"
echo
echo "In the opened YAML, locate subjects: and remove or replace entries like:"
echo " - kind: Group"
echo " name: system:authenticated"
echo
read -r -p "Press Enter after you have completed RBAC edits to re-check access, or Ctrl+C to abort... " _

echo "=== Re-checking access for system:authenticated to secrets after RBAC review ==="
FINAL_ACCESS=$(kubectl auth can-i get,list,watch secrets --all-namespaces --as=system:authenticated || echo "error")
echo "canGetListWatchSecretsAsSystemAuthenticated: ${FINAL_ACCESS}"

echo "=== Completed. ==="