Kubernetes Course
Image Pull Secrets
Public images pull without credentials. The moment you move to a private registry — your company's ECR, GCR, or a self-hosted Harbor — every node that needs to pull an image must authenticate. This lesson covers how the kubelet handles registry authentication, how to create and attach image pull secrets, and how to manage them at scale across many namespaces.
How Image Pulling Works
When the kubelet on a node is instructed to run a Pod, it checks whether the container image is already present locally. If not, it contacts the container registry to pull it. For public registries this works anonymously. For private registries the kubelet needs credentials.
Credentials are supplied via a Secret of type kubernetes.io/dockerconfigjson — a special Secret type that stores a Docker-format credential file. The Pod spec references this Secret in its imagePullSecrets field, and the kubelet uses it when pulling.
Pull request flow for a private image
Pod to Node
imagePullSecrets
credentials to registry
image layers
starts
If no imagePullSecret is attached and the registry requires auth: ErrImagePull → ImagePullBackOff
Creating an Image Pull Secret
The scenario: Your team's application images live in a private AWS ECR registry. You need to create an image pull secret in the payments namespace so the kubelet can authenticate when pulling images.
# Method 1: kubectl create secret (simplest)
kubectl create secret docker-registry ecr-pull-secret \
--namespace=payments \
--docker-server=123456789012.dkr.ecr.us-east-1.amazonaws.com \
--docker-username=AWS \
--docker-password=$(aws ecr get-login-password --region us-east-1) \
--docker-email=unused@example.com
# --docker-server: the registry hostname
# --docker-username: for ECR this is always the literal string "AWS"
# --docker-password: ECR uses short-lived tokens (12h) from get-login-password
# --docker-email: required field but ignored by ECR
# Method 2: from a pre-existing Docker config file
# (useful if you already ran 'docker login' and have ~/.docker/config.json)
kubectl create secret generic ecr-pull-secret \
--namespace=payments \
--from-file=.dockerconfigjson=$HOME/.docker/config.json \
--type=kubernetes.io/dockerconfigjson
# What the secret contains:
kubectl get secret ecr-pull-secret -n payments -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
$ kubectl create secret docker-registry ecr-pull-secret \
--namespace=payments \
--docker-server=123456789012.dkr.ecr.us-east-1.amazonaws.com \
--docker-username=AWS \
--docker-password=$(aws ecr get-login-password --region us-east-1)
secret/ecr-pull-secret created
$ kubectl get secret ecr-pull-secret -n payments -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
{
"auths": {
"123456789012.dkr.ecr.us-east-1.amazonaws.com": {
"username": "AWS",
"password": "eyJwYXlsb2FkIjoiWnU...", ← short-lived ECR token
"auth": "QVdTOmV5SnBhV..." ← base64(username:password)
}
}
}What just happened?
The Secret contains the Docker credential format — The .dockerconfigjson key holds a JSON structure identical to what Docker writes to ~/.docker/config.json after a docker login. The kubelet parses this format natively — it knows to use these credentials whenever it pulls an image from the matching auths hostname.
ECR tokens expire in 12 hours — This is the biggest pain point with ECR pull secrets. The token returned by aws ecr get-login-password is valid for 12 hours. After expiry, new Pods on nodes that need to pull the image will get ImagePullBackOff. For production clusters, use the ECR Credential Helper or IRSA-based node role (see the scaling section) instead of static pull secrets.
Attaching Pull Secrets to Pods and Deployments
Once the Secret exists, you attach it to Pod specs in two ways: directly in the Pod template, or via a ServiceAccount so every Pod using that SA automatically gets the pull secret without needing to specify it in every manifest.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
namespace: payments
spec:
replicas: 3
selector:
matchLabels:
app: payment-api
template:
metadata:
labels:
app: payment-api
spec:
# Method A: specify imagePullSecrets directly in the Pod spec
imagePullSecrets:
- name: ecr-pull-secret # Must exist in the same namespace as the Pod
# Can list multiple secrets for multiple registries
- name: ghcr-pull-secret # e.g. also pulling from GitHub Container Registry
containers:
- name: payment-api
image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-api:3.0.0
# Image path must match the registry hostname in the pull secret's 'auths' block
# Method B: attach the pull secret to a ServiceAccount
# Any Pod using this SA automatically gets the imagePullSecrets — no per-Pod spec needed
apiVersion: v1
kind: ServiceAccount
metadata:
name: payment-api-sa
namespace: payments
imagePullSecrets:
- name: ecr-pull-secret # SA-level pull secret — inherited by all Pods using this SA
# This is the preferred pattern for team-wide registry access
$ kubectl apply -f payment-api-sa.yaml
serviceaccount/payment-api-sa configured
$ kubectl describe sa payment-api-sa -n payments
Name: payment-api-sa
Namespace: payments
Image pull secrets: ecr-pull-secret ← attached to SA ✓
$ kubectl apply -f deployment.yaml
deployment.apps/payment-api created
$ kubectl describe pod payment-api-7d9f4-xkp2m -n payments | grep -A3 "Pull Secrets"
Image pull secrets: ecr-pull-secret ← automatically inherited from SA ✓
$ kubectl get events -n payments --field-selector reason=Pulled
LAST SEEN TYPE REASON OBJECT MESSAGE
12s Normal Pulled pod/payment-api-7d9f4-xkp2m Successfully pulled image
"123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-api:3.0.0"What just happened?
SA-level pull secrets scale better — With Method B, every Deployment, StatefulSet, and Job in the namespace that uses payment-api-sa automatically inherits the pull secret. When you rotate the ECR credentials, you update one Secret — not every Pod template. For a namespace with 10 Deployments all pulling from the same registry, this is the right approach.
The pull secret must be in the same namespace — Kubernetes does not support cross-namespace references for imagePullSecrets. If you have 20 namespaces all needing access to the same ECR registry, you need the Secret replicated in each namespace. This is the scaling problem the next section solves.
Scaling Pull Secrets Across Namespaces
The namespace-scoped constraint quickly becomes painful: 20 namespaces × 1 ECR pull secret = 20 copies to keep synchronised. Three approaches solve this at scale, in increasing order of production-readiness.
| Approach | How it works | Best for |
|---|---|---|
| ESO ClusterSecretStore | External Secrets Operator syncs the pull secret from AWS Secrets Manager into every namespace. One source, many replicas, auto-rotated. | Clusters already using ESO. Handles rotation automatically. |
| Node IAM Role (ECR) | EKS nodes have an IAM role. Grant the node role ECR pull permissions. kubelet uses the node's instance metadata to get tokens automatically — no pull secrets at all. | EKS clusters. Zero-configuration — no Secrets to manage. |
| ECR Credential Helper | A DaemonSet that runs on every node, periodically refreshes ECR tokens, and writes them to the node's Docker credential store. Transparent to Pods. | Self-managed clusters on AWS. Handles the 12-hour token expiry problem. |
The scenario: You're using ESO and need the ECR pull secret available in every application namespace. A ClusterSecretStore connects once to the backend; ExternalSecret objects in each namespace reference it.
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore # ClusterSecretStore: cluster-scoped, usable from any namespace
metadata:
name: aws-secrets-manager-cluster # No namespace — this is a cluster-wide resource
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-sa
namespace: external-secrets # The ESO controller's SA namespace
---
# ExternalSecret in the payments namespace
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: ecr-pull-secret-sync
namespace: payments
spec:
refreshInterval: 10h # ECR tokens last 12h — refresh at 10h to stay ahead
secretStoreRef:
name: aws-secrets-manager-cluster
kind: ClusterSecretStore # References the cluster-scoped store
target:
name: ecr-pull-secret
template:
type: kubernetes.io/dockerconfigjson # Set the correct Secret type
data:
.dockerconfigjson: |
{"auths":{"{{ .registry }}":{"username":"AWS","password":"{{ .token }}","auth":"{{ printf "AWS:%s" .token | b64enc }}"}}}
data:
- secretKey: registry
remoteRef:
key: production/ecr/credentials
property: registry_url
- secretKey: token
remoteRef:
key: production/ecr/credentials
property: auth_token
What just happened?
ClusterSecretStore vs SecretStore — A SecretStore is namespace-scoped: it can only be referenced by ExternalSecrets in the same namespace. A ClusterSecretStore is cluster-scoped and can be referenced from any namespace. This means you configure the AWS connection once, and every ExternalSecret in every namespace can use it — no need to create a SecretStore per namespace.
The target template sets the Secret type — When ESO creates the Kubernetes Secret, the template.type field sets it to kubernetes.io/dockerconfigjson. This is required — without it, ESO creates an Opaque Secret which the kubelet won't recognise as registry credentials. The template also lets you construct the JSON format that Docker expects from individual fields stored in Secrets Manager.
The Node IAM Role Pattern (EKS Best Practice)
On EKS, the cleanest solution for ECR access is no pull secrets at all. EKS nodes have an IAM instance role. If that role includes the AmazonEC2ContainerRegistryReadOnly policy, the kubelet automatically obtains ECR tokens via the EC2 instance metadata service. Pods pull images without any imagePullSecrets entry required.
# Option A: eksctl — attach managed policy when creating the node group
eksctl create nodegroup \
--cluster=production \
--name=app-nodes \
--node-type=m5.large \
--nodes=3 \
--attach-policy-arn=arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
# AmazonEC2ContainerRegistryReadOnly: pull (GetAuthorizationToken + BatchGetImage + GetDownloadUrlForLayer)
# Does NOT grant push — read-only as the name implies
# Option B: add to an existing node group's IAM role via AWS CLI
aws iam attach-role-policy \
--role-name eksctl-production-nodegroup-NodeInstanceRole \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
# Verify: test a pull from a node
kubectl run test-pull \
--image=123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-api:3.0.0 \
--restart=Never \
-n payments
kubectl get pod test-pull -n payments # Should reach Running without imagePullSecrets
kubectl delete pod test-pull -n payments
$ kubectl run test-pull \ --image=123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-api:3.0.0 \ --restart=Never -n payments pod/test-pull created $ kubectl get pod test-pull -n payments NAME READY STATUS RESTARTS test-pull 1/1 Running 0 ← pulled successfully with NO imagePullSecrets ✓ $ kubectl describe pod test-pull -n payments | grep "Image pull secrets" Image pull secrets:← no pull secret needed — node IAM role handles it ✓ $ kubectl get events -n payments --field-selector involvedObject.name=test-pull LAST SEEN TYPE REASON MESSAGE 5s Normal Pulled Successfully pulled image [...] in 2.1s
What just happened?
Zero Secrets to manage — The node's IAM role provides ECR access transparently. No Secrets expire, no tokens need rotating, no per-namespace copies. The ECR credential provider built into the kubelet calls GetAuthorizationToken against the EC2 metadata service, gets a fresh 12-hour token, and uses it for the pull — automatically and invisibly.
Scope consideration — The node IAM role grants pull access cluster-wide: every Pod on every node can pull from your ECR registry, regardless of namespace or ServiceAccount. For most clusters this is fine. If you need to restrict specific namespaces from accessing specific ECR repos, you need IRSA (per-ServiceAccount IAM roles) instead of a node-level policy — but this is a rare requirement.
Debugging Image Pull Failures
The two most common error states are ErrImagePull (initial failure) and ImagePullBackOff (Kubernetes backing off after repeated failures). Here is the diagnostic ladder:
# Step 1: See the exact error message
kubectl describe pod failing-pod -n payments
# Look for Events section at the bottom — the pull error appears here
# Common messages:
# "unauthorized: authentication required" → pull secret missing or invalid
# "404 not found" or "manifest unknown" → image tag doesn't exist
# "connection refused" / "no route to host" → registry unreachable (Network Policy?)
# "toomanyrequests" → Docker Hub rate limit (unauthenticated)
# Step 2: Verify the imagePullSecrets field exists on the Pod
kubectl get pod failing-pod -n payments -o jsonpath='{.spec.imagePullSecrets}'
# If empty: the pull secret wasn't specified or the SA doesn't have it
# Step 3: Verify the Secret exists and has the right type
kubectl get secret ecr-pull-secret -n payments
# NAME TYPE DATA AGE
# ecr-pull-secret kubernetes.io/dockerconfigjson 1 2d
# Type MUST be kubernetes.io/dockerconfigjson — Opaque won't work as a pull secret
# Step 4: Decode and inspect the credentials
kubectl get secret ecr-pull-secret -n payments \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq .
# Check: does 'auths' contain the registry hostname from the image path?
# Mismatch between registry in the secret and registry in the image URL → auth fails
# Step 5: Test the credentials manually (on any machine with Docker)
TOKEN=$(kubectl get secret ecr-pull-secret -n payments \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq -r '.auths[].password')
docker login 123456789012.dkr.ecr.us-east-1.amazonaws.com -u AWS -p "$TOKEN"
docker pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-api:3.0.0
$ kubectl describe pod failing-pod -n payments
[...]
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Failed 62s kubelet Failed to pull image
"123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-api:3.0.0":
unauthorized: authentication required
Warning Failed 62s kubelet Error: ErrImagePull
Warning BackOff 30s kubelet Back-off pulling image
"123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-api:3.0.0"
Warning Failed 30s kubelet Error: ImagePullBackOff
$ kubectl get secret ecr-pull-secret -n payments \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq .auths
{
"123456789012.dkr.ecr.us-east-1.amazonaws.com": {
"username": "AWS",
"password": "eyJwYXlsb2FkIjoiWnU..." ← token present but may be expired
}
}
# Root cause: ECR token was created 13h ago — expired after 12h
# Fix: recreate the secret with a fresh token from aws ecr get-login-passwordTeacher's Note: The default ServiceAccount and pull secrets
A common surprise: you attach a pull secret to a custom ServiceAccount, but the Deployment doesn't specify serviceAccountName — so it uses the default SA, which has no pull secret. The Pod gets ImagePullBackOff even though the Secret and the SA both exist and are correctly configured.
Always pair the pull secret attachment with an explicit serviceAccountName in your Pod spec. You can also attach pull secrets to the default SA if you want every Pod in the namespace to inherit them — but be intentional about this rather than assuming it happens automatically.
For production EKS clusters: skip pull secrets entirely and use the node IAM role or IRSA. Static pull secrets with expiring tokens are an operational burden that the platform can eliminate. The effort to set up node IAM role ECR access is 10 minutes; managing expiring pull secrets across 20 namespaces is a recurring on-call incident.
Practice Questions
1. What type must a Secret have for the kubelet to use it as registry credentials for pulling images?
2. In a Pod spec (or ServiceAccount), what field lists the Secrets the kubelet should use for registry authentication when pulling images?
3. A Pod's event log shows it cannot pull an image from a private registry. What error state appears first before it transitions to ImagePullBackOff?
Quiz
1. Pods in your cluster have been pulling from ECR successfully for two days. Overnight, new Pods start getting ImagePullBackOff. The pull secret exists and previously worked. What is the most likely cause?
2. Your namespace has 15 Deployments all pulling from the same private registry. You don't want to add imagePullSecrets to every Pod template. What is the correct approach?
3. You're running EKS and want Pods to pull from ECR without any pull secret management. What is the recommended approach?
Up Next · Lesson 44
Auditing and Compliance
The Kubernetes API server can log every request made to the cluster — who did what, when, and to which resource. This lesson covers audit policy design, log shipping, and the patterns used to satisfy SOC2 and PCI DSS audit requirements.