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.
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.
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
;;
esacTreat 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.
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 -xTrace 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-stdinPassing 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
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.
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}" >&2Even 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.shVerification 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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.