Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLXXVII · Argo CDMultitenancy

Projects and RBAC — the multi-tenant boundary and the policy CSV

Advanced⏱ ~27 mingitargocd

What you'll learn

  • Define an AppProject with source repositories and destination clusters allowlists
  • Read a policy.csv and translate a row into the permission it grants or denies
  • Recognise the failure mode of a project with no source allowlist (any repo deployable)
  • Configure sync windows and cluster resource allowlists for a production project

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.

Argo CD’s AppProject is the multi-tenant boundary. Every Application belongs to a project, and the project decides which sources the Application can read from, which destinations it can apply to, and which actions operators can perform against it. The project is the unit of audit and the unit of blast radius: when something goes wrong, the question is “which project?” and the answer is also “which set of permissions?”. This lesson walks the AppProject spec field by field and explains the policy.csv that turns the project from a namespace into a permissions object.

What an AppProject owns

An AppProject is a Kubernetes CRD. Its spec carries four distinct policy dimensions:

  • Source repositories. A list of Git (or Helm, or OCI) repository URLs the Applications are allowed to read from. The controller rejects Applications whose spec.source.repoURL is not in the list.
  • Destination clusters and namespaces. A list of (server, namespace) pairs the Applications are allowed to apply to.
  • Cluster resource allowlist. A list of Kubernetes resource kinds the Applications are allowed to create. Used to prevent charts from creating cluster-scoped resources.
  • Sync windows. The maintenance windows that gate automated syncs. Covered in LXXVII-04.
argocd proj create payments \
  -d https://kubernetes.default.svc,payments \
  -d https://kubernetes.default.svc,payments-staging \
  -s https://github.com/example/payment-api
flowchart TB
    P["AppProject: payments"] --> SR["sourceRepos allowlist"]
    P --> DR["destinations allowlist"]
    P --> CR["clusterResourceWhitelist"]
    P --> SW["syncWindows"]
    P --> POL["policy.csv"]
    SR --> A["Applications in project"]
    DR --> A
    CR --> A
    SW --> A
    POL --> OPS["Operator permissions"]

Source and destination allowlists

The two most important fields are source and destination allowlists. A production project defines both explicitly:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: payments
  namespace: argocd
spec:
  sourceRepos:
    - https://github.com/example/payment-api
    - https://charts.example.com
  destinations:
    - server: https://kubernetes.default.svc
      namespace: payments
    - server: https://kubernetes.default.svc
      namespace: payments-staging
  clusterResourceWhitelist:
    - group: ''
      kind: Namespace

The fields and what they own:

  • sourceRepos — explicit allowlist of repositories the Applications in this project may read from. An Application whose spec.source.repoURL is not in this list is rejected at admission time.
  • destinations — explicit allowlist of (server, namespace) pairs. An Application whose spec.destination is not in this list is rejected.
  • clusterResourceWhitelist — resource kinds the project is permitted to create at cluster scope. Used to permit Namespaces or CustomResourceDefinitions while denying ClusterRoles and ClusterRoleBindings.

The policy.csv and RBAC

The AppProject’s spec.rbac block carries the policy.csv - a list of rows that grant or deny permissions to operators:

rbac:
  policy: |
    p, role:payments-admin, applications, *, payments/*, allow
    p, role:payments-admin, applications, sync, payments/*, allow
    p, role:payments-viewer, applications, get, payments/*, allow
    p, role:payments-viewer, exec, deny, payments/*, deny
    g, alice, role:payments-admin
    g, bob, role:payments-viewer

The columns and what they own:

  • p rows are policy entries: policy, subject, resource, action, object, effect. An entry grants (allow) or denies (deny) a permission.
  • g rows are group bindings: binding, subject, role. An entry binds a subject (user, group, JWT claim) to a role.
  • The verbs are Argo CD’s: applications (CRUD on Applications), exec (pod exec, port forward), logs (pod logs), sync (invoke sync), get (read resources), and more.

Role names are arbitrary strings; binding rows assign subjects to them. A single role can grant permissions across multiple projects.

flowchart LR
    AL["alice"] -->|g| RA["role:payments-admin"]
    BO["bob"] -->|g| RV["role:payments-viewer"]
    RA -->|p, applications, *, allow| P["payments/*"]
    RA -->|p, applications, sync, allow| P
    RV -->|p, applications, get, allow| P
    RV -->|p, exec, deny| P

Projects as the unit of audit

A project is the unit of audit because every Application belongs to a project, every sync is recorded under the Application, every operator action is recorded against the project’s RBAC. The audit question “who deployed to payments in the last 24 hours?” is answered by listing Applications in the payments project, reading their sync history, and reading the RBAC bindings to identify the operator. Without projects, the audit question is “who deployed to Argo CD?” - and the answer is everyone with a sync permission on the default project.

Projects are also the unit of blast radius. A compromised project cannot reach beyond its destination allowlist. An attacker who compromises a token with payments-admin can sync Applications in the payments project, but cannot create an Application in the identity project because the destination allowlist blocks it. The project is the trust boundary.

Under the hood

The AppProject is a CRD, so it is stored in etcd, versioned by Git, and enforced by the API server’s admission webhook. The controller’s RBAC check happens at admission time, not at sync time. This means an Application with a forbidden source or destination is rejected the moment the engineer applies the manifest, not five minutes later when the controller reconciles. The API server’s admission webhook is the choke point: every Application creation, update, and sync passes through it.

Production discipline

The rules for production AppProjects:

  1. sourceRepos is always explicit. No *, no omitted field. The CI lint rejects any AppProject whose sourceRepos is not a finite list.
  2. destinations matches the environment. A production project lists the production cluster’s server URL and the production namespaces; a staging project lists the staging cluster’s. Cross-environment destinations are forbidden.
  3. clusterResourceWhitelist is the narrowest possible list. Most projects permit only Namespace. CRDs and ClusterRoleBindings are denied by default.
  4. Policy rows are reviewed line by line. A *, *, * allow row is a privilege escalation waiting to happen.

Cross-course references

  • Kubernetes for Production Sysadmins - Parts IX-XI (RBAC) cover the underlying RBAC model that Argo CD’s policy.csv implements.
  • Git, CI/CD & GitOps for Infrastructure Engineers - Part LXXIV (GitOps Controllers) is the architectural layer Argo CD participates in; the AppProject is how that architecture is scoped per team.

Quiz

Knowledge check · 4 questions

  1. Q1. An Application belongs to a project whose sourceRepos field is empty (or absent). What is the operational consequence?

  2. Q2. An Argo CD RBAC deny row restricts permissions independently of any allow row in the same policy.

  3. Q3. Name the four policy dimensions an AppProject's spec section owns.

  4. Q4. Diagnose why an untrusted repository was deployable in a production Argo CD instance.

    A team creates a new AppProject for an experiment. The spec.sourceRepos field is omitted. An engineer creates an Application in the project with repoURL pointing at a personal GitHub account. The Application is admitted and the controller begins syncing manifests from the personal account into a production namespace.

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