Skip to main content
2026-05-14
10 min read

Argo CD CVE-2026-42880: When Read-Only Means Read-Everything-Including-Secrets

Argo CD CVE-2026-42880: When Read-Only Means Read-Everything-Including-Secrets

A week ago, on May 7, 2026, the Argo CD project published GHSA-3v3m-wc6v-x4x3. The summary is short: any authenticated Argo CD user, including everyone on the default role:readonly, can pull plaintext Kubernetes Secret values out of any Application that uses ServerSideDiff with the mutation-webhook annotation. CVSS 9.6, scope-changed because the leak crosses the Argo CD trust boundary into etcd.

If you maintain an Argo CD instance shared by more than one team, you almost certainly have read-only users. If you maintain one Application with the IncludeMutationWebhook=true compare option set, that Application's rendered Secrets are visible to every one of those users. Service account tokens, TLS private keys, database credentials, the lot, sitting one API call away.

This post covers the patch matrix, how to find at-risk Applications in your cluster today, what to do if you cannot upgrade immediately, and why this is the second authorization-bypass of this exact shape inside twelve months.

TL;DR

  • CVE-2026-42880, CVSS 9.6, disclosed 2026-05-07. Affects Argo CD 3.2.0 to 3.2.10 and 3.3.0 to 3.3.8. Fixed in v3.2.11 and v3.3.9. v2.x is not affected.
  • Any authenticated user with applications, get (the default role:readonly grants this) can call ServerSideDiff and receive unmasked Kubernetes Secret values for any Application that has the annotation argocd.argoproj.io/compare-options containing IncludeMutationWebhook=true.
  • Detection is a single jq query against kubectl get applications.argoproj.io -A -o json. See below.
  • If you cannot upgrade today, the practical mitigation is removing IncludeMutationWebhook=true from those annotations. It is safe to remove. Diffs simply revert to filtering out fields injected by mutating webhooks, which is the default ServerSideDiff behavior anyway.
  • This is the second authorization-bypass disclosure in Argo CD in twelve months. Both leaked through endpoints that forgot to call a redaction helper. Treat role:readonly as "read-everything-including-secrets" until proven otherwise.

Prerequisites

  • An Argo CD installation on 3.2.x or 3.3.x. Run argocd version (server) or kubectl -n argocd get deploy argocd-server -o jsonpath='{.spec.template.spec.containers[0].image}' to confirm.
  • kubectl access to the namespace your Applications live in (usually argocd, but the CR can live anywhere).
  • jq for the one-liner. yq works too if you prefer.

What the bug actually is

ServerSideDiff is the Argo CD feature that asks the Kubernetes API server to do a Server-Side Apply dry-run and then diffs the resulting object against the desired state. It was added in 3.x because it produces more accurate diffs than the older client-side approach, especially when controllers or mutating webhooks add fields to the live object.

The vulnerable code path is the gRPC method application.ApplicationService/ServerSideDiff, exposed over REST as /api/v1/applications/{appName}/resource-tree/diff. The handler at server/application/application.go:3051-3062 constructs its response from the raw PredictedLive and NormalizedLive objects returned by the dry-run, without ever calling hideSecretData().

That helper is what every other diff and state endpoint in Argo CD calls before returning a Secret-shaped object to a client. GetManifests, GetManifestsWithFiles, GetResource, PatchResource, all of them route through it. ServerSideDiff was the one handler that missed it.

The vulnerability needs two conditions:

  1. The caller has applications, get permission. In the shipped role:readonly policy this is wildcard, so every authenticated user has it.
  2. The target Application has the compare option IncludeMutationWebhook=true set on the argocd.argoproj.io/compare-options annotation. Without that flag, a secondary filter called removeWebhookMutation() runs over the response and strips fields injected by mutating webhooks, which incidentally catches the leak. The dangerous combination is ServerSideDiff=true,IncludeMutationWebhook=true in the same annotation value.

What leaks is the rendered Kubernetes Secret as it would appear in etcd. The advisory specifically calls out service account tokens, TLS private keys, database credentials, and API keys. SealedSecrets and ExternalSecrets are not decrypted by Argo CD itself, but the bug leaks the Secret object their controllers produce, which is materially the same outcome from the attacker's perspective.

One nuance worth knowing: the CVSS vector is AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N. PR:L means a valid authenticated session is required. The advisory is correctly framed as "every authenticated user", not "every unauthenticated attacker". For most Argo CD deployments this is a distinction without a difference, since SSO is typically wired up to the whole engineering org.

Find at-risk Applications in your cluster

The fastest way to know whether you are affected is to list every Application whose compare-options annotation contains IncludeMutationWebhook=true:

kubectl get applications.argoproj.io -A -o json \
  | jq -r '.items[]
      | select(.metadata.annotations["argocd.argoproj.io/compare-options"]
      | tostring
      | contains("IncludeMutationWebhook=true"))
      | "\(.metadata.namespace)/\(.metadata.name)"'

This returns one namespace/name per affected Application. Empty output means no Application in the cluster carries the dangerous annotation, but that does not mean you can skip the upgrade. The annotation can also be set globally via the resource.compareoptions field in the argocd-cm ConfigMap:

kubectl -n argocd get cm argocd-cm -o jsonpath='{.data.resource\.compareoptions}'

If that output contains IncludeMutationWebhook=true, every Application in the cluster inherits the dangerous setting, even Applications without their own annotation. The upgrade becomes urgent rather than important.

If you want to know whether the bug has actually been exploited against you, the bad news is there is no dedicated audit field. The Argo CD access log records the gRPC method and the subject from the JWT, so the best you can do is grep historical logs for non-admin subjects calling ServerSideDiff:

kubectl -n argocd logs deploy/argocd-server --tail=1000000 \
  | grep ServerSideDiff \
  | grep -v 'sub=admin'

That gives a noisy but reviewable list. If your subjects are organisation emails or group claims, swap the second grep for the pattern that matches your admins.

The upgrade

The patch is a pure bugfix. The diff adds the existing hideSecretData() call to the ServerSideDiff response builder. There are no new flags, no new defaults, no behavior change for legitimate users beyond the obvious one of no longer seeing plaintext Secret values in diffs. Most teams using ServerSideDiff for legitimate reasons (catching drift introduced by mutating webhooks) get back the same masked diff they already get from every other endpoint.

The version mapping is straightforward:

Argo CD 3.3.x  ->  upgrade to v3.3.9
Argo CD 3.2.x  ->  upgrade to v3.2.11
Argo CD 2.x    ->  not affected, no action needed

If you install via the official Helm chart, the 9.5.x line tracks 3.3.x and pins appVersion v3.3.9 from chart 9.5.11 onward. The 3.2.x line is still served by the older chart majors (8.x). Verify the appVersion mapping on Artifact Hub before pinning a chart version, since the Argo team does not always cut chart releases on the same day as the controller release.

Once the new image is running, the on-the-wire fix is verifiable. Authenticated as a read-only user, call the ServerSideDiff endpoint against an Application that carries IncludeMutationWebhook=true and confirm the response no longer contains data fields populated for Secret resources.

If you cannot upgrade today

Two options buy time. The first is annotation removal:

kubectl -n argocd annotate application <name> \
  argocd.argoproj.io/compare-options-

Removing the annotation is safe. It reverts to default ServerSideDiff behavior, which filters mutation-webhook-injected fields. The only consequence is that diffs no longer include those fields. If you were depending on seeing them, you were also leaking Secrets to read-only users, so this is the correct fix regardless. To remove the global setting from argocd-cm, edit the ConfigMap and drop IncludeMutationWebhook=true from the resource.compareoptions value.

The second option is RBAC scope-down. The Argo CD argocd-rbac-cm ConfigMap controls who can call which endpoint. The minimum effective change is to stop defaulting users to role:readonly. Edit the policy.default line:

# argocd-rbac-cm
policy.default: ""          # was: role:readonly
policy.csv: |
  # Existing admin role still gets everything
  g, your-admin-group, role:admin

  # New explicit team scope, no wildcard get on applications
  p, role:dev-readonly, applications, list, */*, allow
  p, role:dev-readonly, repositories, get, *, allow
  p, role:dev-readonly, projects, get, *, allow
  g, your-dev-group, role:dev-readonly

This is more disruptive than annotation removal because it changes what the UI shows to unprivileged users. List works, individual application detail does not, which is what stops the ServerSideDiff endpoint cold. Best to combine annotation removal with the RBAC change rather than rely on either alone.

A Kyverno policy can also block new at-risk Applications at admission time:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: block-include-mutation-webhook
spec:
  validationFailureAction: enforce
  rules:
    - name: deny-include-mutation-webhook
      match:
        any:
          - resources:
              kinds: ["argoproj.io/v1alpha1/Application"]
      validate:
        message: "IncludeMutationWebhook=true leaks Kubernetes Secrets to read-only users (CVE-2026-42880). Remove it or upgrade Argo CD to 3.2.11 / 3.3.9 first."
        pattern:
          metadata:
            =(annotations):
              =(argocd.argoproj.io/compare-options): "!*IncludeMutationWebhook=true*"

The same shape works as a Gatekeeper constraint if you are on OPA. Both are admission-time defences, so they prevent new bad Applications but do not retroactively fix existing ones. Pair with the jq query above to clean up what is already in the cluster.

The pattern: redaction-by-handler is fragile

CVE-2026-42880 is the second authorization-bypass of this exact shape in Argo CD inside twelve months. The previous one was CVE-2025-55190 on September 4, 2025, CVSS 9.9. That bug lived in server/project/project.go and leaked repository credentials through the GetDetailedProject endpoint. Same default RBAC (any authenticated user with projects, get), same missing redaction call.

Both bugs share a structural property worth flagging. Argo CD's redaction is per-handler, not middleware. Every endpoint that returns an object is responsible for calling hideSecretData() or its equivalent before serialising. Adding a new endpoint without that call ships a CVE. Adding a new field that holds a secret to an existing response ships a CVE.

For operators, the practical lesson is to stop treating role:readonly as if read-only is meaningful in a security sense. It grants get against everything, and "get returns Secret values" turns out to be true twice in a row. The realistic default for shared Argo CD instances is:

  • policy.default: "" (no implicit role)
  • Explicit per-team roles with only the verbs and resources the team needs
  • An explicit admin role for the platform team
  • Kyverno or Gatekeeper guards on the annotations and ConfigMap fields known to be dangerous

Treat the next "low-severity read-only information disclosure" advisory from the project the same way you would treat a privilege-escalation one, because the read-only/privilege-escalation distinction has so far been a coin flip.

Sources

Patch, then audit your RBAC. The annotation is the smoking gun. The RBAC default is the loaded weapon.

Published: 2026-05-14|Last updated: 2026-05-14T09:30:00Z

Found an issue?

Also worth your time on this topic