Secrets
CI secrets (API keys, DB passwords, signing certs) live in the platform’s vault — never in source. Inject them at job-time, scope to the smallest job, prefer short-lived tokens via OIDC.
Storage, masking, OIDC, leak prevention
EXAMPLE
# 1) Where secrets live
# - GitHub Actions : Repo/Org/Env Secrets → ${{ secrets.NAME }}
# - GitLab CI : Settings → CI/CD → Variables (mask + protect)
# - CircleCI : Project Settings → Environment Variables
# - Jenkins : Credentials → store as Secret Text/Username-Password/SSH/File
# - AWS / GCP / Azure: native secret managers — fetched by the runner
# 2) GitHub Actions — basics
name: deploy
on: { push: { branches: [main] } }
jobs:
deploy:
runs-on: ubuntu-latest
environment: { name: production } # required for env-scoped secrets
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
env:
DB_URL: ${{ secrets.DB_URL }}
API_TOKEN: ${{ secrets.API_TOKEN }}
# 3) GitHub Actions — short-lived AWS creds via OIDC (NO long-lived keys!)
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # OIDC
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gh-deploy
aws-region: us-east-1
# No secrets in the workflow!
- run: aws s3 sync ./dist s3://my-bucket
# 4) GitLab CI — variables
variables:
NODE_ENV: production
deploy:
stage: deploy
script:
- ./deploy.sh
# $DB_URL and $API_TOKEN come from project CI variables
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
# In project settings:
# - 'Mask variable' so values don't appear in logs
# - 'Protected variable' so only protected branches/tags get it
# - File-type variables for certs (private key, kubeconfig)
# 5) Use secrets from a cloud secret manager at job-time
# AWS Secrets Manager:
- name: Fetch DB password
run: |
DB_PW=$(aws secretsmanager get-secret-value --secret-id prod/db --query SecretString --output text)
echo "::add-mask::${DB_PW}"
echo "DB_PASSWORD=${DB_PW}" >> $GITHUB_ENV
# Vault (HashiCorp):
- uses: hashicorp/vault-action@v3
with:
url: https://vault.example.com
method: jwt # use OIDC from GitHub
role: gh-deployer
secrets: 'secret/data/prod/db url | DB_URL ;'
# 6) Mask + redact in logs
# GitHub Actions masks `secrets.*` automatically. For computed secrets:
echo "::add-mask::${VALUE}"
# Don't `echo $SECRET` for debugging — even masked, the surrounding text leaks structure.
# 7) DON'T do these
# ❌ Hardcode secrets in repo files (config.yml, .env)
# ❌ Print secrets to logs
# ❌ Pass secrets via command-line args (visible in `ps`)
# ❌ Commit private keys, even if 'temporary'
# ❌ Long-lived access keys (AWS_ACCESS_KEY_ID / OPENAI_KEY) when OIDC is available
# ❌ A single ROOT-permission secret used everywhere — split by scope
# 8) Prevent leaks — pre-commit + CI scans
# pre-commit (developer side)
# - repo: https://github.com/gitleaks/gitleaks
# rev: v8.18.0
# hooks:
# - id: gitleaks
# CI (last line of defence)
- uses: gitleaks/gitleaks-action@v2
env: { GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} }
# 9) Rotate + audit
# - Quarterly rotation for service-account secrets
# - Monthly review: who has access to which secret?
# - Alert when a secret is used outside expected workflows/repos
# - On compromise: rotate FIRST, investigate second
# 10) Local dev secrets
# - .env file (in .gitignore!) — load via dotenv
# - 1Password CLI / Doppler / Infisical — sync prod secrets to dev safely
# - Don't share prod secrets in chat — use a secret manager link
# 11) Secret scopes — least privilege
# GLOBAL : almost nothing — only universal infrastructure (Sentry DSN)
# ORG : shared services (shared S3 bucket creds)
# REPO : per-project tokens (deploy keys, third-party APIs)
# ENV : production vs staging (different DB passwords, OAuth keys)
# EPHEMERAL : OIDC-assumed roles — most secure for cloud access
# 12) Special cases
# - Signing keys (Apple, Google Play): use platform-specific encrypted storage
# - GPG keys for signed commits: encrypted env vars + base64
# - SSL certs: GitHub Actions File-type secrets; render to disk at job-time
# 13) Real-world workflow
# Bad commit pushes a secret → immediate panic-rotation:
# 1. Rotate the leaked secret AT THE SOURCE (cloud provider, API)
# 2. Force-push history rewrite is NOT enough — assume the secret is harvested
# 3. Audit usage; look for unauthorized access
# 4. Add the secret pattern to gitleaks ban list
Why it matters
OIDC-assumed roles (no stored AWS keys) and short-lived tokens are the modern CI secret stack. The one inviolable rule: leaked secrets get rotated at the source first — rewriting git history alone never closes the gap.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
# Store in repo / org settings.
# Reference: ${{ secrets.NPM_TOKEN }}
# NEVER echo a secret. Mark with mask-on-output for safety.
Try it Yourself »
Exercise
Reference a secret named NPM_TOKEN.
token: ${{
.NPM_TOKEN }}
Seven letters.
Discussion
Loading…