Namespace-Wide RBAC Read on Secrets Lets Any Pod Read Every Team's Credentials
A Role granting get/list/watch on secrets without resourceNames lets any pod's ServiceAccount read every Secret in the namespace, not just its own, when combined with default token automounting.
Operational summary
At a glance
- Symptom
- A platform team notices during a routine access review that a workload's service account can read Secret objects belonging to unrelated applications in the…
- Likely cause
- The Role manifest defined the rule as:rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list", "watch"]This grants get/list/watch on all Secret objects in whichever…
- Impact
- Any code executing inside the affected pod, including third-party or dynamically loaded plugin code, can enumerate and read every Secret in the namespace, including database credentials, TLS keys…
- Verification signal
- Validation must directly re-test the exact access path identified during diagnosis, not merely confirm the manifest changed.
- Safe correction
- Correct the misconfiguration in two independent layers, because either one alone leaves residual risk.
- Rollback or recovery
- If the corrected Role breaks the application (for example because it also legitimately reads a second Secret that was not identified during diagnosis), revert the Role to its…
Symptom
A platform team notices during a routine access review that a workload’s service account can read Secret objects belonging to unrelated applications in the same namespace. The workload was only ever intended to read its own single Secret, yet a quick kubectl auth can-i check against other Secret names in the namespace returns yes. No incident has occurred yet, but the access review flags it because the workload is internet-facing and runs third-party code via a plugin system.
False Assumption
The team assumed that because the Role was named after the specific application (for example payments-api-role) and was only bound to that application’s ServiceAccount, its permissions were scoped to that application’s own Secret. In Kubernetes RBAC, a Role’s resources: ["secrets"] rule without a resourceNames restriction grants access to every Secret object in the Role’s namespace, not just the one the application creates or consumes. Naming a Role narrowly does not narrow what it actually authorises; only the rule content does that.
Root Cause
The Role manifest defined the rule as:
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
This grants get/list/watch on all Secret objects in whichever namespace the Role is deployed to, because no resourceNames field was set. The RoleBinding then attached this Role to the application’s ServiceAccount. Because Kubernetes documentation on RBAC explicitly separates the authorisation model (rules, verbs, resources, optional resourceNames) from authentication and workload identity, the missing resourceNames field is not a bug in Kubernetes; it is a correctly-applied but under-scoped rule. The default ServiceAccount token was also automounted into the pod (no automountServiceAccountToken: false set at pod or ServiceAccount level), so any process running inside that pod, including a compromised plugin or dependency, inherits the same broad read access to every Secret in the namespace.
Impact
Any code executing inside the affected pod, including third-party or dynamically loaded plugin code, can enumerate and read every Secret in the namespace, including database credentials, TLS keys and API tokens belonging to unrelated teams sharing that namespace. This converts a single compromised or malicious in-pod dependency into a namespace-wide credential-exfiltration path. The severity depends on what else shares the namespace; on a namespace containing only one team’s low-value test data, the blast radius is limited, but on a shared or multi-tenant namespace it is not.
Diagnosis
Confirm the scope of the problem before changing anything. First, identify what the ServiceAccount can actually do, not what the Role’s name implies:
kubectl auth can-i list secrets
--as=system:serviceaccount:payments:payments-api-sa
-n payments
A yes response confirms namespace-wide list access. Next, inspect the Role rule directly to confirm the absence of a resourceNames restriction:
kubectl get role payments-api-role -n payments -o yaml
Look for the secrets resource entry and confirm there is no resourceNames key limiting it to a specific Secret. Then check whether the pod actually mounts a token at all, since a Role with broad rights is only exploitable from inside the pod if a token is present:
kubectl get pod payments-api-7f9c8-abcde -n payments
-o jsonpath='{.spec.automountServiceAccountToken}'
An empty result or true (the Kubernetes default when unset) indicates the token is mounted, satisfying the second half of this trap. Together, the RBAC rule and the token automount confirm both the authorisation gap and the delivery path into the pod.
Correction
Correct the misconfiguration in two independent layers, because either one alone leaves residual risk. First, scope the Role to the single Secret the application actually needs using resourceNames:
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["payments-api-credentials"]
verbs: ["get"]
Note that list and watch cannot be meaningfully combined with resourceNames for namespace-scoped enumeration semantics in the same way get can; if the application only ever fetches a known Secret by name, keep the verb list to get alone. Second, disable automatic token mounting for workloads that do not need the Kubernetes API at all, or that only need it for this one narrow read:
apiVersion: v1
kind: ServiceAccount
metadata:
name: payments-api-sa
namespace: payments
automountServiceAccountToken: false
If the pod does need the token for this specific narrow purpose, keep automount enabled but rely on the corrected Role rather than disabling automount, since the two controls address different failure paths and are not interchangeable.
Validation
Validation must directly re-test the exact access path identified during diagnosis, not merely confirm the manifest changed. After applying the corrected Role, re-run the same can-i check against an unrelated Secret in the namespace:
kubectl auth can-i get secret unrelated-team-secret
--as=system:serviceaccount:payments:payments-api-sa
-n payments
Expect no. Then confirm the application’s own required Secret is still readable:
kubectl auth can-i get secret payments-api-credentials
--as=system:serviceaccount:payments:payments-api-sa
-n payments
Expect yes. Finally, exec into a running instance of the pod (in the isolated validation environment) and confirm the application still starts and reaches its dependencies correctly, since an over-corrected RBAC rule that removes a verb the application genuinely needs will produce a new, different failure at runtime rather than at deploy time.
Rollback
If the corrected Role breaks the application (for example because it also legitimately reads a second Secret that was not identified during diagnosis), revert the Role to its previous rule set using the manifest retained from before the change, then re-run diagnosis to identify every Secret the application actually touches before reapplying a corrected, fully-scoped rule. Do not restore the original unscoped rule as a permanent fix; treat any rollback as temporary while the full set of required Secret names is confirmed. Keep the pre-change Role manifest and RoleBinding as version-controlled artefacts specifically so this reversion is a single kubectl apply -f of a known-good file rather than a manually reconstructed guess.
Prevention
Require resourceNames on any RBAC rule granting access to secrets as a standard policy check in CI or admission control, so a namespace-wide grant cannot merge without an explicit, reviewed exception. Set automountServiceAccountToken: false as the default on ServiceAccount manifests and require an explicit opt-in comment or annotation when a workload genuinely needs API access, so the reviewer sees the decision rather than inheriting it silently. Periodically re-run the same kubectl auth can-i enumeration used in diagnosis against every ServiceAccount in shared namespaces as a scheduled access review, since RBAC drift accumulates quietly as Roles are copied and extended over time.
Apply the safer control
Before you change production
Confirm the affected scope, export the current configuration, and test the replacement control in a non-production environment first.
Fix commands and configuration
kubectl auth can-iyespayments-api-roleVerify, roll back or escalate
Verify
Validation must directly re-test the exact access path identified during diagnosis, not merely confirm the manifest changed.
Rollback
If the corrected Role breaks the application (for example because it also legitimately reads a second Secret that was not identified during diagnosis), revert the Role to its previous rule set using the manifest retained from before the change, then…
Escalate
Escalate when the blast radius is uncertain, the control cannot be tested safely, or remediation requires an outage or security exception.