How to Fix 'Image Pull Secret Not Mounted' in Kubernetes Pod Specs for Harbor Registry
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: The pod spec is missing
imagePullSecrets, or the referenced secret doesn't exist/is in the wrong namespace, so Kubernetes cannot authenticate to Harbor and the pod crashes withErrImagePullorImagePullBackOff. - How to fix it: Create a
kubernetes.io/dockerconfigjsonsecret in the pod's namespace with valid Harbor credentials, then reference it underspec.imagePullSecretsin the pod/deployment spec. - Fast path: Use our Client-Side Sandbox above to auto-refactor this — paste your failing YAML and get corrected output without leaking credentials.
The Incident (What Does the Error Mean?)
You'll see one or both of these in kubectl describe pod <pod-name>:
Failed to pull image "harbor.internal.corp/project/app:1.4.2":
rpc error: code = Unknown desc = failed to pull and unpack image
"harbor.internal.corp/project/app:1.4.2": failed to resolve reference
"harbor.internal.corp/project/app:1.4.2": unexpected status code 401 Unauthorized
Warning Failed 3m kubelet Error: ErrImagePull
Warning Failed 2m kubelet Error: ImagePullBackOff
Kubernetes attempted to pull from a private Harbor registry without presenting credentials. Harbor returned 401. The kubelet enters exponential backoff. The pod never starts. Any Deployment rollout is dead in the water.
The Attack Vector / Blast Radius
This isn't just an ops inconvenience — the absence of a properly scoped pull secret is a credential hygiene failure with real blast radius:
- Deployment outage: Every pod replica fails simultaneously. If this hits during a rollout, your previous ReplicaSet may also be scaling down, leaving you with zero healthy pods.
- Namespace sprawl risk: Teams often "fix" this by making the Harbor project public. A public Harbor project exposes internal images to anyone with network access — including your base images that may contain internal CA certs, hardcoded staging endpoints, or proprietary binaries.
- Secret scope creep: The other common "fix" is attaching the pull secret to the
defaultServiceAccount, which then grants every pod in the namespace implicit pull access — violating least-privilege. - Stale credential time bomb: If the secret was created once and the Harbor robot account password rotated, every namespace referencing that secret silently breaks at the next image pull — often discovered only during a node drain or pod eviction.
How to Fix It
Step 1: Create the Harbor Pull Secret
Use a Harbor robot account, not a human credential.
kubectl create secret docker-registry harbor-pull-secret \
--docker-server=harbor.internal.corp \
--docker-username='robot$ci-puller' \
--docker-password='<ROBOT_ACCOUNT_TOKEN>' \
[email protected] \
--namespace=your-app-namespace
Basic Fix — Reference the Secret in the Pod Spec
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
namespace: your-app-namespace
spec:
template:
spec:
containers:
- name: app
image: harbor.internal.corp/project/app:1.4.2
+ imagePullSecrets:
+ - name: harbor-pull-secret
Enterprise Best Practice — Patch the ServiceAccount (Scoped, Not Global)
Instead of editing every Deployment, bind the pull secret to a dedicated ServiceAccount for this workload only. Do not use default.
-# No ServiceAccount defined — falls back to 'default' with no pull secret
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: app-puller-sa
+ namespace: your-app-namespace
+imagePullSecrets:
+ - name: harbor-pull-secret
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
namespace: your-app-namespace
spec:
template:
spec:
+ serviceAccountName: app-puller-sa
containers:
- name: app
image: harbor.internal.corp/project/app:1.4.2
- # imagePullSecrets missing entirely
Why this matters: The pull secret is now scoped to one ServiceAccount. Other pods in the namespace using default SA cannot pull from Harbor — enforcing least-privilege at the workload level.
Verify It's Working
# Confirm secret exists in correct namespace
kubectl get secret harbor-pull-secret -n your-app-namespace
# Confirm pod spec has the reference
kubectl get pod <pod-name> -n your-app-namespace -o jsonpath='{.spec.imagePullSecrets}'
# Force a re-pull
kubectl rollout restart deployment/app-deployment -n your-app-namespace
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
OPA / Gatekeeper Policy — Block Deployments Without imagePullSecrets
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
startswith(container.image, "harbor.internal.corp/")
count(input.request.object.spec.imagePullSecrets) == 0
msg := sprintf("Pod '%v' pulls from Harbor but has no imagePullSecrets defined.",
[input.request.object.metadata.name])
}
Checkov — Static Analysis in Pipeline
# .checkov.yaml
check:
- CKV_K8S_27 # Do not admit containers that wish to share the host IPC namespace
# Add custom check for imagePullSecrets via checkov custom policies
Run in CI:
checkov -d ./k8s-manifests --framework kubernetes --check CKV_K8S_27
Helm — Enforce via values.schema.json
{
"properties": {
"imagePullSecrets": {
"type": "array",
"minItems": 1,
"description": "At least one imagePullSecret is required for Harbor registry access."
}
},
"required": ["imagePullSecrets"]
}
Harbor Robot Account Rotation — Automate It
- Use External Secrets Operator with your vault (Vault, AWS Secrets Manager) to auto-rotate the
harbor-pull-secretbefore expiry. - Set Harbor robot account expiry to 90 days max. Configure ESO to sync 7 days before expiry.
- Never use a human Harbor account as a pull credential — robot accounts are auditable, revocable, and project-scoped.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: harbor-pull-secret-sync
namespace: your-app-namespace
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: harbor-pull-secret
template:
type: kubernetes.io/dockerconfigjson
data:
- secretKey: .dockerconfigjson
remoteRef:
key: secret/harbor/robot-ci-puller
property: dockerconfigjson