Skip to main content
RunBook Academy

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

Least privilege in GitOps — what the controller can do; what it cannot

Advanced⏱ ~24 mingit

What you'll learn

  • Map Argo CD or Flux components to Kubernetes ServiceAccounts
  • Distinguish the reconcile permission from the prune permission
  • Scope the GitOps ServiceAccount to the namespace it owns
  • Audit the GitOps ServiceAccount with kubectl auth can-i --list --as

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 reconciles the cluster state to the Git state. The reconcile permission is what the controller needs to apply manifests; the prune permission is what the controller needs to remove resources no longer in Git. The controller ServiceAccount holds the minimum of these per namespace, not cluster-admin. The audit command is the same as for any CI ServiceAccount: kubectl auth can-i --list --as.

The GitOps components and their identities

flowchart LR
    G["Git repository"] --> R["Repo server\n(Argo CD)"]
    R --> A["Application controller\n(per-namespace ServiceAccount)"]
    A --> K8s["Kubernetes API"]
    A --> N1["Namespace: prod-app"]
    A --> N2["Namespace: prod-db"]
    subgraph SCOPE["Per-namespace grants"]
        N1
        N2
    end

A typical Argo CD deployment has three components that authenticate to the Kubernetes API:

  • Repo server. Fetches manifests from Git. Needs read on the Git repository and read on any Helm or OCI registries. Does not need write on cluster namespaces.
  • Application controller. Reconciles each Application CRD to the cluster state. One ServiceAccount per application (or per namespace of applications). Holds the namespace-scoped verbs.
  • ApplicationSet controller. Generates Application CRDs from templates. Needs write on the Application CRDs but not on the resources the Applications manage.

The Flux equivalent has a similar split: kustomize-controller, helm-controller, notification-controller, and source-controller each have their own ServiceAccount with their own verbs.

Reconcile versus prune

The GitOps controller reconciles by reading the desired state from Git and the actual state from the cluster, then calling the API to make them match. The permissions split is structural:

flowchart TB
    D["Desired state in Git"] --> C{"Diff against\ncluster state?"}
    C -->|"Missing in cluster"| R["Reconcile\ncreate / apply"]
    C -->|"Different in cluster"| R
    C -->|"Extra in cluster\nAND prune enabled"| P["Prune\ndelete"]
    C -->|"Extra in cluster\nAND prune disabled"| S["Skip\n(orphan)"]
  • Reconcile. Verbs: create, update, patch, apply. The controller calls these on Deployments, StatefulSets, Services, ConfigMaps, Secrets, and any other resource the manifests declare. The most-used verb set.
  • Prune. Verbs: delete. The controller calls this on resources that exist in the cluster but are absent from Git. Prune is opt-in per Application (Argo CD prune: true in the ApplicationSpec) and per Kustomization (Flux prune: true). A controller that holds delete is a controller that can be turned into a cluster wipe by a single Git commit that omits every resource.

A team that wants reconcile but not prune grants create, update, patch and omits delete. Orphaned resources accumulate; the team’s audit process must reconcile them by hand or by a separate prune-enabled controller with stricter trust.

A scoped Application controller

The Argo CD Application controller ServiceAccount can be scoped per namespace with a Role and RoleBinding. A least-privilege grant for an Application that manages prod-app:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: argocd-application-controller-prod-app
  namespace: argocd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: argocd-application-controller-prod-app
  namespace: prod-app
rules:
  - apiGroups: ["apps", "batch"]
    resources: ["deployments", "statefulsets"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["configmaps", "services", "serviceaccounts"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: argocd-application-controller-prod-app
  namespace: prod-app
subjects:
  - kind: ServiceAccount
    name: argocd-application-controller-prod-app
    namespace: argocd
roleRef:
  kind: Role
  name: argocd-application-controller-prod-app
  apiGroup: rbac.authorization.k8s.io

The Role lives in prod-app; the RoleBinding lives in prod-app; the ServiceAccount lives in argocd. The verbs include reconcile and prune; the secrets verbs exclude delete so prune cannot remove a Secret the Git state forgets. The audit:

SA=argocd-application-controller-prod-app
NS=prod-app
kubectl auth can-i --list --as="system:serviceaccount:argocd:$SA" --namespace "$NS"

The output lists the resources and verbs in the namespace. The audit asks:

  1. Is the Role restricted to prod-app, not cluster- wide?
  2. Are the verbs the minimum the Application needs?
  3. Are secrets granted reconcile but not prune?
  4. Are there any * in resources or verbs?

Production discipline

  1. Scope per Application, not per cluster. Each Application gets a ServiceAccount, a Role, and a RoleBinding in the namespace it manages.
  2. Reconcile by default; prune explicitly. Prune is enabled per resource type, not per cluster.
  3. Reconcile Secrets but do not prune them. A Secret deleted from Git should not be deleted from the cluster; the cluster may need the Secret to recover from a bad Git state.
  4. Audit the controller ServiceAccount quarterly. New applications may have widened the grants.
  5. Treat the Git repository as a production surface. The GitOps grant makes Git write equivalent to cluster write.

Cross-course references

  • Part XCI-03 (Scoped Kubernetes RBAC) covers the RBAC primitives in detail.
  • Part LV (GitOps controllers) covers the operational patterns for Argo CD and Flux.
  • Part XXXIII (Signed commits) covers the trust boundary for the Git repository that drives the controller.

Quiz

Knowledge check · 4 questions

  1. Q1. Which verbs should a GitOps Application controller hold in a least-privilege grant for a production namespace?

  2. Q2. Because the GitOps controller runs continuously, its RBAC grant cannot safely be wider than a CI apply job's grant.

  3. Q3. Distinguish the reconcile permission from the prune permission in a GitOps controller, and state why Secrets are typically reconciled but not pruned.

  4. Q4. Diagnose the GitOps privilege escalation and prescribe the namespace-scoped grant.

    Team T deploys Argo CD with the application controller bound to the built-in cluster-admin ClusterRole via a ClusterRoleBinding in argocd. The team's Git repository has two applications: app-prod-app (namespace prod-app) and app-prod-db (namespace prod-db). An attacker commits a typo to the Git repo's prod-db manifests that omits the StatefulSet definition. Argo CD syncs and deletes the StatefulSet. The production database is down.

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