skip to content

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