Hard-coded credentials in source code are the most consistently exploited entry point in cloud breaches — not because developers are careless, but because the tooling used to be painful. In 2026 there is no excuse: secrets managers are cheap, secret scanning runs in CI for free, and rotation is automatable. This guide covers the full stack from local development to production.
What changed in 2026
- Secret scanning is now table stakes. GitHub Advanced Security, GitLab, and tools like Gitleaks and Trufflehog are integrated into most CI pipelines by default.
- Managed rotation reached parity with manual. AWS Secrets Manager, GCP Secret Manager, and HashiCorp Vault all support rotation lambdas/hooks that run on a schedule without manual steps.
- OIDC workload identity replaced long-lived keys for most cloud-to-cloud auth. Pods and functions authenticate via short-lived tokens — no static credentials needed.
- Supply-chain incidents raised the bar. After several dependency-based secret exfiltrations, teams started auditing what third-party packages can read from the environment.
The secrets hierarchy
| Storage method |
Use case |
Risk level |
| Hard-coded in source |
Never |
Critical |
| Committed .env file |
Never |
Critical |
| Environment variable (injected at runtime) |
Local dev, simple services |
Low–medium |
| Secrets manager (Vault, AWS SM, GCP SM) |
Production |
Low |
| OIDC / workload identity |
Cloud-to-cloud |
Lowest |
| Encrypted secret store in K8s |
Kubernetes workloads |
Low (with RBAC) |
Environment variables: the minimum bar
For local development, a .env file that is listed in .gitignore and never committed is acceptable. Use a tool like dotenv (Node) or python-dotenv:
# .env (NEVER commit this file)
DATABASE_URL=postgres://user:pass@localhost/mydb
API_KEY=sk-live-abc123
from dotenv import load_dotenv
import os
load_dotenv()
db_url = os.getenv("DATABASE_URL")
A .env.example with placeholder values is committed, so other developers know which vars to set.
Production: use a secrets manager
Inject secrets at runtime from a managed store — never bake them into images or config maps in plain text.
# AWS Secrets Manager — fetch at deploy time
SECRET=$(aws secretsmanager get-secret-value \
--secret-id prod/myapp/db \
--query SecretString --output text)
export DATABASE_URL=$(echo $SECRET | jq -r '.url')
For Kubernetes, use the Secrets Store CSI Driver to mount secrets from your vault as volumes — avoiding Kubernetes Secrets in plain base64:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: db-secrets
spec:
provider: aws
parameters:
objects: |
- objectName: "prod/myapp/db"
objectType: "secretsmanager"
How to rotate secrets
Rotation should be automated and scheduled, not reactive. The pattern:
- Create a new credential alongside the old one.
- Update the secret store to the new value.
- Restart / reload the services that read the secret.
- Revoke the old credential after confirming the new one works.
AWS Secrets Manager handles steps 1–3 with a rotation Lambda. For database passwords, it uses the "superuser" pattern to rotate without downtime.
How to pick
- Local dev only?
.env file + .gitignore entry is fine. Use a .env.example so teammates know what to set.
- Single cloud provider in production? Use the native manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) — lower ops overhead.
- Multi-cloud or on-prem? HashiCorp Vault or OpenBao (the open-source fork) gives provider-agnostic secrets management.
- Kubernetes? Secrets Store CSI Driver + your cloud manager or Vault. Avoid base64 Kubernetes Secrets for sensitive values.
- Cloud-to-cloud auth? OIDC workload identity — no static credentials at all.
Common mistakes
Committing .env to git. Even in a private repo, every developer and CI runner now has those credentials in history. git secret or pre-commit hooks with gitleaks prevent this.
One secret for all environments. Development, staging, and production must use separate credentials. If dev leaks, production is unaffected.
No rotation. Static credentials that never rotate become permanent liabilities. Set a maximum age (90 days is common for API keys, 30 days for DB passwords).
Logging secret values. Frameworks that log request headers or env dumps will log secrets. Scrub before shipping to your log aggregator.
Overly broad IAM policies. The secret reader should only be able to read the specific secrets it needs — not all secrets in the account.
What to skip
- Encrypting secrets yourself and storing the ciphertext in source — you still have a key management problem.
- Secrets in Docker image layers — even if you delete them in a later layer, they remain in the image history.
- Shared service accounts across teams — individual workload identities make revocation and auditing tractable.
FAQ
Is a .env file ever okay in production?
Only if it is injected at container start from a secrets manager and never persisted to disk or image. The file should exist for milliseconds, not be baked in.
How do I detect a secret leak after the fact?
Tools like Trufflehog and git log --all -S <keyword> can scan full history. Rotate immediately; assume the secret is compromised the moment it leaves your vault.
What is OIDC workload identity?
Your cloud provider issues a short-lived JWT to a pod or function that it can exchange for cloud credentials — no static key stored anywhere. AWS IRSA and GCP Workload Identity Federation both implement this.
Do I need HashiCorp Vault if I am already on AWS?
Not necessarily. AWS Secrets Manager handles most production needs. Vault adds value for multi-cloud, dynamic secrets (DB credentials generated per-request), and complex access policies.
Where to go next
See How to handle errors gracefully in 2026, How to set up CI/CD in 2026, and Environment variables explained in 2026.