Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXCI · Least Privilege CI/CDClusterRBAC

Scoped Kubernetes RBAC — ServiceAccounts, Roles, RoleBindings

Advanced⏱ ~25 mingit

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

Not yet marked complete on this device.

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

  1. One ServiceAccount per job. Distinct identities for plan, apply, verify, and rollback. Shared identities are privilege escalation.
  2. Role and RoleBinding for namespace-scoped work. ClusterRole and ClusterRoleBinding only for cluster-scoped resources (nodes, namespaces).
  3. Audit with kubectl auth can-i. Inspect the output per ServiceAccount, including cluster-wide inheritance.
  4. Tighten on every workflow change. New commands may require new verbs; the audit catches them.
  5. Disable automounting where possible. Set automountServiceAccountToken: false on 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

  1. Q1. An apply job patches Deployments in the prod-app namespace. Which set of RBAC objects is the least-privilege grant?

  2. Q2. Sharing one ServiceAccount across the plan, apply, and verify jobs is acceptable because all three jobs operate on the same namespace.

  3. 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.

  4. 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.