Secrets management
How credentials reach a test run without living in the repository, and what to do when one leaks. Also the discipline that makes the AI layers safe — see what never goes in a prompt.
Every test suite eventually needs a real-looking credential: an API key to hit a staging service, a database password to seed fixtures, or a token to authenticate a mocked call. The moment that value is typed into a file, it can end up in git history, a CI log, or a screenshot artifact forever. Secrets management in testing is the discipline of keeping that from happening. It matters as much for test data management as it does for production infrastructure.
Why secrets leak in the first place
Most leaks are not malicious. A developer hardcodes a key to get a test passing quickly, means to remove it, and forgets. A .env file gets committed because .gitignore was set up after the first commit. A CI job prints an environment variable while debugging a failing step. That log then stays public for anyone with repo access.
Test fixtures are a particularly common leak point because they look disposable. A JSON fixture with a real API response, a recorded HTTP cassette, or a seeded database dump can carry an actual production credential. Someone copied it from a live system just to make the test realistic.
Flaky, rerun-until-green tests make this worse. A team debugging flakiness tends to add verbose logging first and remove it later. "Later" is exactly when a secret sits in a log for months.
Scanning tools that catch it before merge
Three tools cover most of this ground, and they work as pre-commit hooks or CI gates:
- git-secrets: scans commits for patterns matching AWS keys and custom regexes before they land, blocking the commit locally.
- gitleaks: fast, config-driven scanning of a repo's full history, commonly run in CI on every pull request.
- TruffleHog: verifies whether a found secret is still active against the real provider, cutting down false positives.
GitHub's own secret scanning adds a backstop. It watches public and enrolled private repos for known credential formats. It can auto-revoke some provider keys on detection.
Vaulting and rotation instead of hardcoding
The fix upstream of scanning is simple: never let a real secret exist as a literal string in code. HashiCorp Vault and AWS Secrets Manager both let a test runner fetch a short-lived credential at run time. That credential is scoped to exactly what the job needs, and it expires soon after.
For simpler setups, .env files still work if the discipline holds. The file is git-ignored from day one. A .env.example ships with placeholder values in its place. CI injects real values from its own secret store rather than a checked-in file.
# CI pulls a scoped, short-lived token instead of reading a checked-in file
export STAGING_API_TOKEN=$(vault kv get -field=token secret/ci/staging-api)
npm run test:integration
unset STAGING_API_TOKENThe QA-specific angle: your test suite is also a leak vector
Test suites do not just risk leaking secrets from source files. They generate their own exposure surface, and it is easy to miss because it does not look like code.
Never use real production secrets in test data or fixtures, full stop. A "temporary" copy of a live API key pasted into a fixture for realism outlives the person who pasted it. Seed fixtures with clearly fake values instead, generated the same way as any other test data.
Consider a worked example: a team building an order-processing API records a Polly cassette against a staging payment gateway. This lets integration tests run fast without hitting the network. The cassette captures the outbound request headers verbatim, including a real staging API key. It gets committed alongside the test file because nobody thought to check it.
Six months later a contractor forks the repo for a proof of concept, and the staging key ships with it. The fix is a redaction filter on the recording library that strips the Authorization header before the cassette is written. Pair it with a pre-commit gitleaks scan that would have caught the raw key even if the filter had been skipped.
Three places a test run leaks secrets even when the source code is clean:
- Log output: a failing assertion often dumps the full request object, headers included, into CI logs that are retained for months and visible to anyone with repo access.
- Screenshots and video recordings: end-to-end frameworks that capture screenshots on failure will happily capture an auth token sitting in a URL query string or a visible admin field.
- Recorded API mocks: cassette-based mocking tools like VCR or Polly record real HTTP traffic. That includes auth headers, and the cassette gets checked into the repo verbatim.
The fix for all three is the same pattern. Scrub known secret patterns from logs and recordings before they are written. Use sanitized or synthetic credentials in any environment where a mock might be recorded. Wiring a gitleaks scan into CI/CD for test suites catches most of this automatically, before a human ever reviews the diff.
FAQ
Questions people ask
What should I do if a secret was already committed to git history?
Rotate the credential immediately at the provider first. Then rewrite history with BFG Repo-Cleaner or git-filter-repo. Rotation matters more than cleanup, since forks and clones keep old history regardless.
Is a private repo safe enough to skip secret scanning?
No. Private repos still get cloned by CI runners, third-party integrations, and departing employees. Any of those copies can leak independently of the main repo's visibility setting.
How do recorded API mocks end up with real secrets in them?
Cassette tools like VCR record the literal HTTP request and response the first time a test runs against a real service. That includes auth headers, unless the recording config explicitly redacts them.