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
.envfile 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
.envfiles are in.gitignore - A
.env.examplefile 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.
Related deep dive: 3 - Firebase Hosting + GitHub Actions: Deploy Automatico.
Related deep dive: 5 - Performance Budgets e Lighthouse CI in Pipeline.
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.
Commenti
Caricamento commenti...
Sign in to comment