Prefer Using Secrets As Files Over Secrets As Environment
More Info:​
Secrets exposed as environment variables are more easily leaked via process listings, logs, and child processes. Mounting secrets as files reduces this exposure.
Risk Level​
Medium
Address​
Security
Compliance Standards​
- CIS AKS
Triage and Remediation​
- Remediation
Remediation​
Manual Steps
-
Identify pods using secrets as environment variables
- Run on: any machine with kubectl access
kubectl get all --all-namespaces -o jsonpath='{range .items[?(@..secretKeyRef)]}{.kind}{";"}{.metadata.namespace}{";"}{.metadata.name}{"\n"}{end}'- Note each
NAMESPACEandNAMEyou need to fix (usually Deployments, StatefulSets, DaemonSets, Jobs, CronJobs, Pods).
-
Inspect one affected workload’s manifest
- Run on: any machine with kubectl access
ReplaceNAMESPACEandNAMEwith values from step 1:
kubectl get deployment NAME -n NAMESPACE -o yaml > /tmp/NAME.yaml(If the resource is not a Deployment, change
deploymentto its kind in lowercase, e.g.statefulset,daemonset,job,pod.) - Run on: any machine with kubectl access
-
Edit the manifest to mount secrets as files instead of env variables
- Run on: any machine with kubectl access
- Open the file:
vi /tmp/NAME.yaml- In each container spec:
- Remove
envandenvFromentries that usesecretKeyRef. Example to delete:env:- name: DB_PASSWORDvalueFrom:secretKeyRef:name: db-secretkey: passwordenvFrom:- secretRef:name: db-secret - Add a
volumeMountsentry pointing to a new secret-backed volume, and add that volume underspec.template.spec.volumes(orspec.volumesfor a Pod). Example pattern to add:spec:containers:- name: appvolumeMounts:- name: db-secret-volumemountPath: /var/run/secrets/dbreadOnly: truevolumes:- name: db-secret-volumesecret:secretName: db-secret
- Remove
- Coordinate with the application team so the application reads from the mounted files (e.g.
/var/run/secrets/db/password) instead of environment variables before applying.
-
Apply the updated manifest
- Run on: any machine with kubectl access
kubectl apply -f /tmp/NAME.yaml- Repeat steps 2–4 for each affected resource from step 1.
-
(Optional) Confirm pods are using secret volumes and not env-based secrets
- Run on: any machine with kubectl access
kubectl get deployment NAME -n NAMESPACE -o yaml | grep -n "secretKeyRef" || echo "No secretKeyRef in env for this resource"kubectl get deployment NAME -n NAMESPACE -o yaml | grep -n "secret:" -n -
Verify cluster-wide that env-based secret references are removed
- Run on: any machine with kubectl access
output=$(kubectl get all --all-namespaces -o jsonpath='{range .items[?(@..secretKeyRef)]} {.kind} {.metadata.name} {"\n"}{end}')if [ -z "$output" ]; then echo "NO_ENV_SECRET_REFERENCES"; else echo "ENV_SECRET_REFERENCES_FOUND"; echo "$output"; fi
Using kubectl
On any machine with kubectl access:
- Identify the affected workloads
kubectl get all --all-namespaces -o jsonpath='{range .items[?(@..secretKeyRef)]} {.kind} {.metadata.namespace} {.metadata.name} {"\n"}{end}'
For each listed Deployment/StatefulSet/DaemonSet/Pod, get its full manifest:
# Example for a Deployment
kubectl get deployment -n <NAMESPACE> <NAME> -o yaml > /tmp/<NAME>.yaml
- Edit the manifest to use secrets as files
In the downloaded YAML:
- Under each affected container, remove any
envorenvFromentries that referencesecretKeyRef. - Add a
volumethat sources the Secret. - Add a
volumeMountto mount that volume into the container.
Example transformation (before):
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
spec:
template:
spec:
containers:
- name: app
image: my-image:latest
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
envFrom:
- secretRef:
name: other-secret
After refactor:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
spec:
template:
spec:
containers:
- name: app
image: my-image:latest
volumeMounts:
- name: db-secret-vol
mountPath: /var/run/secrets/db
readOnly: true
- name: other-secret-vol
mountPath: /var/run/secrets/other
readOnly: true
volumes:
- name: db-secret-vol
secret:
secretName: db-secret
- name: other-secret-vol
secret:
secretName: other-secret
Application code must be updated to read from the mounted files, e.g. /var/run/secrets/db/password instead of an environment variable.
- Apply the updated manifest
kubectl apply -f /tmp/<NAME>.yaml
Repeat for each affected Deployment/StatefulSet/DaemonSet/Pod.
- Verification
Run the same audit command and confirm it reports no remaining references:
output=$(kubectl get all --all-namespaces -o jsonpath='{range .items[?(@..secretKeyRef)]} {.kind} {.metadata.name} {"\n"}{end}')
if [ -z "$output" ]; then echo "NO_ENV_SECRET_REFERENCES"; else echo "ENV_SECRET_REFERENCES_FOUND"; fi
Automation
#!/usr/bin/env bash
#
# Refactor pods/workloads to mount Secrets as files instead of using them
# as environment variables (env/envFrom with secretKeyRef).
#
# REQUIREMENTS:
# - Run on any machine with kubectl access and correct kubeconfig
# - kubectl v1.20+ (for strategic-merge patches)
#
# LIMITATIONS (IMPORTANT):
# - This script CANNOT safely infer how your application reads secrets.
# - It ONLY reports and proposes changes via YAML patches.
# - You MUST review and apply the generated patches manually.
#
# BEHAVIOR:
# - Detects current use of env/envFrom.secretKeyRef
# - For each offending workload, emits a patch file that:
# * removes env/envFrom entries that reference Secrets
# * adds secret volume + mount at /var/run/secrets/<secret-name>
# - Safe to re-run: it overwrites previous patch files for same object.
# - At the end, shows remaining offending resources.
#
# NOTE:
# - After you APPLY a patch, your pods will be recreated/rolled out
# by the controller (Deployment/StatefulSet/DaemonSet/Job/CronJob).
# - You must adapt your application to read secrets from files.
set -euo pipefail
OUT_DIR="${OUT_DIR:-./secret-env-to-file-patches}"
mkdir -p "${OUT_DIR}"
echo "Discovering Kubernetes resources that use Secret env refs..."
# Resources likely to have pods
KINDS=(
Deployment
StatefulSet
DaemonSet
ReplicaSet
ReplicationController
Job
CronJob
Pod
)
# JSONPath to list any item that has a secretKeyRef in env or envFrom
JSONPATH='{range .items[?(@..secretKeyRef)]}{.kind}{" "}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}'
# Get all offending objects
OFFENDERS=$(kubectl get "${KINDS[@]}" --all-namespaces -o jsonpath="$JSONPATH" 2>/dev/null || true)
if [ -z "${OFFENDERS}" ]; then
echo "NO_ENV_SECRET_REFERENCES"
exit 0
fi
echo "Found the following resources using secrets as environment variables:"
echo "${OFFENDERS}" | sort -u
echo
echo "Generating patch files under: ${OUT_DIR}"
echo "You MUST review and apply them manually with kubectl patch -f or kubectl apply -f."
echo
# Function: build a patch skeleton for workloads with pod template
generate_workload_patch() {
local kind="$1" ns="$2" name="$3"
local jsonpath_template
case "${kind}" in
CronJob)
jsonpath_template='{.spec.jobTemplate.spec.template.spec}'
;;
*)
jsonpath_template='{.spec.template.spec}'
;;
esac
echo " - Inspecting ${kind} ${ns}/${name}"
# Extract full YAML to work on locally
tmp_yaml="$(mktemp)"
kubectl get "${kind}" "${name}" -n "${ns}" -o yaml > "${tmp_yaml}"
# We create a generic patch that:
# * appends secret volumes for each secretKeyRef
# * removes offending env/envFrom entries
# Because exact selective removal is app-specific, we give commented hints.
# Discover distinct secret names used in env/envFrom for this object
mapfile -t secrets < <(kubectl get "${kind}" "${name}" -n "${ns}" -o json \
| jq -r '.. | objects | select(has("secretKeyRef")) | .secretKeyRef.name' \
| sort -u)
if [ "${#secrets[@]}" -eq 0 ]; then
rm -f "${tmp_yaml}"
return
fi
patch_file="${OUT_DIR}/${ns}__${kind}__${name}.patch.yaml"
{
echo "# Patch for ${kind} ${ns}/${name}"
echo "# PURPOSE:"
echo "# - Mount the listed Secrets as volumes under /var/run/secrets/<secret-name>"
echo "# - Replace env/envFrom.secretKeyRef usage in containers to read from files."
echo "# IMPORTANT:"
echo "# - You MUST manually edit this patch:"
echo "# * Remove/replace env and envFrom entries that reference these Secrets."
echo "# * Optionally adjust mountPath and keys to match your application."
echo "# - After editing, apply via one of:"
echo "# kubectl patch ${kind} ${name} -n ${ns} --type=merge --patch-file ${patch_file}"
echo "# kubectl apply -f ${patch_file}"
echo
echo "apiVersion: $(yq '.apiVersion' "${tmp_yaml}")"
echo "kind: ${kind}"
echo "metadata:"
echo " name: ${name}"
echo " namespace: ${ns}"
case "${kind}" in
CronJob)
echo "spec:"
echo " jobTemplate:"
echo " spec:"
echo " template:"
echo " spec:"
indent=" "
;;
*)
echo "spec:"
echo " template:"
echo " spec:"
indent=" "
;;
esac
echo "${indent}volumes:"
echo "${indent} # Existing volumes are not shown here; this is a strategic merge patch."
for s in "${secrets[@]}"; do
echo "${indent} - name: secret-${s}"
echo "${indent} secret:"
echo "${indent} secretName: ${s}"
done
echo
echo "${indent}containers:"
echo "${indent} # For EACH container, add volumeMounts for the above Secrets."
echo "${indent} # Then, MANUALLY remove or adjust env/envFrom entries that use secretKeyRef."
echo "${indent} # Example:"
echo "${indent} # - name: my-container"
echo "${indent} # volumeMounts:"
for s in "${secrets[@]}"; do
echo "${indent} # - name: secret-${s}"
echo "${indent} # mountPath: /var/run/secrets/${s}"
echo "${indent} # readOnly: true"
done
echo "${indent} # env:"
echo "${indent} # # BEFORE (remove this):"
echo "${indent} # # - name: DB_PASSWORD"
echo "${indent} # # valueFrom:"
echo "${indent} # # secretKeyRef:"
echo "${indent} # # name: ${secrets[0]}"
echo "${indent} # # key: password"
echo "${indent} # # AFTER (example of file-based usage, if your app supports it):"
echo "${indent} # # - name: DB_PASSWORD_FILE"
echo "${indent} # # value: /var/run/secrets/${secrets[0]}/password"
} > "${patch_file}"
rm -f "${tmp_yaml}"
}
# Function: handle plain Pods (no controller)
generate_pod_patch() {
local kind="$1" ns="$2" name="$3"
echo " - Inspecting Pod ${ns}/${name}"
tmp_yaml="$(mktemp)"
kubectl get pod "${name}" -n "${ns}" -o yaml > "${tmp_yaml}"
mapfile -t secrets < <(kubectl get pod "${name}" -n "${ns}" -o json \
| jq -r '.. | objects | select(has("secretKeyRef")) | .secretKeyRef.name' \
| sort -u)
if [ "${#secrets[@]}" -eq 0 ]; then
rm -f "${tmp_yaml}"
return
fi
patch_file="${OUT_DIR}/${ns}__Pod__${name}.patch.yaml"
{
echo "# Patch for Pod ${ns}/${name}"
echo "# NOTE: This only affects this specific Pod; consider patching the controller instead (Deployment, Job, etc.)."
echo "# See workload patches in ${OUT_DIR} for a more durable fix."
echo
echo "apiVersion: $(yq '.apiVersion' "${tmp_yaml}")"
echo "kind: Pod"
echo "metadata:"
echo " name: ${name}"
echo " namespace: ${ns}"
echo "spec:"
echo " volumes:"
for s in "${secrets[@]}"; do
echo " - name: secret-${s}"
echo " secret:"
echo " secretName: ${s}"
done
echo
echo " containers:"
echo " # Add volumeMounts and refactor env/envFrom as in workload examples."
echo " # This is a strategic-merge patch; only specified fields are merged."
} > "${patch_file}"
rm -f "${tmp_yaml}"
}
# Generate patches per offending object
echo
echo "Creating patch templates..."
echo "${OFFENDERS}" | sort -u | while read -r kind ns name; do
[ -z "${kind}" ] && continue
case "${kind}" in
Deployment|StatefulSet|DaemonSet|ReplicaSet|ReplicationController|Job|CronJob)
generate_workload_patch "${kind}" "${ns}" "${name}"
;;
Pod)
generate_pod_patch "${kind}" "${ns}" "${name}"
;;
*)
echo " - Skipping unsupported kind: ${kind} ${ns}/${name}"
;;
esac
done
echo
echo "Patch templates generated under: ${OUT_DIR}"
echo "Next steps:"
echo " 1) Review and edit each *.patch.yaml file to:"
echo " - Add volumeMounts under each container."
echo " - Remove env/envFrom entries that reference Secrets."
echo " - Optionally add *_FILE env vars pointing to mounted paths."
echo " 2) Apply each patch, for example:"
echo " kubectl apply -f ${OUT_DIR}/<namespace>__<Kind>__<name>.patch.yaml"
echo
echo "Re-running audit to verify remaining secretKeyRef environment usage..."
output=$(kubectl get all --all-namespaces -o jsonpath='{range .items[?(@..secretKeyRef)]} {.kind} {.metadata.name} {"\n"}{end}')
if [ -z "$output" ]; then
echo "NO_ENV_SECRET_REFERENCES"
else
echo "ENV_SECRET_REFERENCES_FOUND"
echo "Remaining offending objects:"
echo "${output}" | sort -u
fi