
Hardcoded passwords, API tokens, and database strings don’t just invite breaches—they make audits harder, deployments riskier, and incident recovery slower. If you’re searching for how to eliminate hardcoded credentials in codebases, this guide gives you a pragmatic, step-by-step approach you can apply across languages, platforms, and team sizes.
You’ll learn how to find secrets already in your repositories, replace them with safer patterns, integrate a secrets manager, and enforce guardrails so they don’t creep back in.
What Counts as a Hardcoded Credential?
A hardcoded credential is any long-lived secret directly embedded in source code or an artifact committed to version control. Common examples include:
- Database connection strings and passwords in config files committed to Git
- API keys in source files or front-end bundles
- Private keys or certificates stored in the repo
- Cloud provider credentials checked into CI/CD pipelines
The key risk is persistence: once a secret is committed, it can be cloned, forked, cached, and backed up in many places you don’t control.
Step 1: Discover What’s Already Exposed
Start with a full inventory. Combine multiple detectors; no single tool catches everything.
- Pattern-based scanners: gitleaks, trufflehog, detect-secrets
- Custom grep for high-signal patterns (e.g., JWT-like strings, base64 keys, URI schemes)
- Platform scans: Git provider secret scanning, DLP on artifact stores
Example commands:
# gitleaks (scan entire repo history)
gitleaks detect --source . --no-git --redact
gitleaks detect --source . --log-opts="--all" --redact
# trufflehog (history scan)
trufflehog git file://. --only-verified
# ripgrep custom patterns (add your own)
rg --line-number --no-ignore -e 'AWS_SECRET|PRIVATE KEY|BEGIN RSA|password\s*=|api[_-]?key\s*=' .
Classify findings by criticality and blast radius:
- Critical: cloud root keys, production DB creds, code-signing keys
- High: third-party API keys with write scope
- Medium: staging secrets
Rule of thumb: If a secret can alter production state or access customer data, treat it as critical.
Step 2: Freeze the Bleed
Before you rewrite code, stop new leaks.
- Enable branch protection and require PR reviews.
- Add pre-commit hooks with detect-secrets or gitleaks.
- Turn on server-side scanning in your Git provider for all repos.
- Block public forks for sensitive repositories.
# pre-commit example (.pre-commit-config.yaml)
repos:
- repo: https://github.com/zricethezav/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
Step 3: Pick Your Secret Delivery Pattern
You have several safe ways to deliver secrets to apps without hardcoding:
| Pattern | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Environment variables | CI injects secrets as env vars at start | Simple, universal | Process dumps risk; rotation requires restart | 12-factor apps, short-lived jobs |
| Mounted files/volumes | Secrets written to tmpfs or container volume | Least privilege, audit via FS perms | File handling, lifecycle management | Kubernetes, sidecars, agents |
| SDK fetch on startup | App retrieves from a secrets manager with app identity | Strong authz, dynamic rotation | Extra dependency, network calls | Services with stable egress |
| Sidecar/agent | Local proxy fetches/refreshes secrets for the app | Auto-rotation, caching | Operational overhead | High-security microservices |
| SOPS + KMS | Encrypt config in Git; decrypt at deploy | GitOps-friendly, reviewable | Key mgmt, editor integration | Infra-as-code workflows |
Step 4: Wire Up Identity, Not Static Keys
Use workload identity instead of embedding long-lived credentials. Options include OIDC-based identity for CI runners, cloud-managed identities for compute, or mTLS between services. The goal: the code authenticates as itself without a shared secret typed into the repo.
- CI: Use OIDC to exchange a short-lived token for access to your secrets manager.
- Compute: Use a machine identity (e.g., service account) granted least-privilege read access to needed secrets.
- Human access: Use SSO and short-lived, audited sessions for break-glass.
Step 5: Replace Hardcoded Values in Code
Refactor code to read secrets from runtime sources. Keep it ergonomic for developers.
Example: Node.js (env vars with optional refresh)
import http from 'node:http'
function required(name) {
const v = process.env[name]
if (!v) throw new Error(`Missing required env var: ${name}`)
return v
}
const dbUrl = required('DB_URL')
const apiKey = required('PAYMENTS_API_KEY')
// Example request using the secret
// ...
http.createServer((req, res) => {
res.end('OK')
}).listen(3000)
Example: Python (fetch from a secrets endpoint with identity)
import os, requests
SECRETS_ENDPOINT = os.environ.get('SECRETS_ENDPOINT')
TOKEN = open('/var/run/secrets/identity').read().strip() # identity token mounted by platform
resp = requests.get(f"{SECRETS_ENDPOINT}/v1/kv/app/db-url",
headers={"Authorization": f"Bearer {TOKEN}"}, timeout=5)
resp.raise_for_status()
DB_URL = resp.json()["value"]
Example: Kubernetes (projected secret volume)
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: web
image: your/app:latest
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: db-url
volumes:
- name: not-needed-here
If you prefer GitOps, encrypt secrets with SOPS and decrypt at deploy time using KMS keys:
# Encrypt a YAML value with SOPS using your KMS key
sops --encrypt --in-place secrets.yaml
# In CI/CD (with workload identity), decrypt just-in-time
sops --decrypt secrets.yaml | kubectl apply -f -
Step 6: Inject Secrets in CI/CD Without Storing Them
CI should request secrets on demand using short-lived identity. Here’s a minimal GitHub Actions pattern using OIDC to fetch a secret and inject it as an environment variable.
name: build-and-deploy
on: [push]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Authenticate (OIDC) and fetch secret
run: |
# Exchange GitHub OIDC token for a short-lived token to your secrets service
OIDC_TOKEN=$(curl -s $ACTIONS_ID_TOKEN_REQUEST_URL \
-H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" | jq -r .value)
ACCESS=$(curl -s https://secrets.example.com/oidc/exchange \
-H "Authorization: Bearer $OIDC_TOKEN" | jq -r .access_token)
DB_URL=$(curl -s https://secrets.example.com/v1/kv/app/db-url \
-H "Authorization: Bearer $ACCESS" | jq -r .value)
echo "DB_URL=$DB_URL" >> $GITHUB_ENV
- name: Run migrations
run: npm run migrate
- name: Deploy
run: ./deploy.sh
Key idea: no static tokens in the repo; the pipeline receives only short-lived credentials scoped to the job.
Step 7: Rotate and Invalidate Legacy Secrets
Replacing code references isn’t enough. You must assume old secrets are compromised and rotate them.
- Generate new secrets and deploy code that references the new path/version.
- Switch traffic or flip feature flags to the new secret.
- Revoke the old secret and confirm failure of any attempted use.
- Update runbooks and ownership docs.
Automate rotation for credentials that support it, and set explicit max TTLs for all issued secrets.
Step 8: Rewrite Git History (Safely)
If credentials were committed, remove them from Git history to reduce accidental rediscovery.
# Using git-filter-repo (safer than filter-branch)
pip install git-filter-repo
# Remove a specific file from history
git filter-repo --path secrets.yaml --invert-paths
# Force-push after coordination
git push --force --all
Coordinate with all consumers to reclone. Even after history rewrite, operate as if exposed secrets were compromised until rotation is complete.
Step 9: Add Guardrails to Prevent Regression
- Pre-commit hooks required in all repos.
- Server-side scanning on push and in pull requests with blocking status checks.
- Policy-as-code that fails builds when disallowed patterns appear (e.g., dotenv usage in production images).
- Education: make “no secrets in code” a core engineering tenet.
# Example Conftest (OPA) policy (deny secrets in .env committed)
package main
denyr[msg] {
input.path.endswith(".env")
msg := sprintf("Committing .env files is not allowed: %s", [input.path])
}
Security and Compliance Wins
- Least privilege via per-service identities
- Audit trails of secret access and rotation
- Short-lived credentials reduce blast radius
- Deterministic deployments without leaking secrets to Git
These controls map well to common frameworks (e.g., SOC 2 CC6/CC7, ISO 27001 A.8/A.9, CIS controls for secure configuration), helping you reduce both operational and audit risk.
Common Pitfalls (and Fixes)
- Only moving secrets to env vars: Without rotation and IAM boundaries, you’ve just changed location. Add TTLs and per-service access.
- Secrets in build artifacts: Scan container images and packages; strip envs at build time and inject only at runtime.
- Over-permissive roles: Scope policies by path, environment, and service; use explicit deny for wildcards.
- No developer ergonomics: Provide CLI/SDK helpers and clear runbooks to avoid shadow systems.
Engineer-Focused Checklist
- Inventory and classify exposed secrets
- Enable pre-commit and server-side secret scanning
- Choose a delivery pattern (env, file, SDK, sidecar, SOPS)
- Adopt workload identity for CI and services
- Refactor code to read secrets at runtime
- Rotate and revoke legacy credentials
- Rewrite Git history where necessary
- Enforce policies in CI/CD and educate teams
Putting It All Together
Eliminating hardcoded credentials is a journey: discover, replace, rotate, and then enforce. The end state isn’t just “no secrets in Git,” it’s a system where identities are first-class, credentials are short-lived, access is observable, and developers have a smooth workflow.
Whether you standardize on environment injection, sidecars, or an SDK, the core principles remain the same: never commit secrets, authenticate workloads rather than people, and automate rotation. If you’re evaluating a dedicated secrets management platform to support these patterns, solutions like Vaulify can help centralize storage, automate rotation, and integrate with CI/CD while keeping developer experience front and center.