Secrets and Credentials Management
Secure secrets management in CI/CD pipelines including vault integration, rotation, and best practices
Secrets and Credentials Management
Secrets in CI/CD pipelines are prime targets for attackers. A single exposed API key or database password can compromise your entire infrastructure.
The Secrets Problem
Common secrets in pipelines:
- API keys and tokens
- Database credentials
- Cloud provider access keys
- Container registry passwords
- Signing keys
- SSH keys
- Webhook secrets
Never Hardcode Secrets
Bad - Hardcoded secrets:
# NEVER do this!
env:
DATABASE_URL: "postgres://user:[email protected]/prod"
AWS_SECRET_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
Good - Using secret references:
# GitHub Actions
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
Platform-Specific Secrets Management
GitHub Actions Secrets
name: Deploy with Secrets
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- uses: actions/checkout@v4
- name: Configure AWS
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: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
echo "Deploying to production..."
# Secrets are masked in logs
Secret scopes:
- Organization secrets - Shared across repos
- Repository secrets - Specific to one repo
- Environment secrets - Protected by approval rules
GitLab CI/CD Variables
# .gitlab-ci.yml
deploy_production:
stage: deploy
environment:
name: production
script:
- echo "$KUBE_CONFIG" | base64 -d > kubeconfig
- kubectl --kubeconfig=kubeconfig apply -f k8s/
variables:
# Protected variables only available on protected branches
DEPLOY_TOKEN: $CI_DEPLOY_TOKEN
Variable protection levels:
- Protected - Only on protected branches/tags
- Masked - Hidden in job logs
- File - Written to temporary file path
Jenkins Credentials
// Jenkinsfile
pipeline {
agent any
environment {
AWS_CREDENTIALS = credentials('aws-prod-credentials')
DATABASE_CREDS = credentials('postgres-prod')
}
stages {
stage('Deploy') {
steps {
withCredentials([
string(credentialsId: 'api-key', variable: 'API_KEY'),
usernamePassword(
credentialsId: 'docker-registry',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)
]) {
sh '''
docker login -u $DOCKER_USER -p $DOCKER_PASS
./deploy.sh
'''
}
}
}
}
}
HashiCorp Vault Integration
Vault provides centralized secrets management with:
- Dynamic secret generation
- Automatic rotation
- Audit logging
- Fine-grained access control
GitHub Actions with Vault
name: Deploy with Vault
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Import Secrets from Vault
uses: hashicorp/vault-action@v2
with:
url: https://vault.example.com
method: jwt
role: github-actions
secrets: |
secret/data/prod/database url | DATABASE_URL ;
secret/data/prod/api key | API_KEY
- name: Deploy
run: ./deploy.sh
env:
DATABASE_URL: ${{ env.DATABASE_URL }}
Vault Policy Example
# vault-policy.hcl
path "secret/data/prod/*" {
capabilities = ["read"]
}
path "secret/data/staging/*" {
capabilities = ["read", "list"]
}
# Deny access to admin secrets
path "secret/data/admin/*" {
capabilities = ["deny"]
}
Secrets Rotation
Regular rotation limits exposure window if secrets are compromised.
Automated Rotation with Vault
# Database secrets engine - auto-generates credentials
resource "vault_database_secret_backend_role" "app" {
backend = vault_mount.db.path
name = "app-role"
db_name = vault_database_secret_backend_connection.postgres.name
# Credentials valid for 1 hour, renewable up to 24 hours
default_ttl = 3600
max_ttl = 86400
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
]
revocation_statements = [
"DROP ROLE IF EXISTS \"{{name}}\";"
]
}
GitHub Actions Secret Rotation
# rotate_secrets.py
import requests
from nacl import public, encoding
import base64
import secrets
def encrypt_secret(public_key: str, secret_value: str) -> str:
"""Encrypt secret using repository's public key."""
public_key_bytes = base64.b64decode(public_key)
sealed_box = public.SealedBox(public.PublicKey(public_key_bytes))
encrypted = sealed_box.encrypt(secret_value.encode())
return base64.b64encode(encrypted).decode()
def rotate_api_key(repo: str, token: str):
"""Rotate an API key and update GitHub secret."""
headers = {"Authorization": f"token {token}"}
# Get repository public key
key_response = requests.get(
f"https://api.github.com/repos/{repo}/actions/secrets/public-key",
headers=headers
)
key_data = key_response.json()
# Generate new API key
new_key = secrets.token_urlsafe(32)
# Encrypt and update
encrypted = encrypt_secret(key_data["key"], new_key)
requests.put(
f"https://api.github.com/repos/{repo}/actions/secrets/API_KEY",
headers=headers,
json={
"encrypted_value": encrypted,
"key_id": key_data["key_id"]
}
)
return new_key
Preventing Secrets Leakage
Log Masking
Most CI platforms automatically mask secrets in logs, but be careful:
# GitHub Actions - Manual masking
- name: Process data
run: |
# Add custom value to mask list
echo "::add-mask::$COMPUTED_SECRET"
echo "Processing with secret: $COMPUTED_SECRET"
Preventing Exfiltration
# Block outbound network from untrusted code
- name: Run untrusted tests
run: |
# Use network namespace to block egress
unshare --net ./run-tests.sh
Environment Isolation
# Use separate environments with approval gates
jobs:
deploy-staging:
environment: staging
# Staging secrets only
deploy-production:
needs: deploy-staging
environment: production # Requires manual approval
# Production secrets only
OIDC - Avoiding Long-Lived Credentials
OpenID Connect allows pipelines to authenticate without storing cloud credentials.
AWS OIDC with GitHub Actions
name: Deploy with OIDC
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: us-east-1
# No static credentials needed!
- name: Deploy to S3
run: aws s3 sync ./dist s3://my-bucket
AWS IAM Trust Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*"
}
}
}
]
}
Secrets Security Checklist
- No secrets in source code or git history
- All secrets encrypted at rest
- Secrets masked in pipeline logs
- Least privilege access to secrets
- Regular secret rotation (90 days max)
- OIDC instead of long-lived credentials
- Environment separation (dev/staging/prod)
- Approval gates for production secrets
- Audit logging for secret access
- Emergency rotation procedures documented
Key Takeaways
- Never hardcode secrets - Use platform secret stores or Vault
- Use OIDC where possible - Eliminates credential storage
- Rotate regularly - Automated rotation with Vault or scripts
- Separate environments - Different secrets for dev/staging/prod
- Audit everything - Know who accessed what and when
Part of: CI/CD Pipeline Hardening
Updated: 1/24/2025
Found an issue?