#!/usr/bin/env bash
#
# Automation: Prefer using secrets as files over secrets as environment variables
#
# Scope: any machine with kubectl access
#
# WARNING:
# - This script CANNOT automatically rewrite application code to read secrets from files.
# - It performs discovery, creates candidate patched manifests that mount secrets as files,
# and shows diffs. A human must review, adjust application code, and then apply.
#
# Requirements:
# - kubectl configured with sufficient RBAC to get/list Pods/Deployments/ReplicaSets/StatefulSets/DaemonSets/Jobs/CronJobs
# - jq, yq (v4) installed for JSON/YAML processing
#
# Idempotence:
# - Re-running will re-generate the same work directory content and re-run the verification.
#
set -euo pipefail
WORKDIR="${PWD}/env-secret-remediation"
mkdir -p "${WORKDIR}/original" "${WORKDIR}/patched" "${WORKDIR}/diffs"
echo "[1/5] Discovering objects that use env vars sourced from Secrets..."
# Capture all objects that have secretKeyRef in their spec
# (Pods and higher-level controllers)
kubectl get all --all-namespaces -o json > "${WORKDIR}/all.json"
# Extract namespaced objects with secretKeyRef
# We will focus on Pod, Deployment, StatefulSet, DaemonSet, Job, CronJob, ReplicaSet
jq '
.items[]
| select(.. | has("secretKeyRef")?)
| select(.kind == "Pod"
or .kind == "Deployment"
or .kind == "StatefulSet"
or .kind == "DaemonSet"
or .kind == "Job"
or .kind == "CronJob"
or .kind == "ReplicaSet")
| {kind, apiVersion, ns: .metadata.namespace, name: .metadata.name}
' "${WORKDIR}/all.json" > "${WORKDIR}/affected.json"
if [ ! -s "${WORKDIR}/affected.json" ]; then
echo "NO_ENV_SECRET_REFERENCES"
exit 0
fi
echo "ENV_SECRET_REFERENCES_FOUND"
echo "Details will be written under: ${WORKDIR}"
echo
echo "[2/5] Exporting original manifests for editing..."
# Helper to export one object
export_object() {
local ns="$1" kind="$2" name="$3"
local file="${WORKDIR}/original/${ns}___${kind,,}___${name}.yaml"
kubectl get "${kind,,}.${kind,,}.k8s.io" -n "${ns}" "${name}" -o yaml > "${file}" 2>/dev/null || \
kubectl get "${kind}" -n "${ns}" "${name}" -o yaml > "${file}"
}
# Export each affected object once
jq -r '.ns+" "+.kind+" "+.name' "${WORKDIR}/affected.json" | sort -u | while read -r ns kind name; do
echo " - Exporting ${kind}/${name} in namespace ${ns}"
export_object "${ns}" "${kind}" "${name}" || {
echo " WARNING: Failed to export ${kind}/${name} in ${ns}, skipping."
continue
}
done
echo
echo "[3/5] Creating PATCH CANDIDATES that mount secrets as files (NOT applied automatically)..."
cat > "${WORKDIR}/README-HUMAN-ACTION.txt" <<'EOF'
These "patched" manifests are CANDIDATES ONLY.
What has been done:
- For each container env entry like:
env:
- name: MY_SECRET
valueFrom:
secretKeyRef:
name: my-secret
key: password
a corresponding Secret volume and mount have been proposed:
spec:
volumes:
- name: my-secret
secret:
secretName: my-secret
containers:
- name: ...
volumeMounts:
- name: my-secret
mountPath: /var/run/secrets/my-secret
readOnly: true
- The env entries referencing that secret are LEFT IN PLACE.
A human must:
- Update application code to read from the mounted file instead of the env var.
- Then remove the env var(s) once safe.
How to use:
1. Review the diffs in ./diffs to understand proposed volume/volumeMount changes.
2. Adjust the patched YAMLs under ./patched as needed:
- Consolidate multiple keys/paths.
- Ensure no mountPath conflicts.
3. Update your application code to read secrets from files.
4. When ready, apply the patched manifests, for example:
kubectl apply -f ./patched/<file>.yaml
5. Rerun this script to verify that no remaining env->secretKeyRef usages are present.
NOTE:
- This transformation is heuristic; it does NOT understand your app semantics.
- You may decide some env-based secrets are acceptable risk; in that case, document the decision.
EOF
# Function to process one YAML file with yq:
# - For each container/env with .valueFrom.secretKeyRef, ensure:
# * spec.volumes has a secret volume named after the Secret (deduplicated)
# * container.volumeMounts has a mount pointing to /var/run/secrets/<secretName>
process_yaml() {
local in_file="$1"
local out_file="$2"
yq eval '
# Build a map of secret names used in env -> to ensure volumes exist
def secretNames:
(.. | objects | select(has("env")) | .env[]? | select(has("valueFrom"))
| select(.valueFrom.secretKeyRef.name != null)
| .valueFrom.secretKeyRef.name) as $name
| {$name};
# Collect all unique secret names referenced by env vars
. as $root
| ($root
| .. | objects
| select(has("env"))
| .env[]?
| select(has("valueFrom"))
| select(.valueFrom.secretKeyRef.name != null)
| .valueFrom.secretKeyRef.name) as $sec
| $root
| .metadata.annotations."env-secret-remediation/last-processed" = "'"$(date -u +%FT%TZ)"'"
# Ensure volumes list exists
| ( .spec.volumes // [] ) as $vols
| .spec.volumes =
( $vols +
( [ ($sec | select(. != null)) ] | unique | map(
if ($vols[]? | select(.name == .) ) then empty
else
{
name: .,
secret: { secretName: . }
}
end
))
)
# For each container/ephemeralContainer/initContainer, ensure volumeMounts for each secret
| (
(.spec.containers // []) as $containers
| .spec.containers = (
$containers
| map(
. as $c
| ($c.env // []) as $envs
| ($envs
| map(select(has("valueFrom"))
| select(.valueFrom.secretKeyRef.name != null)
| .valueFrom.secretKeyRef.name)
) as $used
| ($c.volumeMounts // []) as $vm
| .volumeMounts =
($vm +
( [ $used[]? ] | unique | map(
if ($vm[]? | select(.name == .)) then empty
else
{
name: .,
mountPath: ("/var/run/secrets/" + .),
readOnly: true
}
end
))
)
)
)
)
# Do the same for initContainers
| (
(.spec.initContainers // []) as $containers
| .spec.initContainers = (
$containers
| map(
. as $c
| ($c.env // []) as $envs
| ($envs
| map(select(has("valueFrom"))
| select(.valueFrom.secretKeyRef.name != null)
| .valueFrom.secretKeyRef.name)
) as $used
| ($c.volumeMounts // []) as $vm
| .volumeMounts =
($vm +
( [ $used[]? ] | unique | map(
if ($vm[]? | select(.name == .)) then empty
else
{
name: .,
mountPath: ("/var/run/secrets/" + .),
readOnly: true
}
end
))
)
)
)
)
# And for ephemeralContainers if present
| (
(.spec.ephemeralContainers // []) as $containers
| .spec.ephemeralContainers = (
$containers
| map(
. as $c
| ($c.env // []) as $envs
| ($envs
| map(select(has("valueFrom"))
| select(.valueFrom.secretKeyRef.name != null)
| .valueFrom.secretKeyRef.name)
) as $used
| ($c.volumeMounts // []) as $vm
| .volumeMounts =
($vm +
( [ $used[]? ] | unique | map(
if ($vm[]? | select(.name == .)) then empty
else
{
name: .,
mountPath: ("/var/run/secrets/" + .),
readOnly: true
}
end
))
)
)
)
)
' "${in_file}" > "${out_file}"
}
for f in "${WORKDIR}/original"/*.yaml; do
[ -e "$f" ] || continue
base="$(basename "$f")"
out="${WORKDIR}/patched/${base}"
echo " - Generating candidate patch for ${base}"
process_yaml "${f}" "${out}" || {
echo " WARNING: Failed to process ${f}, skipping."
continue
}
# Generate a diff for human review
diff_file="${WORKDIR}/diffs/${base}.diff"
diff -u "${f}" "${out}" > "${diff_file}" || true
done
echo
echo "[4/5] NOTE: No changes have been applied to the cluster."
cat <<EOF
Next steps (manual, REQUIRED):
1) Review candidate patches under:
- Original: ${WORKDIR}/original
- Patched: ${WORKDIR}/patched
- Diffs: ${WORKDIR}/diffs
2) For each object:
- Confirm the proposed Secret volumes and volumeMounts are correct.
- Update application code to read secrets from the mounted files at:
/var/run/secrets/<secretName>/
(or adjust mountPath as needed).
- Once code is updated and deployed, remove the corresponding env variables
that use valueFrom.secretKeyRef from your manifests.
- Apply your finalized manifests with:
kubectl apply -f <your-reviewed-file>.yaml
3) After you have deployed your changes, re-run this script to verify.
EOF
echo "[5/5] Verification: checking for remaining env-based Secret references..."
output="$(kubectl get all --all-namespaces -o jsonpath='{range .items[?(@..secretKeyRef)]} {.kind} {.metadata.name} {"\n"}{end}')"
if [ -z "${output}" ]; then
echo "Verification result: NO_ENV_SECRET_REFERENCES"
else
echo "Verification result: ENV_SECRET_REFERENCES_FOUND"
echo "Remaining objects with env->Secret references:"
echo "${output}"
fi