RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
Helm Registry Manifest Error Locator
02. Root Cause Vector

Helm chart deployment fails with 'manifest not found' because the image tag doesn't exist in the private GitLab registry or the pull secr...

03. Rapid Prescription

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

Zero-Trust Client IDE/ Security

Helm Registry Manifest Error Locator

Helm chart deployment fails with 'manifest not found' because the image tag doesn't exist in the private GitLab registry or the pull secret is misconfigured, halting pod scheduling entirely.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(48L)
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
36
37
38
39
40
# Helm chart values.yaml for legacy 'nginx-auth-proxy' service in PCI zone
# TODO: Migrate to ECR after Q4 budget approval, GitLab is temporary (ha!)
# NOTE: Old infra team used 'latest' for everything, new policy requires SHA256 digests
# VULNERABILITY: The image tag resolution silently falls back to 'latest' due to conditional logic in helper template, conflicting with registry's tag deletion policy

image:
  repository: registry.gitlab.internal/pci-secure-zone/legacy-nginx-proxy
  # FIXME: tag should be pinned to a digest, but the CI var is broken
  tag: "{{ .Values.image.tag | default .Chart.AppVersion }}"
  pullPolicy: IfNotPresent

# Legacy service account for cross-cluster access (deprecated)
serviceAccount:
  create: false
  name: "pci-legacy-sa"
  annotations:
    eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/pci-legacy-role"

# Synthetic secrets injected as env vars (DO NOT COMMIT)
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_URI
    value: "postgresql://user:SuperSecret123!@pci-db.internal:5432/legacy_payments"
  - name: SSH_PRIVATE_KEY
    value: |
      -----BEGIN RSA PRIVATE KEY-----
      MIIEowIBAAKCAQEArX8LJpM6Vz7pB8M6pKj6KjKjKjKjKjKjKjKjKjKjKjKjKj
      KjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKjKj
      ... (truncated for brevity) ...
      -----END RSA PRIVATE KEY-----

# Redundant config left from old deployment (commented out but still referenced)
# imagePullSecrets:
#   - name: gitlab-registry-secret

# Override for staging (broken due to missing .Chart.AppVersion in some contexts)
stagingOverrides:
  image:
    repository: registry.gitlab.internal/pci-secure-zone/legacy-nginx-proxy
    tag: "{{ .Values.image.tag | default \"latest\" }}"  # Fallback that triggers the bug when .Chart.AppVersion is empty

# TODO: Clean up this mess after compliance audit
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing Helm 'manifest for nginx:latest not found' Errors with Private GitLab Registry Image Pull Secrets

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 15–30 mins


TL;DR

  • What broke: Kubernetes cannot pull the image because either the tag (latest or a custom tag) does not exist in the private GitLab registry, the imagePullSecret is missing/malformed, or the Helm chart is referencing the wrong registry path.
  • How to fix it: Verify the exact image digest exists in GitLab, create a valid kubernetes.io/dockerconfigjson secret, and wire it into the Helm chart's imagePullSecrets block with a pinned, immutable tag.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor your failing values.yaml and regenerate the pull secret manifest without leaking your registry token.

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

The Incident (What Does the Error Mean?)

This is what kubectl describe pod <pod-name> shows during the outage:

Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  2m                 default-scheduler  Successfully assigned default/app-6d7f9b-xkq2p to node-1
  Normal   Pulling    2m                 kubelet            Pulling image "registry.gitlab.com/myorg/myapp/nginx:latest"
  Warning  Failed     112s               kubelet            Failed to pull image "registry.gitlab.com/myorg/myapp/nginx:latest": rpc error: code = NotFound desc = failed to pull and unpack image "registry.gitlab.com/myorg/myapp/nginx:latest": failed to resolve reference "registry.gitlab.com/myorg/myapp/nginx:latest": unexpected status code 401 Unauthorized
  Warning  Failed     112s               kubelet            Error: ErrImagePull
  Warning  BackOff    98s (x3 over 110s) kubelet            Back-off pulling image "registry.gitlab.com/myorg/myapp/nginx:latest"
  Warning  Failed     98s (x3 over 110s) kubelet            Error: ImagePullBackOff

Immediate consequence: The pod never reaches Running state. It oscillates between ErrImagePull and ImagePullBackOff. Every replica in the Deployment is dead. Your service is down.

There are three distinct failure modes that produce this identical error surface:

  1. Tag does not exist — nginx:latest was never pushed to registry.gitlab.com/myorg/myapp/, or was deleted/overwritten and the manifest digest is gone.
  2. imagePullSecret is absent or bound to the wrong namespace — The kubelet cannot authenticate to the private registry at all (401/403).
  3. Helm chart image values are wrong — image.repository or image.tag in values.yaml points to a non-existent path or uses a stale override.

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

The Attack Vector / Blast Radius

This is not just an ops inconvenience. The blast radius is significant:

Deployment blast radius:

  • All pods in the Deployment/StatefulSet/DaemonSet referencing this image are unschedulable. A rolling update leaves you with zero healthy replicas if the old ReplicaSet was already scaled down.
  • Horizontal Pod Autoscaler scale-out events will also fail silently — new pods spin up into ImagePullBackOff under load.

Security angle — why latest is a liability:

  • Using latest means your CI/CD pipeline can silently push a broken or malicious layer to the tag and every future pod restart pulls it automatically. There is no pinning, no auditability, no rollback path via image tag alone.
  • In a supply chain attack scenario (e.g., a compromised GitLab CI runner), an attacker can overwrite latest with a backdoored image. Because imagePullPolicy: Always re-pulls on every pod restart, the malicious image propagates to all nodes on the next rollout or node failure recovery — without any Helm release change being made.
  • A missing or overly-permissive imagePullSecret stored as a plain Kubernetes Secret with no RBAC restriction means any pod in the namespace can mount it and exfiltrate your GitLab registry credentials.

How to Fix It (The Solution)

Step 1 — Verify the image tag actually exists

# Authenticate and check the tag exists in GitLab registry
curl -s --header "PRIVATE-TOKEN: <your-pat>" \
  "https://registry.gitlab.com/v2/myorg/myapp/nginx/tags/list"

# Or via the GitLab API
curl -s --header "PRIVATE-TOKEN: <your-pat>" \
  "https://gitlab.com/api/v4/projects/<project-id>/registry/repositories"

If latest is not in the tag list, your CI pipeline never pushed it, or it was garbage-collected. Fix the pipeline first.


Step 2 — Create the imagePullSecret correctly

# Create the secret in the SAME namespace as your Helm release
kubectl create secret docker-registry gitlab-registry-secret \
  --docker-server=registry.gitlab.com \
  --docker-username=<gitlab-deploy-token-username> \
  --docker-password=<gitlab-deploy-token-or-pat> \
  --docker-email=ci@yourorg.com \
  --namespace=<your-release-namespace>

⚠️ Use a GitLab Deploy Token (scoped to read_registry only), NOT your personal access token. Rotate it. Store it in Vault or a sealed secret — not in plaintext in your repo.


Step 3 — Fix the Helm values.yaml (Basic Fix vs. Enterprise Best Practice)

Bad configuration (what's breaking you right now):

- image:
-   repository: nginx
-   tag: latest
-   pullPolicy: Always
- imagePullSecrets: []

Corrected configuration (Enterprise Best Practice):

+ image:
+   repository: registry.gitlab.com/myorg/myapp/nginx
+   tag: "1.25.3"   # NEVER use 'latest' in production. Pin to immutable tag or SHA digest.
+   pullPolicy: IfNotPresent  # Avoid Always unless you have a hard requirement
+ imagePullSecrets:
+   - name: gitlab-registry-secret

Even better — pin to the image SHA digest in your Helm values (immutable, tamper-evident):

+ image:
+   repository: registry.gitlab.com/myorg/myapp/nginx
-   tag: "1.25.3"
+   tag: ""  # leave empty when using digest
+   digest: "sha256:a3b4c1d2e5f6..."  # output of: docker inspect --format='{{index .RepoDigests 0}}' <image>
+   pullPolicy: IfNotPresent
+ imagePullSecrets:
+   - name: gitlab-registry-secret

Note: Digest pinning requires your Helm chart's deployment template to support image.digest. If it doesn't, patch the template to render repository@digest when digest is set.


Step 4 — Verify the secret is in the right namespace and re-deploy

# Confirm secret exists in correct namespace
kubectl get secret gitlab-registry-secret -n <your-namespace> -o jsonpath='{.type}'
# Expected output: kubernetes.io/dockerconfigjson

# Validate the encoded credentials are correct
kubectl get secret gitlab-registry-secret -n <your-namespace> \
  -o jsonpath='{.data.\.dockerconfigjson}' | base64 --decode | jq .

# Re-deploy the Helm release
helm upgrade --install my-release ./my-chart \
  --namespace <your-namespace> \
  --set image.tag="1.25.3" \
  --set imagePullSecrets[0].name=gitlab-registry-secret

# Watch pods come up
kubectl rollout status deployment/my-release -n <your-namespace>

💡 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

Stop this from ever hitting production again. These controls are non-negotiable for any team past 5 engineers.

1. Enforce non-`latest` tags with OPA/Gatekeeper

Deploy this ConstraintTemplate to block any image referencing latest:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdenylatesttag
spec:
  crd:
    spec:
      names:
        kind: K8sDenyLatestTag
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sdenylatesttag
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          endswith(container.image, ":latest")
          msg := sprintf("Container '%v' uses ':latest' tag. Pin to an immutable tag or digest.", [container.name])
        }
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not contains(container.image, ":")
          msg := sprintf("Container '%v' has no image tag. Explicit tag or digest required.", [container.name])
        }

2. Checkov scan in your GitLab CI pipeline

# .gitlab-ci.yml
checkov-scan:
  stage: validate
  image: bridgecrew/checkov:latest
  script:
    - checkov -d ./helm-chart/templates --check CKV_K8S_14,CKV_K8S_15
    # CKV_K8S_14: Image Tag should be fixed - not latest
    # CKV_K8S_15: Image should use Always pull policy (paired with digest pinning)
  allow_failure: false

3. Automate secret rotation with External Secrets Operator

Never manually kubectl create secret again. Use External Secrets Operator to sync your GitLab deploy token from HashiCorp Vault or AWS Secrets Manager directly into the namespace as a kubernetes.io/dockerconfigjson secret — with automatic rotation.

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: gitlab-registry-secret
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: gitlab-registry-secret
    template:
      type: kubernetes.io/dockerconfigjson
  data:
    - secretKey: .dockerconfigjson
      remoteRef:
        key: secret/gitlab/registry
        property: dockerconfigjson

4. GitLab CI — always tag with commit SHA, never `latest`

# .gitlab-ci.yml
build-image:
  stage: build
  script:
    - docker build -t registry.gitlab.com/myorg/myapp/nginx:${CI_COMMIT_SHORT_SHA} .
    - docker push registry.gitlab.com/myorg/myapp/nginx:${CI_COMMIT_SHORT_SHA}
    # Optionally also tag as a semver release on tags
    - |
      if [ -n "$CI_COMMIT_TAG" ]; then
        docker tag registry.gitlab.com/myorg/myapp/nginx:${CI_COMMIT_SHORT_SHA} \
                   registry.gitlab.com/myorg/myapp/nginx:${CI_COMMIT_TAG}
        docker push registry.gitlab.com/myorg/myapp/nginx:${CI_COMMIT_TAG}
      fi

This guarantees every image in your registry is traceable to an exact commit. latest never exists. The manifest not found error becomes structurally impossible for correctly-built images.

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 'manifest not found' appear even though the image exists in my GitLab registry?
The kubelet's authentication failure or misalignment with the registry is a symptom of a deeper control plane breakdown. The root cause is a misconfigured or misaligned image reference—often due to a tag name mismatch in your Helm values, an imagePullSecret namespace misplacement, or an expired GitLab deploy token lacking 'read_registry' scope. This is a classic signal that your deployment pipeline's integrity checks are failing before the artifact even reaches the cluster. To build absolute authority over your supply chain, you must first diagnose this precise misalignment.
Q2.Is it safe to use 'imagePullPolicy: Always' with a private GitLab registry?
Manually configuring 'imagePullPolicy: Always' with a mutable tag like 'latest' is a high-risk trap. It introduces a critical blast radius: a network partition or registry outage can cascade into pod startup failures, and a compromised CI pipeline can automatically deploy a backdoored image on the next pod restart without any Helm release change, bypassing your security gates. Manual fixes are error-prone and reactive. Instead of risking this, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that enforces immutable tags and secure pull policies.
Q3.How do I scope the imagePullSecret so it doesn't expose my GitLab token to all pods in the namespace?
Scoping secrets manually with RBAC, project-specific deploy tokens, or external operators is a compliance nightmare at scale, especially across large mono-repos. It leads to credential sprawl, inconsistent enforcement, and audit fatigue. For enterprise-grade, automated pull request generation with Maker-Checker review that works on-prem and enforces policy as code, upgrade to the StackEngine CI/CD Automator (Team Edition). It provides centralized, auditable control over secret lifecycle and access, eliminating the risk of long-lived static credentials and ensuring compliance across all your deployments.