skip to content

How do you scope Kubernetes RBAC so that a workload or a human can read only the Secrets it actually needs, and why is granting the list verb on Secrets almost as dangerous as granting read access to every one of them?

level: seniorimportance: must knowfreq 55%

answer

  1. Role + get + resourceNames, namespaced
  2. resourceNames ignored for list/watch/create
  3. list/watch return data, not just names
  4. create pods / exec / logs = indirect read
  5. view excludes secrets; edit and admin include them

basics

~20 s

Bind a namespaced Role that grants get on named Secrets via resourceNames, never a ClusterRole with list or watch. list and watch return the full objects including data, and resourceNames cannot restrict them, so list on secrets equals read-everything in scope. Also close the Pod-create and exec escalation paths.

solid answer

~50 s

Grant `get` in a namespaced `Role` with `resourceNames` naming the specific Secrets, bound to one ServiceAccount with a `RoleBinding`. That is the tightest thing RBAC expresses. The trap is that `list` and `watch` return whole objects, `data` included, so `list` on `secrets` is bulk read of every Secret in scope. Worse, `resourceNames` cannot constrain `list` or `watch` at all: those verbs operate on a collection, not a named object, so the filter is silently ignored and the rule grants everything. Cluster-scoped `list secrets` is effectively cluster-admin-equivalent, since Secrets hold ServiceAccount tokens and cloud credentials. RBAC is also not the whole story. Anyone who can create a Pod in a namespace can mount any Secret there and read it, and anyone with `pods/exec` inherits a running Pod's credentials. So the namespace is the real isolation boundary; treat `create pods`, `pods/exec`, `pods/log`, `escalate`, `bind`, and `impersonate` as secret-read-adjacent, and audit them with `kubectl auth can-i --list`.

code

yaml · 18 lines
yaml
# tight: one Secret, one verb, one namespace
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: payments
  name: read-db-creds
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["db-creds"]
    verbs: ["get"]
---
# NOT tight: resourceNames is ignored for list, so this reads every Secret in the namespace
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["db-creds"]
    verbs: ["get", "list", "watch"]

go deeper

for a junior

Know that Role plus RoleBinding is namespaced, that ClusterRole plus ClusterRoleBinding is cluster-wide, and that read access to a Secret means access to its actual value.

for a middle

Write the tight rule with resourceNames and explain why list and watch expose data and cannot be name-restricted.

for a senior

Cover the indirect escalation paths (Pod create, exec, logs, token creation, impersonation), the built-in view/edit/admin behaviour, and how you audit the cluster for the dangerous shape.

for a principal

Argue the isolation model: namespace-per-team as the real secret boundary, a policy that forbids authoring wildcard ClusterRoles, and where to push material out of Kubernetes entirely because RBAC cannot express the needed granularity.

## The shape of a least-privilege Secret grant RBAC rules are additive and there are no deny rules, so a subject's access is the union of every rule bound to it. The tightest grant RBAC can express for Secrets is a namespaced `Role` with `verbs: ["get"]`, `resources: ["secrets"]`, and `resourceNames` listing exactly the Secrets involved, bound with a `RoleBinding` to a single ServiceAccount rather than to a group or to `system:authenticated`. Two consequences follow immediately. Because the Role is namespaced, the grant cannot reach Secrets in other namespaces even if names collide, which makes the namespace the practical unit of secret isolation. And because `resourceNames` is a static list, creating a new Secret requires updating the Role, which is friction but also the point. ## Why list and watch are the dangerous verbs A `get` returns one named object. A `list` returns a collection, and a `watch` streams the collection plus every subsequent change. In all three cases the API server returns the *full* object, including the decrypted `data` field. There is no projection that strips values, and no field-level RBAC. So `list secrets` is not metadata access, it is bulk read. The second, less-known part is that `resourceNames` has no effect on `list`, `watch`, `deletecollection`, or `create`. Those verbs act on a collection or on an object whose name is not known at authorization time, so the authorizer cannot match a name and the restriction is simply not applied: a rule granting `list` on `secrets` with `resourceNames` set still grants list over the whole namespace. This surprises people who believe they wrote a tight rule. Scope multiplies the damage. A `ClusterRole` granting `list secrets` bound with a `ClusterRoleBinding` reads every Secret in every namespace, and Kubernetes Secrets typically include ServiceAccount tokens, image-pull credentials, TLS private keys, and cloud IAM keys. That is a straight path to cluster-admin, which is why it belongs on the same shortlist as `escalate`, `bind`, and `impersonate`. ## The paths RBAC does not cover Tightening the `secrets` resource is necessary but not sufficient, because several other permissions grant indirect read: - **`create` on pods (or on any workload controller)** in a namespace: the subject writes a Pod that mounts any Secret in that namespace and prints it, or ships it out over the network. - **`pods/exec` and `pods/attach`**: the subject enters a running Pod and reads whatever credentials it already holds, including its projected ServiceAccount token. - **`pods/log`**: catches anything the application logged, which in practice is where credentials most often end up. - **`create` on `serviceaccounts/token`**: mints a token for another ServiceAccount and inherits its permissions. - **`impersonate`**: acts as another user, group, or ServiceAccount outright. This is why security reviews say the namespace, not the RBAC verb, is the real boundary for Secrets: if a subject can schedule workloads in a namespace, assume it can read every Secret there. ## Operating the model In practice: one ServiceAccount per workload, its Role listing only the Secrets that workload consumes, and no wildcard `resources: ["*"]` or `verbs: ["*"]` in any ClusterRole you author. Audit with `kubectl auth can-i --list --as=system:serviceaccount:ns:sa`, and periodically sweep for the dangerous shape: Also remember that most workloads never need the `secrets` verb at all, because the kubelet, not the Pod, reads the Secret to mount it. Only controllers, operators, and CSI-style integrations legitimately call the Secrets API. If an application ServiceAccount has `get secrets`, ask why the value is not simply mounted. Finally, the aggregated built-in roles matter: `view` deliberately excludes Secrets, but `edit` and `admin` include full Secret access within their namespace, so binding `admin` to a team is granting them every Secret in that namespace, which is usually intended but should be a conscious decision. ## What to say Open with the tight rule (namespaced Role, `get`, `resourceNames`, bound to one ServiceAccount). Then explain that `list`/`watch` return `data` and that `resourceNames` does not constrain them. Close with the indirect paths (Pod create, exec, logs, token creation, impersonation) and the conclusion that the namespace is the isolation unit.

  • You want a subject to see that a Secret exists but never its contents. Can RBAC do that?
    Not with the core Secrets API: every read verb returns the full object including `data`, and there is no field-level filtering. The usual workarounds are to expose the metadata some other way (labels on a ConfigMap, an operator-owned CRD that mirrors names and timestamps), or to move the material behind an external secret manager whose own policy engine supports finer-grained access.
  • A workload ServiceAccount has get on secrets. How do you decide whether that is legitimate?
    Ask whether the Pod itself calls the Kubernetes API. For ordinary applications the answer is no, because the kubelet reads the Secret and mounts it, so the grant is unnecessary and should be removed in favour of a volume mount. Legitimate holders are controllers, operators, admission webhooks, and integrations that must read Secrets they do not have mounted, and even then the grant should be `get` with `resourceNames` rather than `list`.

saying these in an interview costs you the question

  • Believing resourceNames restricts list or watch (it is silently ignored)
  • Treating list as harmless metadata access when it returns full Secret data
  • Granting cluster-scoped Secret access to application workloads that never call the API
  • Assuming tight RBAC on secrets is sufficient while ignoring create pods, pods/exec, and pods/log
  • Thinking RBAC supports deny rules that can carve exceptions out of a broad grant

context