Alright, let’s talk about the three roles you’ll actually use. You can read all the RFCs and design docs you want, but in the real world, 90% of your RBAC needs boil down to these three patterns. They’re the workhorses. Get these right, and you’ve basically won.

The Read-Only Viewer

This is your go-to for anyone who needs to see what’s going on but shouldn’t be able to change a single byte. Think auditors, support teams, or your manager who keeps asking “what’s running in the staging cluster?” You want to give them get, list, and watch on (almost) everything. The key here is to be explicit. Don’t just grant them view access cluster-wide; that default role is a sledgehammer that often includes seeing Secrets, which is a spectacularly bad idea.

Here’s how you build a precise, safe Read-Only role for a specific namespace. We use a ClusterRole so it can be reused across namespaces, and a RoleBinding to tie it to a user or group within a single namespace.

# clusterrole-readonly-precise.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: precise-readonly
rules:
- apiGroups: [""] # The core API group, think 'v1' in 'api/v1'
  resources:
  - pods
  - services
  - configmaps
  - persistentvolumeclaims
  - replicationcontrollers
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources:
  - deployments
  - daemonsets
  - statefulsets
  - replicasets
  verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
  resources:
  - jobs
  - cronjobs
  verbs: ["get", "list", "watch"]
# Notice what we're NOT including: secrets, serviceaccounts, roles, rolebindings.
# rolebinding-readonly.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: readonly-access
  namespace: application-staging # Apply this ONLY in the namespace you want
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: precise-readonly # This is the ClusterRole we made above
subjects:
- kind: User
  name: "alice@example.com"
  apiGroup: rbac.authorization.k8s.io

Pitfall: The most common mistake is forgetting that get and list are separate verbs. You need both for a user to be able to list all resources and then get the details of a specific one. Also, watch out for custom resources (CRDs); you’ll need to add another rule for those if your read-only users need to see them.

The Almighty Namespace Admin

This is the “I own this piece of the cluster” role. It’s for your product team leads who need full control over their namespace but shouldn’t be able to poke the cluster-scoped stuff (like Nodes or PersistentVolumes). They can do anything inside their namespace: create Pods, delete Deployments, even create Roles and RoleBindings within that same namespace. This last part is crucial—it allows them to manage their own access controls, which is a huge win for you, the platform operator.

We build this using a ClusterRole again (for reusability) but bind it with a RoleBinding to keep its power namespace-scoped.

# clusterrole-namespace-admin.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: namespace-admin
rules:
- apiGroups: ["*"] # Yes, everything. Even future ones. Wild.
  resources: ["*"] # Every resource in the API group.
  verbs: ["*"]     # Every verb imaginable.
- apiGroups: [""]
  resources: ["namespaces"] # They need to be able to 'get' their own namespace
  verbs: ["get"]
  resourceNames: ["application-production"] # Lock it down to just their namespace
# rolebinding-namespace-admin.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: namespace-admin-access
  namespace: application-production # The kingdom they rule
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: namespace-admin
subjects:
- kind: User
  name: "bob@example.com"
  apiGroup: rbac.authorization.k8s.io

Why the weird rule for namespaces? Without it, your admin can’t even run kubectl get ns to see the namespace they’re an admin of. It’s one of those quirky, annoying bits of RBAC. The resourceNames constraint is a best practice—it prevents them from using a wildcard to view all namespaces, which is information you might want to restrict.

The CI/CD ServiceAccount

This is the robot that deploys your code. It needs enough power to update Deployments, create Services, and maybe manage Ingresses, but it absolutely should not be able to read Secrets or escalate its own privileges. This is where least privilege becomes a religion. You create a dedicated ServiceAccount in the namespace and give it a tightly scoped role.

First, create the ServiceAccount itself. It’s an identity, not a permission set.

# serviceaccount-cicd.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: cicd-bot
  namespace: application-staging

Now, create a Role that defines exactly what this bot can do. No more.

# role-cicd-bot.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: cicd-deployer
  namespace: application-staging
rules:
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["services", "pods"] # 'pods' is often needed for log streaming or exec in CI
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Notice: No 'secrets' here. Your CI system should handle that, not the in-cluster bot.

Finally, bind the Role to the ServiceAccount.

# rolebinding-cicd-bot.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: cicd-bot-access
  namespace: application-staging
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: cicd-deployer
subjects:
- kind: ServiceAccount
  name: cicd-bot
  namespace: application-staging # This is critical. You must specify the namespace for the SA.

Critical Gotcha: When your Jenkins or GitLab Runner pod runs, it needs to be configured to use this ServiceAccount. This is done in the Pod spec. And remember, a Pod can only use a ServiceAccount that exists in its own namespace. So if your CI pod runs in a ci namespace but needs to deploy to an app namespace, you’ll need a more complex setup involving a RoleBinding in the app namespace that grants roles to the ServiceAccount from the ci namespace. It’s a bit of a mind-bender, but it’s how you keep things secure.