Environment variables are the most common way to pass secrets to applications, but they are not inherently secure. Every secret stored in an environment variable is readable by any process running under the same user, visible in /proc/<pid>/environ on Linux, logged in crash dumps, and often captured by error-tracking services. According to GitGuardian's 2024 State of Secrets Sprawl report, over 12.8 million new secrets were exposed in public GitHub repositories in a single year. Understanding where environment variables fall short — and what to do about it — is the foundation of any secrets management strategy.
Why are environment variables not truly secure?
Environment variables exist as plaintext in process memory. Any user or process with sufficient privileges can read them. Specific exposure vectors include:
- Process listings — on many systems,
ps ewworcat /proc/<pid>/environreveals every env var for a running process. - Crash dumps and core files — when an application crashes, environment variables are captured in the memory dump. If core dumps are enabled and stored insecurely, secrets are exposed.
- Logging and error tracking — frameworks like Sentry, Datadog, and even simple
console.log(process.env)calls can serialize the entire environment into log files or third-party services. - Child process inheritance — every child process inherits the parent's environment. A subprocess you don't control (a build tool, a linter plugin) has full access to every secret you exported.
What is the secrets management hierarchy?
Not all secrets storage mechanisms are equal. From least to most secure, the hierarchy is:
- Plain environment variables — unencrypted, no access control, no audit trail. Acceptable only for local development of non-critical services.
- Encrypted files (SOPS, git-crypt) — secrets at rest are encrypted, but once decrypted they still live in memory as env vars. Better for version control and team sharing.
- Dedicated secrets managers (Vault, AWS Secrets Manager, Doppler) — centralized storage with access control, audit logging, automatic rotation, and short-lived leases. The recommended approach for production workloads.
Move up this hierarchy as your application matures. A side project can start with .env files; a production system handling user data should use a dedicated secrets manager.
How should you apply least privilege to environment variables?
The principle of least privilege means each process, service, and developer should access only the secrets they need. In practice:
- Scope secrets per service — a payment service needs Stripe keys, not your email provider credentials. Use separate secret paths or namespaces.
- Use read-only tokens where possible — if a service only reads from an API, issue a read-only key rather than a full-access one.
- Separate environments strictly — production secrets must never appear in development or staging configurations. Use distinct secret stores or namespaces per environment.
- Restrict CI/CD access — only grant pipelines the secrets they need for their specific stage. Deployment secrets should not be available during linting or testing.
How do you implement secret rotation?
Secret rotation limits the blast radius of a leaked credential. If a key is rotated every 30 days, a compromised key has a maximum 30-day window of exploitation. Effective rotation strategies include:
# Enable automatic rotation with a Lambda function
aws secretsmanager rotate-secret \
--secret-id "myapp/prod/db-password" \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789:function:rotate-db \
--rotation-rules AutomaticallyAfterDays=30
# Verify rotation status
aws secretsmanager describe-secret \
--secret-id "myapp/prod/db-password" \
--query 'RotationEnabled'# Configure a database secrets engine with a 1-hour TTL
vault write database/roles/myapp-role \
db_name=mydb \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
default_ttl="1h" \
max_ttl="24h"
# Request a short-lived credential (auto-revoked after TTL)
vault read database/creds/myapp-roleFor services that do not support automatic rotation, maintain a documented manual rotation procedure with a calendar reminder. Even manual rotation every 90 days drastically reduces long-term exposure.
How do you audit access to secrets?
Audit logging answers the question: who accessed which secret, when, and from where? Without audit trails, you cannot detect unauthorized access or satisfy compliance requirements like SOC 2, HIPAA, or PCI DSS.
# Enable file-based audit logging
vault audit enable file file_path=/var/log/vault/audit.log
# Each access is logged as JSON with timestamp, path, and identity
# Example log entry (abbreviated):
# {
# "type": "response",
# "auth": { "display_name": "deploy-bot", "policies": ["prod-read"] },
# "request": { "path": "secret/data/myapp/prod", "operation": "read" },
# "response": { "data": { ... } }
# }AWS Secrets Manager and AWS SSM Parameter Store automatically log all access events to CloudTrail. Doppler and Infisical provide built-in dashboards showing who accessed or modified each secret.
How do you detect leaked secrets?
Prevention is important, but detection is equally critical. Multiple tools can scan your codebase and commit history for accidentally committed secrets:
# Install git-secrets
brew install git-secrets # macOS
# or: git clone https://github.com/awslabs/git-secrets && cd git-secrets && make install
# Register AWS patterns and install hooks
git secrets --register-aws
git secrets --install
# Scan entire repo history for secrets
git secrets --scan-history# Scan a repository for high-entropy strings and known patterns
trufflehog git file://. --only-verified
# Scan a GitHub organization
trufflehog github --org=your-org --only-verified
# Scan specific commits in CI
trufflehog git file://. --since-commit HEAD~10 --only-verifiedGitHub Secret Scanning is enabled by default on public repositories and available for private repositories on GitHub Advanced Security. It detects over 200 token formats from partners like AWS, Stripe, and Google Cloud, and can automatically revoke some leaked tokens. Enable push protection to block commits containing secrets before they reach the remote.
What should you do when a secret is leaked?
A leaked secret is a security incident. Speed matters — the median time for attackers to exploit a leaked AWS key is under 2 minutes. Follow this procedure:
- Rotate immediately — generate a new credential and deploy it to all services that use it. Do not wait to investigate first; rotate, then investigate.
- Revoke the old credential — disable or delete the leaked key from the provider's console (AWS IAM, Stripe dashboard, etc.).
- Audit usage — check CloudTrail, access logs, or provider dashboards for unauthorized usage during the exposure window.
- Scrub from history — use
git filter-repoor BFG Repo-Cleaner to remove the secret from Git history. Force-push the cleaned history and notify all contributors to re-clone. - Post-incident review — document how the secret leaked, what controls failed, and what changes will prevent recurrence.
# Using git filter-repo (recommended over filter-branch)
pip install git-filter-repo
git filter-repo --replace-text <(echo 'sk_live_leaked_key==>REDACTED')
# Using BFG Repo-Cleaner
java -jar bfg.jar --replace-text passwords.txt repo.git
# Force-push the rewritten history
git push origin --force --allHow do Kubernetes, AWS SSM, and Vault compare for secrets management?
Each platform has its own secrets primitive. Choosing the right one depends on your infrastructure.
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXNxbDovL3VzZXI6cGFzc0BkYi5leGFtcGxlLmNvbTo1NDMyL2FwcA==
API_KEY: c2tfbGl2ZV9leGFtcGxlX2tleQ==
---
# Mount as environment variables in a Pod
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
envFrom:
- secretRef:
name: myapp-secretsKubernetes Secrets are base64-encoded, not encrypted. Enable EncryptionConfiguration with a KMS provider, or use the External Secrets Operator to sync from Vault, AWS Secrets Manager, or GCP Secret Manager directly into Kubernetes Secrets.
# Store an encrypted parameter
aws ssm put-parameter \
--name "/myapp/prod/DATABASE_URL" \
--value "postgresql://user:pass@db.example.com:5432/app" \
--type SecureString \
--key-id alias/myapp-key
# Retrieve with decryption (requires kms:Decrypt permission)
aws ssm get-parameter \
--name "/myapp/prod/DATABASE_URL" \
--with-decryption \
--query Parameter.Value --output text| Feature | K8s Secrets | AWS SSM | HashiCorp Vault |
|---|---|---|---|
| Encrypted at rest | Optional (KMS) | Yes (KMS) | Yes (Shamir/auto-unseal) |
| Auto rotation | No | Via Secrets Manager | Yes (dynamic secrets) |
| Audit logging | Via RBAC audit | CloudTrail | Built-in audit device |
| Access control | RBAC | IAM policies | ACL policies |
How do you secure .env files in development?
The .env file is a development convenience, not a security tool. Treat it accordingly:
- Always add to .gitignore — this is non-negotiable. Add
.env,.env.local, and.env.*.localbefore your first commit. - Set restrictive file permissions — use
chmod 600 .envto ensure only the file owner can read it. - Never bake into Docker images — using
COPY .env .in a Dockerfile embeds secrets into every layer. Anyone with access to the image can extract them. Use runtime injection instead.
# WRONG: secrets baked into the image
COPY .env /app/.env
RUN source /app/.env && node build.js
# RIGHT: inject at runtime via docker run
# docker run --env-file .env myapp
# Or use Docker secrets / BuildKit --mount=type=secret
# docker build --secret id=env,src=.env .
RUN --mount=type=secret,id=env source /run/secrets/env && node build.jsFor team workflows around sharing .env files, use encrypted sharing tools rather than Slack or email.
What are client-side exposure risks with environment variables?
Frontend build tools inline environment variables into the JavaScript bundle at build time. Any variable included in the client bundle is visible to every user via browser DevTools. Each framework uses a different prefix to control which variables are exposed:
| Framework | Public prefix | Example |
|---|---|---|
| Next.js | NEXT_PUBLIC_ | NEXT_PUBLIC_API_URL |
| Create React App | REACT_APP_ | REACT_APP_ANALYTICS_ID |
| Vite | VITE_ | VITE_PUBLIC_KEY |
The critical rule: never prefix a secret with a public prefix. Variables like NEXT_PUBLIC_STRIPE_SECRET_KEY or VITE_DATABASE_URL are shipped directly to the browser. Only use public prefixes for values that are safe to be public: API base URLs, analytics IDs, and feature flags. Keep server-only secrets without the prefix, and access them exclusively in server-side code or API routes.
References
- GitGuardian State of Secrets Sprawl 2024 — annual report quantifying secret leaks across public GitHub.
- AWS Secrets Manager — rotating secrets — official guide for managed and Lambda-backed rotation.
- HashiCorp Vault — database secrets engine — dynamic, short-lived database credentials with TTL.
- Kubernetes Secrets reference — built-in Secret types, encryption-at-rest, and access controls.
- External Secrets Operator — sync secrets from Vault, AWS, GCP, or Azure into Kubernetes.
- GitHub Secret Scanning — detection and push protection for over 200 token formats.
- TruffleHog — open-source scanner that classifies and verifies 800+ secret types.
- git-secrets — pre-commit hook from AWS Labs to block committed credentials.
Validate your environment configuration with the env validator, or read the guide on sharing .env files securely with your team.