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
Remediation
Manual Steps
-
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 -uThis shows all Roles that reference the
secretsresource in any rule. -
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 yamlManually check
.rulesfor:resources: ["secrets"]or includingsecretsverbscontaining any ofget,list,watch(or*)
- Run on: any machine with kubectl access
-
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:orsecretvolumes, or perform runtime secret reads via the API).
- For each Role from step 2, identify which subjects actually use it:
-
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, removesecrets(or*) from its rules, or removeget/list/watchonsecrets:
kubectl edit role -n <namespace> <role-name>In the editor, update
.rulesso either:resourcesno longer includessecrets, orverbsno longer includesget,list, orwatchforsecrets.
- Run on: any machine with kubectl access
-
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
secretsaccess: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:
Inkubectl edit rolebinding -n <namespace> <rolebinding-name>
.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.
- Create a new Role without
-
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.
verbsforsecretsare limited to what is strictly necessary, typically onlyget(and rarelylist/watch).
- Re-run the discovery query:
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):
resourcesincludessecrets(or["*"]) in combination with:- overly broad
verbs, such as["get", "list", "watch"],["*"], or many verbs where onlygetmight 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.
- overly broad
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 grantsget,list,watch, or*on thesecretsresource.
- 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 clusterroleor manifest updates to remove or narrow these permissions.