Pods in a namespace fail with `ErrImagePull` from a private registry. Explain how Kubernetes authenticates image pulls and how you wire credentials with `imagePullSecrets`.
answer
- kubelet pulls, not the app
- type dockerconfigjson, same namespace as the Pod
- ServiceAccount imagePullSecrets merge at Pod creation
- --docker-server must match the image host
- cached image + IfNotPresent = no auth check (AlwaysPullImages)
basics
~20 sThe kubelet pulls images, so it needs credentials: a kubernetes.io/dockerconfigjson Secret in the same namespace as the Pod, referenced either from spec.imagePullSecrets or from the Pod's ServiceAccount. The registry host in the Secret must match the host in the image reference exactly.
solid answer
~50 sImage pulls are done by the **kubelet on the node**, not by the API server or the workload, so the credentials have to reach that node's pull path. The wiring: 1. Create a `kubernetes.io/dockerconfigjson` Secret — `kubectl create secret docker-registry regcred --docker-server=... --docker-username=... --docker-password=...`. 2. Reference it. Either per Pod (`spec.imagePullSecrets: [{name: regcred}]`, also settable in a Deployment's Pod template) or, better at scale, attach it to the ServiceAccount the Pods use — the ServiceAccount controller merges its `imagePullSecrets` into every Pod that uses it, so you set it once per namespace. The usual failure causes: the Secret is in a different namespace (Secrets are namespaced; you must copy it into each one), the `--docker-server` value does not match the registry host in the image reference, the credential expired (many cloud registries issue short-lived tokens and need a refresher), or the Secret is `Opaque` instead of `kubernetes.io/dockerconfigjson`. Alternatives: node-level credential providers (ECR/GCR/ACR plugins) that mint credentials on the node, removing per-namespace Secrets entirely.
code
bash · 12 lineskubectl create secret docker-registry regcred \
--namespace team-a \
--docker-server=registry.example.com \
--docker-username=ci-puller \
--docker-password="$REGISTRY_TOKEN"
kubectl patch serviceaccount default -n team-a \
-p '{"imagePullSecrets":[{"name":"regcred"}]}'
# verify what the kubelet will use
kubectl get secret regcred -n team-a \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -dgo deeper
Know the recipe: kubectl create secret docker-registry in the Pod's namespace, then list it under spec.imagePullSecrets.
Add the ServiceAccount attachment as the per-namespace scalable form, the type requirement, and the namespace and host-matching traps.
Diagnose from events — distinguish 401 (rejected credential) from anonymous-pull 403 (nothing matched) — and cover token expiry, credential replication and the image-cache bypass.
Design the credential model: node credential providers or workload identity instead of long-lived pull Secrets, namespace provisioning that supplies them, and AlwaysPullImages for multi-tenant isolation with its pull-traffic cost.
## Who actually pulls The kubelet on the scheduled node hands the pull to the container runtime (containerd/CRI-O). Neither the API server nor the Pod's own service account is involved in registry authentication. That single fact explains most of the confusion: the credential does not need to be *readable by the app*, it needs to be *findable by the kubelet* through the Pod spec. ## Credential sources, in order The kubelet assembles candidate credentials from: 1. **`imagePullSecrets` on the Pod spec** — a list of Secret names in the Pod's own namespace. 2. **`imagePullSecrets` on the Pod's ServiceAccount** — the ServiceAccount admission controller merges these into the Pod at creation time, so the resulting Pod spec looks identical to case 1. 3. **Node-level credentials** — a credential-provider plugin configured on the kubelet (`--image-credential-provider-config`), which is how EKS/GKE/AKS give nodes access to ECR/Artifact Registry/ACR without any Secret, plus any static config file baked into the node image. The runtime tries the credentials matching the registry host of the image reference. If none matches, it attempts an anonymous pull, which yields a 401/403 surfaced as `ErrImagePull` and then `ImagePullBackOff`. ## The Secret It must be `type: kubernetes.io/dockerconfigjson`, with the payload under the key `.dockerconfigjson` — a Docker config document mapping registry hosts to auths. Build it with `kubectl create secret docker-registry`; hand-writing it goes wrong because the `auth` field is itself base64 of `username:password`, inside a JSON document that is itself base64-encoded in `data`. An `Opaque` Secret containing exactly the same JSON does **not** work — the kubelet selects by type. ## The namespace trap Secrets are namespaced and `imagePullSecrets` can only name a Secret in the Pod's own namespace. There is no cluster-scoped pull secret. So every namespace that runs private images needs its own copy. Teams solve this by: - templating it into each namespace from the platform's namespace-provisioning pipeline; - a replicator controller (e.g. reflector-style tooling) that copies a source Secret into annotated namespaces; - patching the `default` ServiceAccount in each new namespace as part of provisioning; - or eliminating it entirely with node credential providers or workload identity. ## The host-matching trap The registry host in the Secret must match the host in the image reference. `--docker-server=https://index.docker.io/v1/` is the special value for Docker Hub; for anything else it is the bare host (`registry.example.com`, optionally with a port). An image written as `registry.example.com/team/app:1.0` will not match a credential stored under `https://registry.example.com/`. Mismatch here produces the same `ErrImagePull` as a wrong password, which is why the diagnosis is confusing. ## Attaching to the ServiceAccount The scalable form is one patch per namespace: ``` kubectl patch serviceaccount default -n team-a \ -p '{"imagePullSecrets":[{"name":"regcred"}]}' ``` Every Pod created afterwards with that ServiceAccount gets the reference merged in. Two caveats: it applies at Pod-creation time, so existing Pods are unaffected until they are recreated; and it applies per ServiceAccount, so a namespace using several needs each patched. ## Diagnosing a failure Read the events, not the Pod status: `kubectl describe pod` shows the runtime's actual message. `401 Unauthorized` means credentials were sent and rejected — expired token or wrong password. `no basic auth credentials` or an anonymous-pull 403 means nothing matched — wrong namespace, wrong type, missing reference or a host mismatch. `manifest unknown` means auth worked and the tag does not exist. Then verify what the kubelet would use: ``` kubectl get secret regcred -n team-a -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq . kubectl get sa default -n team-a -o jsonpath='{.imagePullSecrets}' ``` ## Two security notes worth raising First, once an image is present in a node's image cache, `imagePullPolicy: IfNotPresent` lets **any** Pod on that node run it without proving it can authenticate — a cross-tenant leak of private images. The `AlwaysPullImages` admission plugin forces a pull (and therefore an authorization check) per Pod; it is the standard hardening for multi-tenant clusters, at the cost of pull traffic. Second, a pull Secret is a registry credential readable by anyone who can read Secrets in that namespace. Prefer short-lived, pull-only credentials, or node credential providers where the node's own cloud identity is exchanged for a short-lived token and no Secret exists at all.
- Why can't you keep one pull Secret in `kube-system` and reference it from every namespace?`imagePullSecrets` resolves names only within the Pod's own namespace — there is no cross-namespace or cluster-scoped form. You either replicate the Secret into each namespace as part of namespace provisioning, run a replicator controller, or drop Secrets entirely in favour of kubelet credential providers that mint short-lived registry tokens from the node's cloud identity.
- A Pod on one node runs a private image fine while the same Pod on another node gets ErrImagePull. What explains that?The working node already had the image in its local cache and `imagePullPolicy: IfNotPresent` skipped the pull, so no credentials were needed; the other node had to pull and had none that matched. It is also a security point — cached images are usable by any Pod on that node, which is what the `AlwaysPullImages` admission plugin exists to prevent.
saying these in an interview costs you the question
- Thinking the Pod's ServiceAccount token authenticates to the registry.
- Putting the pull Secret in a different namespace from the Pod and expecting it to resolve.
- Creating the Secret as `Opaque` with docker JSON inside.
- Registry host mismatch (`https://registry/` vs `registry`) and blaming the password.
- Assuming a successful pull on one node proves the credentials are correct everywhere.