Stop Storing Credentials in Your Pipeline Configuration Files

by Eric Hanson, Backend Developer at Clean Systems Consulting

How Credentials End Up in Configuration Files

It starts small. A developer is setting up a new pipeline step that needs a database connection for integration tests. The fastest path to working is putting the credentials directly in the workflow YAML:

# This appears in a real CI file more often than it should
- name: Run integration tests
  env:
    DB_URL: jdbc:postgresql://test-db.internal:5432/testdb
    DB_USER: ci_user
    DB_PASSWORD: hunter2secretpassword123
  run: ./gradlew integrationTest

The developer ships the feature, the tests run, and the credential is now in version control. If the repository is public, it's already been harvested by automated secret scanners. If it's private, it's in git history permanently — accessible to every current and future team member, and to anyone who gains read access to the repository.

More commonly, the credential is for a test environment with limited blast radius. But the pattern established here — "credentials go in the YAML" — propagates. The next developer uses the same pattern for a less-limited environment.

Why This Is Worse Than It Looks for "Test-Only" Credentials

Git history is forever. Removing the credential from the file does not remove it from git history. Anyone with access to the repository can run git log -p and find deleted credentials. Rotating the credential after detection is required; removing it from the YAML is not sufficient.

"Test-only" credentials often aren't. Test databases frequently contain sanitized copies of production data. Test service accounts often have overly broad permissions ("we'll tighten it later"). The blast radius of a "test-only" credential is often larger than assumed.

Private repos become public. Repositories get transferred, open-sourced, or accidentally made public. The private-repo assumption is not a permanent security property.

Third parties read your repository. GitHub Apps, bots, dependency scanners, and contractors all get read access to your private repository over time. Every read-access grant expands the set of entities that can access your hardcoded credentials.

The Correct Replacements

Platform secret stores are the first-line replacement. GitHub Actions Secrets, GitLab CI/CD Variables, and Bitbucket Pipelines Variables store credentials encrypted at rest, inject them as environment variables at runtime, and mask them in log output. Zero code required beyond the YAML reference:

- name: Run integration tests
  env:
    DB_URL: ${{ secrets.TEST_DB_URL }}
    DB_USER: ${{ secrets.TEST_DB_USER }}
    DB_PASSWORD: ${{ secrets.TEST_DB_PASSWORD }}
  run: ./gradlew integrationTest

The credential is in the platform's secret store, not in the repository. Rotation requires updating the secret in one place; the YAML doesn't change.

Environment-specific secret managers are the right approach for credentials that vary by environment. AWS Secrets Manager, HashiCorp Vault, and GCP Secret Manager store credentials centrally, provide audit logs of every access, support automatic rotation, and can be accessed from any environment without storing values in configuration files:

- name: Fetch test credentials
  run: |
    export DB_PASSWORD=$(aws secretsmanager get-secret-value \
      --secret-id /myapp/test/db-password \
      --query SecretString \
      --output text)
    echo "DB_PASSWORD=$DB_PASSWORD" >> $GITHUB_ENV
    # Note: GitHub Actions automatically masks values set in GITHUB_ENV
    # if the value was previously stored as a secret

Dynamic credentials via Vault or AWS IAM remove the stored credential entirely. Vault can issue a database credential that's valid for 1 hour, specifically for the integration test run, and revoke it automatically when the TTL expires. No rotation required — each pipeline run gets a fresh credential:

- name: Get dynamic DB credentials
  uses: hashicorp/vault-action@v3
  with:
    url: https://vault.internal
    method: jwt
    role: ci-integration-tests
    secrets: |
      database/creds/test-db username | DB_USER ;
      database/creds/test-db password | DB_PASSWORD

Scanning for Existing Exposures

Before adopting better practices, find what's already exposed. TruffleHog scans git history (not just current file state) for credential patterns:

# Scan entire git history for secrets
docker run --rm \
  -v "$(pwd):/pwd" \
  trufflesecurity/trufflehog:latest \
  git file:///pwd --since-commit HEAD~100 --only-verified

The --only-verified flag reduces false positives by testing whether detected credentials are still active. Any verified finding requires immediate credential rotation followed by git history rewriting (via git filter-repo) if the repository is public.

Add TruffleHog or detect-secrets as a pre-commit hook so future commits are scanned before they reach the repository:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

The baseline file records known-safe false positives so they don't block legitimate commits.

The One Rule

If a value is a credential — password, API key, token, certificate private key — it does not belong in a file that is committed to version control. Not as a placeholder, not "temporarily," not in a test environment YAML that "isn't really secret." The rule has no exceptions that are worth their risk.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Why One Developer Cannot Build an Entire Product Alone

“Can one developer build this?” sounds like a cost-saving question. In reality, it’s often the start of a much more expensive problem.

Read more

When Headcount Freezes Hit — How Hong Kong Tech Teams Keep Shipping With Remote Contractors

A headcount freeze doesn't mean the roadmap pauses. Hong Kong startups are finding ways to keep backend work moving without adding permanent staff.

Read more

From CRUD to Domain Logic: Why Backend Systems Need Better Architecture

CRUD operations are easy to understand, but real backend systems do much more. Handling business rules, events, and integrations requires thoughtful architecture.

Read more

Dubai Has No Local Backend Talent Pipeline — Every Hire Is a Global Search

You posted a backend role in Dubai. Half the applicants are in India. A quarter are in Europe. The ones already in the UAE want AED 40K per month. Nobody is local and cheap.

Read more