Skip to main content

Azure Minimize Access Secrets - Security Rule

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)
  • BSI C5 (Germany)
  • Brazil LGPD
  • CCPA / CPRA (California)
  • CIS AKS
  • CIS Critical Security Controls v8
  • 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​

Manual Steps
  1. List all Roles and ClusterRoles that can access Secrets

    • On any machine with kubectl access:
      # Roles with get/list/watch on secrets
      kubectl get roles --all-namespaces -o json | \
      jq -r '.items[]
      | {ns:.metadata.namespace, name:.metadata.name, rules:.rules[]?}
      | select(.rules.resources[]? == "secrets")
      | select((.rules.verbs[]? | IN("get","list","watch")) == true)
      | "\(.ns) \(.name)"' | sort -u

      # ClusterRoles with get/list/watch on secrets
      kubectl get clusterroles -o json | \
      jq -r '.items[]
      | {name:.metadata.name, rules:.rules[]?}
      | select(.rules.resources[]? == "secrets")
      | select((.rules.verbs[]? | IN("get","list","watch")) == true)
      | .name' | sort -u
      (If jq is unavailable, fetch YAML and review .rules for resources: ["secrets"] and verbs: ["get","list","watch"].)
  2. Identify who is bound to these Roles/ClusterRoles

    • For each Role <role> in namespace <ns> from step 1:
      kubectl get rolebinding -n <ns> -o wide | grep -w '<role>' || true
    • For each ClusterRole <cr> from step 1:
      kubectl get clusterrolebinding -o wide | grep -w '<cr>' || true
    • Review subjects (users, groups, service accounts) and decide if each truly needs secret access.
  3. Review necessity of secret access per subject

    • For each subject identified in step 2, determine:
      • What workloads or human processes they support.
      • Whether they require get, list, or watch on secrets, or if access can be removed or narrowed (e.g., only specific namespaces, or no access at all).
    • Document which bindings must keep secret access (justified), and which should have it removed.
  4. Remove unnecessary secret permissions from Roles/ClusterRoles

    • For a Role in a specific namespace:
      kubectl edit role -n <namespace> <role-name>
    • For a ClusterRole:
      kubectl edit clusterrole <clusterrole-name>
    • In the editor, remove any rules entries that include resources: ["secrets"] (or contain secrets) with verbs including get, list, or watch, where you have determined they are not required. Save and exit.
  5. Optionally create least-privileged Roles without secret access and rebind

    • Create a new Role YAML (no secrets in resources) tailored to needed resources/verbs and apply it:
      kubectl apply -f /absolute/path/to/new-least-privileged-role.yaml
    • Update corresponding RoleBindings/ClusterRoleBindings to reference this new Role/ClusterRole instead of the old one that granted secret access:
      kubectl edit rolebinding -n <namespace> <binding-name>
      kubectl edit clusterrolebinding <binding-name>
    • Adjust .roleRef.name to your new Role/ClusterRole.
  6. Verify that secret access is minimized

    • Re-run the evidence-gathering queries to confirm that only intentionally approved Roles/ClusterRoles still have secret access:
      kubectl get roles --all-namespaces -o json | \
      jq -r '.items[]
      | {ns:.metadata.namespace, name:.metadata.name, rules:.rules[]?}
      | select(.rules.resources[]? == "secrets")
      | select((.rules.verbs[]? | IN("get","list","watch")) == true)
      | "\(.ns) \(.name)"' | sort -u

      kubectl get clusterroles -o json | \
      jq -r '.items[]
      | {name:.metadata.name, rules:.rules[]?}
      | select(.rules.resources[]? == "secrets")
      | select((.rules.verbs[]? | IN("get","list","watch")) == true)
      | .name' | sort -u
    • Optionally, perform a spot test with an affected user/service account (e.g., via kubectl auth can-i get secret -n <ns> --as <user> or --as system:serviceaccount:<ns>:<sa>) to ensure removed access is actually denied.
Using kubectl
# 1) List all roles and clusterroles that can access secrets
# Run on: any machine with kubectl access

# Roles (namespace-scoped)
kubectl get roles --all-namespaces -o yaml | \
awk '
$1=="kind:" && $2=="Role" {role=$0}
/rules:/ {inrules=1}
inrules && /resources:/ && /secrets/ {hassecret=1}
inrules && /verbs:/ && ( /get/ || /list/ || /watch/ ) && hassecret {
print "---"; print role; hassecret=0; inrules=0
}
'

# ClusterRoles (cluster-scoped)
kubectl get clusterroles -o yaml | \
awk '
$1=="kind:" && $2=="ClusterRole" {cr=$0}
/rules:/ {inrules=1}
inrules && /resources:/ && /secrets/ {hassecret=1}
inrules && /verbs:/ && ( /get/ || /list/ || /watch/ ) && hassecret {
print "---"; print cr; hassecret=0; inrules=0
}
'

# 2) For clearer review, show affected roles/clusterroles with full rules

# Roles that reference secrets
kubectl get roles --all-namespaces -o json | \
jq -r '
.items[]
| select(.rules[]?
| (.resources[]? == "secrets")
and (.verbs[]? | IN("get","list","watch")))
| "kubectl get role \(.metadata.name) -n \(.metadata.namespace) -o yaml"
' | sh

# ClusterRoles that reference secrets
kubectl get clusterroles -o json | \
jq -r '
.items[]
| select(.rules[]?
| (.resources[]? == "secrets")
and (.verbs[]? | IN("get","list","watch")))
| "kubectl get clusterrole \(.metadata.name) -o yaml"
' | sh

# 3) See who is bound to those roles (rolebindings)

# For a specific role (example: role "app-reader" in namespace "prod")
kubectl get rolebinding -n prod -o wide
kubectl get rolebinding -n prod -o yaml | \
yq '.items[] | select(.roleRef.name=="app-reader")'

# For a specific clusterrole (example: "view")
kubectl get clusterrolebindings -o wide
kubectl get clusterrolebindings -o yaml | \
yq '.items[] | select(.roleRef.name=="view")'

What indicates a problem:

  • Any Role or ClusterRole with:
    • resources: ["secrets"] (or includes secrets), AND
    • verbs including get, list, or watch,
  • Especially if:
    • The role name/description suggests general or broad use (e.g. admin, edit, view, dev-*, team-*).
    • RoleBindings or ClusterRoleBindings attach these roles to:
      • Human users/groups instead of automated controllers, or
      • Service accounts in namespaces that do not strictly require secret read access.

Use this information to decide, per role, whether get/list/watch on secrets is justified. If not, remove or narrow those permissions using kubectl edit or by updating the manifest.

Automation
#!/usr/bin/env bash
# Report all Roles and ClusterRoles that can access Secrets with get/list/watch
# Run on: any machine with kubectl access and current context set to the target cluster

set -euo pipefail

echo "=== ClusterRoles with secret access (get/list/watch) ==="
echo "NAMESPACE,TYPE,NAME,VERBS"
kubectl get clusterroles -o json \
| jq -r '
.items[]
| . as $cr
| .rules[]
| select(
(.resources // []) | index("secrets")
and
((.verbs // []) | map(select(.=="get" or .=="list" or .=="watch")) | length > 0)
)
| "\($cr.metadata.namespace // "-"),ClusterRole,\($cr.metadata.name),\((.verbs // [])|join("|"))"
' \
| sort

echo
echo "=== Namespaced Roles with secret access (get/list/watch) ==="
echo "NAMESPACE,TYPE,NAME,VERBS"
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| . as $r
| .rules[]
| select(
(.resources // []) | index("secrets")
and
((.verbs // []) | map(select(.=="get" or .=="list" or .=="watch")) | length > 0)
)
| "\($r.metadata.namespace),Role,\($r.metadata.name),\((.verbs // [])|join("|"))"
' \
| sort

echo
echo "=== ServiceAccounts bound to those Roles/ClusterRoles (potential consumers) ==="
echo "NAMESPACE,SERVICEACCOUNT,ROLE_TYPE,ROLE_NAME"
# Build a temporary map of risky roles to look up in RoleBindings and ClusterRoleBindings
# 1) Namespaced roles
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| . as $r
| select(
[.rules[]
| select(
(.resources // []) | index("secrets")
and
((.verbs // []) | map(select(.=="get" or .=="list" or .=="watch")) | length > 0)
)
] | length > 0
)
| "\($r.metadata.namespace),Role,\($r.metadata.name)"
' >/tmp/risky-roles.txt

# 2) ClusterRoles
kubectl get clusterroles -o json \
| jq -r '
.items[]
| . as $cr
| select(
[.rules[]
| select(
(.resources // []) | index("secrets")
and
((.verbs // []) | map(select(.=="get" or .=="list" or .=="watch")) | length > 0)
)
] | length > 0
)
| "-,ClusterRole,\($cr.metadata.name)"
' >/tmp/risky-clusterroles.txt

# Find RoleBindings referencing risky Roles
kubectl get rolebindings --all-namespaces -o json \
| jq -r '
.items[]
| . as $rb
| .subjects[]? as $s
| select($s.kind=="ServiceAccount")
| "\($rb.metadata.namespace),\($rb.roleRef.kind),\($rb.roleRef.name),\($s.namespace // $rb.metadata.namespace),\($s.name)"
' \
| while IFS=',' read -r rbns roleKind roleName sans sa; do
if [ "$roleKind" = "Role" ]; then
if grep -q "^$rbns,Role,$roleName$" /tmp/risky-roles.txt 2>/dev/null; then
echo "$sans,$sa,Role,$roleName"
fi
fi
done | sort -u

# Find ClusterRoleBindings referencing risky ClusterRoles
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| . as $crb
| .subjects[]? as $s
| select($s.kind=="ServiceAccount")
| "\($crb.roleRef.kind),\($crb.roleRef.name),\($s.namespace),\($s.name)"
' \
| while IFS=',' read -r roleKind roleName sans sa; do
if [ "$roleKind" = "ClusterRole" ]; then
if grep -q "^-,ClusterRole,$roleName$" /tmp/risky-clusterroles.txt 2>/dev/null; then
echo "$sans,$sa,ClusterRole,$roleName"
fi
fi
done | sort -u

rm -f /tmp/risky-roles.txt /tmp/risky-clusterroles.txt

cat <<'EOF'

How to interpret this report:

1) "ClusterRoles with secret access" and "Namespaced Roles with secret access"
- Any entry here represents a Role/ClusterRole that can access Secrets with get/list/watch.
- These are candidates for review; not all of them are necessarily misconfigured.
- Potential problems:
* Very broad roles (e.g., verbs: "*", resources: "*") that include secrets.
* Generic roles used by many subjects (service accounts, groups, or users).
* Roles in non-system namespaces that were created for applications but have secret read access.

2) "ServiceAccounts bound to those Roles/ClusterRoles"
- Each line shows a ServiceAccount that can use a Role/ClusterRole with secret read access.
- Potential problems:
* ServiceAccounts used by low-privilege or external-facing workloads.
* "default" ServiceAccounts in many namespaces appearing here.
* ServiceAccounts used by CI/CD or automation with more secret access than strictly required.

Next steps (manual review):
- For each suspicious Role/ClusterRole, inspect it in detail:
kubectl get role -n <namespace> <name> -o yaml
kubectl get clusterrole <name> -o yaml
- For each ServiceAccount listed, identify its workloads:
kubectl get pods -A --field-selector spec.serviceAccountName=<serviceaccount-name> -o wide
- Decide case-by-case whether to:
* Remove get/list/watch on "secrets" from that Role/ClusterRole, OR
* Create a new, narrower role without secret access and rebind appropriate subjects to it.

There is no one-shot automated fix; changes must be made carefully per-role and per-workload.
EOF