0Pricing
DevOps Bootcamp · Lesson

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 -x traces, 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" — use printf '%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_TOKEN

Secrets 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_TOKEN

Using 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 s3cr3t

Best practices:

  • Always set chmod 0600 ~/.netrc — curl refuses the file if world-readable on some systems.
  • Use --netrc-file /run/secrets/netrc to point at a tmpfs-backed or container-injected secret.
  • Clean up temporary netrc files with a trap on 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 -i or inline assignment rather than the full inherited environment.
  • Never export a secret variable — use assignment-only (no export) 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_password

Preventing 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'   # unchanged

Isolated 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_TOKEN

Temporary 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/shm and /run/user/<uid> are typically tmpfs mounts.
  • Always pair tmpfs usage with a trap EXIT to 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 shredded

Putting 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 trap cleanup
  • 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 aux and /proc/<pid>/cmdline. Use stdin piping or --netrc-file instead.
  • stdin reads: Use read -rs to collect secrets interactively without terminal echo or shell history exposure.
  • File permissions: Secret files must be chmod 0600 (or 0400). Use install -m 0600 for atomic creation.
  • netrc files: Delegate credentials to a temp file pointed at by --netrc-file; clean up with trap EXIT.
  • Environment hygiene: Immediately unset secret env vars after capturing them locally; never export them unnecessarily; use env -i for child isolation.
  • xtrace guard: Wrap sensitive code in { set +x; } 2>/dev/null ... set -x to 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/$UID or /dev/shm so 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

  1. Preventing Command and Argument Injection
  2. Secure Secret Handling and Environment Hygiene
  3. Least-Privilege Execution and sudo Discipline
  4. Static Analysis and Auditing with ShellCheck
← Back to DevOps Bootcamp