Advanced DevSecOps

Secrets management in the pipeline — no credentials in env vars or logs

Secrets in pipelines are a critical blind spot. Tokens, passwords, and API keys tend to appear in unexpected places: environment variables printed in logs, committed .env files, or secrets passed as command-line arguments visible in process history.

Where secrets leak in pipelines

Common exposure vectors:

1. Source code:
   API_KEY = "sk-prod-abc123..."     # hardcoded in a Python file
   password: "admin123"              # in a committed docker-compose.yml

2. Environment variables printed in logs:
   env | grep -i key                 # casual debug that becomes a permanent log
   echo "Connecting to $DB_PASSWORD" # innocent intent, fatal effect

3. Git history:
   git log -p | grep -i password     # secret removed but still in history

4. Command-line arguments:
   docker run -e DB_PASS=secret123   # visible in ps aux to other processes

5. Build artifacts:
   Dockerfile with ARG SECRET=xxx    # baked into the image layer

Detection: secrets scanning in the pipeline

Gitleaks

# GitHub Actions — gitleaks on PR
jobs:
  secrets-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # full history to detect old secrets
      - name: Gitleaks scan
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Trufflehog

# scan the repository including history
trufflehog git file://. --only-verified

Correct storage: dedicated vaults and stores

Never:
  - Committed .env (even in a private repository)
  - Literal secret value in pipeline YAML
  - Secret in a "temporary" comment

Always:
  - GitHub Actions Secrets / GitLab CI Variables (masked + protected)
  - AWS Secrets Manager / SSM Parameter Store
  - HashiCorp Vault
  - Azure Key Vault / GCP Secret Manager

Example: secret injected by GitHub Actions (correct)

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        env:
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}  # never the literal value
          API_KEY: ${{ secrets.PROD_API_KEY }}
        run: ./deploy.sh

Example: fetching a secret from AWS Secrets Manager at runtime

      - name: Get secret from AWS
        uses: aws-actions/aws-secretsmanager-get-secrets@v2
        with:
          secret-ids: |
            prod/myapp/db-credentials
          parse-json-secrets: true
        # injects DB_USERNAME and DB_PASSWORD as environment variables
        # without exposing the value in the pipeline YAML

Preventing leaks in logs

# Never print environment variables in pipeline debug
# Bad:
set -x   # enables xtrace: prints every command with its values

# Better: enable xtrace only where needed, disable before using secrets
set +x
./script-that-uses-secrets.sh
set -x
In GitHub Actions, any value defined in secrets
is automatically masked in logs: it appears as ***.
But if the secret is assigned to an intermediate variable
and then printed, masking may fail.
Rule: never echo variables derived from secrets.

Rotation and audit

Secret lifecycle best practices:

1. Automatic rotation: AWS Secrets Manager rotates RDS/Redshift natively
2. Short TTL: temporary access tokens (AWS STS AssumeRole) instead of static keys
3. Minimum scope: secret has access only to what it needs
4. Audit: who accessed the secret and when (CloudTrail, Vault audit log)
5. Leak detection: GitHub Secret Scanning, GitGuardian monitor repositories

Summary

A credential in a pipeline is like a password on a sticky note: anyone with access to the log or the repository can see it. Use vaults, inject secrets at runtime, never print them, and scan your history regularly. Automatic rotation eliminates the risk of compromised credentials that remain valid for years.