#!/usr/bin/env bash
#
# Remediation: Move secret-like literal env vars in Pods to a Secret and
# reference them via valueFrom.secretKeyRef.
#
# Scope: Any machine with kubectl access to the EKS cluster.
#
# Requirements:
# - kubectl configured to point at the target cluster
# - jq and yq (https://github.com/mikefarah/yq) installed and in PATH
#
# Notes:
# - This script is idempotent: it only changes Pods with matching env vars.
# - It creates/patches one Secret per Pod: <pod-name>-env-secrets in the
# same namespace, with one key per offending env var.
# - It deletes the original Pod so its controller (Deployment, ReplicaSet,
# StatefulSet, Job, etc.) recreates it with the new env references.
# - Standalone Pods (no controller ownerReference) will be re-created
# directly by the script with the updated spec.
set -euo pipefail
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl not found in PATH" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found in PATH" >&2
exit 1
fi
if ! command -v yq >/dev/null 2>&1; then
echo "yq not found in PATH (https://github.com/mikefarah/yq)" >&2
exit 1
fi
SED_INPLACE=("-i")
if [[ "$(uname -s)" == "Darwin" ]]; then
SED_INPLACE=("-i" "")
fi
TMPDIR="$(mktemp -d)"
trap 'rm -rf "${TMPDIR}"' EXIT
echo "Discovering Pods with sensitive literal env vars..."
# Reuse the provided audit logic to find offending pods and env vars
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| .name as $c
| (.env // [])[]
| select((.value != null) and (.name | test("PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|CREDENTIAL|PRIVATE_KEY"; "i")))
| {
namespace: $m.namespace,
pod: $m.name,
container: $c,
envName: .name,
envValue: .value,
ownerKind: ($own.kind // null),
ownerName: ($own.name // null),
ownerUid: ($own.uid // null)
}
] | .[]' > "${TMPDIR}/offending-envs.json" || true
if [[ ! -s "${TMPDIR}/offending-envs.json" ]]; then
echo "No offending env vars found. Cluster appears compliant."
exit 0
fi
echo "Offending env vars detected. Grouping by pod..."
# Build a list of unique pod identifiers (namespace/pod)
jq -r '[.namespace + "/" + .pod] | unique[]' "${TMPDIR}/offending-envs.json" > "${TMPDIR}/pods.txt"
while IFS=/ read -r NS POD; do
echo "Processing Pod ${NS}/${POD}..."
# Extract all offending env vars for this pod
jq --arg ns "${NS}" --arg pod "${POD}" '
select(.namespace == $ns and .pod == $pod)
' "${TMPDIR}/offending-envs.json" > "${TMPDIR}/pod-envs.json"
if [[ ! -s "${TMPDIR}/pod-envs.json" ]]; then
echo " No offending envs found for ${NS}/${POD} (skipping)."
continue
fi
SECRET_NAME="${POD}-env-secrets"
# Create or update Secret manifest
SECRET_FILE="${TMPDIR}/${NS}-${SECRET_NAME}-secret.yaml"
cat > "${SECRET_FILE}" <<EOF
apiVersion: v1
kind: Secret
metadata:
name: ${SECRET_NAME}
namespace: ${NS}
type: Opaque
data: {}
EOF
# For each env var, add/overwrite key in Secret (base64-encoded)
while read -r ENV_JSON; do
ENV_NAME=$(jq -r '.envName' <<<"${ENV_JSON}")
ENV_VALUE=$(jq -r '.envValue' <<<"${ENV_JSON}")
# Base64 encode value
B64_VALUE=$(printf '%s' "${ENV_VALUE}" | base64 | tr -d '\n')
yq e "${SED_INPLACE[@]}" \
".data.\"${ENV_NAME}\" = \"${B64_VALUE}\"" \
"${SECRET_FILE}"
done < <(cat "${TMPDIR}/pod-envs.json")
echo " Applying Secret ${NS}/${SECRET_NAME}..."
kubectl apply -f "${SECRET_FILE}"
# Get full Pod manifest
POD_FILE="${TMPDIR}/${NS}-${POD}-pod.yaml"
kubectl get pod "${POD}" -n "${NS}" -o yaml > "${POD_FILE}"
# Patch env vars in both containers and initContainers
for PATH in "spec.containers" "spec.initContainers"; do
COUNT=$(yq e ".${PATH} // [] | length" "${POD_FILE}")
if [[ "${COUNT}" -eq 0 ]]; then
continue
fi
for (( i=0; i<COUNT; i++ )); do
# For each offending env in this pod, if present in this container, convert to valueFrom.secretKeyRef
while read -r ENV_JSON; do
ENV_NAME=$(jq -r '.envName' <<<"${ENV_JSON}")
EXISTS=$(yq e ".${PATH}[${i}].env[]? | select(.name == \"${ENV_NAME}\") | length > 0" "${POD_FILE}" || echo "false")
if [[ "${EXISTS}" != "true" ]]; then
continue
fi
echo " Updating ${PATH}[${i}] env ${ENV_NAME} to use Secret ${SECRET_NAME}..."
# Remove literal value and replace with valueFrom.secretKeyRef
yq e "${SED_INPLACE[@]}" "
.${PATH}[${i}].env |=
map(
if .name == \"${ENV_NAME}\" then
{name: .name, valueFrom: {secretKeyRef: {name: \"${SECRET_NAME}\", key: \"${ENV_NAME}\"}}}
else .
end
)
" "${POD_FILE}"
done < <(cat "${TMPDIR}/pod-envs.json")
done
done
# Remove Pod-specific runtime fields that prevent re-creation
yq e "${SED_INPLACE[@]}" '
del(.metadata.uid) |
del(.metadata.resourceVersion) |
del(.metadata.selfLink) |
del(.metadata.creationTimestamp) |
del(.metadata.generation) |
del(.metadata.managedFields) |
del(.status)
' "${POD_FILE}"
# Determine if this Pod has a controller ownerReference
OWNER_KIND=$(jq -r '.[0].ownerKind // ""' "${TMPDIR}/pod-envs.json")
OWNER_NAME=$(jq -r '.[0].ownerName // ""' "${TMPDIR}/pod-envs.json")
echo " Deleting original Pod ${NS}/${POD}..."
kubectl delete pod "${POD}" -n "${NS}" --wait=false || true
if [[ -n "${OWNER_KIND}" && "${OWNER_KIND}" != "null" && -n "${OWNER_NAME}" && "${OWNER_NAME}" != "null" ]]; then
echo " Pod is managed by ${OWNER_KIND}/${OWNER_NAME}; controller will recreate it with updated Secret-based env."
# We do NOT apply the pod manifest directly in this case, as the controller spec still has old env config.
# To fully remediate, the owning controller spec should be updated via its manifest/IaC outside this script.
echo " NOTE: You must update the ${OWNER_KIND} ${OWNER_NAME} spec to reference Secret ${SECRET_NAME} instead of literal env values."
else
echo " Pod appears standalone; recreating updated Pod from manifest..."
kubectl apply -f "${POD_FILE}"
fi
done < "${TMPDIR}/pods.txt"
echo
echo "Re-running verification..."
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| (.spec.nodeName // "") as $node
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| ((.spec.containers // []) + (.spec.initContainers // []))[]
| .name as $c
| (.env // [])[]
| select((.value != null) and (.name | test("PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|CREDENTIAL|PRIVATE_KEY"; "i")))
| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $node == "" then "" else " node=\($node)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)
+ " container=\($c) env=\(.name) is_compliant=false"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'