There's no single winner, and articles that pick one are answering the wrong question. Secret management is five separate problems, and the tools that solve them barely overlap:
- Stopping secrets from entering Git
- Getting secrets into CI without storing them
- Distributing production secrets with audit and rotation
- Committing configuration safely
- Keeping one developer's own keys organised on their laptop
A tool that's excellent at (3) is usually irritating at (5), which is why teams end up with two or three. This is a decision map by layer, with what each tool is actually for and where it stops.
We make one of the tools in layer 5. Treat that section accordingly; the rest is a map.
Minimum viable stack, by situation
| You are | Use |
|---|---|
| Solo, side projects | .gitignore + gitleaks + a local vault. That's it |
| Small team, one product in prod | Above + GitHub Actions secrets + your cloud's secret manager |
| Team with staging and multiple services | Above + Doppler or Infisical for env sync |
| Regulated, needs dynamic credentials | Vault or OpenBao + SSO + audit, plus local vaults for dev ergonomics |
| Kubernetes-heavy | External Secrets Operator or Sealed Secrets + a cloud store |
Do not skip layer 0. A pre-commit scanner costs five minutes and prevents the most common real incident. Teams routinely adopt Vault and still leak keys through git add ., because Vault was never protecting that path.
Layer 0 — Keep secrets out of Git
The cheapest, highest-return layer, and the one most often skipped in favour of something more impressive.
| Tool | Role |
|---|---|
.gitignore + .env.example | Baseline. Free, mandatory |
| gitleaks | Scan staged changes locally and in CI |
| trufflehog | Deeper scanning; can verify whether a found key is live |
| pre-commit | Framework for running hooks consistently across a team |
| GitHub secret scanning + push protection | Blocks known patterns at push time. Free for public repos |
brew install gitleaks
# Block a commit before the secret exists
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
gitleaks protect --staged --redact --verbose || {
echo "gitleaks: staged changes contain a probable secret. Aborted."
exit 1
}
EOF
chmod +x .git/hooks/pre-commit
Share hooks across a team with core.hooksPath pointing at a committed directory, so it isn't per-developer opt-in. Guide: keep secrets out of Git.
trufflehog's verification is worth knowing about: it can attempt a call against the provider to check whether a discovered credential is still valid, which turns a list of hits into a prioritised list of live exposures.
Layer 1 — CI without stored secrets
| Platform | Mechanism |
|---|---|
| GitHub Actions | secrets.*, environment protection rules, OIDC to cloud providers |
| GitLab CI | Masked and protected variables, OIDC |
| CircleCI / Buildkite | Contexts and project-level secrets |
Prefer OIDC over stored cloud keys wherever it's available. Instead of an AKIA… key in your CI secrets, the runner presents a short-lived identity token and assumes a role:
permissions:
id-token: write # required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: eu-west-1
There is no long-lived credential to leak, rotate, or forget. This is the single biggest improvement available to most teams' CI, and it removes work rather than adding it. (Configuring OIDC in AWS)
Note that masking is not secrecy: CI providers redact known secret values from logs, but a secret transformed — base64-encoded, embedded in JSON, split across lines — often slips through unredacted.
Layer 2 — Team and production secret managers
| Tool | Strength | Cost |
|---|---|---|
| HashiCorp Vault | Dynamic secrets, PKI, policy engine | High operational burden |
| OpenBao | Community fork after the licence change | Same burden; evaluate governance |
| Doppler | Best-in-class CLI and env sync for teams | SaaS, per-seat |
| Infisical | Open source, self-hostable Doppler alternative | Self-host or SaaS |
| AWS / GCP / Azure secret stores | Native IAM integration | Cheap, cloud-locked |
| 1Password | Human logins and machine credentials in one | Per-seat |
Vault's distinguishing feature is dynamic secrets — it generates a database credential on request, valid for minutes, then revokes it. Nothing durable exists to leak, which is categorically stronger than rotating a static secret well. If you need that, nothing else on this list substitutes.
Vault's cost is equally real. Unsealing, storage backend, HA, upgrades, policy authoring. Adopting it to fix .env sprawl means taking on a control plane to solve an ergonomics problem — you'll get the operational burden and keep the sprawl.
If you're in one cloud, that cloud's secret manager is usually the right default. An app reads a secret via its instance role with no stored credential, and it's inexpensive. The lock-in is real but rarely the binding constraint.
Deeper: local-first vs cloud secret managers · Doppler vs local .env management.
Layer 3 — Encrypted configuration in Git
For config that belongs in version control but must not be plaintext.
| Tool | Strength |
|---|---|
| SOPS | Encrypts values in YAML/JSON/env, keys stay readable — so diffs work |
| age | Modern, simple key layer. Far less friction than GPG |
| Sealed Secrets | Kubernetes-native: commit an encrypted Secret, the controller decrypts in-cluster |
| External Secrets Operator | Syncs from a cloud store into Kubernetes Secrets |
SOPS's design choice is the good one: encrypting only values means a diff still shows which keys changed, so code review remains meaningful. Fully encrypting the file makes every change an opaque blob.
brew install sops age
age-keygen -o ~/.config/sops/age/keys.txt
sops --encrypt --age <public-key> secrets.yaml > secrets.enc.yaml
The catch, and it's the whole game: you've moved the problem to key management. Now you must protect the age or KMS key, and getting it onto developer machines and CI runners securely is the actual work. SOPS with a cloud KMS key is usually cleaner than SOPS with distributed personal keys, because the cloud IAM handles access. See open source secret managers compared.
Layer 4 — Frontend and build-time exposure
Not usually listed as a "secret management layer", and it should be: this is where the highest-severity leaks happen.
Build tools inline prefixed variables into JavaScript sent to browsers — NEXT_PUBLIC_*, VITE_*, REACT_APP_*. The secret ends up on a CDN, cached, possibly archived. No storage tool prevents this.
# Add to CI: fail the build if a live key reached the bundle
rg -q 'sk_live_[A-Za-z0-9]+' .next/static && { echo "LIVE KEY IN BUNDLE"; exit 1; }
The fix is architectural: secrets are read only in server code, and the browser calls your backend rather than the third party directly. How to prevent leaking API keys in frontend apps.
Layer 5 — The developer laptop
The layer cloud managers structurally don't reach, and where a large share of real leaks originate.
| Tool | Strength | Limitation |
|---|---|---|
| Apple Keychain | Built in, hardware-backed | No project model, no auto-lock, poor bulk UX |
| PassStore | macOS-native, local-first, free, MIT | macOS 26+ only, no sharing, no recovery |
| 1Password / Bitwarden desktop | Sync, sharing, cross-platform | Built around login forms; use the CLI |
pass | Scriptable, plain files, Git history | GPG key management; leaks secret names |
PassStore: AES-256-GCM vault encryption, Argon2id key derivation, Touch ID unlock, auto-lock, clipboard auto-clear, workspace grouping per repo, vault health audit for reused secrets, encrypted backup export. No cloud sync and no account server, so there's no backend for data to reach. Source: github.com/ilmakio/PassStore · Security
Being explicit about the trade: no account means no password recovery, and no sharing. If either matters, use a cloud manager for that class of secret.
Roundup: best macOS apps for API keys · Guide: API key manager.
The rule that ties the layers together
One owner per secret. Everything else holds a pointer, not a copy.
| Secret class | Owner |
|---|---|
| Production credentials | Cloud secret manager / Vault. Never on a laptop |
| CI credentials | CI secret store, or OIDC with nothing stored |
| Shared team credentials | A manager with sharing and audit |
| Your own dev and test keys | Local vault |
| Non-secret config | .env, committed as .env.example |
If a value exists in three systems, at least one is stale, and you'll find out which at the worst possible time. Most "our secret management is a mess" situations are really duplication problems, not tooling problems.
What should I use if I'm just one developer?
.gitignore plus gitleaks plus a local vault. Genuinely nothing else.
You don't need Vault, Doppler, SOPS, or a Kubernetes operator. Those solve team distribution and governance — problems you don't have. Adopting them solo means real operational cost for no benefit, and the usual result is abandoning them and going back to .env files.
Spend the effort on the two things that actually matter at your scale: a pre-commit scanner so you can't commit a key, and test-mode credentials instead of production ones on your machine. Those two prevent nearly every incident a solo developer realistically faces.
What changed going into 2026?
Three shifts worth planning around:
Short-lived credentials became the default answer. OIDC in CI, aws sso login locally, GitHub App installation tokens. The strategic move is no longer "manage static secrets better" but "have fewer static secrets." A credential that expires in an hour needs almost no storage strategy.
Push-time blocking became normal. Detection after the fact is now the fallback rather than the primary control, and free secret scanning on public repos has made "we didn't know" much rarer.
The OpenBao fork matured. HashiCorp's licence change pushed some organisations toward the community fork. If you're choosing now, evaluate governance and release cadence rather than assuming either is a drop-in for the other indefinitely.
What hasn't changed: the most common leak is still a plaintext file in a project directory, committed by someone in a hurry.