Any pod in your cluster that can open a TCP connection to Argo CD's repo-server is, for practical purposes, an authenticated attacker with a path to full cluster takeover. No stolen token. No compromised git remote. No RBAC role to escalate into — just network reachability to one internal gRPC endpoint, plus a second one (Redis), and a crafted GenerateManifest request does the rest.
That is the finding Synacktiv published on July 1, 2026 after reporting it to the Argo CD maintainers in January 2025. The part that should unsettle every self-hosted platform team is not the exploit's cleverness: it is how ordinary the precondition is.
If you run Argo CD, here is the fix first, before a single paragraph of mechanism:
# 1. Check whether the policies exist (expect 7, including repo-server and redis)
kubectl get networkpolicy -n argocd
# 2a. Helm installs: enable the shipped policies (or upgrade to chart 10.0.0+,
# which installs them by default) and re-check step 1
helm upgrade <release> argo/argo-cd -n argocd --reuse-values \
--set global.networkPolicy.create=true
# 2b. Raw-manifest installs: the policies ship in install.yaml — verify they
# were actually applied, then re-check step 1One critical disclaimer before you skim the rest: chart 10.0.0 only flips the NetworkPolicy default (tracked as CVE-2026-62185). The unauthenticated gRPC endpoint itself — CVE-2026-15416 — remains unpatched upstream. Network isolation is not a stopgap until you upgrade; until Argo CD ships authentication on repo-server, network isolation is the fix.
What Synacktiv found, concretely
Argo CD's architecture splits deploy work across components: the API server handles auth and RBAC, the application controller reconciles live state against git, and repo-server sits in the middle rendering manifests — cloning repos, running kustomize, helm template, or config-management plugins, and returning YAML. The API server talks to repo-server over gRPC, on the /repository.RepoServerService/GenerateManifest endpoint.
Synacktiv found the bug by building a custom CodeQL model pack that treated the second parameter of every Service/Server gRPC handler as attacker-controlled. The headline result: repo-server's gRPC server enforces no authentication at all, so anyone who can reach TCP 8081 can call GenerateManifest directly.
Worse, GenerateManifest accepts a KustomizeOptions struct — BuildOptions and BinaryPath — that the legitimate API path only ever populates from administrator-defined server configuration. There is no check that the caller is the API server. Your options struct arrives, repo-server obeys it.
The exploit Synacktiv demonstrated against Argo CD v2.13.3 abuses a stock kustomize feature, not a kustomize bug:
kustomize build <attacker-controlled repo> --enable-helm --helm-command ./exfil.shPoint GenerateManifest at an attacker Git repo containing a kustomization.yaml with a helmCharts entry and a shell script, set BuildOptions to --enable-helm --helm-command ./exfil.sh, and repo-server clones the repo and executes the script during manifest rendering. Synacktiv's proof of concept used the Perl interpreter present in the repo-server image to exfiltrate the REDIS_PASSWORD environment variable to a listener. Remote code execution inside repo-server, from an unauthenticated request.
Note what this collapses. Three attacker positions that your threat model probably treats as three different severity tiers turn out to be the same attack:
| Attacker starting position | What they need | What they get |
|---|---|---|
| Compromised application pod (tenant workload RCE) | TCP reachability to repo-server:8081 | Unauthenticated GenerateManifest → RCE in repo-server |
| Misconfigured service mesh / flat network | Same: one reachable port, no credentials | Identical primitive, identical RCE |
| Adjacent workload with local code execution | Same | Identical primitive, identical RCE |
No git credentials appear anywhere in that chain. The request carries its own repo URL, its own revision, its own kustomize options. Reachability is the credential.
Why RCE in repo-server means cluster takeover
Code execution in a manifest-rendering pod sounds contained until you follow the second stage, which Synacktiv worked out in full. The REDIS_PASSWORD exfiltration matters because Argo CD's Redis holds the rendered-manifest cache — gzip-encoded JSON under mfst|… keys — and that cache is trusted, not verified.
Back in 2024, Cycode had already shown that poisoning this cache could deploy arbitrary manifests, but their path needed the target application's selfHeal option enabled. Synacktiv removed that requirement. The trick is a second Redis key family, git-refs|…, which records each repo's known commit SHAs. The attack runs:
- Write the malicious manifest into the cached
mfst|…entry for the victim app's current commit. - Backdate the
git-refs|…entry for the victim repo to an older commit. - Wait for Auto Sync (or a manual sync). Argo CD sees HEAD pointing at a "newer" commit than the recorded ref, finds the poisoned cache entry for that commit, and applies it — malicious manifest included.
The cache has no signature, so nothing distinguishes the attacker's JSON from repo-server's own output. Argo CD's docs describe Redis as a throwaway cache that "will be rebuilt without loss of service," which is true for availability and dangerously wrong for integrity: a cache the controller treats as ground truth is not a cache, it is a datastore with no authentication story. Synacktiv packaged both stages into a tool, argo-cdown, whose release they deliberately delayed to give defenders time to isolate the ports first.
Are you exposed? It depends on your install path
Upstream Argo CD ships NetworkPolicies that wall repo-server off from everything except its own components — and, crucially, those policies exist in the raw manifests but were not applied by the Helm chart, where networkPolicy.create defaulted to false. That default is its own CVE now (CVE-2026-62185, CVSS 8.6, advisory GHSA-47m3-95c7-g2g8), fixed by flipping the default in chart 10.0.0. But "fixed default" only protects fresh installs and upgraders who notice; every install path needs the same verification:
| Install path | Default policy state | Check command | Exposed if… |
|---|---|---|---|
| Helm chart below 10.0.0 | Policies off (create: false) | kubectl get networkpolicy -n argocd | The repo-server / redis policies are absent — the common case |
| Helm chart 10.0.0+ | Policies on by default | Same, plus helm list -n argocd for the chart version | You assumed the upgrade retro-applied policies it didn't, or you overrode the default |
Raw manifests / Kustomize (install.yaml) | Policies included | kubectl get networkpolicy -n argocd | Install was trimmed ("we don't need those") or a CNI that doesn't enforce policies is underneath |
| Argo CD Operator | Depends on operator version and config — verify, don't assume | kubectl get networkpolicy -n argocd | Absent policies, whatever the operator's docs imply |
Two footnotes apply to every row. First, a NetworkPolicy object your CNI ignores is documentation, not a control — enforcement needs Calico, Cilium, or equivalent, not kubenet or a CNI with policy support disabled. Second, repeat this per Argo CD instance: a fleet with one hardened management cluster and three tenant clusters running their own Argo CD has four answers, and the audit that checks one is theater.
A healthy install shows all seven policies, exactly as Synacktiv's verification output lists them:
NAMESPACE NAME POD-SELECTOR
argocd argocd-application-controller-network-policy app.kubernetes.io/name=argocd-application-controller
argocd argocd-applicationset-controller-network-policy app.kubernetes.io/name=argocd-applicationset-controller
argocd argocd-dex-server-network-policy app.kubernetes.io/name=argocd-dex-server
argocd argocd-notifications-controller-network-policy app.kubernetes.io/name=argocd-notifications-controller
argocd argocd-redis-network-policy app.kubernetes.io/name=argocd-redis
argocd argocd-repo-server-network-policy app.kubernetes.io/name=argocd-repo-server
argocd argocd-server-network-policy app.kubernetes.io/name=argocd-serverIf argocd-repo-server-network-policy or argocd-redis-network-policy is missing, you are in the vulnerable set regardless of which row of the table you arrived from.
The concrete lockdown for your fleet
If the policies are absent, apply them. The repo-server policy below is the upstream shape Synacktiv quotes: ingress on TCP 8081 restricted to the four Argo CD components that legitimately call it, and nothing else:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: argocd-repo-server-network-policy
namespace: argocd
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: argocd-repo-server
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: argocd-server
- podSelector:
matchLabels:
app.kubernetes.io/name: argocd-application-controller
- podSelector:
matchLabels:
app.kubernetes.io/name: argocd-notifications-controller
- podSelector:
matchLabels:
app.kubernetes.io/name: argocd-applicationset-controller
ports:
- protocol: TCP
port: 8081Redis needs the same treatment on TCP 6379 — repo-server and the API server in, everything else out — because the second attack stage only needs the Redis port plus the password the first stage hands it:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: argocd-redis-network-policy
namespace: argocd
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: argocd-redis
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: argocd-repo-server
- podSelector:
matchLabels:
app.kubernetes.io/name: argocd-server
- podSelector:
matchLabels:
app.kubernetes.io/name: argocd-application-controller
ports:
- protocol: TCP
port: 6379Match the app.kubernetes.io/name labels against your own install — Helm chart label values have shifted across versions, and a policy selecting labels nothing carries selects nothing. After applying, verify from the attacker's position, not just the object's existence: exec into an ordinary workload pod and confirm connections to repo-server:8081 and redis:6379 time out, while Argo CD's own sync, refresh, and UI flows still work. If you run a default-deny-ingress posture on the namespace (the chart's defaultDenyIngress value), these allow-rules compose with it; without default-deny they still close exactly the two ports this attack needs.
What breaks if you over-restrict: anything outside the four listed callers that talks to repo-server — external config-management-plugin sidecars in custom setups, some monitoring scrapers hitting the metrics port, ad-hoc argocd CLI port-forward debugging. Those are allowlist additions with known identities, not reasons to leave the port open to the cluster. Add them as named exceptions and keep the default closed.
What this means if you run your own GitOps reconciler
The TODO item behind this post names the audience precisely: teams running Argo CD or a bespoke GitOps reconciler modeled on the same pattern as their deploy pipeline. If you built your own manifest renderer — and plenty of self-hosted PaaS teams have, because Argo CD's weight never fit a small fleet — this disclosure is a design review of your architecture, not just someone else's bug. CSO's coverage framed it as treating GitOps infrastructure as tier zero, and the checklist writes itself:
- Authenticate your internal RPC. If your renderer exposes gRPC or HTTP that any pod can reach, "internal" is doing authentication's job and it will fail exactly this way. mTLS between your components, a shared token at minimum — unauthenticated-by-design internal APIs are now a published RCE pattern, not a theoretical concern.
- Treat your render cache as a trust boundary. An unsigned cache the reconciler applies from is a second control plane with the security properties of /tmp. Sign cache entries, or re-render rather than trusting stored manifests for sync decisions.
- Give the renderer the least privilege you can. Repo-server's pod env held the Redis password; the Redis keys held deploy authority. Separate the credentials that render manifests from the credentials that deploy them, so one compromised renderer cannot mint its own sync.
- Ship the NetworkPolicies with restrictive defaults. The Helm chart's
create: falsedefault is the cautionary tale: your installer is the security boundary most users actually get. Policies on by default, with documented opt-outs, beats perfect policies nobody applied.
None of these requires Argo CD's scale. A single-namespace reconciler on a three-node fleet needs all four, because the attack never cared how big the fleet was — only whether one pod could reach one port.
The seam this exposes
Step back and the shape of this vulnerability is familiar: a component whose threat model said "only trusted callers," enforced by nothing. Kubernetes gives every pod a routable IP and, absent policy, a flat network; "the manifest-rendering service only talks to git and the API server" was a statement about intended topology, and intended topology is not a control. Synacktiv's CodeQL work deserves credit for the find, but the finding itself is a reminder that internal services on a flat network are public services with extra steps.
For self-hosted platform teams the action list is short and entirely today's work, not whenever-a-fix-lands work: verify the seven policies on every Argo CD instance you run, enable or apply them where missing, confirm your CNI enforces them, and extend the same "reachability equals authentication" audit to every internal endpoint your own deploy pipeline exposes. The 93% of Argo CD users running it in production — per the project's own 2023 survey — are the blast radius. Make sure your fleet is the part of it that read the disclosure and closed the port.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



