Sensitive Values Should Not Be Passed As Literal Env Vars
More Info:
Verifies secret-like env vars are not set as literal values. Literal values land in the pod manifest, logs and kubectl describe.
Risk Level
Medium
Address
Security
Compliance Standards
- Cloudanix Best Practice
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
List offending Pods and identify the owning workload (run on any machine with kubectl access):
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'From each line, note
ns=...,owner=...(e.g.,Deployment/default/my-app/...),container=..., andenv=.... -
For each offending env var, create or update a Secret holding the sensitive value (run on any machine with kubectl access, per namespace). Replace placeholders with the real values:
kubectl -n default create secret generic my-app-secret \--from-literal=DB_PASSWORD='actual-password-value' \--dry-run=client -o yaml > /tmp/my-app-secret.yamlkubectl apply -f /tmp/my-app-secret.yamlUse one key per env var (e.g.,
DB_PASSWORD,API_TOKEN). -
Patch the owning workload manifest to use
valueFrom.secretKeyRefinstead of a literalvalue:(run on any machine with kubectl access). First, export the current manifest:kubectl -n default get deployment my-app -o yaml > /tmp/my-app-deploy.yamlThen edit
/tmp/my-app-deploy.yaml:- Find the container with the offending env, e.g.:
env:- name: DB_PASSWORDvalue: "actual-password-value"
- Replace with:
env:- name: DB_PASSWORDvalueFrom:secretKeyRef:name: my-app-secretkey: DB_PASSWORD
- Ensure you do this for every env var flagged by the audit.
- Find the container with the offending env, e.g.:
-
Apply the updated workload manifest so new Pods use the Secret (run on any machine with kubectl access):
kubectl apply -f /tmp/my-app-deploy.yamlFor controllers that don’t roll Pods automatically (e.g., some StatefulSets/Jobs), trigger a restart as appropriate, understanding this will restart the affected Pods.
-
Confirm new Pods no longer contain literal sensitive values in their env definition (run on any machine with kubectl access):
kubectl -n default get pods -l app=my-app -o yaml | \grep -A2 "name: DB_PASSWORD"You should see only a
valueFrom: secretKeyRef:block and novalue:line for the sensitive variable names. -
Re-run the benchmark audit to verify compliance (run on any machine with kubectl access):
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'The cluster is compliant when the output shows
is_compliant=trueand nois_compliant=falserows.
Using kubectl
On any machine with kubectl access to the cluster:
-
Identify the offending Pod and env var from the finding output, for example:
- Namespace:
app-namespace - Pod name:
web-7c9c6c7d4b-abcde - Container:
web - Env var:
DB_PASSWORD
- Namespace:
-
Extract the current Pod spec (for reference only; do not apply this Pod directly because it is usually managed by a controller such as a Deployment):
kubectl get pod web-7c9c6c7d4b-abcde -n app-namespace -o yaml > /tmp/web-pod.yaml
- Determine the owning controller (e.g., Deployment) from the
owner=field in the finding or via:
kubectl get pod web-7c9c6c7d4b-abcde -n app-namespace -o jsonpath='{.metadata.ownerReferences[0].kind}{" "}{.metadata.ownerReferences[0].name}{"\n"}'
Assume the result is Deployment web.
- Create a Secret that will hold the sensitive value (if one does not already exist). Replace the literal with the real value:
kubectl -n app-namespace create secret generic db-credentials \
--from-literal=DB_PASSWORD='REPLACE_WITH_REAL_PASSWORD'
- Patch the owning Deployment to reference the Secret via
valueFrom.secretKeyRefinstead of a literalvalue:. First, fetch the Deployment manifest:
kubectl get deployment web -n app-namespace -o yaml > /tmp/web-deploy.yaml
- Edit
/tmp/web-deploy.yaml:- In the relevant container under
spec.template.spec.containers[], find the existing env entry:
- In the relevant container under
env:
- name: DB_PASSWORD
value: "REPLACE_WITH_REAL_PASSWORD"
- Replace it with:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- Apply the updated Deployment manifest:
kubectl apply -f /tmp/web-deploy.yaml
This will recreate Pods managed by the Deployment with the Secret-based env var.
- Verification: rerun the audit (or a scoped variant) and confirm no
is_compliant=falserows remain for this Pod/container/env:
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'
Automation
#!/usr/bin/env bash
#
# Fix CBP C4.1: Sensitive values should not be passed as literal env vars
#
# This script:
# - Scans all non-system namespaces for Pods with secret-like env var NAMES
# (PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|CREDENTIAL|PRIVATE_KEY) that have
# literal values set via `env[].value`.
# - Emits guidance and manifests you can use to convert them to Secret
# references (`valueFrom.secretKeyRef`).
#
# IMPORTANT:
# - There is no fully automatic, safe way to invent Secrets and wire them
# into apps without knowing the correct values and ownership model.
# - This script is NON-DESTRUCTIVE: it never deletes or patches live Pods.
# - Use the generated manifests and commands as a starting point, then
# review and apply manually.
#
# Requirements (run on any machine with kubectl access):
# - kubectl
# - jq
#
# Re-runnable: it only re-generates report files and guidance.
set -euo pipefail
WORKDIR="${PWD}/cbp-c4-1-remediation"
REPORT="${WORKDIR}/literal-secret-envvars.jsonl"
GUIDE="${WORKDIR}/literal-secret-envvars-remediation.txt"
mkdir -p "${WORKDIR}"
echo "Scanning cluster for Pods with sensitive literal 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.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",
namespace: $m.namespace,
pod: $m.name,
uid: $m.uid,
apiVersion: "v1",
created: ($m.creationTimestamp // ""),
node: $node,
labels: $labels,
ownerKind: ($own.kind // ""),
ownerName: ($own.name // ""),
ownerUID: ($own.uid // ""),
container: $c,
envName: .name,
# DO NOT write the env value to disk, just record that it exists
hasLiteralValue: true
}
] as $rows
| if ($rows | length) == 0
then "[]" # empty JSON array
else $rows
end
' > "${REPORT}"
if [ "$(jq 'length' "${REPORT}")" -eq 0 ]; then
echo "No Pods with sensitive literal env vars found. Cluster appears compliant."
echo "is_compliant=true"
exit 0
fi
echo "Found $(jq 'length' "${REPORT}") offending env vars. Writing remediation guidance to:"
echo " - ${REPORT} (structured JSONL-style array)"
echo " - ${GUIDE} (human-readable guidance)"
cat > "${GUIDE}" <<'EOF'
CBP C4.1 REMEDIATION GUIDE
==========================
Sensitive values must NOT be passed as literal env vars via `env[].value`.
Instead, use:
- `env[].valueFrom.secretKeyRef` (recommended), or
- a mounted Secret volume.
For every row in literal-secret-envvars.jsonl:
1. Identify the owning controller (Deployment/StatefulSet/Job/etc.) using:
- fields ownerKind, ownerName, namespace in the JSON.
2. On a machine with kubectl access, inspect the owning object, for example:
- kubectl -n <namespace> get deployment <ownerName> -o yaml
3. For each container/envName pair:
a. Retrieve the CURRENT value from a live Pod (one-time, to seed a Secret):
POD="<pod_name_from_report>"
NS="<namespace_from_report>"
kubectl -n "${NS}" get pod "${POD}" -o json \
| jq -r '
(.spec.containers[] + (.spec.initContainers // []))[]
| select(.name=="<container_from_report>")
| .env[]
| select(.name=="<envName_from_report>")
| .value
'
NOTE: Run this only long enough to bootstrap the Secret, then delete
any local copies. Treat the output as sensitive.
b. Create (or reuse) a Secret to hold this key. Example:
NS="<namespace_from_report>"
SECRET_NAME="<app>-sensitive-env"
kubectl -n "${NS}" create secret generic "${SECRET_NAME}" \
--from-literal="<envName_from_report>=<VALUE_FROM_STEP_3A>"
If the Secret already exists, update it instead:
kubectl -n "${NS}" create secret generic "${SECRET_NAME}" \
--from-literal="<envName_from_report>=<VALUE_FROM_STEP_3A>" \
--dry-run=client -o yaml | kubectl apply -f -
c. Edit the owning controller to replace `value:` with `valueFrom:`:
kubectl -n <namespace_from_report> edit <ownerkind_from_report>/<ownerName_from_report>
In the relevant container's env section, change:
- name: <envName_from_report>
value: "<literal_value>"
TO:
- name: <envName_from_report>
valueFrom:
secretKeyRef:
name: <SECRET_NAME>
key: <envName_from_report>
Save and exit. The controller will roll out new Pods using the Secret.
4. Pods not managed by a controller (bare Pods):
- Prefer to replace them with a Deployment/Job and follow the same pattern.
- If you must keep a bare Pod, update its manifest in your IaC / manifests
to use `valueFrom.secretKeyRef` and recreate the Pod.
5. Post-change verification for each namespace:
- Re-run the audit:
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
'
- You should ultimately see only:
is_compliant=true
EOF
echo
echo "Next steps:"
echo " 1) Review ${REPORT} to see each violating Pod/container/envName."
echo " jq -C . ${REPORT} | less -R"
echo " 2) Follow the step-by-step instructions in:"
echo " ${GUIDE}"
echo
echo "When you have updated the owning controllers to use valueFrom.secretKeyRef,"
echo "re-run this script or the audit command to confirm you get: is_compliant=true"
echo
echo "Running verification audit now (read-only):"
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
'