CI/CD Course
Secrets Management in CI/CD
In this lesson
Secrets management is the discipline of storing, distributing, rotating, and auditing sensitive credentials — API keys, database passwords, deployment tokens, TLS certificates — in a way that makes them available to the pipeline at runtime without ever exposing them in source code, log files, or build artifacts. A pipeline that cannot access credentials cannot deploy. A pipeline that handles credentials carelessly becomes an attack surface. Secrets management is the practice that threads this needle: credentials are available exactly where they are needed, and nowhere else.
Secrets vs Configuration — A Critical Distinction
Not all per-environment values are secrets. A secret is a value whose exposure would cause harm — a private key, an authentication token, a database password. A configuration value is a value that varies per environment but carries no inherent risk if seen — a database hostname, a feature flag state, a log level. Treating these two categories identically — storing everything as a secret — produces an unmanageable secret store and obscures which values actually need protection. Treating configuration as secrets adds management overhead for no security benefit.
Secrets vs Configuration — Examples and Storage Approach
API keys and tokens
TLS private keys
OAuth client secrets
Cloud provider credentials
SSH private keys
API endpoint URLs
Feature flag states
Log levels
Cache TTL values
Service discovery addresses
The Hotel Key Card Analogy
A hotel gives every guest a key card programmed for a specific room, for a specific duration — not a master key, not a permanent key. The card works exactly where it needs to and nowhere else. When the guest checks out, the card is deactivated. Secrets in a CI/CD pipeline should work identically: scoped to the specific job that needs them, valid for the duration of that job's run, and automatically invalidated when the context changes. A secret shared across all jobs in all environments is a master key — and master keys are the thing hotels are most careful never to leave lying around.
GitHub Secrets — Built-In Secret Storage
GitHub Actions provides encrypted secret storage at three levels: repository secrets (available to all workflows in a repository), environment secrets (available only to jobs targeting a specific environment), and organisation secrets (available to selected repositories across an organisation). Environment-scoped secrets are the most important for CI/CD pipelines — they ensure that production credentials are only accessible to jobs that explicitly target the production environment, and staging credentials to staging jobs.
GitHub secrets are encrypted at rest using libsodium sealed boxes, masked in logs automatically — any value that matches a secret is replaced with *** in the workflow output — and are never exposed to the runner's environment in a way that allows them to be read by other processes. They are injected as environment variables at the step level and are not accessible between jobs unless explicitly passed as outputs.
Secrets in a GitHub Actions Pipeline
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Only production-scoped secrets are available here
steps:
- name: Deploy to production
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }} # Injected at step level only
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} # Masked in all log output
run: |
./deploy.sh \
--db-password "$DB_PASSWORD" \
--token "$DEPLOY_TOKEN"
- name: Notify on success
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} # Different secret, same pattern
run: |
curl -X POST "$SLACK_WEBHOOK" \
-d '{"text": "Production deployment succeeded"}'
# This job has NO access to production secrets — different environment scope
test:
runs-on: ubuntu-latest
environment: staging
steps:
- run: npm test
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }} # This is the STAGING DB_PASSWORD
# not the production one
What just happened?
Secrets are injected at the step level rather than exported globally, environment scoping ensures the production DB_PASSWORD is unreachable from the staging job even though both use the same secret name, and all secret values are automatically masked in log output. The staging job gets the staging database password; the production job gets the production one — with no additional code required beyond targeting the correct environment.
External Secrets Managers — Vault, AWS Secrets Manager, and Beyond
GitHub Secrets is sufficient for most pipelines, but larger organisations often centralise secret management in a dedicated secrets manager — HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, or Azure Key Vault. These platforms offer capabilities that GitHub Secrets does not: fine-grained audit logs of every secret access, dynamic secrets that are generated on demand and expire automatically, secret versioning with rollback, and cross-platform availability for secrets used outside of GitHub Actions.
Dynamic secrets are one of the most powerful features of dedicated secrets managers. Rather than storing a long-lived database password, Vault generates a unique, time-limited credential for each pipeline run — valid for 15 minutes, then automatically revoked. If the credential is leaked, the attacker has a narrow window before it expires. This is categorically more secure than rotating a static password on a schedule, and it eliminates the class of incident where a leaked credential remains valid for months because nobody noticed it was compromised.
OIDC and Keyless Authentication — Eliminating Long-Lived Credentials
The most secure secret is one that does not exist. OpenID Connect (OIDC) is a protocol that allows GitHub Actions to authenticate directly to cloud providers — AWS, GCP, Azure — without storing any long-lived credentials in GitHub Secrets at all. Instead, the pipeline requests a short-lived token from the cloud provider at runtime, proving its identity through GitHub's OIDC token. The cloud provider validates the token and grants the permissions defined in a trust policy.
The security advantage is significant. There is no static AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY stored anywhere — nothing to rotate, nothing to leak, nothing to expire and break pipelines at 3am. The trust policy on the cloud provider side defines exactly which repositories, branches, and environments can assume which roles. A compromised repository cannot use OIDC to escalate to a role it was not explicitly granted. This pattern is now the recommended approach for all AWS, GCP, and Azure deployments from GitHub Actions.
OIDC Authentication to AWS — No Stored Credentials
permissions:
id-token: write # Required for OIDC token request
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsDeployRole
aws-region: eu-west-1
# No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY stored anywhere
# GitHub requests a short-lived token from AWS STS at runtime
- name: Deploy to ECS
run: aws ecs update-service --cluster prod --service api --force-new-deployment
What just happened?
The pipeline authenticated to AWS without a single stored credential. GitHub's OIDC provider issued a token proving this workflow run came from the correct repository, branch, and environment. AWS validated that token against its trust policy and issued a short-lived session token scoped to the deploy role. The token expires when the job ends. There is nothing to rotate, nothing to leak, and nothing stored in GitHub Secrets for this authentication path.
Warning: Hardcoded Secrets in Pipeline YAML Are a Permanent Breach
A secret committed to a Git repository — even if deleted in a subsequent commit — remains in the repository's history forever and is trivially recoverable. GitHub's secret scanning will flag known credential patterns, but it cannot protect against patterns it does not recognise, and it cannot un-expose a credential that has already been seen. Treat any credential committed to version control as permanently compromised: revoke it immediately, issue a new one, and audit all systems it had access to. The fix is never "delete the commit" — it is "rotate the credential and understand what had access to it."
Key Takeaways from This Lesson
Teacher's Note
If your pipeline authenticates to AWS with a stored access key, migrate to OIDC — it takes about 30 minutes to set up and permanently removes the category of incident where a rotated key breaks production at the worst possible time.
Practice Questions
Answer in your own words — then check against the expected answer.
1. What is the term for credentials generated on demand for a specific pipeline run and automatically revoked after a short time window — a feature of dedicated secrets managers like HashiCorp Vault that is categorically more secure than storing long-lived static passwords?
2. What protocol allows GitHub Actions to authenticate to cloud providers like AWS and GCP at runtime using short-lived tokens — without storing any long-lived access keys or passwords in GitHub Secrets?
3. A developer accidentally commits an API key to a public GitHub repository. They immediately delete the commit and force-push. What must still happen, and why is the delete insufficient?
Lesson Quiz
1. A pipeline has a staging job and a production job. Both reference secrets.DB_PASSWORD. The staging job must connect to the staging database and the production job to the production database. What GitHub mechanism ensures each job gets the correct credential without any conditional logic in the YAML?
2. A team stores their database hostname, database password, and log level all as GitHub encrypted secrets. A senior engineer says one of these does not belong there. Which one and why?
3. A team currently stores AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as GitHub Secrets for their production deploy job. A colleague proposes switching to OIDC. What is the concrete security improvement OIDC provides over the current approach?
Up Next · Lesson 24
Environment Variables & Configs
Secrets keep credentials safe. Environment variables and config files carry everything else — the non-sensitive, per-environment values that shape how an application behaves when it runs.