Kubernetes RBAC patterns: per-namespace roles in GitOps
Published: 2026-04-08
RBAC in a multi-cluster, multi-team setup needs a consistent structure. Ad-hoc kubectl create rolebinding commands get lost. All roles and bindings go in the infra repo and are applied by Flux.
Role hierarchy
| Role | Who | Scope |
|---|---|---|
view (built-in) |
Developers | Read-only: pods, logs, events, services |
developer (custom) |
App teams | view + exec into pods, port-forward |
devops (custom) |
Platform team | developer + deploy (Deployments, ConfigMaps, Secrets) |
cluster-admin (built-in) |
Flux SA only | Everything |
No one gets cluster-admin interactively. Flux has it; humans don't.
Custom ClusterRole: developer
yamlapiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: developer
labels:
rbac.example.com/aggregate-to-devops: "true"
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.authorization.k8s.io/aggregate-to-view: "true"
rules:
- apiGroups: [""]
resources: ["pods/exec", "pods/portforward"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
aggregationRule automatically merges all view-aggregate rules. Adding a new CRD to the view set propagates automatically — you don't need to update every custom role.
The rbac.example.com/aggregate-to-devops: "true" label makes developer automatically included in devops via aggregation.
Custom ClusterRole: devops
yamlapiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: devops
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.example.com/aggregate-to-devops: "true"
rules:
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["configmaps", "secrets", "serviceaccounts"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["helm.toolkit.fluxcd.io"]
resources: ["helmreleases"]
verbs: ["get", "list", "watch", "patch"]
devops aggregates developer via label and adds deployment capabilities. The explicit helmreleases rule allows manual patch to trigger reconcile without full cluster-admin.
RoleBindings per namespace
yaml# spoke/rbac/dev-bindings.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-team-developer
namespace: dev
subjects:
- kind: Group
name: "example/dev-team"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: developer
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: platform-team-devops
namespace: dev
subjects:
- kind: Group
name: "example/platform-team"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: devops
apiGroup: rbac.authorization.k8s.io
RoleBinding (not ClusterRoleBinding) scopes the permission to one namespace. The same binding file is applied to test, staging, etc. with only the namespace changing — handled by Kustomize namespace transformation.
Read-only cluster-wide access for all teams
Monitoring namespace (Prometheus, Grafana) should be readable by everyone:
yamlapiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: all-teams-view-observability
subjects:
- kind: Group
name: "example/dev-team"
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: "example/platform-team"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
ClusterRoleBinding + view — engineers can read all namespaces but write nothing outside their own.
RBAC file structure
spoke/rbac/
clusterroles.yaml # developer + devops ClusterRole definitions
dev-bindings.yaml # RoleBindings for dev namespace
test-bindings.yaml # RoleBindings for test namespace
monitoring-binding.yaml # ClusterRoleBinding for observability read
Kustomize namespace transformation
Adding a new environment doesn't require copying binding files:
yaml# kustomization.yaml for test namespace
resources:
- ../../base/rbac
namespace: test
Kustomize rewrites all metadata.namespace fields. Same binding manifests, different namespace.
Auditing RBAC
Check who can do what in a namespace:
bashkubectl auth can-i --list -n dev --as=developer-user
kubectl auth can-i create deployments -n dev --as=developer-user
List all rolebindings in a namespace:
bashkubectl get rolebindings,clusterrolebindings -n dev -o wide
Common mistakes
Using ClusterRoleBinding where RoleBinding is enough — grants access to all namespaces. Use RoleBinding for namespace-scoped access even when referencing a ClusterRole.
Attaching cluster-admin to CI service accounts — use minimal permissions. CI only needs to apply specific resource types.
Manual kubectl for access grants — changes are not tracked, not reviewed, and get overwritten by Flux on next reconcile.