Stage 2 · Tools
DevOps Scripts in Practice
Secret Management in Scripts
Avoid hardcoded secrets — use Vault, env vars, and masked variables safely.
Secret Risks
Hardcoded secrets in scripts are a security disaster. They end up in version control, log files, process lists, and backups. Every script must treat secrets as sensitive data.
| Risk | Impact | Prevention |
|---|---|---|
| Secrets in git | Anyone with repo access sees them | Pre-commit hooks, git-secrets |
| Secrets in logs | Exposed in log aggregation systems | Log masking, structured logging |
| Secrets in ps output | Visible to all users on the system | Use files or env vars, not arguments |
| Secrets in core dumps | Persistent in crash dumps | Avoid in memory when possible |
Environment Variables
#!/usr/bin/env bash
set -euo pipefail
# Read secrets from environment
DATABASE_URL="{DATABASE_URL:?DATABASE_URL is required}"
API_KEY="{API_KEY:?API_KEY is required}"
# Use secrets (never print them)
curl -H "Authorization: Bearer $API_KEY" https://api.example.com
# Validate required env vars
require_env() {
local var="$1"
if [[ -z "{!var:-}" ]]; then
echo "Error: $var is not set" >&2
exit 1
fi
}
require_env "DATABASE_URL"
require_env "API_KEY"
require_env "AWS_SECRET_ACCESS_KEY"Environment variables keep secrets out of code. {VAR:?} syntax errors if the variable is unset. require_env() validates required variables before the script proceeds.
Never log or echo secret values. Use set +x before handling secrets, or use PS4 to mask them: export PS4='+{BASH_SOURCE}:{LINENO}: '. Secrets in logs are exposed to anyone with log access.
Vault CLI
#!/usr/bin/env bash
set -euo pipefail
# Login to Vault
vault login method=token "$VAULT_TOKEN"
# Read a secret
DB_PASSWORD=$(vault kv get -field=password secret/myapp/database)
# Read multiple fields
eval "$(vault kv get -field=env secret/myapp/config)"
# Use Vault Agent for automatic token renewal
# vault agent -config=vault-agent.hcl
# Dynamic database credentials
DB_CREDS=$(vault read -format=json database/creds/readonly)
DB_USER=$(echo "$DB_CREDS" | jq -r '.data.username')
DB_PASS=$(echo "$DB_CREDS" | jq -r '.data.password')
# Use the credentials
PGPASSWORD="$DB_PASS" psql -U "$DB_USER" -h db.example.com myapp
# Revoke the credentials when done
vault write -f database/revoke/readonlyVault provides dynamic, short-lived secrets. Database credentials are generated on-demand and revoked after use. This eliminates long-lived credentials that can be compromised.
envsubst
#!/usr/bin/env bash
set -euo pipefail
# Create a template
cat > config.yml.tmpl << 'EOF'
database:
host: $DB_HOST
port: $DB_PORT
user: $DB_USER
password: $DB_PASSWORD
api:
key: $API_KEY
secret: $API_SECRET
EOF
# Set secrets as env vars (from Vault, AWS SM, etc.)
export DB_HOST="db.example.com"
export DB_PORT="5432"
export DB_USER="myapp"
export DB_PASSWORD=$(vault kv get -field=password secret/db)
export API_KEY=$(aws ssm get-parameter --name "/myapp/api-key" --with-decryption --query "Parameter.Value" --output text)
# Substitute variables
envsubst < config.yml.tmpl > config.yml
# Secure the generated file
chmod 600 config.yml
echo "Configuration generated"envsubst replaces $VAR references in a template with environment variable values. The secrets are injected from secure sources (Vault, AWS SSM) and never appear in the template file.
CI/CD Secrets
#!/usr/bin/env bash
set -euo pipefail
# In GitHub Actions, secrets are available as env vars
# Set in repository settings: Settings > Secrets > Actions
# Use secrets
if [[ -n "{DEPLOY_KEY:-}" ]]; then
echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh -i ~/.ssh/deploy_key deploy@server "echo 'Connected'"
rm -f ~/.ssh/deploy_key
fi
# AWS credentials
if [[ -n "{AWS_ACCESS_KEY_ID:-}" ]]; then
aws s3 sync ./dist s3://my-bucket/
fi
# Mask secrets in logs
mask_secret() {
local secret="$1"
local masked="{secret:0:4}****{secret: -4}"
echo "::add-mask::$secret"
echo "Using secret: $masked"
}CI systems store secrets encrypted. They are available as environment variables during builds. Always clean up secret files after use. Use add-mask to prevent accidental logging.
Secret Management Best Practices
#!/usr/bin/env bash
set -euo pipefail
# 1. Never commit secrets
# Add to .gitignore
cat >> .gitignore << 'EOF'
.env
*.pem
*.key
config.yml
EOF
# 2. Use a secrets file with restricted permissions
SECRETS_FILE=".env"
if [[ -f "$SECRETS_FILE" ]]; then
# shellcheck source=/dev/null
source "$SECRETS_FILE"
else
echo "Error: $SECRETS_FILE not found" >&2
echo "Create it with: DATABASE_URL=... API_KEY=..." >&2
exit 1
fi
# 3. Clean up temp files containing secrets
cleanup() {
rm -f /tmp/secrets_* 2>/dev/null
rm -f ~/.ssh/deploy_key 2>/dev/null
}
trap cleanup EXIT
# 4. Use temporary files for complex secrets
SECRET_FILE=$(mktemp)
chmod 600 "$SECRET_FILE"
echo "$COMPLEX_SECRET" > "$SECRET_FILE"
# Use the file
rm -f "$SECRET_FILE"
# 5. Rotate secrets regularly
echo "Last secret rotation: $(cat .last-rotation 2>/dev/null || echo 'never')"
echo "Rotate secrets every 90 days"Best practices: 1) Never commit secrets to git. 2) Use .env files with restricted permissions. 3) Clean up temp files. 4) Use temp files for complex secrets. 5) Rotate regularly.
Treat every secret as compromised. Rotate regularly. Use short-lived credentials (Vault dynamic secrets). Audit access. Never share secrets via email, chat, or commit messages.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.