Variable Substitution#
Kyverno's variable substitution engine resolves {{ expression }} placeholders in policy rules against the evaluation context before the rule is applied. The engine is implemented in pkg/engine/variables/vars.go and drives all rule types β validate, mutate, generate, and preconditions.
Entry Points#
All substitution flows eventually call the private substituteAll() , which runs two passes in order:
- Reference substitution (
substituteReferences) β resolves$(path)self-referential pointer expressions against the current document structure . - Variable substitution (
substituteVars) β resolves{{ jmespath_expr }}patterns against the evaluation context via a pluggableVariableResolver.
Public entry points and their use cases:
| Function | Use case |
|---|---|
SubstituteAll | General substitution with the default resolver |
SubstituteAllInPreconditions | Preconditions β errors are suppressed, returning nil instead of failing |
SubstituteAllInRule | Full typed kyvernov1.Rule substitution |
SubstituteAllInType[T] | Generic typed substitution via JSON round-trip |
SubstituteAllInConditions | AnyAllConditions arrays |
SubstituteAllForceMutate | Mutation β uses placeholder values when no context is available |
ReplaceAllVars | Low-level replacement with a custom function |
Pattern Detection#
Variable tokens are identified by regex patterns defined in pkg/engine/variables/regex/vars.go:
RegexVariablesβ matches{{...}}at start-of-string or preceded by a non-backslash character .RegexVariableInitβ matches{{...}}anchored at position 0, used to distinguish a variable that occupies the entire value from one that is embedded mid-string .RegexReferencesβ matches$(...)self-reference syntax .RegexEscpReferencesβ matches\$(...)escaped references that should pass through without resolution .
Core Substitution Loop#
substituteVariablesIfAny is the jsonUtils.Action applied to every leaf node during traversal. Its key behaviors:
- Whole-node replacement: if the entire leaf value equals the variable expression, the resolved value is returned directly as its original type (e.g., a map or slice stays a map or slice) .
- Embedded substitution: if the variable is embedded in a larger string, the resolved value is JSON-marshalled and string-spliced into the pattern via
substituteVarInPattern. - Nested iteration: after each substitution pass the string is re-scanned for new variables, enabling chained resolution .
- Delete request rewriting:
request.objectreferences are automatically rewritten torequest.oldObjectforDELETEadmission operations . @current-document shorthand: the@variable is expanded into the JMESPath for the current node's path within the policy document .
Shallow Substitution ({{- ... }})#
Prefixing an expression with a hyphen β {{- expr }} β enables shallow substitution. The hyphen is detected by replaceBracesAndTrimSpaces, which strips the {{- ... }} delimiters and returns isShallow = true.
When shallow mode is active :
- Any
{{sequences inside the resolved value are escaped to\{{β preventing recursive variable expansion of content that may itself contain brace-delimited template strings. - The substitution loop is terminated after one pass (
vars = []string{}), skipping the re-scan for nested variables. - After all variable processing completes,
\{{is unescaped back to{{.
Purpose: this lets a field whose resolved value contains Kubernetes-native or app-level template syntax (e.g., https://example.com/{{user_id}}) pass through without Kyverno attempting to resolve the inner braces as JMESPath .
Known Bugs and Fixes#
Unsafe type assertion in shallow substitution (PR #12039 / #13805)#
Before PR #12039, the shallow-substitution escaping line was:
if shallowSubstitution {
substitutedVar = strings.ReplaceAll(substitutedVar.(string), "{{", "\\{{")
}
The bare assertion substitutedVar.(string) panics when the resolver returns nil. The fix added a nil guard β if shallowSubstitution && substitutedVar != nil β making the path safe. PR #13805 cherry-picks the same fix to the release branch .
forEach patchesJson6902 panic (PR #15887)#
A separate bare type assertion in the forEach mutation handler (pkg/engine/mutate/mutation.go) β fe["patchesJson6902"].(string) β would panic when a variable inside patchesJson6902 resolved to nil, crashing the background controller into a CrashLoopBackOff . The fix changed this to a safe assertion:
jsonPatch, _ := fe["patchesJson6902"].(string)
This is tracked under security advisory GHSA-fpjq-c37h-cqcv .
Template literals in resource fields misinterpreted as JMESPath (Issue #11865)#
When a resource being validated contains application-level template strings like {{user_id}} in environment variable values or annotations, Kyverno's substitution engine attempts to resolve them as JMESPath expressions and fails with Unknown key "user_id" in path . The {{- expr }} shallow substitution syntax was intended as a workaround but only helps when applied within the policy itself β it has no effect on literal brace sequences that already exist in the incoming resource object and are processed as part of an apiCall data payload.
Key Files#
| File | Purpose |
|---|---|
pkg/engine/variables/vars.go | Core substitution engine β all entry points and logic |
pkg/engine/variables/regex/vars.go | Regex patterns for detecting variables and references |
pkg/engine/variables/ | Package boundary; also contains operator/, utils/ helpers |