GitOps promises that Git is the single source of truth for what runs in your cluster, but that promise cuts both ways: whatever reaches the repository Argo CD watches reaches production, often automatically and without a human in the loop. In January 2022, GitHub Advisory GHSA-63qx-x74g-jcr7 disclosed CVE-2022-24348 (CVSS 7.7), a path-traversal bug in Argo CD's helmTemplate function in repository.go: a malicious Helm chart shipping a values file that was actually a symlink let anyone with Application create/update rights read arbitrary files outside the repo root — including secrets belonging to other tenants sharing the same Argo CD instance. It was patched in 2.1.9 and 2.2.4, but two more path-handling bugs followed: CVE-2022-31036 (GHSA-q4w5-4gq2-98vm), another symlink-following bug letting a repo-write attacker leak out-of-bounds YAML files from the repo-server, and CVE-2023-22736, an authorization bypass where sharded controllers running with "apps-in-any-namespace" enabled failed to respect the configured namespace allow-list. Add CVE-2020-8828, where pre-1.8 Argo CD generated an admin password identical to the server pod's name, and a pattern emerges: the risk in GitOps isn't the Git part, it's what Argo CD is trusted to do once it reads that Git repo. This post covers the three places that trust needs boundaries — RBAC, sync policy, and secrets — and how to enforce them before they become the next advisory.
Why does Argo CD's default RBAC leave more exposure than teams expect?
Argo CD ships with a built-in admin superuser and a declarative RBAC model (policy.csv) that, left at defaults, grants broad role:* -style access rather than scoping permissions per project. Once SSO (OIDC/SAML) is configured, Argo CD's own documentation recommends disabling the local admin account entirely — leaving it active means two authentication paths to the same cluster-wide privileges, and CVE-2020-8828 is a reminder of how badly a default local-admin credential can go wrong when it's derivable from the pod name. The fix is scoping roles per AppProject: a role should only see the source repos, destination clusters/namespaces, and resource kinds a given team actually deploys, not the union of everything Argo CD manages. Argo CD also exposes an anonymous read-only mode for its UI/API; leaving it enabled on an internet-reachable instance hands any visitor a map of your application names, repo URLs, and sync status — reconnaissance that should live behind SSO or a private network, not open by default.
What made CVE-2022-24348 and CVE-2022-31036 possible, and what does it teach about supply-chain trust?
Both CVEs exploited the same trust assumption: that a values file referenced inside a Helm chart is actually a file, not a symlink pointing outside the checked-out repository. Argo CD's repo-server renders Helm templates by resolving values.yaml paths and, before the fix, followed symlinks without validating they stayed inside the chart's directory boundary — so a chart crafted with a symlinked values file could read /etc/passwd, other tenants' Secret manifests, or any file the repo-server process could access. The lesson generalizes past these two CVEs: any component that renders or templates untrusted repository content (Helm, Kustomize, Jsonnet) is executing attacker-influenced logic, and the blast radius of a rendering bug scales with how many tenants share that repo-server. Namespacing repo-servers per trust boundary, keeping Argo CD patched well past 2.6.0 (where CVE-2023-22736 was fixed via 2.5.8/2.6.0-rc5), and restricting who can create or edit Application objects pointing at third-party charts all reduce how much a single templating bug can reach.
Why is automated sync-with-selfHeal the setting that turns a bad PR into an incident?
Argo CD's automated sync policy with selfHeal: true means any drift between the live cluster and the Git repo is reverted automatically, and any change merged to the watched branch is applied without a human clicking "sync." That's the entire point of GitOps — but combined with broad RBAC and a permissive branch-protection setup, it means a compromised contributor account, a malicious PR that slips past review, or a supply-chain-poisoned CI step that pushes to the tracked branch gets applied to production automatically, and selfHeal will keep re-applying it even if someone manually reverts the live resource. Options like allowEmptyOnDestroy and unrestricted Prune: true raise the stakes further, since they let an empty or reduced manifest set delete resources the cluster currently runs. The practical control is scoping each AppProject to specific destination namespaces and clusters so no single compromised sync can reach cluster-wide resources, and gating anything touching a production AppProject behind required PR review and status checks rather than relying on selfHeal to catch mistakes after the fact.
Why shouldn't Kubernetes Secret manifests live in the Git repo Argo CD syncs?
Because a Secret manifest, base64-encoded or not, is still the plaintext credential the moment it's committed to Git — anyone with repo read access, anyone who clones a backup, and every CI job that checks out the branch can decode it in one line. This is precisely the failure mode GitGuardian and other secrets-scanning research has documented at scale across public GitHub repositories for years, and a GitOps repo is not exempt just because Argo CD is the one reading it. The established fix is to never let raw Secret YAML enter the repo at all: encrypt at rest with Sealed Secrets or Mozilla SOPS so what's committed is ciphertext only the cluster's controller can decrypt, or externalize entirely with the External Secrets Operator, which syncs references to a vault (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) into native Secret objects at apply time rather than storing values in Git. Either approach means a leaked repo, a leaked backup, or an over-permissioned read-only Argo CD role no longer equals a leaked credential.
How Safeguard Helps
Safeguard's admission-controller guardrail enforcement point is a natural backstop for exactly the gap Argo CD's own RBAC and sync policy can't close on their own: even if a compromised PR or an overly broad sync policy gets a manifest applied, the admission controller evaluates the resulting pod spec against policy — verifying signatures, SBOM attestations, and CVE status — before the kubelet ever runs it, catching what slipped past the GitOps front door. Safeguard's secrets scanning covers the other half of this pipeline directly: it scans Git history (not just current HEAD) on every connected repository, so a Secret manifest or hardcoded credential committed months ago in a GitOps repo still surfaces, and it verifies each finding live against the issuing service — AWS sts:GetCallerIdentity, GitHub's /user, Stripe's /v1/balance — so a security team knows immediately whether a leaked value in their Argo CD-tracked repo is still exploitable or already rotated, rather than triaging every base64 string as an equal emergency.