Module 05 · Share the cluster · 50 min · You are: Platform team
Two teams, one cluster: namespaces and RBAC
In this module
- You learn: How Kubernetes decides who may do what: a Role lists verbs on resources, a RoleBinding gives a Role to a user, a group or a ServiceAccount inside one namespace. And why the cluster-wide versions of both are the dangerous ones.
- You do: Onboard a second team, team-catalog, by writing its folders yourself. Give each team's people read access and each team's deploy account write access, in their own namespace only. Prove every boundary by asking the API server as someone else.
- You end with: Two tenants,
checkout-devandcatalog-dev, each with a quota, defaults, a running app and anrbac.yaml. Members ofteam-checkoutcan readcheckout-devand nothing incatalog-dev, and the reverse. Neither team can read Secrets. The checkout and catalog pods carry no API token.
Until now you were cluster-admin, like everyone on a cluster that grew without a plan. A namespace on its own separates names, not people: without RBAC every user can change every team's objects. The platform team's job is to hand out the smallest set of permissions that lets a team work, bound to its namespace, and to be able to show an auditor that the boundary holds. You will not take anyone's word for it, including your own YAML: you will ask the API server.
Open this module in the interactive course to track progress.
Concepts
- Subject
- Who is asking: a user, a group, or a ServiceAccount. Users and groups are not objects in Kubernetes; they are names that come from the login (a certificate, your company's single sign-on). ServiceAccounts are objects, and are for programs.
- Role and ClusterRole
- A list of rules: API group, resource, verbs. A Role lives in a namespace. A ClusterRole is cluster-wide and can also be reused inside namespaces.
- RoleBinding and ClusterRoleBinding
- A RoleBinding grants a Role, or a ClusterRole, inside its own namespace. A ClusterRoleBinding grants a ClusterRole everywhere. One word of difference decides whether a team sees its namespace or the whole cluster.
- Additive only
- There is no deny rule. A subject may do everything any of its bindings allows. You remove access by removing a binding.
- Impersonation
- An admin can ask "as someone else" with
--asand--as-group. It is how you test permissions without having that person's login.
What each subject may do
| Subject | In its own namespace | In the other team's | Cluster-wide |
|---|---|---|---|
Group team-<team> (people) | Read everything except Secrets (view) | Nothing | Nothing |
ServiceAccount deployer (the pipeline) | Create, change and delete Deployments, Services, ConfigMaps, Ingresses, budgets; read pods | Nothing | Nothing |
ServiceAccount <app> (the workload) | Nothing, and no token mounted | Nothing | Nothing |
| Platform team | Everything | Everything | Everything |
Files to know
Open them in your editor or on GitHub; the bullets say what to look at.
tenants/checkout/rbac.yaml(reference, your repo): Who may do what incheckout-dev: five objects.tenants/catalog/rbac.yaml(reference, your repo): The same file for team-catalog.- One name changed: the group L10–12
Steps
1. Onboard team-catalog: the platform half
A second team arrives. Copy checkout's tenant files inside your own repo, then change the names.
cd ~/platformengr/k8s-platform && mkdir -p tenants/catalog apps/catalog
cp tenants/checkout/namespace.yaml tenants/checkout/quota.yaml tenants/checkout/limits.yaml tenants/catalog/
In all three new files, change the namespace name. In namespace.yaml change the team label too:
Change in ~/platformengr/k8s-platform/tenants/catalog/namespace.yaml
- name: checkout-dev
- labels:
- team: checkout
+ name: catalog-dev
+ labels:
+ team: catalog
Change in ~/platformengr/k8s-platform/tenants/catalog/quota.yaml
- namespace: checkout-dev
+ namespace: catalog-dev
Change in ~/platformengr/k8s-platform/tenants/catalog/limits.yaml
- namespace: checkout-dev
+ namespace: catalog-dev
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "Onboard team-catalog: namespace, quota, defaults" && git push -q
Apply the namespace first. kubectl reads a folder in alphabetical order, and limits.yaml would come before the namespace it lives in.
kubectl apply -f tenants/catalog/namespace.yaml
Then the whole folder (the namespace again shows unchanged):
kubectl apply -f tenants/catalog/
Two team namespaces. Expect checkout-dev and catalog-dev:
kubectl get namespaces -l team
2. Onboard team-catalog: the team half
team-catalog runs the same demo app. Copy checkout's five app files and turn them into catalog's.
cd ~/platformengr/k8s-platform && cp apps/checkout/deployment.yaml apps/checkout/service.yaml apps/checkout/ingress.yaml apps/checkout/configmap.yaml apps/checkout/pdb.yaml apps/catalog/
In each of the five files in apps/catalog/, replace every checkout with catalog (your editor's find and replace, whole folder). That covers names, labels, the namespace, the host catalog.localtest.me and the banner catalog (dev). Then three edits by hand:
catalog has no database, so remove the password from its Deployment:
Delete from ~/platformengr/k8s-platform/apps/catalog/deployment.yaml
- env:
- - name: DB_PASSWORD # a reference to a Secret by name; the value is not here
- valueFrom:
- secretKeyRef:
- name: catalog-db
- key: password
A new team starts its settings at version 1: in the ConfigMap, and in the two places the Deployment names it.
Change in ~/platformengr/k8s-platform/apps/catalog/configmap.yaml
- name: catalog-settings-v2
+ name: catalog-settings-v1
Change in ~/platformengr/k8s-platform/apps/catalog/deployment.yaml
Both places: under envFrom: and under volumes:.
- catalog-settings-v2
+ catalog-settings-v1
If you kept the Black Friday banner in checkout, set catalog's to catalog (dev) now. Then save and apply:
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "Onboard team-catalog: the app" && git push -q
kubectl apply -f apps/catalog/
kubectl -n catalog-dev rollout status deployment/catalog --timeout=120s
Expect "message": "catalog (dev)":
curl -s http://catalog.localtest.me:8090/api/info | grep message
3. Who are you?
Before you hand out permissions, look at your own.
Expect Username kubernetes-admin and a group that ends in cluster-admins: the identity kind wrote into your kubeconfig.
kubectl auth whoami
May you do everything, everywhere? Expect yes:
kubectl auth can-i '*' '*' --all-namespaces
4. Before: a team member can do nothing
alice is a developer in team-checkout. She does not exist as an object: her name and group would come from her login. Ask the API server as her. Access starts at zero.
Expect no:
kubectl auth can-i list pods -n checkout-dev --as=alice --as-group=team-checkout
5. Grant: copy rbac.yaml for checkout
Five objects: a RoleBinding that gives group team-checkout the built-in ClusterRole view in this namespace; a ServiceAccount deployer for the pipeline, with a Role and a RoleBinding; and a ServiceAccount checkout for the workload, with no token. Read it in Quick look above before you apply it.
cd ~/platformengr/k8s-platform && cp ~/platformengr/kubernetes-platform/tenants/checkout/rbac.yaml tenants/checkout/
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout-dev: RBAC" && git push -q
kubectl apply -f tenants/checkout/rbac.yaml
6. Prove it, one question per line
Each question gets one answer, yes or no. The ones that must say no matter more than the ones that say yes.
alice may list pods in her namespace: yes.
kubectl auth can-i list pods -n checkout-dev --as=alice --as-group=team-checkout
She can read logs: yes.
kubectl auth can-i get pods --subresource=log -n checkout-dev --as=alice --as-group=team-checkout
She cannot read Secrets: no. view leaves Secrets out on purpose: reading one is reading the password.
kubectl auth can-i get secrets -n checkout-dev --as=alice --as-group=team-checkout
People read; the pipeline writes. no:
kubectl auth can-i delete deployments -n checkout-dev --as=alice --as-group=team-checkout
And nothing in the other team's namespace: no.
kubectl auth can-i list pods -n catalog-dev --as=alice --as-group=team-checkout
The deploy account may create Deployments here: yes.
kubectl auth can-i create deployments -n checkout-dev --as=system:serviceaccount:checkout-dev:deployer
A team cannot raise its own quota: no.
kubectl auth can-i update resourcequotas -n checkout-dev --as=system:serviceaccount:checkout-dev:deployer
Or give itself more rights: no.
kubectl auth can-i create rolebindings -n checkout-dev --as=system:serviceaccount:checkout-dev:deployer
Or deploy into another team's namespace: no.
kubectl auth can-i create deployments -n catalog-dev --as=system:serviceaccount:checkout-dev:deployer
The whole list for one subject, as a table of resources and verbs:
kubectl auth can-i --list -n checkout-dev --as=system:serviceaccount:checkout-dev:deployer
can-i asks. This one does it, and shows the error a team member would paste into a ticket. Read its four parts: who, verb, resource, namespace. Expect secrets is forbidden: User "alice" cannot list resource "secrets" in API group "" in the namespace "checkout-dev".
kubectl -n checkout-dev get secrets --as=alice --as-group=team-checkout
7. Take the API token out of the workload
Every pod gets a ServiceAccount token mounted by default. checkout never calls the Kubernetes API, so it should not hold a credential for it.
Before. Expect ca.crt, namespace and token:
kubectl -n checkout-dev exec deployment/checkout -- ls /var/run/secrets/kubernetes.io/serviceaccount
Add to ~/platformengr/k8s-platform/apps/checkout/deployment.yaml under the pod spec:, above topologySpreadConstraints:
serviceAccountName: checkout
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "checkout: run as ServiceAccount checkout" && git push -q
kubectl apply -f apps/checkout/deployment.yaml
kubectl -n checkout-dev rollout status deployment/checkout --timeout=120s
After. Expect No such file or directory (and an exit code: the folder is gone):
kubectl -n checkout-dev exec deployment/checkout -- ls /var/run/secrets/kubernetes.io/serviceaccount
8. Write catalog's rbac.yaml yourself
The same five objects for catalog-dev, group team-catalog, workload ServiceAccount catalog. Try writing it from checkout's before you look at this one.
Create ~/platformengr/k8s-platform/tenants/catalog/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-view
namespace: catalog-dev
subjects:
- kind: Group
name: team-catalog
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: deployer
namespace: catalog-dev
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer
namespace: catalog-dev
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["policy"]
resources: ["poddisruptionbudgets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deployer
namespace: catalog-dev
subjects:
- kind: ServiceAccount
name: deployer
namespace: catalog-dev
roleRef:
kind: Role
name: deployer
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: catalog
namespace: catalog-dev
automountServiceAccountToken: false
Add to ~/platformengr/k8s-platform/apps/catalog/deployment.yaml under the pod spec:, above topologySpreadConstraints:
serviceAccountName: catalog
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "catalog-dev: RBAC, and catalog runs as its own ServiceAccount" && git push -q
kubectl apply -f tenants/catalog/rbac.yaml
kubectl apply -f apps/catalog/deployment.yaml
kubectl -n catalog-dev rollout status deployment/catalog --timeout=120s
Prove the mirror image with bob, in team-catalog. His own namespace: yes.
kubectl auth can-i list pods -n catalog-dev --as=bob --as-group=team-catalog
checkout's: no.
kubectl auth can-i list pods -n checkout-dev --as=bob --as-group=team-catalog
catalog's deploy account in checkout's namespace: no.
kubectl auth can-i create deployments -n checkout-dev --as=system:serviceaccount:catalog-dev:deployer
Simplified for the course
alice and bob are impersonated. Real users log in through your identity provider (OIDC), which supplies their name and groups; the bindings stay exactly the same.
deployerstands in for a pipeline that applies manifests. With Argo CD no pipeline holds cluster credentials at all: Argo CD applies, and its AppProjects decide what each team may deploy (Argo CD, module 09).
kubectl --tokenis not used to test the ServiceAccount: kind's kubeconfig logs in with a client certificate, which wins over a token, so you would be testing yourself.--asis the reliable test.People get read-only access. Many teams also allow
pods/execandpods/portforwardin dev; that is a decision to write down, not a default.
Break it on purpose: ClusterRole, so ClusterRoleBinding
A new platform engineer gives team-catalog's auditors read access. The role is the ClusterRole view, so they reach for a ClusterRoleBinding: the names match, the apply succeeds, and catalog can see catalog. The check they ran passes.
You practise: finding which binding grants a permission that should not exist, and removing access from a cluster that does not forget what you delete from Git.
1. Break it
Create ~/platformengr/k8s-platform/tenants/catalog/auditors.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: team-catalog-auditors
subjects:
- kind: Group
name: team-catalog
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
cd ~/platformengr/k8s-platform && git add -A && git commit -qm "catalog: read access for auditors" && git push -q
kubectl apply -f tenants/catalog/auditors.yaml
The check they ran. yes, as intended:
kubectl auth can-i list pods -n catalog-dev --as=bob --as-group=team-catalog
2. What you see
Expected: kubectl auth can-i list configmaps -n checkout-dev --as=bob --as-group=team-catalog prints no. You get: yes.
Try -n kube-system and -n traefik: yes and yes. Nothing failed and nothing warned. Every check from earlier in this module still gives the expected answer, because none of them asked this question.
3. Fix it yourself
Find the object that gives team-catalog access outside its namespace, and remove that access from Git and from the cluster. bob must keep read access in catalog-dev. Done when the can-i lines for checkout-dev, kube-system and traefik print no and the one for catalog-dev prints yes.
Hint 1
Access to every namespace can only come from a cluster-wide binding. List them with their subjects: kubectl get clusterrolebindings -o wide | grep team-.
Hint 2
Delete tenants/catalog/auditors.yaml and commit. Then check can-i again: still yes. kubectl apply never deletes; you have to, with kubectl delete clusterrolebinding.
The fix
bob keeps catalog-dev through the team-view RoleBinding you wrote. The same ClusterRole view, bound with a RoleBinding, stops at the namespace.
cd ~/platformengr/k8s-platform && git rm -q tenants/catalog/auditors.yaml && git commit -qm "Remove cluster-wide view for catalog" && git push -q
The file is gone; the object is not. Delete it:
kubectl delete clusterrolebinding team-catalog-auditors
Three no and one yes:
kubectl auth can-i list configmaps -n checkout-dev --as=bob --as-group=team-catalog
kubectl auth can-i list configmaps -n kube-system --as=bob --as-group=team-catalog
kubectl auth can-i list configmaps -n traefik --as=bob --as-group=team-catalog
kubectl auth can-i list configmaps -n catalog-dev --as=bob --as-group=team-catalog
After you fixed it: the five questions
- What failed?
- A boundary: one team could read every namespace's objects, including other teams' config.
- How was it detected?
- By nobody, until someone asked the right
can-iquestion. Over-permission never produces an error. - Who responds?
- The platform team: it owns every binding.
- How is it corrected?
- Remove the ClusterRoleBinding from Git and from the cluster.
- How is it prevented?
- A rule in review and CI: no
ClusterRoleBindingundertenants/(your lab check does exactly this). A script ofcan-iquestions with expected answers, run against a test cluster on every pull request, including the questions that must sayno. And a controller that removes what Git no longer holds: Argo CD.
Quiz
- A RoleBinding in
checkout-devrefers to the ClusterRoleview. Where does it grant read access?- In every namespace, because a ClusterRole is cluster-wide wherever it is bound
- In
checkout-devonly: a RoleBinding never reaches beyond its own namespace - Nowhere: a RoleBinding can only refer to a Role, so the API server rejects it
- How do you take a permission away from a group?
- Add a Role with the verb
denyfor that resource and bind it to the group - Bind the group to the ClusterRole
view, which overrides any wider roles it has - Remove the binding, or the rule in the role, that grants it: RBAC has no deny rules
- Add a Role with the verb
- Why give the checkout pods a ServiceAccount with
automountServiceAccountToken: false?- So the containers hold no credential for the Kubernetes API that an attacker inside them could use
- So the pods start faster, since mounting the token delays every container on start
- So the Deployment can be applied by
deployer, which is not allowed to create tokens
Answers
- In
checkout-devonly: a RoleBinding never reaches beyond its own namespace The binding sets the scope, not the role. - Remove the binding, or the rule in the role, that grants it: RBAC has no deny rules Permissions are the union of all bindings.
- So the containers hold no credential for the Kubernetes API that an attacker inside them could use A workload that never calls the API should not be able to.
Field note
Every RBAC review I have done found the problem in the tests that were missing, not in the roles that were written. People check that the right person gets in. Write down the three things that must be refused and check those.