Secrets Management in CI/CD Pipelines
Learn how to securely manage secrets in CI/CD pipelines using environment variables, vault integrations, and automated rotation strategies.
What you'll learn
- ✓Identify common secrets management anti-patterns in CI/CD
- ✓Store and inject secrets securely using platform-native tools
- ✓Integrate HashiCorp Vault with your CI/CD pipeline
- ✓Implement secret rotation without pipeline downtime
Prerequisites
- •Basic CI/CD pipeline experience
- •Familiarity with environment variables
Secrets like API keys, database passwords, and TLS certificates are essential for your applications to function. They are also among the most dangerous things in your codebase if mishandled. A single leaked secret can give an attacker full access to your production database, cloud provider, or customer data.
CI/CD pipelines make secrets management particularly tricky because they need access to many sensitive credentials to build, test, and deploy your code. This guide covers how to handle secrets safely at every stage of your pipeline.
The Problem with Secrets in Pipelines
CI/CD pipelines need secrets for many tasks: pulling private container images, connecting to databases for integration tests, deploying to cloud providers, and sending notifications. The challenge is giving pipelines access to these secrets without exposing them in logs, artifacts, or source code.
Common mistakes that lead to secret exposure include hardcoding secrets in pipeline configuration files, printing environment variables in debug output, storing secrets in build artifacts, and committing .env files to version control.
Platform-Native Secrets in GitHub Actions
GitHub Actions provides encrypted secrets at the repository, environment, and organization level. These secrets are masked in logs and only available to workflows that run on the repository.
Setting Up Repository Secrets
# Using the GitHub CLI to set secrets
gh secret set DATABASE_URL --body "postgresql://user:pass@db.example.com:5432/mydb"
gh secret set AWS_ACCESS_KEY_ID --body "AKIAIOSFODNN7EXAMPLE"
gh secret set AWS_SECRET_ACCESS_KEY --body "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# Set secrets for a specific environment
gh secret set DATABASE_URL --env production \
--body "postgresql://prod-user:prod-pass@prod-db.example.com:5432/mydb"
Using Secrets in Workflows
name: Deploy Application
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Uses environment-specific secrets
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/myapp:$IMAGE_TAG .
docker push $ECR_REGISTRY/myapp:$IMAGE_TAG
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster production \
--service myapp \
--force-new-deployment
Environment Protection Rules
GitHub environments let you add approval requirements and restrict which branches can deploy:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # No approvals needed
steps:
- name: Deploy to staging
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.sh staging
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment: production # Requires manual approval
steps:
- name: Deploy to production
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.sh production
Integrating HashiCorp Vault
For teams that need centralized secrets management across multiple pipelines and services, HashiCorp Vault is the industry standard. Vault provides dynamic secrets, automatic rotation, and detailed audit logging.
Setting Up Vault for CI/CD
First, configure Vault with the secrets your pipeline needs:
# Enable the KV secrets engine
vault secrets enable -path=cicd kv-v2
# Store application secrets
vault kv put cicd/myapp/production \
database_url="postgresql://prod-user:prod-pass@db.example.com:5432/mydb" \
api_key="sk-prod-abc123def456" \
redis_url="redis://:secret@redis.example.com:6379"
vault kv put cicd/myapp/staging \
database_url="postgresql://staging-user:staging-pass@staging-db.example.com:5432/mydb" \
api_key="sk-staging-xyz789" \
redis_url="redis://:staging-secret@staging-redis.example.com:6379"
Vault Policy for CI/CD
Create a policy that gives your pipeline read-only access to the secrets it needs:
# cicd-policy.hcl
cat <<'EOF' > cicd-policy.hcl
path "cicd/data/myapp/*" {
capabilities = ["read"]
}
path "cicd/metadata/myapp/*" {
capabilities = ["read", "list"]
}
# Allow token renewal
path "auth/token/renew-self" {
capabilities = ["update"]
}
EOF
vault policy write cicd-reader cicd-policy.hcl
JWT Authentication for GitHub Actions
The most secure way to connect GitHub Actions to Vault is using JWT/OIDC authentication, which avoids storing any long-lived Vault tokens:
# Configure JWT auth method in Vault
vault auth enable jwt
vault write auth/jwt/config \
bound_issuer="https://token.actions.githubusercontent.com" \
oidc_discovery_url="https://token.actions.githubusercontent.com"
# Create a role for your repository
vault write auth/jwt/role/myapp-deploy \
role_type="jwt" \
bound_audiences="https://github.com/my-org" \
bound_claims_type="glob" \
bound_claims='{"repository":"my-org/myapp"}' \
user_claim="repository" \
policies="cicd-reader" \
ttl="10m"
GitHub Actions Workflow with Vault
name: Deploy with Vault Secrets
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Import secrets from Vault
id: vault
uses: hashicorp/vault-action@v3
with:
url: https://vault.example.com
method: jwt
role: myapp-deploy
jwtGithubAudience: https://github.com/my-org
secrets: |
cicd/data/myapp/production database_url | DATABASE_URL ;
cicd/data/myapp/production api_key | API_KEY ;
cicd/data/myapp/production redis_url | REDIS_URL
- name: Deploy application
run: |
# Secrets are available as environment variables
# DATABASE_URL, API_KEY, REDIS_URL are set automatically
./deploy.sh production
- name: Run smoke tests
env:
API_KEY: ${{ steps.vault.outputs.API_KEY }}
run: |
curl -H "Authorization: Bearer $API_KEY" \
https://myapp.example.com/health
Dynamic Secrets with Vault
One of Vault’s most powerful features is dynamic secrets. Instead of storing static credentials, Vault generates short-lived credentials on demand. When they expire, they are automatically revoked.
Database Dynamic Secrets
# Enable the database secrets engine
vault secrets enable database
# Configure PostgreSQL connection
vault write database/config/myapp-db \
plugin_name=postgresql-database-plugin \
allowed_roles="myapp-readonly,myapp-readwrite" \
connection_url="postgresql://{{username}}:{{password}}@db.example.com:5432/mydb" \
username="vault-admin" \
password="vault-admin-password"
# Create a role that generates read-only credentials
vault write database/roles/myapp-readonly \
db_name=myapp-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
Use dynamic secrets in your pipeline:
- name: Get dynamic database credentials
id: db-creds
uses: hashicorp/vault-action@v3
with:
url: https://vault.example.com
method: jwt
role: myapp-deploy
secrets: |
database/creds/myapp-readonly username | DB_USERNAME ;
database/creds/myapp-readonly password | DB_PASSWORD
- name: Run integration tests
env:
DATABASE_URL: "postgresql://${{ steps.db-creds.outputs.DB_USERNAME }}:${{ steps.db-creds.outputs.DB_PASSWORD }}@db.example.com:5432/mydb"
run: npm run test:integration
# Credentials automatically expire after 1 hour
Preventing Secret Leaks
Even with proper secrets storage, leaks can happen through careless logging or misconfigured pipelines. Here are defensive measures to implement.
Git Pre-Commit Hooks
Use tools like gitleaks to prevent secrets from being committed:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
# Install and run manually
brew install gitleaks
gitleaks detect --source . --verbose
Pipeline Secret Scanning
Add secret scanning as a step in your CI pipeline:
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run TruffleHog
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
Masking Secrets in Logs
If you must log output that might contain secrets, use masking:
steps:
- name: Mask dynamic values
run: |
SECRET_VALUE=$(vault read -field=value secret/myapp/api-key)
echo "::add-mask::$SECRET_VALUE"
echo "API_KEY=$SECRET_VALUE" >> "$GITHUB_ENV"
Secret Rotation Strategy
Secrets should be rotated regularly. Here is a rotation workflow you can schedule:
name: Rotate Secrets
on:
schedule:
- cron: '0 0 1 * *' # First day of each month
workflow_dispatch: # Allow manual trigger
jobs:
rotate:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Generate new API key
id: new-key
run: |
NEW_KEY=$(openssl rand -hex 32)
echo "::add-mask::$NEW_KEY"
echo "key=$NEW_KEY" >> "$GITHUB_OUTPUT"
- name: Update secret in Vault
uses: hashicorp/vault-action@v3
with:
url: https://vault.example.com
method: jwt
role: secret-rotator
secrets: ""
exportToken: true
- name: Write new secret to Vault
run: |
vault kv patch cicd/myapp/production \
api_key="${{ steps.new-key.outputs.key }}"
- name: Update application config
run: |
# Trigger a redeployment to pick up the new secret
kubectl rollout restart deployment/myapp
Wrapping Up
Secrets management in CI/CD comes down to three principles: never store secrets in code, use the least privilege necessary, and rotate credentials regularly. Start with platform-native secrets like GitHub Actions encrypted secrets. When your team grows and you need centralized management across multiple repositories and environments, move to a dedicated solution like HashiCorp Vault with OIDC authentication. Always add secret scanning to your pipeline to catch accidental leaks before they reach production.
Related articles
- DevOps HashiCorp Vault for Secrets Management
Learn how to use HashiCorp Vault to store, access, and rotate secrets securely across your infrastructure and applications.
- CI/CD GitHub Actions vs Jenkins: CI/CD Compared
Compare GitHub Actions and Jenkins for CI/CD pipelines. Covers setup, configuration, plugins, pricing, and when to choose each tool for your team.
- CI/CD CI/CD Artifact Management: Build, Store, and Promote
Learn how to manage build artifacts in CI/CD pipelines including versioning, storage, promotion strategies, and cleanup policies.
- CI/CD Optimize Docker Builds in CI/CD Pipelines
Speed up Docker builds in CI/CD — multi-stage builds, layer caching, BuildKit, registry caching, and slim base images for faster deployments.