Internal ≠ Isolated (Or Secure): The Argo CD Repo-Server Flaw

On 1 July 2026, Synacktiv published an unauthenticated remote code execution flaw in Argo CD’s repo-server. As of now, there is no fix or CVE for this vulnerability.
Synacktiv reported it to the maintainers roughly eighteen months earlier, in January 2025, and published the details to warn end-users of the potential risk it poses. The mechanism is a single gRPC call and the blast radius may extend to your entire cluster (or fleet of them).
The attack path for this vulnerability features a number of threats we laid out in the CNCF Argo CD End User Threat Model back in 2023.
The attack path
The repo-server exposes an internal gRPC service, GenerateManifest, that performs no authentication. Anyone who can reach the port can ask it to render manifests.
By abusing Kustomize’s Helm integration, that rendering step generates arbitrary command execution. Synacktiv’s POC uses it to read the Redis password from an environment variable, connect to Argo CD’s cache, poison the stored manifests, and let auto-sync deploy an attacker-controlled workload on the next reconcile.
A quick refresher on the moving parts
At its functional core, Argo CD is a handful of components and controllers deployed to a shared Kubernetes namespace.
- API server: the UI and CLI front door, the part everyone thinks of as “Argo CD”.
- repo-server: clones Git repositories and renders the raw manifests (Helm, Kustomize, plain YAML) into Kubernetes resources.
- application-controller: continuously monitors running applications, comparing live cluster state against the rendered (“desired”) state and taking corrective action when they drift. It’s also the component that connects to the managed (tenant) clusters and applies those changes, using each cluster’s credentials.
- Redis: caches rendered manifests and Git metadata so the repo-server doesn’t have to re-render on every reconcile.
The important thing to note is that the repo-server and Redis were designed as internal services. The assumption baked into the architecture is that only other Argo CD components would ever talk to them. The problem with this is, as Synacktiv states it quite succinctly, that internal does not mean isolated by default (or secure, for that matter).
How the exploit works
1. An unauthenticated gRPC service
The repo-server listens for gRPC on port 8081. Its GenerateManifest method takes a request describing a repository and how to render it, and hands back Kubernetes manifests. There is no token, mTLS, or caller identity verification. The trust model is entirely network-based: if you can reach me, you’re allowed to ask.
Synacktiv found the path with CodeQL rather than by hand, writing a custom model that treats the second parameter of any Server/Service method (the ones taking a context.Context first) as attacker-controlled, then tracing that taint across ~238,000 lines of Go until it reached a command execution sink. The sink was an exec.Command call in the Kustomize integration.
2. Turning manifest rendering into command execution
Argo CD lets administrators set global Kustomize build options through the kustomize.buildOptions field in argocd-cm. Normally, that’s an admin-only lever. However, the GenerateManifest gRPC request carries its own KustomizeOptions.BuildOptions string, supplied by the caller. This is what the repo-server passes straight through to the kustomize build command line.
Kustomize’s Helm chart inflation supports a --helm-command flag that tells it which Helm binary to execute. With that in mind, the attacker can do the following:
- Stand up a Git repository they control, containing a Kustomize overlay that inflates a Helm chart plus a script, for example
exfil.sh. - Sends a
GenerateManifestrequest pointing at that repo, withBuildOptionsset to--enable-helm --helm-command ./exfil.sh. - The repo-server dutifully runs
kustomize build ... --enable-helm --helm-command ./exfil.sh, and Kustomize executesexfil.shwhere it expected Helm.
That’s unauthenticated RCE inside the Argo CD control plane, dressed up as an ordinary manifest render.
3. From code execution to the Redis password
A year earlier, CVE-2024-31989 (critical, CVSS 9.0) had already shown that an unauthenticated Redis was enough to take over a cluster, and the maintainers fixed it by shipping an initial Redis password.
However, a password only helps if the attacker can’t read it, and repo-server RCE means exfil.sh can read REDIS_PASSWORD from the pod’s environment variables.
Adding a Redis password was only one mitigation, the threat model noted three reasons to pair it with other controls rather than lean on it alone:
- It lives in a Kubernetes Secret (
argocd-redis), which is base64-encoded, not encrypted. Anyone who can read Secrets in theargocdnamespace (or the underlying etcd, which is less likely) can decode it. - Redis traffic is plaintext by default (TLS is opt-in, see ATM-015), so the password crosses the pod network in the clear on every
AUTH, and an on-network attacker could sniff it, or tamper with cached manifests in transit, without ever landing on the repo-server. - Per ATM-014, Argo CD historically did not support password-based Redis authentication in high-availability mode. A number of production HA installs may be running with no Redis password whatsoever.
4. Poisoning the manifest cache
With the password in hand, the attacker connects to Redis and rewrites the cache the way CVE-2024-31989 first demonstrated:
- Overwrite the
mfst|...key with a malicious manifest (Synacktiv used a privileged “BadPod” deployment). - Rewrite the matching
git-refs|...entry to point at an older commit SHA, so the controller sees a difference between desired and live state. - Recompute the cache’s FNV-64a integrity hash. As the cache is not cryptographically signed, a few lines of script produce a hash that Argo CD accepts as genuine.
5. Auto-sync closes the loop
The application-controller detects a “new” desired state, and reconciles it into the tenant cluster using its high-privilege service account. The attacker’s workload lands with the full authority Argo CD carries. Game over.
Below is a vertical flowchart visually describing the attack path with an ultimate goal of tenant cluster takeover.
- Step 1: an attacker controls a pod in the Operations cluster.
- Step 2: it reaches the repo-server gRPC port
8081, which requires no authentication. - Step 3: a crafted request abuses Kustomize’s
--helm-commandflag to run code on the repo-server. - Step 4: it reads
REDIS_PASSWORDfrom the pod’s environment. - Step 5: it poisons the Redis manifest cache.
- Step 6: auto-sync applies the poisoned manifest to the tenant cluster.
The two green callouts down the right-hand side mark where a default-deny NetworkPolicy would break the chain: at the repo-server port 8081 (step 2) and the Redis port 6379 (step 5).
Internal ≠ Isolated (or secure)
It’s easy to dismiss this as “the repo-server isn’t external-facing, so who cares?” That reasoning is the gap CSO Online and InfoWorld flagged when they argued GitOps engines should be treated as tier-zero infrastructure.
Argo CD sits at the intersection of source code, configuration, and your live clusters. It has read access to private repositories, sync access to target clusters, and custody of deployment secrets. Any pod that can reach its repo-server becomes, functionally, an authenticated attacker.
Modern intrusions rarely start at the perimeter. They often start with a single compromised workload (a vulnerable app, poisoned dependencies, or leaked tokens) somewhere inside the cluster. From that single foothold, if nothing stands between pods and the Argo CD control plane, the whole east-west path in the diagram above is wide open. The severity is amplified by two other threats that the hardening guide also called out:
- The automatically created
defaultproject is assigned to any Application without a specified project and by default permits deployments from any source repo, to any cluster, and all resource Kinds. Under this default configuration, any user with access to create Applications in the default project could deploy malicious resources (including cluster-wide resource) from any repo to any tenant cluster (ATM-011). - Argo CD’s default service accounts are highly privileged. The Argo CD Service Account has cluster-admin permissions on the Operations cluster (ATM-018). Argo CD ClusterRole and ClusterRoleBinding on the tenant cluster have cluster-admin permissions in case of non-namespaced cluster add process. When access to the tenant cluster is descoped to namespace-level access, starting from cluster-wide access, these RBAC entities are not updated (ATM-019).
Putting it all together
Read together, three of the threat model’s findings chain into exactly this attack:
- ATM-014: Redis is not password-protected by default. The stated threat: “Through ability to inject different manifests, the integrity of the data can be compromised and thus deploy malicious resources.” That’s cache poisoning, described two years before it became CVE-2024-31989. The same finding also warned that the Redis password, if used, shouldn’t live in an environment variable but be mounted as a file, which this attack capitalises on.
- ATM-015: Redis communications are not encrypted by default. Without TLS between Argo CD and Redis the cache, and the Redis password on the wire, can be sniffed or man-in-the-middled from the pod network.
- ATM-017: Lack of network segregation for the Argo CD resources. The threat, verbatim: “Exploitation of vulnerability in Argo CD component due to lateral movement from compromised workload in Operations cluster.” The recommendation: a default-deny ingress Network Policy for every pod in the Argo CD namespace.
Why there’s no ‘fix’
The repo-server’s gRPC service was designed for traffic between trusted Argo CD components. It doesn’t authenticate callers (by default), because the architecture assumes only Argo CD’s own components are ever on the other end. Under that model, keeping untrusted callers off the port is a network-isolation problem for the operator to solve, not necessarily a bug in the application itself.
The plain-manifest install bundles the network policies, while the Helm chart, which is how most production Argo CD is deployed, ships them disabled with networkPolicy.create set to false.
What you can do about it
Network isolation is the best defense against the threat this flaw poses. Turn on network policies so that only Argo CD’s own components can reach the repo-server and Redis ports.
Start by checking what you already have:
kubectl get networkpolicy -A
A hardened install should show policies covering each Argo CD component, including the repo-server and Redis. Argo CD provides these policy files, but you as the end-user must deploy them. If you deploy via Helm, enable them explicitly:
networkPolicy:
create: true
defaultDenyIngress: true
Only Argo CD’s own components should be able to reach the repo-server on 8081 and Redis on 6379. That network isolation breaks the attack chain in at least two places.
One caveat: NetworkPolicy is only enforced if your CNI implements it, and some CNIs expose their own policy APIs, such as CiliumNetworkPolicy or Calico’s GlobalNetworkPolicy. Whichever your cluster uses, the goal is effectively the same: deny traffic to repo-server and redis ports listed above (by default), and allow only Argo CD’s own components to communicate with eachother.
Then you can work down the rest of the list to realize defense in depth:
- Scope your AppProjects. Don’t leave workloads in the unrestricted
defaultproject (ATM-011). Restrict which repos, clusters, and namespaces each project may deploy to. - Least-privilege the sync service accounts. The
argocd-manageraccount on each tenant cluster should hold the minimum permissions needed to sync its apps, not cluster-admin (ATM-019). This helps contain the blast radius when a manifest does get poisoned. - Secure Redis communication. Enable TLS between Argo CD and Redis (ATM-015), restrict who can read Secrets in the
argocdnamespace, and turn on etcd encryption at rest so the Redis password isn’t just base64 encoded. If you run Argo CD in HA mode, confirm Redis authentication is enabled (historically this wasn’t supported, ATM-014). - Patch the flaws that do have fixes. The repo-server RCE has no patch, but recent max-severity issues do — notably CVE-2025-55190 (CVSS 10.0), where project-scoped API tokens could read repository credentials, fixed in 2.13.9 / 2.14.16 / 3.0.14 / 3.1.2.
- Threat model everything, not just the ‘front door’. Ask which workloads can reach your control plane, assume breach, and architect as if something is already compromised inside the cluster.
Let us help you
If you run GitOps at scale and share some of the concerns we talked about today, ControlPlane’s threat modelling practice does exactly this work. We wrote the Argo CD End User Hardening Guide and the Envoy Gateway threat model, and have been Hardening Git for GitOps since 2017. Get in touch.
Related blogs

Validating Zero Trust: Network Policy Testing with Flux CD and Netassert

Defusing CanisterWorm: How Bun and Deno Secure the JavaScript Supply Chain
