Secure Secret Handling and Environment Hygiene
Keep credentials out of process listings and logs using stdin, files, and scrubbed environments.
Secure Secret Handling and Environment Hygiene is a free DevOps Bootcamp lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Secret Hygiene Matters
Secrets — API keys, passwords, tokens — are the most sensitive data in any system. Mishandling them in Bash scripts is one of the most common and damaging security mistakes.
- Process listings: Arguments passed to commands appear in
ps aux,/proc/<pid>/cmdline, and system audit logs — visible to all users on the host. - Shell history: Commands typed interactively (and sometimes scripts) are recorded in
~/.bash_history. - Log files:
set -xtraces, application logs, and CI/CD output can capture variable values. - Environment leakage: Child processes inherit the full environment of their parent, including any exported secrets.
A hardened script treats secrets like radioactive material — minimise exposure time, limit surface area, and sanitise everything on the way out.
The Process Listing Attack Surface
When you pass a secret as a command-line argument, every user on the system can read it immediately via ps. This is not theoretical — it is exploited routinely in shared hosting and container environments.
The snippet below demonstrates the problem and the fix side by side.
#!/usr/bin/env bash
# DANGEROUS: password visible in 'ps aux' output
# curl -u "admin:SuperSecret123" https://api.example.com/data
# SAFE: pass credentials via stdin or a flag that reads from a file
# Many tools support reading secrets from stdin with '-' or dedicated flags:
# Option 1 — pipe the secret so it never appears in argv
echo 'SuperSecret123' | curl -u 'admin' --password-stdin \
https://api.example.com/data 2>/dev/null || true
# Option 2 — write a temporary netrc and point curl at it
# (covered in a later scene)
echo 'Secret never touches the command line this way'Reading Secrets from stdin
The safest interactive pattern is to read a secret at runtime using read -rs. The -s flag suppresses echo so the characters are never displayed, and -r prevents backslash interpretation.
Key points:
- The variable is never exported, so child processes cannot see it via
/proc/<pid>/environ. - After use, unset the variable immediately to shrink the exposure window.
- Avoid
echo "$SECRET"— useprintf '%s'to prevent a trailing newline from corrupting the value and to stay invisible in traces.
#!/usr/bin/env bash
set -euo pipefail
# Prompt on stderr so stdout stays clean for piping
read -rsp 'Enter API token: ' API_TOKEN <&2
printf '\n' >&2
# Use the secret — printf keeps it out of argv
response=$(printf '%s' "$API_TOKEN" | curl -sS -X POST \
-H 'Content-Type: application/json' \
--data-binary @- \
https://httpbin.org/post 2>/dev/null) || true
echo "Request sent."
# Scrub immediately — unset removes it from shell memory
unset API_TOKENSecrets in Files: Permissions and Ownership
When a secret must persist on disk (e.g., a service account key), the file's permissions are your primary defence.
- Mode 0600 — readable and writable only by the owner. No group, no world access.
- Mode 0400 — read-only for the owner. Prefer this for keys you should never accidentally overwrite.
- Store secret files under a dedicated directory such as
~/.secrets/or/run/secrets/(the latter is a RAM-backed tmpfs on many Linux systems and survives only until reboot). - Never place secret files inside a git-tracked directory without a rock-solid
.gitignore.
#!/usr/bin/env bash
set -euo pipefail
SECRETS_DIR="${HOME}/.secrets"
mkdir -p "$SECRETS_DIR"
chmod 700 "$SECRETS_DIR" # directory: only owner can list contents
KEY_FILE="${SECRETS_DIR}/api_token"
# Write secret — atomically restrict permissions before writing content
install -m 0600 /dev/null "$KEY_FILE"
printf '%s' 'my-super-secret-token' > "$KEY_FILE"
echo "Permissions:"
ls -la "$KEY_FILE"
# Read back safely — no subshell, no echo
API_TOKEN=$(< "$KEY_FILE")
echo "Token length: ${#API_TOKEN} chars (value not printed)"
unset API_TOKENUsing a .netrc File with curl
curl supports a ~/.netrc file (or an arbitrary path via --netrc-file) that maps hostnames to credentials. This keeps authentication data completely out of the command line and script body.
The file format is simple:
machine api.example.com
login admin
password s3cr3tBest practices:
- Always set
chmod 0600 ~/.netrc— curl refuses the file if world-readable on some systems. - Use
--netrc-file /run/secrets/netrcto point at a tmpfs-backed or container-injected secret. - Clean up temporary netrc files with a
trapon EXIT.
#!/usr/bin/env bash
set -euo pipefail
TMP_NETRC=$(mktemp)
chmod 0600 "$TMP_NETRC"
# Trap ensures cleanup even on error or signal
trap 'rm -f "$TMP_NETRC"' EXIT
# Write credentials to the temp netrc
cat > "$TMP_NETRC" <<'EOF'
machine httpbin.org
login myuser
password mypassword
EOF
curl -fsS --netrc-file "$TMP_NETRC" \
https://httpbin.org/basic-auth/myuser/mypassword \
-o /dev/null -w 'HTTP %{http_code}\n' || true
# trap fires here: $TMP_NETRC is deleted
echo 'Temp netrc cleaned up by trap.'Environment Variable Hygiene
Environment variables are a popular way to inject secrets into scripts (12-factor apps, CI/CD pipelines). However, they leak to every child process and appear in /proc/<pid>/environ for the lifetime of the process.
Defensive patterns:
- Import the secret into a local variable immediately and unset the env var so child processes cannot inherit it.
- Pass secrets to specific commands using
env -ior inline assignment rather than the full inherited environment. - Never
exporta secret variable — use assignment-only (noexport) when possible.
#!/usr/bin/env bash
set -euo pipefail
# Simulate a secret arriving via environment (e.g., from CI system)
export DB_PASSWORD='hunter2' # set by CI — we did not choose this
# Capture locally, then strip from environment immediately
db_password="$DB_PASSWORD"
unset DB_PASSWORD
# Verify the env var is gone before spawning any child process
if printenv DB_PASSWORD 2>/dev/null; then
echo 'ERROR: DB_PASSWORD still in environment!' >&2
exit 1
fi
echo 'Secret captured and env var scrubbed.'
echo "Password length: ${#db_password}"
unset db_passwordPreventing Secrets from Appearing in set -x Traces
set -x (xtrace) is invaluable for debugging but will print the value of every variable it expands — including secrets — to stderr. These traces often end up in CI logs or syslog.
Strategies to protect secrets while keeping tracing useful:
- Temporarily disable tracing around sensitive operations with
{ set +x; } 2>/dev/null. - Re-enable afterwards with
set -x. - Redirect the xtrace output to a separate file descriptor that goes to a protected log file, not to the public log stream.
#!/usr/bin/env bash
set -euo pipefail
set -x # tracing ON — safe for non-sensitive sections
echo 'Building application...'
SRC_DIR='/tmp/build'
mkdir -p "$SRC_DIR"
# Disable xtrace around secret handling (suppress the set +x line itself)
{ set +x; } 2>/dev/null
read -rsp 'Token (hidden from trace): ' SECRET_TOKEN <&2
printf '\n' >&2
token_len=${#SECRET_TOKEN}
unset SECRET_TOKEN
set -x # tracing back ON
echo "Token captured (length=$token_len). Continuing build..."
ls "$SRC_DIR"Scrubbing Secrets from Log Files
Even when you are careful, secrets sometimes find their way into log output — especially in verbose or legacy scripts. A logging wrapper function that redacts known patterns adds a safety net.
This pattern uses a regex-based replacement on all log output. It is a last-resort layer, not a substitute for the other hygiene practices already covered.
#!/usr/bin/env bash
set -euo pipefail
# A logging function that scrubs common secret patterns before writing
log() {
local line
# Replace anything that looks like key=VALUE or password=VALUE
line=$(printf '%s\n' "$*" \
| sed -E 's/(password|token|secret|key)=[^[:space:]]*/\1=***REDACTED***/gi')
printf '[%s] %s\n' "$(date -u '+%T')" "$line"
}
# Usage:
log 'Connecting to database with password=hunter2'
log 'Loaded API token=sk-abc123xyz secret'
log 'Build step completed successfully' # unchangedIsolated Environments with env -i
env -i starts a command with a completely empty environment, preventing any inherited variables — including accidental secrets — from reaching the child process. You then explicitly pass only what is needed.
This is especially useful when running untrusted scripts, build tools, or third-party utilities that might exfiltrate environment data.
#!/usr/bin/env bash
set -euo pipefail
# Polluted parent environment (simulating a CI runner)
export AWS_SECRET_ACCESS_KEY='AKIAIOSFODNN7EXAMPLE'
export GITHUB_TOKEN='ghp_faketoken123'
export HOME="$HOME"
export PATH="$PATH"
echo '--- Child sees full environment:'
env | grep -E 'AWS|GITHUB' | head -5
echo '--- Sanitised child (env -i) sees nothing secret:'
env -i HOME="$HOME" PATH="$PATH" TERM="${TERM:-dumb}" \
bash -c 'env | grep -E "AWS|GITHUB" || echo "No secrets visible"'
unset AWS_SECRET_ACCESS_KEY GITHUB_TOKENTemporary Secret Files on tmpfs
tmpfs is a RAM-backed filesystem. Files written there are never flushed to disk, eliminating the risk of secrets surviving in swap, disk cache, or snapshots.
- On Linux,
/dev/shmand/run/user/<uid>are typically tmpfs mounts. - Always pair tmpfs usage with a
trap EXITto delete files when the script finishes. - In containers (Docker, Kubernetes), secrets can be mounted as tmpfs volumes directly into
/run/secrets.
#!/usr/bin/env bash
set -euo pipefail
# Prefer /run/user/$UID (user-owned tmpfs) or /dev/shm (world-readable dir!)
if [[ -d "/run/user/$UID" ]]; then
TMPFS_DIR="/run/user/$UID"
elif [[ -d '/dev/shm' ]]; then
TMPFS_DIR='/dev/shm'
else
# Fallback: warn that disk will be used
echo 'WARNING: No tmpfs available; using /tmp (disk-backed)' >&2
TMPFS_DIR='/tmp'
fi
SECRET_FILE=$(mktemp "${TMPFS_DIR}/secret.XXXXXX")
chmod 0600 "$SECRET_FILE"
trap 'shred -u "$SECRET_FILE" 2>/dev/null || rm -f "$SECRET_FILE"' EXIT
printf '%s' 'my-runtime-token' > "$SECRET_FILE"
echo "Secret stored in: $SECRET_FILE"
df -T "$SECRET_FILE" | awk 'NR==2 {print "Filesystem type:", $2}'
# Use the secret...
token=$(< "$SECRET_FILE")
echo "Token length: ${#token}"
unset token
# trap fires on exit: file shreddedPutting It All Together: A Hardened Deployment Script
The following script combines every technique from this lesson into a realistic deployment helper. Observe how each layer of defence reinforces the others:
- stdin read with
-s— no terminal echo - tmpfs secret file with
trapcleanup - env scrubbing — secret unset before any subprocess
- xtrace guard — trace paused around sensitive code
- log redaction — safety-net regex before writing to log
#!/usr/bin/env bash
set -euo pipefail
### 1. Redacting logger
log() {
local msg
msg=$(printf '%s' "$*" \
| sed -E 's/(password|token|secret|key)=[^[:space:]]*/\1=***/gi')
printf '[%s] %s\n' "$(date -u +%T)" "$msg"
}
### 2. tmpfs secret store
TMPFS_DIR="${XDG_RUNTIME_DIR:-/tmp}"
SECRET_FILE=$(mktemp "${TMPFS_DIR}/deploy_token.XXXXXX")
chmod 0600 "$SECRET_FILE"
trap 'rm -f "$SECRET_FILE"; log "Secret file cleaned up."' EXIT
### 3. Read secret without trace
{ set +x; } 2>/dev/null
read -rsp 'Deploy token: ' _tok <&2; printf '\n' >&2
printf '%s' "$_tok" > "$SECRET_FILE"
unset _tok
set -x
### 4. Scrub inherited env vars before subprocess
unset DEPLOY_TOKEN 2>/dev/null || true
log 'Starting deployment...'
# Simulate deploy using secret from file (token never in argv)
# curl -H "Authorization: Bearer $(< $SECRET_FILE)" https://api.example.com/deploy
log 'Deployment complete. token=hidden_by_redactor'
echo 'Done.'Knowledge Check: Secret Exposure via Process Listing
Test your understanding of how secrets leak through process listings and how to prevent it.
Lesson Recap: Secure Secret Handling
You have completed Secure Secret Handling and Environment Hygiene. Here is a concise reference of everything covered:
- Process listings: Never pass secrets as command-line arguments — they appear in
ps auxand/proc/<pid>/cmdline. Use stdin piping or--netrc-fileinstead. - stdin reads: Use
read -rsto collect secrets interactively without terminal echo or shell history exposure. - File permissions: Secret files must be
chmod 0600(or0400). Useinstall -m 0600for atomic creation. - netrc files: Delegate credentials to a temp file pointed at by
--netrc-file; clean up withtrap EXIT. - Environment hygiene: Immediately
unsetsecret env vars after capturing them locally; neverexportthem unnecessarily; useenv -ifor child isolation. - xtrace guard: Wrap sensitive code in
{ set +x; } 2>/dev/null ... set -xto prevent debug traces from leaking values. - Log redaction: Use a
sed-based logger as a last-resort safety net. - tmpfs: Store runtime secrets in
/run/user/$UIDor/dev/shmso they never touch disk; shred on exit.
Defence in depth is the key mindset: no single measure is sufficient, but layering them together makes secret leakage extremely difficult.
Frequently asked questions
Is the “Secure Secret Handling and Environment Hygiene” lesson free?
Yes — the full text of “Secure Secret Handling and Environment Hygiene” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.
What will I learn in “Secure Secret Handling and Environment Hygiene”?
Keep credentials out of process listings and logs using stdin, files, and scrubbed environments. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DevOps Bootcamp?
No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Secure Secret Handling and Environment Hygiene” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DevOps Bootcamp lesson?
Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Preventing Command and Argument Injection
- Secure Secret Handling and Environment Hygiene
- Least-Privilege Execution and sudo Discipline
- Static Analysis and Auditing with ShellCheck