skip to content

Chart Trust & Secrets

OpenPGP chart provenance, the kubeconfig identity a release is created with, and what happens to a secret passed through values.yaml. Installing a chart runs someone else's templates with your credentials.

part ofHelmoverview, primer and where to startread it →
on this pageshow

explore

questions

20

Helm runs entirely client-side — which identity actually creates a release's Kubernetes objects?

level: juniorimportance: must knowfreq 72%

answer

  1. Nothing of Helm's runs in the cluster
  2. Look at your kubeconfig context
  3. Helm 2 had a server; Helm 3 removed it
  4. The release record is your write too

basics

~20 s

The caller's own kubeconfig identity. Helm 3 and 4 ship no in-cluster server component: the CLI renders the chart locally, then writes every rendered object — and the release record — as whoever ran the command.

solid answer

~40 s

Helm is a pure client. `helm install` and `helm upgrade` render the chart on your machine and then talk to the Kubernetes API server directly, authenticating with the current kubeconfig context (or the pod's mounted ServiceAccount token when Helm runs inside a cluster with no kubeconfig). `--kubeconfig`, `--kube-context` and `-n`/`HELM_NAMESPACE` select which identity and namespace that is. There is no privileged intermediary: Helm 2's cluster-side Tiller, which applied releases under its own broad identity, was removed in Helm 3 and nothing replaced it. So Helm can never create an object your credentials could not create, and the release record it stores — a Secret named `sh.helm.release.v1.<name>.v<rev>` in the release namespace — is written with those same credentials.

code

bash · 8 lines
bash
# Renders locally, contacts no cluster, needs no credential at all
helm template tiles ./tile-server -n geo-staging > rendered.yaml

# Applies as the current kubeconfig context's identity
helm upgrade --install tiles ./tile-server -n geo-staging

# Same chart, a different identity: pick another context
helm upgrade --install tiles ./tile-server -n geo-staging --kube-context ci-deployer

go deeper

for a junior

Be ready to say in one sentence that Helm is just a client and uses your kubeconfig, and to show where the context and namespace come from. Knowing that helm template needs no cluster at all is an easy extra mark.

for a middle

Explain the mechanics: how the identity is selected, that the release record Secret is written with it too, and that Helm applies objects one at a time with no permission pre-flight, so failures land mid-release.

for a senior

Show that you treat the runner's credential as the real blast radius. Talk about which identity your pipelines install as, what a compromised job could reach with it, and how you would confirm that from the outside.

for a principal

Own the argument that Helm deliberately has no privileged intermediary, and what that costs: every delegation decision moves into identity design and chart review rather than into the tool. Be able to compare that with a model where a cluster-side component holds the credential.

## No server, no proxy, no escalation Helm is a command-line client and nothing else. `helm install`, `helm upgrade`, `helm rollback` and `helm uninstall` all run in your terminal or your CI runner: the CLI loads the chart, renders its templates locally into a set of manifests, opens a connection to the Kubernetes API server, and writes those objects itself. Nothing that belongs to Helm runs on the cluster — no Deployment, no controller, no webhook, no ServiceAccount of Helm's own. That was not always true. Helm 2 shipped a cluster-side component called Tiller that received a release from the client and applied it. Because Tiller usually ran with a very broad identity, being able to talk to Tiller was effectively being able to do whatever Tiller could do, and the client's own permissions barely mattered. Helm 3 deleted Tiller, and Helm 4 has not brought anything like it back. The sentence to say in an interview is therefore: **a release is created with exactly the credentials of whoever ran the command, and Helm can never do anything in a cluster that those credentials cannot do.** ## Where the identity comes from Helm loads client configuration the same way other Kubernetes clients do: - the `KUBECONFIG` file (or `--kubeconfig`), using the current context unless `--kube-context` names another; - `-n`/`--namespace`, or the `HELM_NAMESPACE` environment variable, or the namespace baked into the context, otherwise `default` — this is what lands in `.Release.Namespace`; - when Helm runs inside a pod and no kubeconfig is present, the pod's mounted ServiceAccount token, which is why a `helm upgrade` inside a Kubernetes Job authenticates as that Job's ServiceAccount; - optionally `--kube-as-user`/`--kube-as-group`, which ask the API server to impersonate someone else — and impersonation itself is a permission the caller must already hold. None of these grant anything. They only select which existing identity is used. ## What this changes in practice **Your access is the blast radius.** A CI runner that holds a broad cluster credential so that `helm upgrade` "just works" is a runner that can do everything that credential can do, from any job, chart or not. Reducing what a Helm pipeline can break is entirely a matter of narrowing the identity it runs as; there is no Helm-side switch for it. **Helm does not pre-flight your permissions.** It renders, sorts the manifests into its install order, and applies them one at a time. If the identity may create a Deployment but not a cluster-scoped object, the Deployment is created and the run then fails partway through, leaving the release recorded as `failed` with some objects present and some missing. Rerunning after the permission is granted is the normal fix, but the intermediate state is real and you should expect it. **The release record is written by you too.** With the default storage driver, each revision is a Secret named `sh.helm.release.v1.<name>.v<rev>` in the release namespace, created by the same identity as the rest of the release. Two consequences follow: an identity that cannot write Secrets in that namespace cannot install anything there at all, however permissive it is about Deployments; and anyone who can read Secrets in that namespace can read every stored revision, including the values that produced it. `HELM_DRIVER` can move that storage to ConfigMaps, to memory, or to an external SQL database, but the write is still the caller's. **Rendering is offline.** `helm template` does not contact a cluster at all: it renders with default capabilities and produces YAML on stdout. That makes it the safe way to look at a chart you were handed before any credential is involved — and the reason a render succeeding tells you nothing about whether the install will be permitted. ## A worked example A team ships a geospatial tile server as a chart that a GitOps controller pins elsewhere, but during development they install it by hand: ``` helm upgrade --install tiles ./tile-server -n geo-staging ``` Everything the chart contains — Deployment, Service, ConfigMap — plus the Secret `sh.helm.release.v1.tiles.v1` is created as the engineer's own user. Move the same command into a Job in the cluster and every one of those writes is attributed to that Job's ServiceAccount instead. Nothing about the chart changed; the identity did, and that is the only thing that decides whether the install is allowed.

  • Where does Helm get the namespace a release is installed into?
    In order: the `-n`/`--namespace` flag, the `HELM_NAMESPACE` environment variable, the namespace set on the current kubeconfig context, then `default`. Whatever wins becomes `.Release.Namespace` inside templates and the namespace the release record Secret is written to. Helm does not create that namespace unless you pass `--create-namespace`, and because namespaces are cluster-scoped, doing so is a cluster-scoped write that a purely namespace-scoped identity cannot make.
  • If Helm applies as me, why did my install still fail halfway through with a permissions error?
    Helm performs no authorization pre-flight. It renders the chart, sorts the manifests into its install order and writes them one by one, so the first object the API server refuses is the first one you hear about — after everything ahead of it was already created. The release is then recorded as failed with partial resources in place. On install, `--rollback-on-failure` (the flag `--atomic` is now a deprecated alias for it) makes Helm clean that up instead of leaving it.
  • What was Tiller, and why does its removal matter for permissions?
    Tiller was Helm 2's in-cluster server: the client sent it a release and Tiller applied the manifests under its own ServiceAccount, typically a very broad one. Anyone who could reach Tiller inherited its reach, so a user's own Kubernetes permissions were largely irrelevant. Helm 3 removed it, making the caller's identity the only one involved, and Helm 4 kept that design. Least privilege for Helm is now purely a question of who runs the command.

Helm is closer to a stamping press than to a concierge: it prints the paperwork and then hands over your badge at the door. It has no badge of its own to lend you.

saying these in an interview costs you the question

  • Says a Tiller-like server applies the manifests
  • Thinks Helm has its own cluster-admin ServiceAccount
  • Believes helm install can exceed the caller's own access
  • Assumes a controller writes the release record
  • Thinks helm template needs cluster credentials
  • Assumes Helm checks permissions before applying anything

context

open as a page

What does b64enc in a Helm chart's Secret template protect, and what does it not?

level: juniorimportance: must knowfreq 74%

basics

~20 s

Nothing confidential. b64enc is base64 encoding, needed only because a Kubernetes Secret's data field must hold base64 text. It is reversible by anyone, and the same plaintext still sits in the values file and in the stored release record.

open as a page

Why does a Helm chart write {{ .Values.image.tag | quote }} instead of the bare value?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A values entry is caller input pasted into YAML before anything parses it. Unquoted, its characters are read as YAML syntax: 1.10 becomes 1.1, off becomes a boolean, a newline can add fields. quote forces one string scalar.

open as a page

Before installing a third-party Helm chart, how do you see every object it will create?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Render it before you trust it: helm template, run with the values you will actually install with, prints every manifest the chart applies and touches no cluster. Add --include-crds — the chart's crds/ directory is excluded by default.

open as a page

Does helm install verify a chart's provenance by default, and how do you turn it on?

level: middleimportance: must knowfreq 52%

basics

~20 s

No. Helm checks a chart's OpenPGP signature only when you pass --verify, and only if the publisher signed the chart with helm package --sign so a .prov file exists, and the public key sits in the keyring you point --keyring at.

open as a page

Where does a password passed to helm upgrade --set end up besides the rendered Secret?

level: middleimportance: must knowfreq 65%

basics

~20 s

In the release record. Helm stores each revision as a Secret named sh.helm.release.v1.<release>.v<revision> holding both the supplied values and the rendered manifest, so helm get values and helm get manifest print the password back, for every retained revision.

open as a page

What must a Helm deploy identity be able to write beyond the objects its chart renders?

level: middleimportance: should knowfreq 55%

basics

~10 s

The release records. With the default storage driver Helm writes one Secret per revision, sh.helm.release.v1.<name>.v<rev>, in the release namespace, and must also read, list and delete them for upgrades, history and --history-max pruning.

open as a page

What can a caller's values string do when a Helm chart passes it through tpl?

level: middleimportance: should knowfreq 42%

basics

~20 s

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

open as a page

Which parts of a third-party Helm chart run code you never asked for?

level: middleimportance: should knowfreq 50%

basics

~20 s

Manifests annotated helm.sh/hook — usually Jobs — run at install and upgrade time, before your workloads exist. So do initContainers and sidecars inside otherwise ordinary pod specs, and the pods helm test starts. All of them execute images the chart chose.

open as a page

A Helm chart contains a CRD and a ClusterRole, but the pipeline identity is namespace-scoped — how do you ship it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Split the install by scope. Have a privileged, rarely-used path install the cluster-scoped parts once — the CRDs from crds/, then the cluster-scoped objects — and let the pipeline install the rest with --skip-crds and the chart's cluster-scoped resources switched off in values.

open as a page

helm install --verify fails on a chart that installs fine without it. How do you diagnose it?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Split it into three causes: no .prov served beside the .tgz, a keyring that lacks the signer's public key or is in a format Helm cannot read, or a digest mismatch because the tarball was re-packaged after signing. Reproduce locally with helm pull then helm verify.

open as a page

Why does a Helm chart that generates a password with randAlphaNum break on upgrade?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Because every helm upgrade re-renders the template from scratch and randAlphaNum returns a new string each time. The Secret is overwritten with a fresh password while the application, or whatever trusts the old one, keeps using it.

open as a page

Whose credentials does Helm's lookup function use, and when does it return nothing?

level: seniorimportance: should knowfreq 38%

basics

~20 s

lookup reads the live cluster from the Helm client, using the kubeconfig of whoever ran the command - there is no in-cluster component and no separate identity. It returns an empty value during helm template and a client-side dry run.

open as a page

How do you find every container image a third-party Helm chart will pull, and pin it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Enumerate from the render, not the values file: helm template with your real values shows every image line, including initContainers and hook Jobs. Pinning works only where a template exposes that reference as a value.

open as a page

Every deploy pipeline in your estate runs helm upgrade with a cluster-admin credential. How would you cut that back?

level: principalimportance: should knowfreq 36%

basics

~20 s

Measure what the charts actually write, then tier the install paths: per-namespace identities for application charts, and a rare, reviewed privileged path for the cluster-scoped ones. Helm offers no least-privilege switch — the identity is the only control.

open as a page

How would you make Helm chart provenance verification actually enforced across an estate installing from an internal chart repository?

level: principalimportance: should knowfreq 27%

basics

~20 s

Verification is a client-side, opt-in flag, so enforcement means designing a single ingress path that verifies and re-signs charts, curating the keyring as a governed artifact, and accepting that a flag every install site must remember is not a control.

open as a page

Would you let Helm charts render Secret objects from values across a 12-namespace fleet?

level: principalimportance: should knowfreq 38%

basics

~20 s

For long-lived shared credentials, no: charts should accept the name of a Secret something else owns, so nothing sensitive reaches the values or the release record. Keep the self-contained form only for disposable environments, and rotate before migrating.

open as a page

What does helm package --sign write beside the .tgz, and what is inside that file?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

It writes <chart>-<version>.tgz.prov: a clear-signed OpenPGP document whose readable body is the chart's metadata plus a files: block mapping the tarball name to its SHA-256, with the signature armoured underneath.

open as a page

Your Helm chart splices a values string into a container's sh -c command. What is the risk?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

The value crosses two parsers. quote makes it one safe YAML scalar, but that scalar is handed to a shell inside the container, where its metacharacters run as commands. Use an args list with no shell instead.

open as a page

How would you make review of third-party Helm chart upgrades repeatable across teams?

level: principalimportance: nice to knowfreq 32%

basics

~20 s

Make the review artefact a rendered diff, not a chart. Pin exact chart versions, render the old and new version with the same production values, and have a human read the delta — new hooks, new cluster-scoped objects, changed images.

open as a page