Skip to main content

4 - Secrets Management in CI/CD Pipelines

Best practices for secure management of secrets, API keys and credentials in CI/CD pipelines: GitHub Secrets, Vault and environment variables.

What do you think about this article?

At a Glance

Best practices for secure management of secrets, API keys and credentials in CI/CD pipelines: GitHub Secrets, Vault and environment variables.

Introduction: Why Secret Management Is Critical

A single exposed API key in a public repository can cost thousands of euros in minutes. Automated bots constantly scan GitHub looking for accidentally committed credentials: AWS keys, Firebase tokens, database passwords. Secret management is not optional in CI/CD pipelines, it is an absolute necessity.

In this article we will explore in depth how to manage secrets securely in CI/CD pipelines, starting from GitHub Secrets all the way to enterprise solutions like HashiCorp Vault. Each strategy is illustrated with practical examples specific to Angular applications.

What You Will Learn in This Article

  • Why secret management is fundamental for security
  • How GitHub Secrets work (repository, environment, organization)
  • The difference between environment variables and secrets
  • How to manage .env file security
  • The difference between runtime and build-time secrets
  • Secret rotation strategies
  • Vault solutions: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager
  • How to prevent secret leaks with git-secrets and pre-commit hooks
  • Best practices for audit trails and compliance

1. GitHub Secrets: The Foundation of Secret Management

GitHub offers three levels of secrets, each with a different scope of visibility. Understanding when to use each level is fundamental for effective management.

GitHub Secrets Levels

Level Scope Use Case Who Can Access
Repository Single repository Project-specific API keys All repo workflows
Environment Specific environment (staging, production) Production credentials Only workflows with specified environment
Organization All organization repositories Shared tokens (npm, Docker Hub) Selected repositories by admin

Configuring Repository Secrets

To add a secret to your repository, go to Settings > Secrets and variables > Actions > New repository secret. Here is how to use them in workflows:


# Accessing repository secrets
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Firebase
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
        run: firebase deploy --token "$FIREBASE_TOKEN"

      # Secrets are automatically masked in logs
      - name: Debug (secret shown as ***)
        run: echo "Token is ${{ secrets.FIREBASE_TOKEN }}"
      

Environment Secrets

Environment secrets are the best choice for separating staging and production credentials. They require the environment keyword in the job:


# Environment secrets
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Deploy to staging
        env:
          # Uses the "staging" environment secret
          API_KEY: ${{ secrets.API_KEY }}
          DEPLOY_URL: ${{ vars.DEPLOY_URL }}
        run: |
          echo "Deploying to $DEPLOY_URL"
          firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}"

  deploy-production:
    runs-on: ubuntu-latest
    environment: production
    needs: deploy-staging
    steps:
      - name: Deploy to production
        env:
          # Uses the "production" environment secret (different!)
          API_KEY: ${{ secrets.API_KEY }}
        run: firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}"
      

Protecting Production Environments

Always configure Environment Protection Rules for the production environment: require manual reviewer approval before deployment, limit which branches can deploy to production (only main), and enable the wait timer for a safety buffer.

2. Environment Variables vs Secrets

It is important to distinguish between environment variables and secrets. GitHub Actions supports both but with different characteristics:

Variables vs Secrets

Feature Variables (vars) Secrets (secrets)
Log visibility Visible in plain text Masked with ***
Encryption No Yes (Libsodium sealed box)
Readable after creation Yes No (only overwritable)
Typical use URLs, environment names, feature flags Tokens, passwords, API keys

# Example: variables vs secrets
jobs:
  build:
    runs-on: ubuntu-latest
    environment: production
    env:
      # Variables: non-sensitive configuration
      API_URL: ${{ vars.API_URL }}
      APP_NAME: ${{ vars.APP_NAME }}
      ENABLE_ANALYTICS: ${{ vars.ENABLE_ANALYTICS }}
    steps:
      - uses: actions/checkout@v4
      - name: Build with environment config
        env:
          # Secrets: sensitive information
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
          ANALYTICS_KEY: ${{ secrets.ANALYTICS_KEY }}
        run: |
          echo "Building for $API_URL"
          npm run build -- --configuration=production
      

3. .env File Security

.env files are convenient for local development but represent a significant risk if managed poorly. Here are the fundamental rules:

Recommended Structure


# .env file structure in the project
project/
  .env                 # NEVER committed (in .gitignore)
  .env.example         # Template WITHOUT real values (committed)
  .env.development     # Development values (optional, in .gitignore)
  .env.production      # NEVER committed, NEVER in the repository
      

# .env.example - Template committed to the repository
# Copy to .env and fill in with real values
API_URL=https://api.example.com
FIREBASE_PROJECT_ID=your-project-id
ANALYTICS_ID=
SECRET_KEY=

# .env - Local file (MUST be in .gitignore)
API_URL=https://api.example.com
FIREBASE_PROJECT_ID=my-actual-project
ANALYTICS_ID=G-XXXXXXXXXX
SECRET_KEY=super-secret-value-123
      

Golden Rule for .gitignore


# .gitignore - ALWAYS exclude .env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.production

# But do NOT exclude the template
!.env.example
      

4. Runtime vs Build-Time Secrets

For Angular applications, it is crucial to understand the difference between secrets available at build time and secrets available at runtime. Angular is a frontend framework: everything included in the bundle is visible in the browser.

Fundamental Rule for Frontend

Never include secrets in the frontend bundle. Any value present in Angular's environment.ts files is visible to anyone who opens the browser developer tools. Public API keys (like Google Maps or Google Analytics) are acceptable, but private tokens, encryption keys, and passwords must never be in frontend code.


// environment.ts - What is safe and what is NOT

// SAFE: public keys with domain restrictions
export const environment = {
  production: true,
  apiUrl: 'https://api.example.com',
  googleMapsKey: 'AIza...',  // OK: restricted by domain in Google console
  analyticsId: 'G-XXXXX',   // OK: it's a public identifier
  sentryDsn: 'https://xxx@sentry.io/123',  // OK: restricted by domain
};

// NEVER DO THIS:
// firebaseAdminKey: 'xxxxx',    // DANGEROUS: full admin access
// stripeSecretKey: 'sk_xxxxx',  // DANGEROUS: payment access
// dbPassword: 'password123',     // DANGEROUS: database access
      

Managing Build-Time Secrets in the Pipeline


# Inject safe values at build time
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      # Replace placeholders in the environment file
      - name: Configure environment
        run: |
          cat > src/environments/environment.prod.ts << 'ENVEOF'
          export const environment = {
            production: true,
            apiUrl: '${{ vars.API_URL }}',
            analyticsId: '${{ vars.ANALYTICS_ID }}',
          };
          ENVEOF

      - run: npm ci
      - run: npx ng build --configuration=production
      

5. Secret Rotation

Periodic secret rotation is a fundamental security practice. Even if a secret has not been compromised, rotation reduces the exposure window in case of an undetected leak.

Recommended Rotation Schedule

Secret Type Frequency Reason
Deploy tokens (Firebase, Vercel) Every 90 days Access to production resources
Third-party API keys Every 180 days Risk of compromise over time
npm/Docker Hub tokens Every 90 days Access to package publishing
Service Account JSON Every 365 days Long-term credentials
Compromised secret Immediately Security emergency

# Workflow for rotation reminders
name: Secret Rotation Reminder

on:
  schedule:
    - cron: '0 9 1 */3 *'  # First day of every quarter

jobs:
  remind:
    runs-on: ubuntu-latest
    steps:
      - name: Check secret age
        run: |
          echo "Reminder: Time to rotate repository secrets."
          echo "Secrets to check:"
          echo "- FIREBASE_SERVICE_ACCOUNT"
          echo "- NPM_TOKEN"
          echo "- DOCKER_HUB_TOKEN"
          echo "Go to Settings > Secrets to update values."

      - name: Create GitHub Issue
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: 'Quarterly secret rotation',
              body: 'Time to rotate CI/CD secrets.',
              labels: ['security', 'maintenance']
            });
      

6. Enterprise Vault Solutions

For organizations with advanced security requirements, GitHub Secrets may not be sufficient. Vault solutions offer additional features like detailed auditing, automatic rotation, and advanced encryption.

HashiCorp Vault


# Integrating HashiCorp Vault with GitHub Actions
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Import Secrets from Vault
        uses: hashicorp/vault-action@v3
        with:
          url: https://vault.example.com
          method: jwt
          role: github-actions
          secrets: |
            secret/data/production/firebase token | FIREBASE_TOKEN ;
            secret/data/production/api key | API_KEY ;

      - name: Use imported secrets
        run: |
          echo "Firebase token is available as env var"
          firebase deploy --token "$FIREBASE_TOKEN"
      

Vault Solutions Comparison

Vault Solutions Compared

Feature GitHub Secrets HashiCorp Vault AWS Secrets Manager GCP Secret Manager
Cost Free Self-hosted or Cloud Pay-per-use Pay-per-use
Automatic rotation No Yes Yes Yes
Audit log Limited Complete CloudTrail Cloud Audit
Complexity Low High Medium Medium
Ideal for Small teams Enterprise AWS ecosystem GCP ecosystem

7. Preventing Secret Leaks

Prevention is more effective than cure. Here are the tools and practices to prevent secrets from ending up in the repository.

git-secrets: Pre-Commit Scanning


# Install git-secrets
# macOS
brew install git-secrets

# Linux
git clone https://github.com/awslabs/git-secrets.git
cd git-secrets && make install

# Configure in the repository
cd my-project
git secrets --install
git secrets --register-aws

# Add custom patterns
git secrets --add 'FIREBASE_TOKEN[=:]\s*[A-Za-z0-9]+'
git secrets --add 'sk_live_[A-Za-z0-9]+'
git secrets --add 'AIza[0-9A-Za-z\\-_]{35}'

# Scan the existing repository
git secrets --scan
      

Pre-Commit Hook with Husky


# Install husky
npm install -D husky
npx husky init

# Create the pre-commit hook
cat > .husky/pre-commit << 'HOOKEOF'
#!/bin/sh

# Check that no secrets are being committed
echo "Checking for secrets..."

# Common secret patterns
PATTERNS=(
  'AKIA[0-9A-Z]{16}'           # AWS Access Key
  'sk_live_[a-zA-Z0-9]+'       # Stripe Secret Key
  'AIza[0-9A-Za-z\-_]{35}'     # Google API Key
  'ghp_[a-zA-Z0-9]{36}'        # GitHub Personal Token
  'xox[bpoas]-[a-zA-Z0-9-]+'   # Slack Token
)

for pattern in "${PATTERNS[@]}"; do
  if git diff --cached --name-only -z | xargs -0 grep -lP "$pattern" 2>/dev/null; then
    echo "ERROR: Possible secret detected!"
    echo "Pattern: $pattern"
    exit 1
  fi
done

echo "No secrets detected."
HOOKEOF

chmod +x .husky/pre-commit
      

GitHub Secret Scanning

GitHub also offers a built-in Secret Scanning feature that automatically detects known secret patterns in pushed commits. For public repositories it is active by default; for private repositories, it is available with GitHub Advanced Security.

8. Audit Trail and Compliance

For organizations with compliance requirements (SOC 2, GDPR, ISO 27001), it is necessary to maintain a record of who accesses secrets and when:


# Workflow with detailed logging
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Log deployment attempt
        run: |
          echo "=== DEPLOYMENT AUDIT LOG ==="
          echo "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
          echo "Actor: ${{ github.actor }}"
          echo "Ref: ${{ github.ref }}"
          echo "SHA: ${{ github.sha }}"
          echo "Workflow: ${{ github.workflow }}"
          echo "Run ID: ${{ github.run_id }}"
          echo "Environment: production"
          echo "==========================="

      - name: Deploy
        run: firebase deploy --token "${{ secrets.FIREBASE_TOKEN }}"

      - name: Log deployment result
        if: always()
        run: |
          echo "Deploy status: ${{ job.status }}"
      

Secret Security Checklist

  • All .env files are in .gitignore
  • A .env.example file exists without real values
  • Production secrets use GitHub Environment Secrets
  • The production environment has protection rules active
  • Pre-commit hooks check for secret patterns
  • Secret rotation is planned quarterly
  • The team knows what to do in case of a leak (emergency procedure)
  • No private secrets are present in frontend code

Conclusion

Secret management is a security aspect that cannot be neglected. In this article we explored the entire spectrum of solutions: from GitHub Secrets for small teams, to enterprise vaults for organizations with advanced compliance requirements.

The fundamental principles to remember are: never commit secrets to the repository, use environment levels to separate staging and production, implement pre-commit hooks for prevention, plan regular secret rotation, and always keep an emergency procedure ready in case of a leak.

In the next article we will focus on performance: we will configure performance budgets and Lighthouse CI in the pipeline to ensure that every deployment maintains high quality standards.

Frequently asked questions

What is the main reason why secret management is critical in CI/CD pipelines?

A single exposed API key can cost thousands of euros in minutes, and automated bots constantly scan GitHub for accidentally committed credentials.

How do you add a secret to your repository on GitHub?

To add a secret to your repository, go to Settings > Secrets and variables > Actions > New repository secret.

What is the difference between environment variables and secrets in GitHub Actions?

Environment variables are supported by GitHub Actions but have different characteristics than secrets; they require the keyword in the job.

Why should you never include secrets in the frontend bundle of an Angular application?

Any value present in Angular's files is visible to anyone who opens the browser developer tools, and public API keys are acceptable only if they are private tokens, encryption keys, or passwords must never be in frontend code.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects