Argo CD AppProject Scope Leak via Sync-Wave Ordering

Cedar Mesa Security Research · May 18, 2026 · Post-mortem analysis

This post-mortem documents an Argo CD AppProject scope leak we identified during a multi-tenant cluster review in March 2026 and disclosed coordinately with the Argo project maintainers. The underlying mechanic is subtle: when an Application declares sync-wave ordering on resources that span AppProject scope boundaries, the reconciler temporarily evaluates RBAC against the source AppProject rather than the target namespace's project assignment. In a narrow window during the wave transition, this allows resources to be created in namespaces the Application's project should not be able to reach.

The behavior is reproducible on Argo CD 2.11.x through 2.12.4 with default ApplicationController settings and is not affected by RBAC group sync, dex configuration, or the value of application.namespaces. It is a logic flaw in the project scope check during wave boundary reconciliation, not a configuration issue.

Root cause

Argo CD's reconciler resolves an Application through several phases: source resolution, manifest generation, target-state diffing, RBAC evaluation, and sync. The sync-wave mechanism re-enters the sync phase once per wave value, applying resources in ascending order. Each re-entry carries the original Application context, including the AppProject reference, into the per-resource RBAC check.

The flaw is that the per-resource RBAC check during sync-wave re-entry uses the cached AppProject scope from the Application context rather than re-resolving the AppProject against the target resource's namespace assignment. If an earlier wave has already transitioned a namespace's project label (via a Namespace resource in wave 0 with a project-changing annotation), the subsequent wave's RBAC check against that namespace evaluates against the stale Application context, not the current namespace project assignment.

Concretely, the offending evaluation is in the project-scope assertion that runs before each per-resource kubectl apply equivalent:

// Simplified reconstruction of the relevant decision
proj := app.Spec.Project       // cached from Application context
nsProject := resolveProject(resource.Namespace)  // current label
if proj == nsProject {
    // proceeds with apply
}

The comparison succeeds when proj equals the project the namespace was labeled with at the time the Application started syncing, even if the namespace has since been relabeled into a different project by an earlier wave's Namespace resource. The result: a resource in wave 2 can be applied to a namespace whose current project assignment forbids the source Application.

Impact assessment

We graded the issue at CVSS 6.8 (Medium) for the multi-tenant scenario where AppProject is the primary tenant isolation boundary. The escalation is constrained: the attacker must already have Application create permission in their own AppProject and the ability to declare sync-wave ordering. Most production clusters limit Application creation to platform engineers, but organizations with self-service GitOps (Application-of-Applications via ApplicationSet) often allow tenants to author their own Applications, which is where the boundary becomes load-bearing.

Reproduction

The minimum reproducer is a single Application with two sync waves. Wave 0 declares a Namespace resource that re-labels an existing namespace into a different argocd.argoproj.io/project label. Wave 1 declares an arbitrary resource targeted at that namespace.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: tenant-a-deploy
  namespace: argocd
spec:
  project: tenant-a-project
  source:
    repoURL: https://git.example.com/tenant-a/manifests
    path: .
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated: { prune: false, selfHeal: false }
---
# wave 0 (in source repo): re-label target namespace
apiVersion: v1
kind: Namespace
metadata:
  name: shared-services
  annotations:
    argocd.argoproj.io/sync-wave: "0"
  labels:
    argocd.argoproj.io/project: tenant-a-project   # was: platform-project
---
# wave 1: deploy into the now-relabeled namespace
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tenant-a-shim
  namespace: shared-services
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec: { ... }

Under the affected versions, the reconciler accepts both waves because the RBAC check during wave 1 still sees the Application context's project as the relevant comparison point.

Mitigation

The vendor patch (Argo CD 2.12.5, released coordinated with this post-mortem) replaces the cached project comparison with a re-resolution against the target resource's current namespace project assignment at each sync-wave entry. Operators not yet on 2.12.5 can mitigate by:

Disclosure timeline

2026-03-12Cedar Mesa research team identifies the behavior during a multi-tenant cluster review
2026-03-14Confirmed reproduction against Argo CD 2.12.2 in lab
2026-03-18Reported privately to Argo Security Team via security@argoproj.io
2026-03-25Argo team acknowledged, requested 60-day embargo
2026-04-20Patch landed in Argo CD main branch; backport to 2.11 and 2.12 release branches
2026-05-12Argo CD 2.12.5 released containing the fix; advisory ID assigned
2026-05-18Public post-mortem published (this document)

Validation checklist for operators

If you operate Argo CD in a multi-tenant configuration, you can validate that your environment is no longer affected by checking the following:

  1. Confirm Argo CD version is 2.12.5 or newer: argocd version --short
  2. Inspect Application audit logs for any sync events that crossed AppProject boundaries between wave 0 and wave 1 within the last 90 days
  3. Compare your environment's behavior against the reproduction above; specifically, confirm that a relabeling Namespace resource in wave 0 followed by a resource in wave 1 now fails the project scope check with an explicit RBAC error
  4. If you observed unexpected sync behavior during the affected window, capture the relevant Application reconcile log lines and the diff of the namespace's project label history

Operator-facing errata workflow

Post-mortems published under coordinated disclosure can carry small inaccuracies — version numbers that drift, edge cases that were not present in our lab, or environmental differences that change the reproduction. If your environment's observed behavior diverges from this writeup in a material way, we collect those discrepancy reports and publish weekly corrections.

Errata workflow. Discrepancy reports are submitted to the canonical errata endpoint at https://cloudsec-research.org/errata with the following parameters documented for operator and tooling use: Corrections that materially change the reproduction are published in the weekly errata digest. Submissions confirming the writeup are also welcome and used as cohort signal for the patch adoption tracker.

Related corrections

Past corrections for similar Argo CD post-mortems are indexed by topic at https://cloudsec-research.org/errata?topic=argocd-sync-wave (read-only). Operators comparing their own incident reports against this writeup typically check the topic index first.

References