RBAC and Service Accounts in Kubernetes
Implement fine-grained access control with Kubernetes RBAC, service accounts, and least-privilege security patterns.
When I first configured RBAC on my k3s homelab cluster, I made the classic mistake of running everything under the default service account with cluster-admin privileges. It worked, until I accidentally deployed a misconfigured test pod that could read Secrets from every namespace, including the production database credentials stored by External Secrets Operator. That wake-up call sent me down a rabbit hole of Role definitions, RoleBindings, and the subtle differences between namespace-scoped and cluster-scoped permissions. I spent an entire weekend rewriting every service account in the cluster to follow least-privilege principles, and I have not regretted a single minute of that effort.
Introduction
Kubernetes defaults to a permissive stance, but in production, “everyone is an admin” is a recipe for disaster. Role-Based Access Control (RBAC) allows you to define who can do what within your cluster. This guide covers implementing least-privilege access patterns using Service Accounts, Roles, and RoleBindings, ensuring that both users and applications operate within their designated boundaries.
[Using RBAC Authorization] — Kubernetes Project , 2024-08-01 [Kubernetes Security and Observability] — Brendan Creane, Amit Gupta , 2021-10-01Architecture Overview
Access to resources in Kubernetes is governed by the connection between an Identity (Subject), a set of permissions (Role), and the link between them (Binding).
flowchart TB
Subject["👤 User / 🤖 ServiceAccount"] --> Binding["🔗 RoleBinding"]
Binding --> Role["📜 Role / ClusterRole"]
Role --> Allow["✅ Allowed Actions<br/>(get, list, watch)"]
Allow --> Resource["📦 Resource<br/>(Pod, ConfigMap)"]
Subject -.-> Deny["🚫 Direct Access<br/>(Implicit Deny)"] -.-> Resource
classDef primary fill:#7c3aed,color:#fff
classDef secondary fill:#06b6d4,color:#fff
classDef db fill:#f43f5e,color:#fff
classDef warning fill:#fbbf24,color:#000
class Subject warning
class Role,Binding secondary
class Resource db
Implementation
RBAC Components
| Resource | Scope | Purpose |
|---|---|---|
| Role | Namespace | Define permissions within a namespace |
| ClusterRole | Cluster | Define cluster-wide permissions |
| RoleBinding | Namespace | Grant Role to users/service accounts |
| ClusterRoleBinding | Cluster | Grant ClusterRole cluster-wide |
Service Accounts
Application Service Account
# apps/myapp-api/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-api
namespace: myapp-staging
labels:
app.kubernetes.io/name: myapp-api
automountServiceAccountToken: false
[Configure Service Accounts for Pods]
— Kubernetes Project , 2024-06-15
Pod Using Service Account
# apps/myapp-api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-api
namespace: myapp-staging
spec:
template:
spec:
serviceAccountName: myapp-api
automountServiceAccountToken: false
containers:
- name: api
image: myapp-api:latest
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
Namespace Roles
Developer Role
# core-infra/security/roles/developer-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: myapp-staging
rules:
# Read pods and logs
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
# Read deployments and services
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["services", "configmaps"]
verbs: ["get", "list", "watch"]
# Port-forward for debugging
- apiGroups: [""]
resources: ["pods/portforward"]
verbs: ["create"]
# Exec for troubleshooting (limited)
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
Operator Role
# core-infra/security/roles/operator-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: operator
namespace: myapp-staging
rules:
# Full deployment management
- apiGroups: ["apps"]
resources: ["deployments", "replicasets", "statefulsets"]
verbs: ["get", "list", "watch", "update", "patch"]
# Pod management
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods/log", "pods/exec", "pods/portforward"]
verbs: ["get", "create"]
# ConfigMap and Secret read
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
# Service management
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "update", "patch"]
# HPA management
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["get", "list", "watch", "update", "patch"]
Cluster Roles
Read-Only Cluster Role
# core-infra/security/clusterroles/readonly.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-readonly
rules:
- apiGroups: [""]
resources: ["namespaces", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "networkpolicies"]
verbs: ["get", "list", "watch"]
Flux Controller Role
# core-infra/security/clusterroles/flux-controller.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: flux-reconciler
rules:
# Full resource management for GitOps
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# CRD management
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Role Bindings
User RoleBinding
# core-infra/security/rolebindings/developer-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: myapp-staging
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
- kind: User
name: admin@example.local
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
Service Account Binding
# Bind ClusterRole to ServiceAccount in specific namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: external-secrets-reader
namespace: myapp-staging
subjects:
- kind: ServiceAccount
name: external-secrets
namespace: external-secrets
roleRef:
kind: ClusterRole
name: external-secrets-controller
apiGroup: rbac.authorization.k8s.io
ClusterRoleBinding
# core-infra/security/clusterrolebindings/readonly-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cluster-readonly-binding
subjects:
- kind: Group
name: readonly-users
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-readonly
apiGroup: rbac.authorization.k8s.io
Aggregated ClusterRoles
Kubernetes supports aggregated ClusterRoles, which automatically combine permissions from multiple ClusterRoles based on label selectors. This is a powerful pattern for composing permissions modularly.
[Aggregated ClusterRoles] — Kubernetes Project , 2024-08-01Aggregation Labels
# core-infra/security/clusterroles/monitoring-base.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-base
labels:
rbac.example.local/aggregate-to-monitoring: "true"
rules:
- apiGroups: [""]
resources: ["pods", "nodes", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets"]
verbs: ["get", "list", "watch"]
Aggregated Role
# core-infra/security/clusterroles/monitoring-aggregated.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.example.local/aggregate-to-monitoring: "true"
rules: [] # Automatically aggregated
Application-Specific Roles
External Secrets Operator
# infrastructure/external-secrets/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-secrets-controller
rules:
# Manage secrets
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Read configmaps for configuration
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
# Manage ExternalSecret CRs
- apiGroups: ["external-secrets.io"]
resources: ["externalsecrets", "secretstores", "clustersecretstores"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: ["external-secrets.io"]
resources: ["externalsecrets/status"]
verbs: ["update", "patch"]
# Events for status reporting
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
CNPG Operator
# infrastructure/cnpg/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cnpg-manager
rules:
# Manage PostgreSQL clusters
- apiGroups: ["postgresql.cnpg.io"]
resources: ["*"]
verbs: ["*"]
# Pod management
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/log"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# PVC management
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Secrets and ConfigMaps
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Auditing RBAC
Regularly auditing who has access to what is just as important as defining the roles in the first place.
[RBAC Good Practices] — Kubernetes Project , 2024-07-20Check User Permissions
# Check if user can perform action
kubectl auth can-i create deployments -n myapp-staging --as=admin@example.local
# List all permissions for a user
kubectl auth can-i --list --as=admin@example.local -n myapp-staging
# Check service account permissions
kubectl auth can-i --list --as=system:serviceaccount:myapp-staging:myapp-api -n myapp-staging
List Role Bindings
# List all role bindings in namespace
kubectl get rolebindings -n myapp-staging -o wide
# List cluster role bindings
kubectl get clusterrolebindings -o wide | grep -v system:
# Describe specific binding
kubectl describe rolebinding developer-binding -n myapp-staging
Best Practices
Least Privilege Checklist
| Principle | Implementation |
|---|---|
| Namespace isolation | Use Roles over ClusterRoles when possible |
| Minimal verbs | Only grant required actions (get, list vs *) |
| Specific resources | Target specific resource types, not wildcards |
| Service account per app | Unique SA for each application |
| No token auto-mount | Disable unless API access needed |
| Regular audit | Review bindings periodically |
Conclusion
RBAC is the primary mechanism for securing access to the Kubernetes API. By strictly defining roles and binding them only to the service accounts that need them, you minimize the potential impact of compromised credentials and ensure compliance with security standards.
Reflecting on my own journey, the hardest part was not writing the YAML manifests — it was developing the discipline to audit them regularly and resist the temptation to grant broad permissions “just to get things working.” Every time I took a shortcut, I paid for it later with a confusing debugging session or, worse, a security gap I only discovered by accident. Now I treat RBAC definitions as first-class code: they live in Git, they go through pull request review, and Flux reconciles them automatically. That workflow has given me genuine confidence that nothing in the cluster has more access than it needs.
[Using RBAC Authorization] — Kubernetes Project , 2024-08-01Next Steps
- Integrate RBAC auditing into your CI/CD pipeline with tools like
kubiscanorrakkess. - Implement NetworkPolicies alongside RBAC for defense-in-depth.
- Explore Open Policy Agent (OPA) Gatekeeper for policy enforcement beyond RBAC.