Git, CI/CD & GitOpsXCI · Least Privilege CI/CDGitOpsRBAC
Least privilege in GitOps — what the controller can do; what it cannot
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
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 CDprune: truein the ApplicationSpec) and per Kustomization (Fluxprune: true). A controller that holdsdeleteis 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:
- Is the Role restricted to
prod-app, not cluster- wide? - Are the verbs the minimum the Application needs?
- Are secrets granted reconcile but not prune?
- Are there any
*in resources or verbs?
Production discipline
- Scope per Application, not per cluster. Each Application gets a ServiceAccount, a Role, and a RoleBinding in the namespace it manages.
- Reconcile by default; prune explicitly. Prune is enabled per resource type, not per cluster.
- 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.
- Audit the controller ServiceAccount quarterly. New applications may have widened the grants.
- 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
Q1. Which verbs should a GitOps Application controller hold in a least-privilege grant for a production namespace?
Q2. Because the GitOps controller runs continuously, its RBAC grant cannot safely be wider than a CI apply job's grant.
Q3. Distinguish the reconcile permission from the prune permission in a GitOps controller, and state why Secrets are typically reconciled but not pruned.
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.