RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
Containerd CRI Sandbox Pull Secret Fixer
02. Root Cause Vector

Containerd CRI fails to pull sandbox images when imagePullSecrets reference a non-existent, mismatched, or incorrectly scoped registry cr...

03. Rapid Prescription

Sort lock acquisition order deterministically, or configure explicit session timeouts.

Zero-Trust Client IDE/ Security

Containerd CRI Sandbox Pull Secret Fixer

Containerd CRI fails to pull sandbox images when imagePullSecrets reference a non-existent, mismatched, or incorrectly scoped registry credential secret, halting pod scheduling entirely.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(35L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: v1
kind: Pod
metadata:
  name: pci-secure-zone-api
  namespace: legacy-apps
spec:
  imagePullSecrets:
  - name: prod-registry-secret  # TODO: This was rotated last sprint, but the old one still works for now.
  # - name: legacy-registry-secret  # Commented out after migration, but kept for rollback.
  containers:
  - name: api-container
    image: my-registry.internal/legacy-api:v2.7.1
    env:
    - name: AWS_ACCESS_KEY_ID
      value: AKIAIOSFODNN7EXAMPLE
    - name: AWS_SECRET_ACCESS_KEY
      value: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    - name: STRIPE_API_KEY
      value: sk_live_4eC39HqLyjWDarjtT1zdp7dc
    - name: DB_CONNECTION_STRING
      value: "postgresql://dbuser:SuperSecretP@ssw0rd!@pci-db.internal:5432/legacy_db?sslmode=require"
    securityContext:
      privileged: false
  # VULNERABILITY: The pod spec's imagePullSecrets references a secret in the 'legacy-apps' namespace, but the secret's data is keyed for a different registry domain due to a copy-paste from the staging config.
---
apiVersion: v1
kind: Secret
metadata:
  name: prod-registry-secret
  namespace: legacy-apps  # This matches the pod namespace, but the .dockerconfigjson is misconfigured.
data:
  .dockerconfigjson: ewogICJhdXRocyI6IHsKICAgICJteS1yZWdpc3RyeS5pbnRlcm5hbC1zdGFnaW5nIjogewogICAgICAiYXV0aCI6ICJkMmx1WkdsbmNpTmpjbWxpT2lKMWNtNDZZWEJ3YjNKMExYTnphVzVuTG1oMGJXd2lPbnNpYVdRaU9pSXhNREkxTURJeU1qSXpNak0yTURBd01EQWlmUSIKICAgIH0KICB9Cn0=  # Decodes to auth for 'my-registry.internal-staging', not 'my-registry.internal'.
type: kubernetes.io/dockerconfigjson
# TODO: The base64 was generated from the staging kubeconfig during the emergency patch last quarter. Fix after PCI audit.
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing Containerd CRI 'Failed to Create Pod Sandbox' Caused by Image Pull Secret Mismatch

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins


TL;DR

  • What broke: Containerd's CRI shim cannot pull the pause/sandbox image (or the app image) because the imagePullSecret referenced in the Pod spec either doesn't exist in the correct namespace, has a malformed .dockerconfigjson, or references the wrong registry host.
  • How to fix it: Verify the Secret exists in the same namespace as the Pod, decode and validate the .dockerconfigjson payload, and ensure the registry hostname in the secret matches the image reference exactly.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor your failing Pod spec and Secret manifest — paste both, get corrected YAML output instantly.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

You'll see this in kubectl describe pod <pod-name> or in the kubelet journal:

Warning  Failed     3s    kubelet  Failed to create pod sandbox:
  rpc error: code = Unknown
  desc = failed to pull image "registry.internal.corp/pause:3.9":
  failed to pull and unpack image "registry.internal.corp/pause:3.9":
  failed to resolve reference "registry.internal.corp/pause:3.9":
  unexpected status code 401 Unauthorized

or:

Warning  Failed  2s  kubelet  Failed to create pod sandbox:
  rpc error: code = Unknown
  desc = failed to get sandbox image "registry.internal.corp/pause:3.9":
  error getting credentials - err: docker-credential-ecr-login:
  resolving credentials: secret "regcred" not found

Immediate consequence: The Pod never leaves ContainerCreating. No init containers run. No app containers start. If this is a Deployment rollout, the new ReplicaSet stalls, and depending on your maxUnavailable setting, the old pods may have already been terminated — you're now at zero healthy replicas.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This failure mode has two distinct blast radii:

1. Operational (most common): A namespace-scoped Secret was created in default but the workload runs in production. Containerd's CRI plugin calls the kubelet credential provider, which calls the Kubernetes API for the secret — it's not found, auth fails, sandbox never initializes. Every pod in that Deployment fails simultaneously if the rollout is already in progress.

2. Security regression (the dangerous one): Teams under pressure "fix" this by switching private images to public mirrors or by granting imagePullSecrets with a service account that has wildcard registry access. A misconfigured dockerconfigjson with "auths": {"https://index.docker.io/v1/": {}} (empty credentials) silently falls back to unauthenticated pulls on some runtimes — meaning your image supply chain is now unverified. Worse, if the secret contains credentials for *.amazonaws.com ECR and is bound to a service account with overly broad RBAC, a compromised pod can enumerate and pull any image in that registry account.

Cascading risk: HPA-triggered scale-out events will repeatedly attempt and fail sandbox creation, generating thundering-herd kubelet log spam and elevated API server load from credential resolution retries.


How to Fix It

Step 1 — Confirm the secret exists in the right namespace

kubectl get secret regcred -n production
# If NotFound — that's your problem.

# Check what namespace it actually lives in:
kubectl get secrets --all-namespaces | grep regcred

Step 2 — Decode and validate the dockerconfigjson

kubectl get secret regcred -n production \
  -o jsonpath='{.data.'\''.dockerconfigjson'\''}'  | base64 -d | jq .

You must see the exact registry hostname that matches your image reference:

{
  "auths": {
    "registry.internal.corp": {
      "username": "svc-k8s-pull",
      "password": "<token>",
      "auth": "<base64(user:pass)>"
    }
  }
}

If your image is registry.internal.corp/pause:3.9 but the secret has https://registry.internal.corp/v2/ — that mismatch is the bug. Containerd does exact-prefix matching on registry hosts.

Basic Fix — Recreate the secret with the correct hostname

kubectl create secret docker-registry regcred \
  --docker-server=registry.internal.corp \
  --docker-username=svc-k8s-pull \
  --docker-password="$(cat /vault/secrets/registry-token)" \
  -n production \
  --dry-run=client -o yaml | kubectl apply -f -

Enterprise Best Practice — Attach the secret to the namespace's default ServiceAccount

Instead of adding imagePullSecrets to every Pod spec (which gets missed), patch it onto the ServiceAccount so all pods in the namespace inherit it automatically:

 apiVersion: v1
 kind: ServiceAccount
 metadata:
   name: default
   namespace: production
+imagePullSecrets:
+- name: regcred

Pod spec correction:

 apiVersion: v1
 kind: Pod
 metadata:
   name: app-worker
   namespace: production
 spec:
+  imagePullSecrets:
+  - name: regcred
   containers:
   - name: app
     image: registry.internal.corp/app:v2.1.0
-  # imagePullSecrets was missing or pointed to wrong secret name

Secret manifest (correct form):

 apiVersion: v1
 kind: Secret
 metadata:
-  name: reg-cred          # wrong name — Pod spec referenced 'regcred'
+  name: regcred
-  namespace: default      # wrong namespace
+  namespace: production
 type: kubernetes.io/dockerconfigjson
 data:
-  .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL3JlZ2lzdHJ5LmludGVybmFsLmNvcnAvdjIvIjp7fX19
-  # ^ decoded: registry URL has https:// prefix + /v2/ suffix — containerd won't match this
+  .dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5pbnRlcm5hbC5jb3JwIjp7InVzZXJuYW1lIjoic3ZjLWs4cy1wdWxsIiwicGFzc3dvcmQiOiJ0b2tlbiIsImF1dGgiOiJiYXNlNjRlbmNvZGVkIn19fQ==
+  # ^ decoded: bare hostname 'registry.internal.corp' — correct for containerd

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. OPA/Gatekeeper policy — enforce imagePullSecrets presence:

package k8srequiredimagepullsecrets

violation[{"msg": msg}] {
  input.review.object.kind == "Pod"
  count(input.review.object.spec.imagePullSecrets) == 0
  not input.review.object.spec.serviceAccountName  # no SA-level secret either
  msg := sprintf("Pod '%v' in namespace '%v' has no imagePullSecrets",
    [input.review.object.metadata.name, input.review.object.metadata.namespace])
}

2. Checkov scan in your pipeline:

checkov -f pod.yaml --check CKV_K8S_35  # Ensures imagePullSecrets is set

3. Helm chart values.yaml guard:

# In your chart's _helpers.tpl, fail fast at render time:
{{- if and .Values.image.registry (not .Values.imagePullSecrets) }}
  {{- fail "imagePullSecrets must be set when using a private registry" }}
{{- end }}

4. Namespace bootstrap automation: Use a Namespace provisioning controller (e.g., Hierarchical Namespace Controller or a simple operator) that automatically copies the regcred secret into every new namespace and patches the default ServiceAccount. Never rely on humans remembering to do this during incident-driven namespace creation.

5. Validate secret format in CI before deploy:

# In your GitHub Actions / GitLab CI pre-deploy step:
kubectl create secret docker-registry regcred \
  --docker-server="$REGISTRY_HOST" \
  --docker-username="$REGISTRY_USER" \
  --docker-password="$REGISTRY_PASS" \
  --dry-run=client -o json | \
  jq -r '.data[".dockerconfigjson"]' | \
  base64 -d | jq -e '.auths | keys[] | test("^[a-z0-9.-]+$")' \
  || (echo "FATAL: Registry hostname has invalid format for containerd" && exit 1)

This regex rejects https:// prefixes and trailing slashes before the secret ever reaches the cluster.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does containerd require a bare hostname in dockerconfigjson while Docker Desktop accepts https:// prefixed URLs?
Containerd's CRI plugin performs a direct string prefix match between the image reference hostname and the keys in the dockerconfigjson 'auths' map. Docker Engine normalizes URLs internally, but containerd does not; if your image is 'registry.corp/app:v1' and your secret key is 'https://registry.corp/v2/', the match fails, leading to a 401 error. The root cause is a mismatch in credential key formatting—always use the bare hostname 'registry.corp' as the key. This is a foundational DevSecOps principle: precise credential mapping is non-negotiable for zero-trust runtime security.
Q2.The secret exists and the hostname matches, but I'm still getting 'failed to create pod sandbox' — what else could cause this?
Manually debugging this exposes critical vulnerabilities: (1) The sandbox/pause image may be on a separate registry requiring auth—check 'sandbox_image' in /etc/containerd/config.toml. (2) Containerd may not have reloaded credentials, risking node-wide outages if restarted without proper draining. (3) Expired auth tokens in secrets can silently break deployments, creating a blast radius across clusters. Manual fixes are error-prone and introduce security gaps. Instead, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that validates credentials, patches configs, and prevents unauthorized access without human intervention.
Q3.How do I automatically sync imagePullSecrets to all namespaces without manual intervention?
Using third-party controllers like 'kubernetes-replicator' or Reflector introduces compliance nightmares at scale: they lack audit trails, can over-propagate secrets to untrusted namespaces, and create drift in large mono-repos. Manual synchronization across hundreds of namespaces is a ticking security risk. For enterprise-grade, on-prem-capable remediation, upgrade to the StackEngine CI/CD Automator (Team Edition). It integrates directly into your CI/CD pipeline to automate secret synchronization with policy enforcement, ensuring compliance and eliminating 'secret exists in default but not in production' failures across your entire infrastructure.