Designing Roles With Least Privilege
Goal
You create roles and bindings yourself to design least privilege, and build the habit of verifying the boundary of permissions with kubectl auth can-i.
Why it matters
The point where people get most confused in RBAC is the fact that it is the binding, not the role, that decides the scope. Even the same ClusterRole is valid cluster-wide if attached with a ClusterRoleBinding, and only in that namespace if attached with a RoleBinding. Thanks to this property, you can define a common bundle of permissions once as a ClusterRole and have each team reuse it in its own namespace. The second trap is subresources. pods, pods/log, and pods/exec are different resource names, so permissions do not follow automatically. You need this distinction to keep shell access from coming along with a bot that only reads logs. Finally, a permission is not something you write but something that is judged, so instead of reading the manifest with your eyes, ask through impersonation. Do not check only yes; check no as well to know whether the permission is broader than necessary.
Steps
- Create the namespace
rbac-laband in it create three service accounts:app-reader,inspector, andlog-bot. - In
rbac-lab, create the Rolepod-reader. There is one rule;apiGroupsis the core group (empty string),resourcesispods, andverbsare the threeget,list, andwatch. The wildcard*must not be used anywhere. - In
rbac-lab, create the RoleBindingread-pods.roleRefis kindRole, namepod-reader, and the subject is kindServiceAccount, nameapp-reader, namespacerbac-lab. - Impersonate
app-readerto check permissions and save the result to/root/ops/rbac/out/can-i.txt. This file must contain bothyesandnoresponses. The items to check are at least the following three — listing Pods inrbac-lab(yes), deleting Pods inrbac-lab(no), and listing Pods in thedefaultnamespace (no). - Create the ClusterRole
node-viewer.resourcesmust includenodes, and theverbsof every rule must be onlyget,list, andwatch. Then, with the ClusterRoleBindingnode-viewer-binding, give this role to therbac-labservice accountapp-reader. As a result,app-readermust be able to get nodes but not delete them. - Create the ClusterRole
ns-inspector(it must be able to read ConfigMaps). Then, inrbac-lab, create the RoleBindinginspect-herewithroleRef.kindasClusterRoleand name asns-inspector, and the subject as therbac-labservice accountinspector. As a result,inspectormust be able to read ConfigMaps inrbac-labbut not indefault. - In
rbac-lab, create the Rolelog-reader. Inresources, put onlypods/log, and do not includepods. Bind this role tolog-botwith a RoleBinding. As a result,log-botmust be able to readpods/log, must not be able to readpodsitself, and must not be able to usepods/execeither. - Create
/root/ops/rbac/out/rbac-audit.json. There are four fields.cluster_admin_bindingsis an array of the names of ClusterRoleBindings whoseroleRef.nameiscluster-admin, and it must exactly match the number in the real cluster and include the default bindingcluster-admin.wildcard_rolesis an array of the names of roles that use*(1 or more),secret_readersis an array of the names of roles that handlesecrets(1 or more), andleast_privilege_violationsis an array of items judged to be least-privilege violations.
Reference
- The formal subject name of a service account is
system:serviceaccount:<네임스페이스>:<이름>(namespace and name). Ask in the formkubectl auth can-i <동사> <리소스> -n <네임스페이스> --as=<주체>(verb, resource, namespace, and subject). - With
kubectl create role/create clusterrole/create rolebinding, adding--dry-run=client -o yamlgives you a correct manifest skeleton. If you use--serviceaccount=<네임스페이스>:<이름>(namespace and name), the subject's namespace field is filled in automatically. - Counting step 8 by hand almost always goes wrong. Extract it with
kubectl get clusterrolebindings -o json | jq. For wildcard roles, filterkubectl get roles,clusterroles -A -o jsonfor ones whereverbsorresourcescontains*. - Common mistake 1: In
apiGroups, writing"v1"or"core". The name of the core group is an empty string. - Common mistake 2: putting
podsandpods/logtogether in step 7. Then the Pods themselves become readable and the subresource separation practice is lost. - Common mistake 3: creating a ClusterRole in step 5 and binding it with a RoleBinding. A node has no namespace, so a RoleBinding does not give access.
- The lab Pod comes up fresh for each lab, so the cluster state created in the earlier lab does not remain. Create the namespaces and service accounts yourself within this lab. This is exactly why operational procedures must be left in runbooks and manifests rather than in memory.
Prepare the namespace and service accounts
Create the namespace rbac-lab and in it create three service accounts: app-reader, inspector, and log-bot.
You can grant permissions only if there are subjects. A service account belongs to a namespace and can be created with just a name.
Create a read-only Role
In rbac-lab, create the Role pod-reader. There is one rule; apiGroups is the core group (empty string), resources is pods, and verbs are the three get, list, and watch. The wildcard * must not be used anywhere.
Check what the API group name of core resources is. The read verbs are three, and you do not use a wildcard.
Bind the Role to a service account
In rbac-lab, create the RoleBinding read-pods. roleRef is kind Role, name pod-reader, and the subject is kind ServiceAccount, name app-reader, namespace rbac-lab.
When the subject is a service account, the name alone is not enough. You must also state which namespace the account is in.
Check the permission boundary with impersonation
Impersonate app-reader to check permissions and save the result to /root/ops/rbac/out/can-i.txt. This file must contain both yes and no responses. The items to check are at least the following three — listing Pods in rbac-lab (yes), deleting Pods in rbac-lab (no), and listing Pods in the default namespace (no).
Remember the formal name format of a service account. You must check both what works and what does not for the boundary to show.
Create a ClusterRole for a cluster-scoped resource
Create the ClusterRole node-viewer. resources must include nodes, and the verbs of every rule must be only get, list, and watch. Then, with the ClusterRoleBinding node-viewer-binding, give this role to the rbac-lab service account app-reader. As a result, app-reader must be able to get nodes but not delete them.
A node is a resource with no namespace, so a Role cannot handle it. To apply across the whole cluster, the binding must also be cluster-scoped.
Try attaching a ClusterRole with a RoleBinding
Create the ClusterRole ns-inspector (it must be able to read ConfigMaps). Then, in rbac-lab, create the RoleBinding inspect-here with roleRef.kind as ClusterRole and name as ns-inspector, and the subject as the rbac-lab service account inspector. As a result, inspector must be able to read ConfigMaps in rbac-lab but not in default.
What decides the scope is the kind of binding, not the kind of role. Check in which namespace alone the same role becomes valid.
Create a role that opens only a subresource
In rbac-lab, create the Role log-reader. In resources, put only pods/log, and do not include pods. Bind this role to log-bot with a RoleBinding. As a result, log-bot must be able to read pods/log, must not be able to read pods itself, and must not be able to use pods/exec either.
Logs have a resource name different from Pods. If you also open the parent resource, this step loses its meaning.
Build a permission audit report
Create /root/ops/rbac/out/rbac-audit.json. There are four fields. cluster_admin_bindings is an array of the names of ClusterRoleBindings whose roleRef.name is cluster-admin, and it must exactly match the number in the real cluster and include the default binding cluster-admin. wildcard_roles is an array of the names of roles that use * (1 or more), secret_readers is an array of the names of roles that handle secrets (1 or more), and least_privilege_violations is an array of items judged to be least-privilege violations.
Do not count by hand; extract it from the cluster. The number of cluster-admin bindings must match the real value exactly.