Skip to main content

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

Manual Steps
  1. Identify pods using Secret environment variables

    • Run this on any machine with kubectl access:
      kubectl get pods --all-namespaces -o json \
      | jq -r '.items[]
      | select(.spec.containers[].env? // [] | map(select(.valueFrom.secretKeyRef?)) | length > 0
      or .spec.containers[].envFrom? // [] | map(select(.secretRef?)) | length > 0)
      | "\(.metadata.namespace) \(.metadata.name)"' | sort -u
    • Save the list for review.
  2. Inspect pod specs and confirm how each Secret is used

    • For each NAMESPACE POD from 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).
  3. 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>).
  4. Refactor manifests to mount Secrets as volumes instead of env vars

    • On any machine with kubectl access, edit the workload manifests (Deployment/StatefulSet/DaemonSet/Job/CronJob, not the running Pod) to:
      • Add a Secret volume:
        spec:
        template:
        spec:
        volumes:
        - name: app-secret
        secret:
        secretName: SECRET_NAME
      • Mount it into each container:
        containers:
        - name: app
        volumeMounts:
        - name: app-secret
        mountPath: /var/run/secrets/app
        readOnly: true
      • Remove the corresponding env / envFrom entries that reference the same Secret keys, and update container args/config to use file paths instead of env vars.
    • Apply the updated manifest:
      kubectl apply -f UPDATED_MANIFEST.yaml
  5. 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).
  6. 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 > 0
      or .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.
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 see valueFrom.secretKeyRef, for example:
    env:
    - name: DB_PASSWORD
    valueFrom:
    secretKeyRef:
    name: db-secret
    key: password
  • Or under spec.initContainers[].env[] you see the same pattern.
  • Or envFrom.secretRef is 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.secretKeyRef
  • spec.containers[].envFrom[].secretRef
  • spec.initContainers[].env[].valueFrom.secretKeyRef
  • spec.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.