Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCV · GitOps Anti-PatternsControllerPrivilege

Cluster-admin everywhere — the controller holds the keys to the cluster

Advanced⏱ ~24 mingitkubectl

What you'll learn

  • Verify the effective permissions of a controller ServiceAccount with kubectl auth can-i
  • Identify the three blast-radius multipliers in a cluster-admin controller binding
  • Design a scoped ServiceAccount per AppProject with the verbs each resource actually needs
  • Distinguish impersonation in the controller from impersonation in the applied manifests

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 GitOps controller is the single identity that writes to every namespace the platform manages. When that identity is cluster-admin, the blast radius of any mistake it makes is the whole cluster. A typo in a chart that contains a wide RBAC manifest, a Helm hook that escalates a ServiceAccount, or a templating bug that emits * for a verb is a single apply that grants a binding the rest of the cluster inherits.

The cluster-admin binding in plain text

The Argo CD default install creates a ClusterRoleBinding that grants the application controller’s ServiceAccount the cluster-admin ClusterRole. Flux ships similar bindings. Both work because the controllers manage many namespaces and many resource kinds; both are also the reason an unintended manifest becomes an unintended cluster-wide permission.

    flowchart LR
        A["App manifest with RBAC"] --> B["Controller applies"]
        B --> C["ClusterRoleBinding created"]
        C --> D["Cluster-wide permission granted"]
        D --> E["Lateral movement to every namespace"]
        D --> F["Identity impersonation at will"]
        D --> G["Secrets readable across the cluster"]

The verification step most teams skip:

kubectl auth can-i --list --as=system:serviceaccount:argocd:argocd-application-controller

The output lists every Resource, every Verb, and every API group the controller is permitted to touch.

The scoped ServiceAccount per project

The replacement is a ServiceAccount per project, with a RoleBinding (not a ClusterRoleBinding) bound to a Role that names only the resources and verbs the manifests in that project actually use.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: prod-deployer
  namespace: prod-platform
rules:
  - apiGroups: ["apps"]
    resources: ["deployments", "statefulsets"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  - apiGroups: [""]
    resources: ["services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]

The Role names four resource kinds and six verbs. The controller bound to this Role cannot create a ClusterRoleBinding, cannot read a Secret outside prod-platform, and cannot impersonate another identity. The blast radius is the namespace.

The three blast-radius multipliers

Three properties of a cluster-admin controller make every mistake worse than the same mistake would be for a human:

  • Cross-namespace reach. A wide RBAC manifest in a chart becomes a binding the rest of the cluster inherits.
  • Impersonation. A controller that may bind cluster roles can impersonate other identities to read Secrets.
  • Continuous reconciliation. A controller reapplies the mistake every reconcile.

A scoped ServiceAccount removes all three.

Production discipline

  1. Run kubectl auth can-i --list --as=... on every controller in every cluster.
  2. One ServiceAccount per project, bound to a Role.
  3. CI fails any chart that contains a ClusterRole or a ClusterRoleBinding.

Cross-course references

  • This course, Part LXXXIII (GitOpsRBAC) - Argo CD and Flux RBAC models, AppProject scoping, multi-tenancy.
  • Kubernetes for Production Sysadmins, Part LVIII (RBAC) - Role, RoleBinding, and ClusterRoleBinding in depth.

Quiz

Knowledge check · 4 questions

  1. Q1. Which kubectl command shows the effective permissions of the Argo CD application controller ServiceAccount?

  2. Q2. A controller that holds cluster-admin only applies manifests that engineers have reviewed, so the blast radius is bounded by review.

  3. Q3. Name the three blast-radius multipliers a cluster-admin controller adds to every manifest error.

  4. Q4. Diagnose a controller-privilege incident and recommend the architecture that prevents it.

    A team runs Argo CD with the default cluster-admin binding on the application controller. A developer copies a Helm chart from a public tutorial; the chart contains a ClusterRole granting `*` on `*`. The chart is merged; the controller reconciles; the ClusterRoleBinding is created; every ServiceAccount in the cluster is now cluster-admin reachable.

Passing score: 75%. Answers are checked in this browser.