Consider External Secret Storage
More Info:
Kubernetes supports mounting secrets as data volumes or as environment variables. Minimize the use of environment variable secrets.
Risk Level
High
Address
Security
Compliance Standards
- APRA CPS 234 (Australia)
- BSI C5 (Germany)
- Brazil LGPD
- CCPA / CPRA (California)
- CIS AKS
- CIS Critical Security Controls v8
- CMMC 2.0
- CSA Cloud Controls Matrix v4
- DPDPA
- Digital Operational Resilience Act (EU)
- Essential 8
- ISO/IEC 27017
- ISO/IEC 27018
- ISO/IEC 27701
- KSA PDPL
- MAS Technology Risk Management (Singapore)
- MITRE ATT&CK (Cloud)
- NIS2 Directive
- NIST SP 800-171
- NYDFS 23 NYCRR 500
- SWIFT Customer Security Controls Framework
- Sarbanes-Oxley IT General Controls
- UK NCSC Cyber Assessment Framework
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Inventory all Secrets in use (any machine with kubectl access)
kubectl get secrets -A -o wideExport details for review (base64-decoded data omitted in this step):
kubectl get secrets -A -o yaml > /tmp/all-secrets.yaml -
Identify workloads consuming Secrets as environment variables (any machine with kubectl access)
kubectl get deploy,sts,ds,job,cronjob -A -o yaml > /tmp/all-workloads.yamlSearch for env-based secret usage:
grep -nE 'valueFrom:[[:space:]]*$' -n /tmp/all-workloads.yaml -A5 | grep -n 'secretKeyRef' -A3 -
Review and classify secret usage (manual review on any machine)
- From
/tmp/all-workloads.yaml, list containers usingenv/envFromwithsecretKeyReforsecretRef. - For each, determine:
- Is the secret long‑lived or highly sensitive (DB passwords, API keys, TLS private keys)?
- Is the secret needed only at startup (e.g., DB migration) or throughout runtime?
- Prefer candidates that are long‑lived and runtime‑sensitive for external secret storage.
- From
-
Evaluate external secret storage options and integration pattern (design/decision step)
- Choose provider: e.g., Azure Key Vault, HashiCorp Vault, SOPS + KMS, or cloud‑native secret manager.
- Decide access pattern per workload:
- App fetches secrets directly via SDK/API at runtime, or
- Use an operator (e.g., external-secrets) to sync from external store to Kubernetes Secret, or
- Sidecar/agent that mounts secrets from external store into the container filesystem.
- Document the chosen pattern and access controls (identity, RBAC, network).
-
Refactor one workload as a pilot away from env var secrets (any machine with kubectl access)
- For the selected workload:
- Remove
env/envFromentries that reference sensitive secrets. - Add configuration needed for the external store pattern (e.g., volume mount from sidecar, SDK config, or ExternalSecret CRD).
- Remove
- Apply changes:
kubectl apply -f <updated-manifest>.yaml
- Coordinate with application owners to update code/config to read secrets from the new mechanism (not environment variables).
- For the selected workload:
-
Verify reduction of environment-variable secrets and repeat iteratively (any machine with kubectl access)
- Re-run workload inspection:
kubectl get deploy,sts,ds,job,cronjob -A -o yaml > /tmp/all-workloads-post.yamlgrep -n 'secretKeyRef' -n /tmp/all-workloads-post.yaml -A3
- Confirm the pilot workload no longer pulls sensitive secrets via
env/envFrom. - Expand the same process to additional workloads, prioritizing the most sensitive secrets first.
- Re-run workload inspection:
Using kubectl
# 1) List all namespaces
# Run on: any machine with kubectl access
kubectl get ns
# 2) For each namespace, list pods and show env and envFrom sections
# (replace <namespace> with each namespace name)
kubectl get pods -n <namespace> -o yaml | \
sed -n '/env:/,/^ *[^- ]/p;/envFrom:/,/^ *[^- ]/p' | \
sed 's/^/ /'
What to look for as a problem
In the output, inspect env: and envFrom: blocks under each container:
- Direct secret keys in env:
env:- name: DB_PASSWORDvalueFrom:secretKeyRef:name: db-credentialskey: password
- Whole-secret imports into env:
envFrom:- secretRef:name: app-secrets
These patterns indicate secrets are exposed as environment variables instead of via mounted volumes or external secret management.
# 3) Show where each Secret is referenced by Pods (cluster-wide)
kubectl get secrets --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' | \
while read ns name; do
echo "=== Secret: $ns/$name ==="
kubectl get pods -n "$ns" -o yaml | \
grep -nE "secretKeyRef:|secretRef:" -n || echo " (no direct pod references found)"
done
What to look for as a problem
For secrets that contain high-value data (DB credentials, API keys, tokens), any use via secretKeyRef / secretRef in env or envFrom should be flagged for review as a candidate to move to:
- projected/volume-mounted secrets, or
- an external secret management solution.
# 4) Inspect containers that use many env vars from secrets
kubectl get pods --all-namespaces -o yaml | \
awk '/name: / {cname=$2} /envFrom:/,/^ *[^- ]/ {print "Container: " cname " => " $0}'
What to look for as a problem
Containers that pull entire secrets into their environment via envFrom.secretRef are higher risk and stronger candidates for redesign.
# 5) Optional: focus on pods in critical namespaces (kube-system, production, etc.)
kubectl get pods -n kube-system -o yaml | \
sed -n '/env:/,/^ *[^- ]/p;/envFrom:/,/^ *[^- ]/p'
What to look for as a problem
Environment-based secrets in critical infrastructure or production workloads should be prioritized for migration to external secret storage or more secure patterns.
These commands only surface current usage; a human must decide which secrets should be moved to external storage and how to refactor each workload.
Automation
#!/usr/bin/env bash
#
# Report Kubernetes workload use of Secrets as environment variables vs volumes.
# Run on: any machine with kubectl access and appropriate RBAC.
# Requires: kubectl, jq
#
# This DOES NOT change anything; it only reports for human review.
set -euo pipefail
# Helper: print a section header
hdr() {
printf '\n===== %s =====\n' "$1"
}
# 1) Cluster‑wide inventory of Secrets (for reference)
hdr "All Secrets by type (cluster-wide)"
kubectl get secrets --all-namespaces -o json \
| jq -r '
.items[]
| [.metadata.namespace, .metadata.name, .type]
| @tsv' \
| column -t
# 2) Workloads using Secrets as environment variables
#
# These are the primary cases to review and potentially migrate
# to volume-mounted secrets or external secret stores.
workload_kinds=(
Deployment
StatefulSet
DaemonSet
ReplicaSet
Job
CronJob
)
for kind in "${workload_kinds[@]}"; do
hdr "Secrets referenced as ENV VARS in ${kind}s"
# We look for:
# - env[].valueFrom.secretKeyRef
# - envFrom[].secretRef
kubectl get "$kind" --all-namespaces -o json 2>/dev/null \
| jq -r '
.items[]
| . as $w
| (
.spec.template.spec.containers[]
| {
ns: $w.metadata.namespace,
kind: "'$kind'",
name: $w.metadata.name,
c: .name,
env: ( .env // [] | map(select(.valueFrom.secretKeyRef)) ),
envFrom: ( .envFrom // [] | map(select(.secretRef)) )
}
) // empty
| select((.env|length) > 0 or (.envFrom|length) > 0)
| (
.env[]
| {
ns,kind,name,c,
ref_type:"env",
secret: .valueFrom.secretKeyRef.name,
key: .valueFrom.secretKeyRef.key
}
)?,(
.envFrom[]
| {
ns,kind,name,c,
ref_type:"envFrom",
secret: .secretRef.name,
key:"*"
}
)?
' 2>/dev/null \
| jq -r '[.ns,.kind,.name,.c,.ref_type,.secret,.key] | @tsv' \
| sort -u \
| column -t || echo "No ${kind}s found or no env-based secret usage."
done
# 3) Workloads using Secrets as mounted volumes
#
# These typically align better with the recommendation to avoid
# environment-variable secrets, but still require review.
for kind in "${workload_kinds[@]}"; do
hdr "Secrets used as VOLUME MOUNTS in ${kind}s"
kubectl get "$kind" --all-namespaces -o json 2>/dev/null \
| jq -r '
.items[]
| . as $w
| {
ns: .metadata.namespace,
kind: "'$kind'",
name: .metadata.name,
vols: (.spec.template.spec.volumes // [])
}
| select(.vols | length > 0)
| .vols[]
| select(.secret)
| [ .ns, .kind, .name, .name as $vname | ., "secret", .secret.secretName ]
| @tsv
' 2>/dev/null \
| sort -u \
| column -t || echo "No ${kind}s found or no secret volumes."
done
# 4) Pods directly (for completeness; may include static pods or bare pods)
hdr "Standalone Pods using Secrets as ENV VARS"
kubectl get pods --all-namespaces -o json 2>/dev/null \
| jq -r '
.items[]
| . as $p
| (
.spec.containers[]
| {
ns: $p.metadata.namespace,
kind: "Pod",
name: $p.metadata.name,
c: .name,
env: ( .env // [] | map(select(.valueFrom.secretKeyRef)) ),
envFrom: ( .envFrom // [] | map(select(.secretRef)) )
}
) // empty
| select((.env|length) > 0 or (.envFrom|length) > 0)
| (
.env[]
| {
ns,kind,name,c,
ref_type:"env",
secret: .valueFrom.secretKeyRef.name,
key: .valueFrom.secretKeyRef.key
}
)?,(
.envFrom[]
| {
ns,kind,name,c,
ref_type:"envFrom",
secret: .secretRef.name,
key:"*"
}
)?
' \
| jq -r '[.ns,.kind,.name,.c,.ref_type,.secret,.key] | @tsv' \
| sort -u \
| column -t || echo "No pods or no env-based secret usage."
hdr "Standalone Pods using Secrets as VOLUME MOUNTS"
kubectl get pods --all-namespaces -o json 2>/dev/null \
| jq -r '
.items[]
| . as $p
| {
ns: .metadata.namespace,
kind: "Pod",
name: .metadata.name,
vols: (.spec.volumes // [])
}
| select(.vols | length > 0)
| .vols[]
| select(.secret)
| [ .ns, .kind, .name, .name as $vname | ., "secret", .secret.secretName ]
| @tsv
' \
| sort -u \
| column -t || echo "No pods or no secret volumes."
echo
echo "Review guidance:"
echo " - ENV VAR problems: any lines labeled 'env' or 'envFrom' above indicate"
echo " secrets exposed as environment variables, which the benchmark says to"
echo " minimize. Prioritize these for migration to:"
echo " * volume-mounted secrets, and/or"
echo " * an external secret manager (cloud-native or third party)."
echo " - VOLUME usage: lines in the VOLUME sections show where secrets are"
echo " mounted as files. These are generally preferred to env-based secrets,"
echo " but still should be evaluated for potential migration to external"
echo " secret storage where appropriate."
What output indicates a problem
- Any rows in the
Secrets referenced as ENV VARS in ...sections (including the standalone Pods section) show containers that are using Kubernetes Secrets via:ref_type=env(per-variablevalueFrom.secretKeyRef), orref_type=envFrom(all keys from a secret imported as environment variables).
Those rows are the candidates you should review and, where feasible, migrate away from environment-variable secrets toward volume-mounted secrets and/or an external secret manager.