Skip to main content

Azure Minimize Access To Secrets

More Info:

Roles that grant get, list, or watch on secrets expose sensitive credentials. Access to secrets should be limited to the workloads that genuinely need it.

Risk Level

High

Address

Security

Compliance Standards

  • CIS AKS

Triage and Remediation

Remediation

Manual Steps
  1. List all Roles that can access Secrets

    • Run on: any machine with kubectl access
    kubectl get roles --all-namespaces -o json \
    | jq -r '
    .items[]
    | select(
    .rules[]
    | select(
    (.resources // []) | index("secrets")
    )
    )
    | "\(.metadata.namespace) \(.metadata.name)"' \
    | sort -u

    This shows all Roles that reference the secrets resource in any rule.

  2. Review exactly which secret permissions each Role has

    • Run on: any machine with kubectl access
      For each <namespace> <role-name> pair from step 1, inspect in detail:
    kubectl get role -n <namespace> <role-name> -o yaml

    Manually check .rules for:

    • resources: ["secrets"] or including secrets
    • verbs containing any of get, list, watch (or *)
  3. Determine if each Role’s secret access is justified

    • For each Role from step 2, identify which subjects actually use it:
      kubectl get rolebinding -n <namespace> -o yaml \
      | yq '.items[] | select(.roleRef.name == "<role-name>") | .subjects'
    • For each ServiceAccount / user / group listed:
      • Identify the workloads (Pods/Deployments/Jobs, etc.) using that ServiceAccount.
      • Decide whether those workloads truly need to read Kubernetes Secrets (for example, they use envFrom: secretRef: or secret volumes, or perform runtime secret reads via the API).
  4. Adjust Roles to least privilege where secret access is not required

    • Run on: any machine with kubectl access
      For a Role whose subjects do not need to read Secrets, remove secrets (or *) from its rules, or remove get/list/watch on secrets:
    kubectl edit role -n <namespace> <role-name>

    In the editor, update .rules so either:

    • resources no longer includes secrets, or
    • verbs no longer includes get, list, or watch for secrets.
  5. Create replacement least-privileged Roles where needed
    If a Role is shared and some subjects still require secret access while others do not:

    • Create a new Role without secrets access:
      kubectl create role <new-role-name> \
      -n <namespace> \
      --verb=<needed-verb-1>,<needed-verb-2> \
      --resource=<needed-resource-1>,<needed-resource-2>
    • Rebind the subjects that should not have secret access to the new Role:
      kubectl edit rolebinding -n <namespace> <rolebinding-name>
      In .roleRef.name, change the Role to <new-role-name> for the appropriate subjects, or split RoleBindings so that only the intended subjects keep the original Role with secret access.
  6. Verify that Roles with secret access are now minimal and intentional

    • Re-run the discovery query:
      kubectl get roles --all-namespaces -o json \
      | jq -r '
      .items[]
      | select(
      .rules[]
      | select(
      (.resources // []) | index("secrets")
      )
      )
      | "\(.metadata.namespace) \(.metadata.name)"' \
      | sort -u
    • For the remaining Roles listed, repeat step 2 quickly to confirm that:
      • Only workloads that genuinely need secret read access are bound.
      • verbs for secrets are limited to what is strictly necessary, typically only get (and rarely list/watch).
Using kubectl
# 1) List all Roles in all namespaces
# Run on: any machine with kubectl access
kubectl get roles --all-namespaces -o wide

# 2) Show only Roles that reference "secrets" in any rule
# (quick text filter; may show false positives that still need review)
kubectl get roles --all-namespaces -o yaml | grep -B4 -A6 "secrets"

# 3) Detailed inspection of Roles that have "secrets" in their rules
# This prints full YAML for human review
kubectl get roles --all-namespaces -o yaml > /tmp/all-roles.yaml
less /tmp/all-roles.yaml

In /tmp/all-roles.yaml, look for rules entries where:

rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", ...]

Indications of a problem (these need human judgment against your workload requirements):

  • resources includes secrets (or ["*"]) in combination with:
    • overly broad verbs, such as ["get", "list", "watch"], ["*"], or many verbs where only get might be needed, or none at all.
    • overly broad resourceNames (omitted or ["*"]), granting access to all secrets in the namespace instead of specific ones.
    • use in generic/system-wide Roles rather than workload-specific Roles tied to a narrow ServiceAccount.

Examples that warrant review:

# Overly broad
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]

# Very high risk
rules:
- apiGroups: [""]
resources: ["*"]
verbs: ["*"]

To focus on one suspicious Role you find in the YAML, use:

# Replace <namespace> and <role-name> with values from the list
kubectl get role -n <namespace> <role-name> -o yaml

Use these outputs to decide, per Role, whether:

  • it truly needs any access to secrets at all, and
  • if so, whether you can scope it down (fewer verbs, specific resourceNames, or moving access into a more targeted Role).
Automation
#!/usr/bin/env bash
# Report Roles and ClusterRoles that can access Secrets via get/list/watch
# Run on: any machine with kubectl access and current-context pointing at the target cluster

set -euo pipefail

echo "### Roles with secret access (namespaced) ###"
echo
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| {ns: .metadata.namespace, name: .metadata.name, rules: .rules}
| select(.rules != null)
| select(
any(.rules[]?;
(.resources? // []) | index("secrets") and
(
(.verbs? // []) | any(. == "get" or . == "list" or . == "watch" or . == "*")
)
)
)
| [
.ns,
.name,
(
.rules[]
| select(
(.resources? // []) | index("secrets") and
(
(.verbs? // []) | any(. == "get" or . == "list" or . == "watch" or . == "*")
)
)
| "resources=" + ((.resources // []) | join(",")) +
" verbs=" + ((.verbs // []) | join(","))
)
]
| @tsv
' 2>/dev/null \
| awk 'BEGIN { FS="\t"; OFS="\t"; print "NAMESPACE","ROLE","RULE_DETAILS" } { print }'

echo
echo "### ClusterRoles with secret access (cluster-wide) ###"
echo
kubectl get clusterroles -o json \
| jq -r '
.items[]
| {name: .metadata.name, rules: .rules}
| select(.rules != null)
| select(
any(.rules[]?;
(.resources? // []) | index("secrets") and
(
(.verbs? // []) | any(. == "get" or . == "list" or . == "watch" or . == "*")
)
)
)
| [
.name,
(
.rules[]
| select(
(.resources? // []) | index("secrets") and
(
(.verbs? // []) | any(. == "get" or . == "list" or . == "watch" or . == "*")
)
)
| "resources=" + ((.resources // []) | join(",")) +
" verbs=" + ((.verbs // []) | join(","))
)
]
| @tsv
' 2>/dev/null \
| awk 'BEGIN { FS="\t"; OFS="\t"; print "CLUSTERROLE","RULE_DETAILS" } { print }'

Explanation of output indicating a problem:

  • Any line listed under either:
    • ### Roles with secret access (namespaced) ###, or
    • ### ClusterRoles with secret access (cluster-wide) ### shows a Role/ClusterRole that grants get, list, watch, or * on the secrets resource.
  • These entries must be manually reviewed to determine whether each subject truly requires that level of access; where not required, use kubectl edit role / kubectl edit clusterrole or manifest updates to remove or narrow these permissions.