Prefer Using Secrets As Files Over Secrets As Environment
More Info:
Secrets exposed as environment variables are more easily leaked through logs and process inspection than secrets mounted as files. Prefer file mounts.
Risk Level
Medium
Address
Security
Compliance Standards
- CIS Kubernetes
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify pods using Secret environment variables
- Run this on any machine with
kubectlaccess:kubectl get pods --all-namespaces -o json \| jq -r '.items[]| select(.spec.containers[].env? // [] | map(select(.valueFrom.secretKeyRef?)) | length > 0or .spec.containers[].envFrom? // [] | map(select(.secretRef?)) | length > 0)| "\(.metadata.namespace) \(.metadata.name)"' | sort -u - Save the list for review.
- Run this on any machine with
-
Inspect pod specs and confirm how each Secret is used
- For each
NAMESPACE PODfrom step 1, inspect full spec:kubectl get pod POD -n NAMESPACE -o yaml - In
spec.containers[*].env/envFrom, note:- Secret name and keys.
- Whether the same Secret is already mounted as a volume.
- Application expectations (e.g., variable names used by the app).
- For each
-
Decide whether each Secret can be consumed via file instead of env
- For each env-based Secret:
- Review application code/config (and documentation) to see if it can read from a file path (config option, CLI flag, or code change).
- If the app cannot be changed (e.g., third-party image requiring env vars), document this as an accepted exception with justification and skip to the next Secret.
- If it can be changed, plan the mount path and per-key filenames (e.g.,
/var/run/secrets/<secret-name>/<key>).
- For each env-based Secret:
-
Refactor manifests to mount Secrets as volumes instead of env vars
- On any machine with
kubectlaccess, edit the workload manifests (Deployment/StatefulSet/DaemonSet/Job/CronJob, not the running Pod) to:- Add a Secret volume:
spec:template:spec:volumes:- name: app-secretsecret:secretName: SECRET_NAME
- Mount it into each container:
containers:- name: appvolumeMounts:- name: app-secretmountPath: /var/run/secrets/appreadOnly: true
- Remove the corresponding
env/envFromentries that reference the same Secret keys, and update container args/config to use file paths instead of env vars.
- Add a Secret volume:
- Apply the updated manifest:
kubectl apply -f UPDATED_MANIFEST.yaml
- On any machine with
-
Roll out and validate application behavior
- Ensure updated workloads are running:
kubectl rollout status deploy/DEPLOYMENT_NAME -n NAMESPACE
- Confirm the application reads secrets from files (e.g., health checks succeed, functional tests pass, or logs indicate successful secret loading).
- Ensure updated workloads are running:
-
Re-audit to confirm reduced use of Secret environment variables
- Re-run the detection from step 1:
kubectl get pods --all-namespaces -o json \| jq -r '.items[]| select(.spec.containers[].env? // [] | map(select(.valueFrom.secretKeyRef?)) | length > 0or .spec.containers[].envFrom? // [] | map(select(.secretRef?)) | length > 0)| "\(.metadata.namespace) \(.metadata.name)"' | sort -u
- Verify that only documented exceptions remain and that all other workloads now consume Secrets via mounted files.
- Re-run the detection from step 1:
Using kubectl
# 1) List all namespaces
# Run on: any machine with kubectl access
kubectl get ns
Review each relevant namespace (or all, if unsure):
# 2) List all Pods in a namespace, including their containers and env definitions (wide view)
# Replace <namespace> with the namespace under review.
kubectl get pods -n <namespace> -o yaml
In the output, a Pod is potentially problematic if:
- Under
spec.containers[].env[]you seevalueFrom.secretKeyRef, for example:env:- name: DB_PASSWORDvalueFrom:secretKeyRef:name: db-secretkey: password - Or under
spec.initContainers[].env[]you see the same pattern. - Or
envFrom.secretRefis used:envFrom:- secretRef:name: app-secrets
These indicate Secrets are injected as environment variables.
To focus specifically on Pods that use secretKeyRef in env or envFrom:
# 3) Grep for secretKeyRef and envFrom->secretRef in a namespace
kubectl get pods -n <namespace> -o yaml | grep -nE 'secretKeyRef|secretRef'
Any matches under env: or envFrom: signal Pods that should be reviewed for possible migration to file-based secret mounts.
For a single Pod you are investigating:
# 4) Inspect one Pod in detail
kubectl get pod <pod-name> -n <namespace> -o yaml
Again, look specifically for:
spec.containers[].env[].valueFrom.secretKeyRefspec.containers[].envFrom[].secretRefspec.initContainers[].env[].valueFrom.secretKeyRefspec.initContainers[].envFrom[].secretRef
Presence of these fields means the Pod is using Secrets as environment variables and should be considered for refactoring so the application reads Secrets from mounted files instead.
Automation
#!/usr/bin/env bash
# Report Pods that consume Secrets via environment variables (directly or via envFrom)
# Run on: any machine with kubectl access and jq installed
set -euo pipefail
# Namespace filter: set to "" to scan all namespaces, or to a specific namespace
NAMESPACE_FILTER=""
if [[ -n "$NAMESPACE_FILTER" ]]; then
NS_ARGS=( -n "$NAMESPACE_FILTER" )
echo "Scanning namespace: $NAMESPACE_FILTER" >&2
else
NS_ARGS=( -A )
echo "Scanning all namespaces" >&2
fi
# Collect all Pods once as JSON
pods_json="$(kubectl get pods "${NS_ARGS[@]}" -o json)"
# 1) Pods using env[].valueFrom.secretKeyRef
echo "=== Pods using Secrets via env[].valueFrom.secretKeyRef (per-container, per-env var) ==="
echo "$pods_json" | jq -r '
.items[]
| .metadata as $m
| .spec.containers[]? as $c
| $c.env[]?
| select(.valueFrom.secretKeyRef != null)
| {
namespace: $m.namespace,
pod: $m.name,
container: $c.name,
env_var: .name,
secret_name: .valueFrom.secretKeyRef.name,
secret_key: .valueFrom.secretKeyRef.key
}
| [.namespace, .pod, .container, .env_var, .secret_name, .secret_key]
| @tsv
' | awk 'BEGIN { OFS="\t"; print "NAMESPACE","POD","CONTAINER","ENV_VAR","SECRET_NAME","SECRET_KEY" } 1'
echo
# 2) Pods using envFrom[].secretRef
echo "=== Pods using Secrets via envFrom[].secretRef (whole Secret as env vars) ==="
echo "$pods_json" | jq -r '
.items[]
| .metadata as $m
| .spec.containers[]? as $c
| $c.envFrom[]?
| select(.secretRef != null)
| {
namespace: $m.namespace,
pod: $m.name,
container: $c.name,
secret_name: .secretRef.name
}
| [.namespace, .pod, .container, .secret_name]
| @tsv
' | awk 'BEGIN { OFS="\t"; print "NAMESPACE","POD","CONTAINER","SECRET_NAME" } 1'
echo
# 3) Summary counts per namespace
echo "=== Summary: count of Pods per namespace that use Secrets via env or envFrom ==="
echo "$pods_json" | jq -r '
.items[]
| .metadata as $m
| {
namespace: $m.namespace,
pod: $m.name,
has_env_secret: (
(.spec.containers[]?.env[]?.valueFrom.secretKeyRef // empty) as $x
| ($x | length) >= 0
) and (
(.spec.containers[]?.env[]?.valueFrom.secretKeyRef) != null
),
has_envfrom_secret: (
(.spec.containers[]?.envFrom[]?.secretRef // empty) as $y
| ($y | length) >= 0
) and (
(.spec.containers[]?.envFrom[]?.secretRef) != null
)
}
| select(.has_env_secret or .has_envfrom_secret)
| .namespace
' | sort | uniq -c | awk 'BEGIN { print "COUNT\tNAMESPACE" } { print $1 "\t" $2 }'
How to interpret the output
- Any line in the first table (
env[].valueFrom.secretKeyRef) or second table (envFrom[].secretRef) indicates a Pod/container that is exposing a Secret via environment variables. - These Pods are candidates for remediation: refactor the application to read from Secrets mounted as files instead of env vars.
- The summary section helps you see which namespaces have the largest number of such Pods so you can prioritize review.