Secrets in Your Pipeline Are a Security Risk You Cannot Ignore
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Credentials Your Pipeline Needs to Operate
A typical CI/CD pipeline for a cloud-deployed service needs: credentials to pull from your container registry, credentials to push to it, credentials to deploy to your Kubernetes cluster or ECS, database credentials for running integration tests, API keys for third-party services used in tests, and secrets for any managed services the pipeline configures. That's a significant collection of high-privilege credentials — all in one place.
If those credentials are compromised, an attacker can push malicious container images, deploy arbitrary code to your infrastructure, and exfiltrate data from your test databases. Your pipeline's credential surface is one of the most valuable targets in your entire organization. Most teams treat it as an afterthought.
How Secrets Get Exposed
Printed to logs is the most common accidental exposure. A developer adds debug logging, a third-party action prints environment variables, or a stack trace includes a connection string. Logs are often accessible to more people than the secrets themselves, retained longer, and sometimes forwarded to external log aggregation services.
# This is how secrets end up in logs
- name: Debug environment
run: env # Prints ALL environment variables, including secrets
GitHub Actions masks registered secrets in log output — but only if the secret is stored as a GitHub secret. Secrets passed through environment variables from other sources, or secrets reconstructed from parts, may not be masked.
Checked into version control happens more than it should. A developer hardcodes a credential "temporarily" for debugging, forgets to remove it, and commits it. Git history is permanent unless deliberately rewritten. Secrets committed to public repos are harvested by automated scanners within minutes.
Exposed through PR pipelines is a subtler risk. In many CI configurations, workflows triggered by pull requests from forks run with access to the same secrets as internal PRs. A malicious contributor opens a PR, the workflow runs with production secrets, and the contributor's PR code has access to those secrets. GitHub Actions addresses this with pull_request_target vs pull_request distinction, but many pipelines are misconfigured.
Correct Secret Management Patterns
Use the platform's secret store, not files. GitHub Actions secrets, GitLab CI/CD variables, and equivalent features in other platforms inject secrets as environment variables at runtime without storing them in the repository. They're encrypted at rest, masked in logs, and auditable.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to ECS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws ecs update-service --cluster prod --service myapp \
--task-definition myapp:${{ github.sha }}
Prefer short-lived credentials over long-lived ones. AWS IAM supports OIDC-based authentication for GitHub Actions — the pipeline requests a temporary credential from AWS for each run, scoped to the specific permissions needed, that expires after the job completes. No long-lived access key stored anywhere.
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsDeployRole
aws-region: ap-southeast-1
# No static credentials stored — uses OIDC token exchange
This eliminates the access key entirely. The IAM role policy restricts what the pipeline can do; the token is valid only for the duration of the job.
Use a secrets manager for application credentials. Pipeline secrets are one category. Application secrets (database passwords, API keys used at runtime) should not be stored in the pipeline at all — they should be pulled by the application at startup from AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager. The pipeline only needs permission to deploy the application, not access to the secrets the application uses.
Scanning for Accidental Exposure
Add secret scanning to your pipeline as a pre-commit and CI check:
- name: Scan for secrets
uses: trufflesecurity/trufflehog@v3
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
TruffleHog detects over 700 credential patterns (AWS keys, GitHub tokens, database connection strings, etc.) and fails the build if any are found. Running this in CI catches accidental commits before they merge; running it as a pre-commit hook catches them before they're pushed.
The scan should run against the full commit diff, not just the current file state — a secret that was committed and then removed is still in git history and still compromised.
The Rotation Policy
Secrets that are never rotated are a deferred incident. Establish rotation schedules: service account tokens monthly, database credentials quarterly at minimum, and immediately after any personnel change for credentials that were accessible to a departing engineer.
Document which pipeline secrets exist, what they have access to, and when they were last rotated. That documentation is the starting point for your response when a secret is compromised — and the input for your rotation schedule before it is.