Git, CI/CD & GitOpsXCI · Least Privilege CI/CDClusterRBAC
Scoped Kubernetes RBAC — ServiceAccounts, Roles, RoleBindings
What you'll learn
- Distinguish Role/RoleBinding from ClusterRole/ClusterRoleBinding
- Create one ServiceAccount per pipeline job and bind only the verbs the job needs
- Audit effective permissions with kubectl auth can-i --list --as
- Use namespaced grants to scope a deploy job to one namespace
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A least-privilege pipeline gives Kubernetes one
ServiceAccount per job, grants only the verbs the job
needs, on only the namespace the job touches. Role
and RoleBinding are namespace-scoped; ClusterRole and
ClusterRoleBinding are cluster-scoped. The audit
command is kubectl auth can-i --list --as — it tells
the engineer exactly what the ServiceAccount can do
when the impersonation is applied.
The four objects
flowchart LR
SA["ServiceAccount\nnamespace: ci, name: ci-apply-prod"] --> RB["RoleBinding\nnamespace: prod-app"]
SA2["ClusterRole\n(template)"] --> CRB["ClusterRoleBinding\n(cluster-wide grant)"]
R["Role\nnamespace: prod-app"] --> RB
Kubernetes RBAC has four object types, two namespace- scoped and two cluster-scoped:
- ServiceAccount. The identity. Lives in a namespace; pipelines mount its token to authenticate to the API server.
- Role. A namespace-scoped grant: verbs on resources within one namespace.
- RoleBinding. A namespace-scoped attachment: binds a Role to a subject (ServiceAccount, User, Group) in the RoleBinding’s namespace.
- ClusterRole. A cluster-scoped grant: verbs on cluster-scoped resources (nodes, namespaces) or namespaced resources aggregated across namespaces.
- ClusterRoleBinding. A cluster-scoped attachment.
The structural rule is Role + RoleBinding for namespace-scoped permissions; ClusterRole + ClusterRoleBinding only for cluster-scoped permissions.
One ServiceAccount per job
A pipeline that shares one ServiceAccount across plan, apply, and verify jobs shares one identity across three threat levels. Three jobs, three identities:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-apply-prod-app
namespace: ci
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-apply-prod-app
namespace: prod-app
rules:
- apiGroups: ["apps", "batch"]
resources: ["deployments", "statefulsets"]
verbs: ["get", "list", "patch", "update"]
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-apply-prod-app
namespace: prod-app
subjects:
- kind: ServiceAccount
name: ci-apply-prod-app
namespace: ci
roleRef:
kind: Role
name: ci-apply-prod-app
apiGroup: rbac.authorization.k8s.io
The ServiceAccount lives in ci (the pipeline’s
namespace). The Role and RoleBinding live in prod-app
(the application namespace). The grant is namespace-
scoped to prod-app: the ServiceAccount can patch
Deployments in prod-app but not in prod-db,
staging-app, or kube-system.
Auditing with kubectl auth can-i
The audit command answers “what can this ServiceAccount do?” by impersonating it and listing the effective verbs:
SA=ci-apply-prod-app
NS=prod-app
kubectl auth can-i --list --as="system:serviceaccount:ci:$SA" --namespace "$NS"
The output lists every resource and verb the
ServiceAccount can act on, in every namespace it has
been granted access to. A clean audit output for a
least-privilege apply role is a small list, scoped to
prod-app. An output that includes * for any
resource or verb, or that lists namespaces the job
does not touch, is a binding that needs tightening.
The cluster-wide audit, run without --namespace,
returns the union of every binding the ServiceAccount
inherits across the cluster:
kubectl auth can-i --list --as="system:serviceaccount:ci:$SA"
If this output includes namespaces other than
prod-app, the ServiceAccount has been bound
elsewhere. The audit asks “why?”
The plan role versus the apply role
The plan ServiceAccount holds read verbs on the same resources:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-plan-prod-app
namespace: prod-app
rules:
- apiGroups: ["apps", "batch"]
resources: ["deployments", "statefulsets"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list"]
The verbs are get and list only. There is no
create, update, patch, or delete. A plan job
that attempts a write fails with Forbidden: User "system:serviceaccount:ci:ci-plan-prod-app" cannot patch resource "deployments" in API group "apps" in the namespace "prod-app". The failure is the audit
working: the role is structurally unable to write.
Production discipline
- One ServiceAccount per job. Distinct identities for plan, apply, verify, and rollback. Shared identities are privilege escalation.
- Role and RoleBinding for namespace-scoped work. ClusterRole and ClusterRoleBinding only for cluster-scoped resources (nodes, namespaces).
- Audit with
kubectl auth can-i. Inspect the output per ServiceAccount, including cluster-wide inheritance. - Tighten on every workflow change. New commands may require new verbs; the audit catches them.
- Disable automounting where possible. Set
automountServiceAccountToken: falseon pipeline ServiceAccounts that do not need it.
Cross-course references
- Part XCI-01 (The validation versus deployment identity) covers the structural split between read and write.
- Part XCI-02 (Scoped IAM roles) covers the analogous cloud-side grant.
- Part LII-04 (Policy — Conftest and Kyverno) covers admission-time policy enforcement that complements the RBAC grant.
Quiz
Knowledge check · 4 questions
Q1. An apply job patches Deployments in the prod-app namespace. Which set of RBAC objects is the least-privilege grant?
Q2. Sharing one ServiceAccount across the plan, apply, and verify jobs is acceptable because all three jobs operate on the same namespace.
Q3. State the structural difference between Role+RoleBinding and ClusterRole+ClusterRoleBinding, and write the kubectl command that audits the effective permissions of a ServiceAccount in a target namespace.
Q4. Diagnose the RBAC grant that lets a fork-PR job write to production, and prescribe the fix.
Team T's deploy pipeline uses one ServiceAccount, ci-deploy, in namespace ci, bound to the built-in cluster-admin ClusterRole via a ClusterRoleBinding. The pipeline runs plan and apply jobs with the same kubeconfig. A fork-PR opens; the workflow mounts the same ServiceAccount token; the fork-PR runs kubectl and can patch any Deployment in any namespace.
Passing score: 75%. Answers are checked in this browser.