env.dev

Env Variables Security: Secrets, Leaks & Best Practices

Why environment variables are not truly secure and what to do about it: secret rotation, leak detection, client-side risk, and secrets managers.

Last updated:

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 eww or cat /proc/<pid>/environ reveals 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:

  1. Plain environment variables — unencrypted, no access control, no audit trail. Acceptable only for local development of non-critical services.
  2. 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.
  3. 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:

AWS Secrets Manager — automatic rotation
# 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'
HashiCorp Vault — dynamic secrets with TTL
# 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-role

For 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.

Vault audit log — enable and query
# 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:

Pre-commit scanning with git-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
Deep scanning with TruffleHog
# 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-verified

GitHub 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:

  1. Rotate immediately — generate a new credential and deploy it to all services that use it. Do not wait to investigate first; rotate, then investigate.
  2. Revoke the old credential — disable or delete the leaked key from the provider's console (AWS IAM, Stripe dashboard, etc.).
  3. Audit usage — check CloudTrail, access logs, or provider dashboards for unauthorized usage during the exposure window.
  4. Scrub from history — use git filter-repo or BFG Repo-Cleaner to remove the secret from Git history. Force-push the cleaned history and notify all contributors to re-clone.
  5. Post-incident review — document how the secret leaked, what controls failed, and what changes will prevent recurrence.
Remove a secret from Git history
# 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 --all

How 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.

Kubernetes Secret — base64-encoded (not encrypted by default)
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-secrets

Kubernetes 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.

AWS SSM Parameter Store — SecureString
# 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
FeatureK8s SecretsAWS SSMHashiCorp Vault
Encrypted at restOptional (KMS)Yes (KMS)Yes (Shamir/auto-unseal)
Auto rotationNoVia Secrets ManagerYes (dynamic secrets)
Audit loggingVia RBAC auditCloudTrailBuilt-in audit device
Access controlRBACIAM policiesACL 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.*.local before your first commit.
  • Set restrictive file permissions — use chmod 600 .env to 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 vs right — secrets in Docker
# 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.js

For 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:

FrameworkPublic prefixExample
Next.jsNEXT_PUBLIC_NEXT_PUBLIC_API_URL
Create React AppREACT_APP_REACT_APP_ANALYTICS_ID
ViteVITE_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

Validate your environment configuration with the env validator, or read the guide on sharing .env files securely with your team.

Was this helpful?

Read next

How to Share .env Files With Your Team Securely

Share .env files securely with send.env.dev — end-to-end encrypted, burn-on-first-read links, EU-hosted, zero dependencies. Plus 1Password, Doppler, Vault.

Continue →

Frequently Asked Questions

Are environment variables secure?

Not inherently. Environment variables are visible in process listings (/proc/PID/environ on Linux), crash dumps, logs, and child processes. They are better than hardcoding secrets in source code but worse than a dedicated secrets manager.

What should I do if a secret is leaked?

Immediately rotate the compromised credential (generate a new key, revoke the old one). Audit access logs to determine if the secret was used maliciously. Update all deployments with the new credential. Investigate how the leak occurred and fix the root cause.

What is the NEXT_PUBLIC_ / REACT_APP_ / VITE_ exposure risk?

Variables with these prefixes are embedded into client-side JavaScript bundles at build time. They are visible to anyone inspecting your site. Never put API keys, database URLs, or any secret in a client-side exposed variable.

Stay up to date

Get notified about new guides, tools, and cheatsheets.