Search across all documentation pages
Write the contract down before you encode it in RBAC.
Role plus RoleBinding.Here is a self-service contract for a typical app team, expressed as RBAC.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: team-payments
name: app-self-service
rules:
# Own your workloads.
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
# Own your config and your own Secrets.
- apiGroups: [""]
resources: ["configmaps", "secrets", "services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Debug your own Pods.
- apiGroups: [""]
resources: ["pods", "pods/log", "pods/exec"]
verbs: ["get", "list", "watch", "delete"]
# Route your own traffic through a Gateway you do not own.
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Scale yourself.
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["get", "list", "watch", "create", "update", "patch"]apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: team-payments
name: app-self-service
subjects:
- kind: Group
name: eng-payments
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: app-self-service
apiGroup: rbac.authorization.k8s.ioNote the deliberate absences: no resourcequotas, no nodes, no gateways, no namespaces, no CRD management, and nothing cluster-scoped.
Verify the contract from the developer's seat rather than reading the YAML back to yourself.
kubectl auth can-i create deployments --namespace team-payments --as-group eng-payments
kubectl auth can-i update resourcequotas --namespace team-payments --as-group eng-payments
kubectl auth can-i delete nodes --as-group eng-paymentsThe first should print yes; the other two should print no.
RBAC is the enforcement. The table is the documentation, and the table is what people actually read.
| Change | Self-service | Platform ticket |
|---|---|---|
| Image tag / new release | Yes | - |
| Replica count, HPA min/max | Yes | - |
| Env vars, ConfigMaps | Yes | - |
| Own Secrets (via External Secrets) | Yes | - |
| Requests and limits (within quota) | Yes | - |
HTTPRoute on an existing Gateway | Yes | - |
| New hostname / TLS certificate | - | Yes |
| Namespace quota increase | - | Yes |
| New namespace | - | Yes |
| Node pool, taints, tolerations | - | Yes |
| Installing a CRD or operator | - | Yes |
| PriorityClass above default | - | Yes |
| Cluster-scoped RBAC | - | Yes |
The right-hand column is short on purpose. If it grows, your platform is becoming a bottleneck.
A team that can edit its own ResourceQuota does not have a quota - it has a suggestion.
Quotas are the mechanism that converts "you crashed the cluster" into "you hit your budget," so the object must sit outside the tenant's write scope.
The same logic applies to PriorityClass: a team that can grant itself high priority can preempt everyone else's Pods.
The interesting cases are the shared ones, and the pattern is always the same: split the object into a piece the platform owns and a piece the tenant owns.
Gateway API does this natively. The platform owns the Gateway (listeners, TLS, IP); the team owns the HTTPRoute that attaches to it.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: payments
namespace: team-payments
spec:
parentRefs:
- name: shared-gateway
namespace: infra-gateways
hostnames:
- "payments.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api/payments
backendRefs:
- name: payments-api
port: 8080The team routes its own traffic and cannot reconfigure the front door for anyone else.
Whether a route in one namespace may attach to a Gateway in another is decided by the listener's allowedRoutes.namespaces - exactly the platform-owned approval point.
A ReferenceGrant is a different control: it authorizes cross-namespace references, such as a route whose backendRefs target a Service in another namespace, or a Gateway whose certificateRefs read a Secret in another namespace.
RBAC that permits a change does not make the change easy.
Ship a golden-path template - a Helm chart or Kustomize base - that already carries probes, resource requests, a non-root securityContext, and a PodDisruptionBudget.
Then self-service means editing five values, not authoring 120 lines of YAML from memory.
Granting secrets read across the namespace leaks the ServiceAccount tokens of every workload in it. Scope Secret access by name with resourceNames where the risk is real, or use External Secrets so the source of truth is the vault.
pods/exec is close to a root shell in the container. It is reasonable in dev and staging, questionable in production; prefer ephemeral debug containers and logs, and audit exec events.
patch on a Deployment lets a team route around admission-time policy only if you have no policy. Keep Pod Security Standards on the namespace so RBAC breadth does not become privilege breadth.
Teams cannot see why they were denied. A bare Error from server (Forbidden) is a ticket generator; make policy engines emit the rule name and a link to the contract table.
The platform column silently grows. Review it quarterly and ask which entries could become self-service primitives instead.
Full cluster-admin for app teams. Fast at three engineers, ungovernable at thirty. Use only in throwaway dev clusters.
Ticket-everything. Maximum control, and the reason teams stand up shadow clusters on a corporate card. Reserve it for genuinely cluster-scoped changes.
Cluster-per-team. Real isolation and no shared-resource seam, at the cost of N control planes and N upgrade cycles. Good for strict compliance boundaries, expensive as a default.
GitOps-only self-service. Nobody holds write RBAC in production; teams open PRs and Argo CD reconciles. This is the strongest posture for prod and pairs well with direct RBAC in lower environments.
Internal developer portal. A UI or CLI that opens the right PR for you. Best experience, most build cost, and it needs the contract to exist first anyway.
Should self-service differ between staging and production? Yes. Direct write in staging and PR-driven change in production is the common and defensible split.
Who owns requests and limits? The team, bounded by a platform-owned quota and a LimitRange that supplies defaults when they forget.
How do teams get Secrets without cluster access? External Secrets Operator or a CSI secrets driver pulls from your vault, so the team references a secret name and never handles the material.
Does a namespace admin Role work instead of an explicit list? It is convenient but grants RBAC-editing rights inside the namespace, which lets a team widen its own permissions. Prefer an explicit rule list.
What if a team needs a cluster-scoped resource often? That is a signal to build a namespaced abstraction for it, not to grant a ClusterRole.
How do I stop the contract from rotting? Tie it to the same review cadence as the golden-path template, and treat every recurring ticket type as a self-service candidate.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026