Initializing Enclave...

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 with ErrImagePull or ImagePullBackOff.
  • How to fix it: Create a kubernetes.io/dockerconfigjson secret in the pod's namespace with valid Harbor credentials, then reference it under spec.imagePullSecrets in 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 default ServiceAccount, 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-secret before 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

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →