What can a caller's values string do when a Helm chart passes it through tpl?
answer
- a values field that is executed, not copied
- the string runs in the chart's context
- whatever the chart reaches, it reaches
- no shell, and env was deleted
- lookup uses the caller's credentials
basics
~20 stpl renders the string as a Go template in the chart's context, so the field is code, not data. It can read every value in scope, the chart's bundled files and release fields, call named templates, and call lookup with the caller's credentials.
solid answer
~50 s`tpl` takes a string and a context - `{{ tpl .Values.endpointTemplate . }}` - and runs the string through the template engine as if the chart author had written it. Any values field a chart tpl-renders is therefore an **executable** field, not a data field. Whoever supplies that string gets the chart's full template capability: `.Values`, `.Release`, `.Chart`, `.Files` for files bundled in the chart, every named template it defines, the sprig function set, and `lookup`, which reads live cluster objects using the kubeconfig of whoever ran `helm`. The ceiling matters too: rendering is client-side, there is no shell, and Helm deliberately removes sprig's `env` and `expandenv`, so the string cannot read environment variables. The exposure is template capability plus the caller's cluster read access, not code execution. So tpl a field only when you mean it to be a template, and document it.
code
yaml · 10 lines# values.yaml of a Redis chart
endpointTemplate: "redis://{{ .Release.Name }}-redis.{{ .Release.Namespace }}.svc:6379"
---
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-transcoder
data:
endpoint: {{ tpl .Values.endpointTemplate . | quote }}go deeper
Know what tpl is for: a values file is plain YAML, so a value containing template syntax is only literal text until a chart passes it through tpl. Be able to give one legitimate use, such as embedding the release name in a host.
Explain the mechanics - a string plus a context, executed at render time - and enumerate what the string reaches: values, release, chart files, named templates, sprig and lookup. Be precise about the ceiling too: client-side, no shell, env removed.
An interviewer expects you to treat a tpl-rendered field as an executable input on a shared chart: who writes that values file, what lookup would return under your credentials, and how you constrain or document the field rather than trusting its author.
Own the policy for platform charts: which values fields, if any, are template-rendered; whether tenants may supply them; and how a chart's input surface is documented and reviewed so a field's power matches the trust level of whoever fills it in.
## What tpl is `tpl` is one of the functions Helm adds on top of sprig. Its arguments are a string and a context, as in `tpl "{{ .Release.Name }}-redis" .`. Helm parses that string as a Go template and executes it against the context you pass, then returns the rendered text. It exists because `values.yaml` is plain YAML - a value written there is a literal, and template delimiters sitting in a values file are just characters. `tpl` is the escape hatch that lets a value contain a template the chart evaluates at render time. Chart authors reach for it constantly and for good reasons: an ingress host that must embed the release name, a connection string that references another value, a sidecar config block a consumer supplies, a subchart field that has to know the parent's fullname. All of that works because the string is executed rather than copied. ## Why that is a trust decision The moment a chart writes `tpl .Values.something .`, that values field stops being data and becomes code. A values file is input - it arrives from a tenant's pull request, a CI variable, a `-f` file someone in another team maintains, or an application manifest in a delivery repository. Whoever writes that field is now authoring template source that executes in the chart's context. Concretely, the supplied string can: - read **every value in scope**, including values placed there by other people and values belonging to sibling subcharts if the context you pass has them, and print them into the rendered output; - read `.Release` (name, namespace, revision, service, and the install and upgrade booleans) and `.Chart`; - read files bundled inside the chart with `.Files.Get` and `.Files.Glob` - the chart's own files, not arbitrary paths on disk, and not anything under `templates/`; - call any named template the chart defines with `include`, which may itself do more than the author intended when handed a different context; - call the whole sprig function set, including the encoding and random helpers; - call `lookup`, which performs a live read against the cluster using the credentials of whoever is running the `helm` command. The last one is the sharp edge. `lookup` turns a values string into a request the caller could not necessarily make themselves - if a privileged operator runs `helm upgrade` with a values file authored by a less privileged team, a `lookup` in that string reads with the operator's rights, and whatever the template does with the result lands in the rendered manifest. ## What it cannot do Being precise about the ceiling matters as much as naming the risk, because overstating it gets you marked down. Rendering happens **client-side**, inside the Helm process, before anything is sent to the API server. Charts are rendered only by Go's text/template - there is no shell, no process execution, and no way to spawn one. Helm deliberately deletes sprig's `env` and `expandenv` functions from the function map, so a template cannot read the environment of the machine running `helm` - which is exactly where CI systems keep their tokens. `.Files` is scoped to the chart's own files. So the honest characterisation is: a tpl-rendered values string gets **template capability plus the caller's cluster read access**, not arbitrary code execution on the Helm client. It is an information-disclosure and manifest-shaping risk rather than a remote-shell risk - and the manifest a template shaped is then applied by that same caller's identity. ## Rendering happens every time A second, quieter property: the tpl'd string is rendered on **every** operation that renders the chart, against the context at that moment. A string that reads `.Release.Revision` or the upgrade boolean, or that calls a random helper, produces different output on the next upgrade. That is a correctness trap as much as a security one - a values field that looks like a constant in Git may not be a constant in the cluster. Double evaluation is possible too: if a value is tpl'd and the result itself contains template delimiters, whether it renders again depends on how many times the chart passes it through. Charts that tpl a value which was itself produced by a template are hard to reason about; avoid the second pass. ## How to author this safely - **tpl only fields you meant to be templates.** Do not tpl every string on the way out of `.Values` just in case. A field documented as a plain name should never go through `tpl`. - **Document the field's status.** In `values.yaml` comments and the chart README, mark the field as template-rendered so a consumer knows they are writing code. - **Constrain the field where you can.** `values.schema.json` can require a pattern or an enum for a field, which is far stronger than hoping the string is benign; a field that must be a hostname does not need to be a template at all. - **Pass the narrowest context that works.** `tpl .Values.snippet .` hands over the whole root. If a small dict is enough, build one and pass that. - **Still quote the result.** `tpl` returns text; whether that text is a safe YAML scalar is a separate question, so pipe it through `quote` when the field is a scalar. - **Read the render.** `helm template` with the actual values shows exactly what a supplied string produced, and it is the only review that sees the post-tpl form.
- Does a hostile tpl string give an attacker code execution on the machine running helm?No. Charts are rendered by Go's text/template inside the Helm process - there is no shell and no process spawn, and Helm removes sprig's `env` and `expandenv` so the string cannot read the client's environment variables. What it does get is template capability over the chart's context plus `lookup`, which reads cluster objects as the caller. Treat it as disclosure and manifest shaping, not remote execution.
- A values field is tpl-rendered and calls a random string helper. What goes wrong on the next upgrade?It is re-rendered from scratch, so it produces a different string and the field changes on every upgrade, restarting whatever consumes it. A tpl'd field is not evaluated once and stored as a constant - it runs at every render against that render's context, which also means values reading `.Release.Revision` or the upgrade boolean are not stable.
- Which context should a chart pass to tpl?The narrowest one that works. `tpl .Values.snippet .` hands the supplied string the entire root context - all values, release, chart, files and named templates. If the string only needs two fields, build a small dict and pass that instead, so the field's reach matches what the field is documented to do.
Passing a values field through tpl is like letting a form field hold a spreadsheet formula instead of a number: the same box now computes, and it can reference every other cell on the sheet.
saying these in an interview costs you the question
- Says tpl runs shell commands on the Helm client
- Thinks tpl can read the CI runner's environment variables
- Believes tpl is evaluated once and cached
- Assumes quoting a value stops it being tpl-rendered
- Calls a values file trusted because it is in Git
- Thinks the string executes inside the cluster