Chapter 16Lesson 02~105 minutes

Environment Variables, Secrets, and Masked Output

CI systems inject configuration and credentials through environment variables and files, but those values are not automatically safe. Production Bash must validate ordinary variables and treat secrets as a separate trust boundary.

IntermediateCI/CD automationHands-on lab

Learning objectives

By the end of this lesson

  • Validate required and optional CI environment variables.
  • Keep secrets out of logs and shell tracing.
  • Reduce secret scope and lifetime.
  • Avoid unsafe cross-step secret persistence.
  • Understand the risk of secrets in untrusted contributions.

1. CI configuration enters Bash mostly through environment and files

Pipeline variables, repository settings, runner metadata, secrets, matrix values, and step outputs are often exposed as environment variables or temporary files. Bash should normalize and validate those inputs before using them.

CI input boundary
flowchart LR
  C["CI configuration"] --> E["environment / files"]
  E --> B["Bash normalization"]
  B --> V["validation"]
  V --> A["operation"]

2. Validate required environment variables

: "${DEPLOY_ENV:?DEPLOY_ENV is required}"
: "${ARTIFACT_ID:?ARTIFACT_ID is required}"

printf 'environment=%s artifact=%s\n' \
  "$DEPLOY_ENV" "$ARTIFACT_ID"

Required inputs should fail early before a network request or deployment begins.

3. Normalize optional variables once

LOG_LEVEL=${LOG_LEVEL:-INFO}
DRY_RUN=${DRY_RUN:-false}
MAX_ATTEMPTS=${MAX_ATTEMPTS:-3}

case $DRY_RUN in
  true|false) ;;
  *)
    printf 'DRY_RUN must be true or false\n' >&2
    exit 65
    ;;
esac

Treat environment variables as untrusted strings until validated.

4. A masked secret is still a secret

CI platforms can mask known secret values in logs, but masking is a last line of defense, not permission to print credentials.

Do not depend on redaction

Derived values, transformed secrets, substrings, encoded forms, or multiline content may not be masked the way you expect.

5. set -x can leak secret-bearing commands

set +x
token=$API_TOKEN
response=$(
  curl \
    --header "Authorization: Bearer $token" \
    --silent \
    "$url"
)
# Re-enable only if safe:
set -x

Trace mode prints shell commands after expansion. Scope it carefully or avoid it entirely around credentials.

6. Command arguments may be observable

# Prefer a supported credential file/stdin mechanism when available.
printf '%s' "$TOKEN" |
some_cli login --token-stdin

Passing a secret as a plain command-line argument can expose it to process inspection or logs. Tool-specific stdin or credential-provider mechanisms are usually safer.

7. Secret files need restrictive permissions and cleanup

umask 077
secret_file=$(mktemp) || exit 1

cleanup() {
  local status=$?
  rm -f -- "$secret_file"
  exit "$status"
}
trap cleanup EXIT

printf '%s\n' "$PRIVATE_KEY" > "$secret_file"

Do not assume the workspace is private merely because it belongs to a CI job.

8. Give secrets the narrowest possible scope

ScopeMeaningRisk
Step scopeOnly one command group receives itBest when feasible
Job scopeAvailable throughout one jobBroader exposure
Workflow/project scopeAvailable to many jobs or pipelinesHighest accidental-exposure risk

Minimize both who can access a secret and how long it exists.

9. Treat untrusted contributions as a different trust zone

Pull requests or patches from untrusted contributors can modify scripts. A secure CI design should not expose powerful production credentials to code that has not crossed an appropriate trust or approval boundary.

Code can exfiltrate secrets

If untrusted code can run with a credential, it can usually attempt to send that credential elsewhere. Masking logs does not prevent exfiltration.

10. Do not place secrets in cross-step outputs

# Safe-style output:
printf 'artifact_id=%s\n' "$artifact_id" >> "$CI_OUTPUT_FILE"

# Avoid persisting credentials as job outputs or artifacts.

Cross-step output mechanisms are designed for workflow state, not long-lived secret distribution.

11. Log safe metadata instead of values

if [[ -n ${API_TOKEN:-} ]]; then
  printf 'auth_token=present\n' >&2
else
  printf 'auth_token=missing\n' >&2
  exit 65
fi

printf 'token_length=%d\n' "${#API_TOKEN}" >&2

Even metadata such as length may be sensitive in some threat models, so only log what is operationally necessary.

12. Hands-on lab: secret-safe CI input normalization

mkdir -p "$HOME/devops-academy/bash/chapter16/lesson02"
cd "$HOME/devops-academy/bash/chapter16/lesson02"

cat > normalize-ci-env.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail

: "${DEPLOY_ENV:?DEPLOY_ENV is required}"
: "${API_TOKEN:?API_TOKEN is required}"

LOG_LEVEL=${LOG_LEVEL:-INFO}
DRY_RUN=${DRY_RUN:-false}

case $DEPLOY_ENV in
  dev|staging|prod) ;;
  *)
    printf 'invalid DEPLOY_ENV=%q\n' "$DEPLOY_ENV" >&2
    exit 65
    ;;
esac

case $DRY_RUN in
  true|false) ;;
  *)
    printf 'invalid DRY_RUN=%q\n' "$DRY_RUN" >&2
    exit 65
    ;;
esac

printf 'deploy_env=%s\n' "$DEPLOY_ENV"
printf 'log_level=%s\n' "$LOG_LEVEL"
printf 'dry_run=%s\n' "$DRY_RUN"
printf 'api_token=present\n' >&2
EOF

chmod u+x normalize-ci-env.sh

DEPLOY_ENV=staging \
API_TOKEN='do-not-print-this' \
DRY_RUN=true \
bash ./normalize-ci-env.sh

Verification checklist

13. Knowledge check

Question 1. Why should masked secrets still not be printed?

Question 2. What does set -x risk exposing?

Question 3. Why validate environment variables?

Question 4. Should untrusted pull-request code receive production secrets?

14. Summary

CI variables are an input boundary and secrets are a trust boundary. Normalize configuration, validate required values, keep credentials out of logs and command arguments where possible, scope them narrowly, and never treat masking as a substitute for secret-safe design.

15. Further reading

  • GNU Bash Reference Manual — shell parameters and tracing.
  • OWASP Secrets Management Cheat Sheet.
  • OWASP Logging Cheat Sheet.
  • CI platform documentation for secret masking, protected variables, and untrusted contributions.
Next lesson

Artifacts, Workspaces, and Cross-Step State

Continue Chapter 16 by making Bash behavior inside CI/CD pipelines more explicit and portable.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.