Safeguard
DevSecOps

The GitOps security model: risks and controls

GitOps turns a Git repo into a deploy button — Argo CD's own CVE-2022-24348 path traversal proved what happens when that button isn't locked down.

Safeguard Research Team
Research
6 min read

Weaveworks CEO Alexis Richardson coined the term "GitOps" in a 2017 blog post to describe a pattern his team had already been running in production: declare your infrastructure and application state in Git, then let a controller inside the cluster continuously pull and reconcile reality to match it. Argo CD and Flux CD turned that pattern into the default deployment model for a large share of Kubernetes shops, and the model has a real security upside — the controller pulls from Git rather than a pipeline pushing cluster-admin credentials outward, so those credentials never have to leave the cluster. But moving the source of truth into Git and putting a privileged, always-on reconciliation loop in front of it also creates two new high-value targets that didn't exist in a traditional push-based pipeline: the repo itself, and the controller reading it. Argo CD's own vulnerability history makes the point concretely. CVE-2022-24348, patched in versions 2.1.9 and 2.2.4, let a crafted Helm chart's value-file reference escape its intended directory via path traversal and read files an attacker shouldn't see. This piece walks through what actually changes in the threat model and the controls that keep it closed.

What changes when Git becomes the source of truth?

The biggest change is that write access to the Git repository becomes functionally equivalent to deploy access to production. In a traditional pipeline, a compromised CI credential lets an attacker push an artifact; in GitOps, a compromised merge to the reconciled branch is the deployment — there's no separate approval gate unless you build one back in. That's why branch protection on the environment-tracking branch, mandatory PR review, CODEOWNERS enforcement on manifest directories, and signed commits are not optional hygiene in a GitOps repo the way they might be treated in an application repo — they are the actual authorization boundary for production changes. Teams that carry over source-repo policies from an app codebase (loose review requirements, force-push allowed, no signature verification) are effectively carrying over authorization gaps into infrastructure control. The repo's commit history also becomes your primary audit trail for "who changed production and when," so incomplete or squashed history undermines incident response, not just code archaeology.

Why is the reconciliation controller itself a bigger target than the CI runner it replaces?

Because the controller holds broad, standing, in-cluster credentials and a live network path to the Git repo, making it a more concentrated target than a CI runner that only exists for the duration of a job. Argo CD has shipped two vulnerabilities that illustrate this directly. CVE-2022-24348 (GHSA-63qx-x74g-jcr7), affecting Argo CD before 2.1.9 and 2.2.x before 2.2.4, let an authenticated user submit a Helm chart whose value-file reference used path-traversal sequences, letting the helmTemplate function read arbitrary files reachable by the Argo CD process — including other applications' repository credentials. CVE-2024-37152 (GHSA-87p9-x75h-p4j2) let an unauthenticated caller hit the /api/v1/settings endpoint and pull sensitive configuration that should have required a session. Neither bug required breaking into the Git repo at all — the controller's own attack surface was enough. That argues for network-level restriction of the Argo CD/Flux API, least-privilege RBAC scoped per application rather than cluster-wide, and a patching cadence that treats the controller like the privileged control-plane component it is.

How does drift detection change incident response?

Drift detection changes incident response by giving you a second signal — divergence from Git — that exists independently of whatever logging your cluster already has, but only if you treat drift as a security event and not just an operational nuisance. When someone runs kubectl edit directly against a reconciled resource, the controller will either silently overwrite the change on its next sync or flag it as out-of-sync depending on configuration, and either behavior can mask an incident if nobody is watching for it. A live compromise that modifies a running Deployment to add a malicious sidecar, for instance, may get auto-reverted by reconciliation before anyone notices — which looks like the problem resolved itself, when actually the attacker never touched Git and could try again through a different path. Mature GitOps postures alert on drift events themselves, correlate them to kubectl audit logs to identify who or what made the out-of-band change, and treat repeated drift on the same resource as a signal worth escalating rather than a sync-log line to ignore.

Do manifests and Helm charts need the same scanning as application source code?

Yes, because manifests, Helm charts, and Kustomize overlays are executable configuration that determines privilege levels, network exposure, and secret mounting — the same category of risk as source code, just expressed in YAML instead of a programming language. A chart that sets privileged: true, mounts the host filesystem, or references a plaintext Secret checked into the repo is a finding that a code-focused SAST tool will never see, because there's no code to analyze — just declarative state that a reconciliation loop will faithfully apply. Secrets are a particularly common failure mode: teams that would never hardcode a database password in application code sometimes paste one into a values.yaml "temporarily," and because that file lives in the same repo the reconciliation controller trusts by design, it gets applied to a live cluster with no additional review. Scanning manifests pre-merge — for exposed secrets, overly permissive RBAC bindings, and privileged pod specs — closes a gap that supply-chain security programs built around source code and container images routinely miss.

How Safeguard helps

Because a GitOps repo is still, mechanically, a Git repository, it fits into the same continuous-scanning model Safeguard already applies to any source-code integration: Safeguard connects to any Git host — GitHub, GitLab, self-hosted Gitea or Forgejo, or a generic HTTPS/SSH URL — and rescans on every push plus an hourly pull to catch anything a webhook missed, so manifests, Helm charts, and Kustomize overlays get the same continuous re-evaluation as application code rather than a one-time scan at initial connection. On the cluster side, Safeguard's admission controller evaluates workloads as they're created and its runtime collector reconciles running state roughly every 15 minutes, which gives you an independent check on what the GitOps controller actually converged to — useful precisely because a compromised or misconfigured reconciliation loop is the scenario where you can't fully trust the deployer's own view of "success." Neither replaces branch protection or RBAC hardening on the reconciliation controller itself, but both close the gap between "the manifest looked fine at merge time" and "the cluster is still fine six hours later."

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.