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 Linux Command Line & Bash Scripting Mastery lesson on CoddyKit. This is 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 Linux Command Line & Bash Scripting Mastery learning path, and your progress syncs across the web and the CoddyKit app. The Linux Command Line & Bash Scripting Mastery course includes 4 lessons in total.
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.
Learn Bash with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 22
- Lessons
- 88
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. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Linux Command Line & Bash Scripting Mastery course, upgrade to CoddyKit PRO. The Linux Command Line & Bash Scripting Mastery course includes 4 lessons in total.
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 Linux Command Line & Bash Scripting Mastery 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 Linux Command Line & Bash Scripting Mastery?
No prior experience is required. Linux Command Line & Bash Scripting Mastery on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.
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 Linux Command Line & Bash Scripting Mastery lesson?
Yes. Every Linux Command Line & Bash Scripting Mastery 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