Fixing Init Container Exit Code 1: Secret Volume Mount Path '/etc/secrets' Not Found in Multi-Container Pods
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins
TL;DR
- What broke: The init container exited with code 1 because
volumeMounts[].mountPath: /etc/secretsreferences a volume that is either not declared underspec.volumes, has a mismatched name, or points to a non-existentSecretobject in the namespace. - How to fix it: Align the
volumes[].name→volumeMounts[].namechain, confirm thesecretNameexists in the same namespace, and verify the init container has read access via RBAC. - Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your pod spec and get corrected YAML without sending secrets to any external server.
The Incident (What Does the Error Mean?)
Raw output from kubectl describe pod <pod-name> or kubectl logs <pod-name> -c <init-container-name>:
Init Containers:
init-secret-loader:
State: Terminated
Reason: Error
Exit Code: 1
Message: "/etc/secrets: no such file or directory"
Events:
Warning Failed kubelet Error: failed to create containerd task:
failed to create shim: OCI runtime create failed:
container_linux.go:380: starting container process caused:
process_linux.go:545: container init caused:
rootfs_linux.go:76: mounting "/etc/secrets" ...:
mount /etc/secrets: no such file or directory
Immediate consequence: Every container in the pod — including your primary application containers — is blocked. Kubernetes does not start any containers[] until all initContainers[] exit with code 0. This is a full pod startup failure. In a Deployment with minReadySeconds and health checks, this cascades into a rollout deadlock.
The Attack Vector / Blast Radius
This isn't just an ops misconfiguration — it's a latent secret injection failure with two blast radii:
1. Availability blast radius: If this pod is part of a Deployment, kubectl rollout stalls. If maxUnavailable: 0, your old ReplicaSet stays up but your new one never becomes ready. Silent partial outage. If maxUnavailable > 0, you lose capacity immediately.
2. Security blast radius (the one people miss): Teams working around this error often "quick-fix" it by mounting the secret as an environment variable directly in the container spec or by temporarily switching secretName to a permissive ConfigMap. Both moves expose secret material in kubectl describe pod output, which is readable by anyone with get pods RBAC permission — a dramatically wider audience than get secrets. The correct path is always a proper volume mount with tight file permissions.
Additionally, if the Secret object doesn't exist in the namespace yet (common in GitOps flows where secrets are provisioned out-of-band via Vault or Sealed Secrets), the pod enters Pending with ContainerCreating indefinitely — no error surfaced to on-call until someone checks events manually.
How to Fix It (The Solution)
Root Cause Checklist (run these first)
# 1. Does the Secret exist in the correct namespace?
kubectl get secret <secret-name> -n <namespace>
# 2. What does the pod spec actually declare for volumes?
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.volumes}'
# 3. What does the init container declare for volumeMounts?
kubectl get pod <pod-name> -n <namespace> -o jsonpath='{.spec.initContainers[0].volumeMounts}'
Basic Fix: Align Volume Name and secretName
The most common cause is a name mismatch between spec.volumes[].name and initContainers[].volumeMounts[].name, or a wrong secretName.
spec:
initContainers:
- name: init-secret-loader
image: busybox:1.36
volumeMounts:
- - name: app-secrets-vol
+ - name: secret-volume
mountPath: /etc/secrets
readOnly: true
containers:
- name: app
image: my-app:latest
volumeMounts:
- - name: app-secrets-vol
+ - name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- - name: app-secrets-vol
- secret:
- secretName: my-app-config # <-- this Secret does not exist
+ - name: secret-volume
+ secret:
+ secretName: my-app-secrets # <-- verified: kubectl get secret my-app-secrets -n <ns>
+ defaultMode: 0400 # owner read-only; never 0777
Enterprise Best Practice: Projected Volumes + RBAC + Optional Secret Handling
In production, you should never hard-fail pod startup because a secret doesn't exist yet (common in Vault dynamic secrets, Sealed Secrets, or External Secrets Operator flows). Use optional: true for non-critical secrets, and projected volumes to merge multiple sources.
spec:
serviceAccountName: app-sa # bind only the RBAC needed
initContainers:
- name: init-secret-loader
image: busybox:1.36
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 1000
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
volumeMounts:
- - name: raw-secret
- mountPath: /etc/secrets
+ - name: projected-secrets
+ mountPath: /etc/secrets
+ readOnly: true
volumes:
- - name: raw-secret
- secret:
- secretName: my-app-secrets
+ - name: projected-secrets
+ projected:
+ defaultMode: 0400
+ sources:
+ - secret:
+ name: my-app-secrets
+ optional: false # hard dependency — fail fast if missing
+ - secret:
+ name: my-app-tls-secrets
+ optional: true # soft dependency — pod starts without it
+ - serviceAccountToken:
+ path: token
+ expirationSeconds: 3600
+ audience: vault
RBAC: ensure the pod's ServiceAccount cannot read secrets it doesn't own:
# Least-privilege Role — scoped to specific secret names, not wildcard
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["my-app-secrets"] # never omit resourceNames
verbs: ["get"]
💡 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
This class of error is 100% preventable before kubectl apply is ever run.
1. Conftest / OPA Policy (blocks mismatched volume names at PR time)
# policy/secret-volume-mount.rego
package kubernetes.secret_mount
deny[msg] {
container := input.spec.initContainers[_]
mount := container.volumeMounts[_]
mount.mountPath == "/etc/secrets"
not volume_exists(mount.name)
msg := sprintf("Init container '%v' mounts volume '%v' at /etc/secrets but no matching volume is declared in spec.volumes", [container.name, mount.name])
}
volume_exists(name) {
input.spec.volumes[_].name == name
}
2. Checkov (add to your GitHub Actions pipeline)
# .github/workflows/k8s-lint.yml
- name: Checkov Kubernetes Scan
uses: bridgecrewio/checkov-action@master
with:
directory: k8s/
framework: kubernetes
check: CKV_K8S_35,CKV_K8S_28,CKV_K8S_30
# CKV_K8S_35: secrets as env vars (blocks the bad workaround)
# CKV_K8S_28: readOnlyRootFilesystem
# CKV_K8S_30: runAsNonRoot
3. Pre-deploy Secret Existence Check (shell gate in CI)
#!/bin/bash
# scripts/verify-secrets.sh — run before helm upgrade or kubectl apply
set -euo pipefail
REQUIRED_SECRETS=("my-app-secrets" "my-app-tls-secrets")
NAMESPACE="production"
for secret in "${REQUIRED_SECRETS[@]}"; do
if ! kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
echo "[FATAL] Secret '$secret' not found in namespace '$NAMESPACE'. Aborting deploy."
exit 1
fi
done
echo "[OK] All required secrets verified."
4. External Secrets Operator (for Vault/ASM-backed secrets)
If your secrets are provisioned dynamically, use External Secrets Operator with a SecretStore → ExternalSecret → Kubernetes Secret pipeline. The ExternalSecret object will surface sync errors in its .status.conditions before your pod ever tries to mount, giving you a clean failure signal in your GitOps tooling (ArgoCD/Flux) rather than a cryptic init container exit.