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.