RBAC least privilege with resourceNames, the secure way
The app needed one Secret, so the Role said resourceNames: [db-password]. Then the client library tried to list, got a 403, and someone deleted the resourceNames line to make the error go away. Now it can read every Secret in the namespace.
The short answer
Use resourceNames for get, update, patch and delete on the exact objects a workload needs. Never grant list or watch on Secrets without it, and teach clients to use a metadata.name field selector when they must watch. resourceNames cannot limit create or deletecollection, so limit create with a ValidatingAdmissionPolicy, and check every Role with kubectl auth can-i.
On this page
What goes wrong
An RBAC rule grants verbs on resources. resourceNames narrows a rule to
named objects, so a workload can read one Secret instead of all of them. It
works for requests that name a single object: get, update, patch,
delete, and subresources such as pods/exec.
It does not work the same way for the rest:
createcannot be limited by name: the name may not be known when RBAC decides, so a top-level create request carries no name. A rule withresourceNamesnever matches an empty name, so a rule withresourceNamesandcreateallows no creates.deletecollectioncannot be limited by name either.listandwatchare allowed withresourceNamesonly if the client asks with ametadata.namefield selector. Most informers andkubectl get secretsdo not, so they get403 Forbidden.
The 403 is where least privilege usually dies. Someone drops
resourceNames to make the error go away, and list on Secrets returns the
full contents of every Secret in the namespace. The rule looks harmless in
review because it says "list", not "get".
What the docs say
You cannot restrict deletecollection or top-level create requests by resource name.
Source: Kubernetes docs, Using RBAC Authorization
If you restrict list or watch by resourceName, clients must include a metadata.name field selector in their list or watch request (that matches the specified resourceName) in order to be authorized.
Source: Kubernetes docs, Using RBAC Authorization
It is also important to note that list and watch access also effectively allow for users to reveal the Secret contents.
Source: Kubernetes docs, Role Based Access Control Good Practices
The RBAC page explains the limits but offers no way to limit create by
name. That job belongs to admission control, and the page does not point
there.
The secure configuration
1. A workload that reads one Secret and watches one ConfigMap.
# k8s-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: web-config
namespace: app
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["web-db-credentials"]
verbs: ["get"] # no list, no watch: they reveal contents
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["web-settings"]
verbs: ["get", "list", "watch"] # list and watch only with a metadata.name field selector
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: web-config
namespace: app
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: web-config
subjects:
- kind: ServiceAccount
name: web
namespace: app
---
# 2. A deployer that may roll out exactly one Deployment.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: web-deployer
namespace: app
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
resourceNames: ["web"]
verbs: ["get", "patch", "update"] # change the image of "web", nothing else
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create"] # cannot be scoped by name; see the policy below
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: web-deployer
namespace: app
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: web-deployer
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: app3. Limit create by name with a ValidatingAdmissionPolicy. The CI
account may create only ConfigMaps whose names start with release-:
# k8s-create-policy.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: ci-deployer-configmap-names
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["configmaps"]
matchConditions:
- name: only-ci-deployer
expression: "request.userInfo.username == 'system:serviceaccount:app:ci-deployer'"
validations:
- expression: "object.metadata.name.startsWith('release-')"
message: "ci-deployer may only create ConfigMaps named release-*"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: ci-deployer-configmap-names
spec:
policyName: ci-deployer-configmap-names
validationActions: ["Deny"]
matchResources:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: app4. Watch one object the way RBAC allows. In kubectl:
kubectl -n app get configmaps --field-selector metadata.name=web-settings --watchIn a client-go informer, set the same field selector in the list options
(fields.OneTermEqualSelector("metadata.name", "web-settings")).
Prove it
Run on a lab cluster (Kubernetes 1.34) with the two manifests above, plus the
ServiceAccounts, web-settings and web-db-credentials they refer to.
Real output:
1. What the web ServiceAccount can and cannot do:
SA=system:serviceaccount:app:web
kubectl -n app auth can-i get secret/web-db-credentials --as=$SA
kubectl -n app auth can-i get secret/other-secret --as=$SA
kubectl -n app auth can-i list secrets --as=$SA
kubectl -n app auth can-i watch configmap/web-settings --as=$SAyes
no
no
yes2. List needs the field selector:
kubectl -n app get configmaps --as=$SA
kubectl -n app get configmaps --field-selector metadata.name=web-settings --as=$SAError from server (Forbidden): configmaps is forbidden: User "system:serviceaccount:app:web" cannot list resource "configmaps" in API group "" in the namespace "app"
NAME DATA AGE
web-settings 1 4s3. The deployer touches only its Deployment, and create is policed:
CI=system:serviceaccount:app:ci-deployer
kubectl -n app auth can-i patch deployment/web --as=$CI
kubectl -n app auth can-i patch deployment/api --as=$CI
kubectl -n app create configmap scratch --from-literal=a=b --as=$CI
kubectl -n app create configmap release-42 --from-literal=a=b --as=$CIyes
no
error: failed to create configmap: configmaps "scratch" is forbidden: ValidatingAdmissionPolicy 'ci-deployer-configmap-names' with binding 'ci-deployer-configmap-names' denied request: ci-deployer may only create ConfigMaps named release-*
configmap/release-42 created4. Nobody lists Secrets by accident:
kubectl auth can-i --list -n app --as=$SAResources Non-Resource URLs Resource Names Verbs
configmaps [] [web-settings] [get list watch]
secrets [] [web-db-credentials] [get]Mistakes people make
Deleting resourceNames to fix a 403
The 403 came from a list without a field selector. Fix the client (field
selector, or get by name), not the Role. Without resourceNames, list
returns every Secret's data.
Adding resourceNames to a create rule
A rule with create and resourceNames matches no create request, so it
grants nothing. People then add a second rule without resourceNames, which
grants create for any name. Use admission policy for names on create.
Forgetting subresources
pods/exec, pods/portforward and serviceaccounts/token are separate
resources. A rule on pods does not grant them, and a rule on pods/exec
without resourceNames grants exec into every pod in the namespace. Scope
them by name like any other object.
Wildcards "for now"
resources: ["*"] also covers every resource type added later, including
CRDs that hold credentials. Name the resources.
Trusting the Role name
A Role called read-only that grants list on Secrets is not read-only.
Review with kubectl auth can-i --list --as=..., not by name.
Checklist
- No Role grants
listorwatchon Secrets withoutresourceNames. - Every rule for a workload names the exact objects with
resourceNameswhere the verb allows it. - Clients that watch single objects use a
metadata.namefield selector. createis never combined withresourceNames; names on create are enforced by a ValidatingAdmissionPolicy.- Subresources (
pods/exec,pods/portforward,serviceaccounts/token) are granted by name only. - No rule uses
*for resources or verbs outside cluster-admin. - Every ServiceAccount's permissions were checked with
kubectl auth can-i --list --as=....
`resourceNames` is a scalpel that only cuts in some directions. Know which verbs it covers, use admission policy for the rest, and never trade it for a quiet 403.
H2-CIAE
Learn it on a live range
Authorization as code, in Identity and Access Engineering: a real host in your browser, and every objective checked on the machine.
Start freeThe Secure Way
More on nodes and clusters
Talos Linux, Kubernetes API hardening, service account tokens, RBAC and the cloud underneath.
All nodes and clusters guides