Secrets in Environment Variables: Why `process.env` Is Not a Safe Secret Store
June 25, 2026
The Comfortable Illusion of Environment Variables
The conventional wisdom is almost universally repeated: "Don't hardcode secrets—use environment variables." It's good advice, as far as it goes. But many teams stop there, treating process.env.API_KEY (or its equivalent in Python, Go, or Ruby) as a solved problem. It isn't.
Environment variables are a better default than hardcoded strings, but they are not a secret store. They are a runtime value injection mechanism, and they leak in at least half a dozen well-documented ways that most developers never think about until after an incident.
This article maps every realistic leakage vector, explains why each one is dangerous, and gives you concrete steps to close each gap.
How Environment Variables Actually Leak
1. Process Listings and /proc on Linux
On Linux, every process's environment is readable at /proc/<pid>/environ. If any other process running as the same user—or as root—wants to read your secrets, it can. In containerized workloads on shared nodes (common in some Kubernetes multi-tenant clusters and legacy EC2 setups), this is a meaningful lateral-movement path.
Fix: Scope container privileges tightly. Run workloads as non-root, set readOnlyRootFilesystem: true, and use a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) to inject secrets at startup rather than baking them into the process environment for the full lifetime of the process.
2. Crash Dumps and Error Reports
When a process crashes, many error-reporting tools—including some popular APM agents—serialize the entire process environment alongside the stack trace. Those reports travel over the network and land in a third-party SaaS dashboard. Check your error-reporter's documentation for environment variable scrubbing; it is often opt-in, not opt-out.
Fix: Explicitly configure your error reporter to deny-list environment variable capture, or use an allowlist that includes only non-sensitive keys (e.g., NODE_ENV, PORT). For Sentry in Node.js:
Sentry.init({
dsn: process.env.SENTRY_DSN,
beforeSend(event) {
// Strip all environment context
delete event.extra;
if (event.contexts) delete event.contexts.runtime;
return event;
},
});
3. Diagnostic and Debug Endpoints
Frameworks and platforms often expose debugging endpoints that dump environment context. Spring Boot Actuator's /actuator/env endpoint is a classic example—it has exposed database passwords and API tokens in countless misconfigured deployments. Express apps that call res.json(process.env) in a debug route share the same fate.
Fix: Audit every diagnostic or health endpoint before deploying to production. If the framework exposes environment data by default, disable or restrict it. For Spring Boot, set management.endpoints.web.exposure.include to the minimum required set and never include env in production.
4. Child Processes Inherit the Full Environment
When your application spawns a child process—a shell command, a script, a compiler invocation—it inherits the complete environment of the parent by default. If that child process then logs its own environment, writes a debug file, or is exploitable via command injection, every secret in the parent's environment is now in scope.
Fix: When spawning child processes, pass an explicit, minimal environment:
import { spawn } from 'child_process';
spawn('some-tool', ['--flag'], {
env: { PATH: process.env.PATH }, // only what the tool needs
});
5. Container Image Build Arguments and Layer History
A common anti-pattern is passing secrets as Docker ARG or ENV instructions during a build. Even if the final image doesn't reference them, the values are baked into the image layer history and are readable with docker history --no-trunc or by inspecting the manifest directly.
Fix: Never pass secrets as build arguments. Use Docker BuildKit's secret mounts (--mount=type=secret) for secrets needed only at build time. For runtime secrets, inject them at container start via your orchestrator—not the Dockerfile.
6. CI/CD Log Bleed
CI pipelines often print the environment during setup steps (env, printenv, or a framework's debug mode). Many pipelines also echo commands before running them. A single set -x in a shell script that uses an environment variable will print the resolved secret value to the build log.
Fix: Never run printenv or env in a CI step unless the output is explicitly masked. Audit shell scripts for set -x in contexts where secrets are present. Most CI platforms (GitHub Actions, GitLab CI) will mask declared secrets in logs, but only if the secret is registered as a masked variable—ad hoc environment variables set inline are not automatically masked.
7. Serialized Application State and Telemetry
Distributed tracing agents, metrics shippers, and feature-flag SDKs sometimes capture process metadata on startup. If your secret is named something generic like TOKEN or KEY, it may not be caught by a scrubber looking for patterns like AWS_SECRET. Telemetry libraries that serialize "all environment context" are particularly risky.
Fix: Review the data your telemetry stack collects on initialization. Use structured naming conventions for secrets (e.g., prefix with SECRET_) and configure scrubbing rules that catch the prefix. Better still, don't put long-lived credentials in the environment at all—fetch them from a secrets manager at runtime and keep them in application memory, not in process.env.
The Better Model: Secrets Manager + Short-Lived Credentials
The end state to aim for is:
- No long-lived secrets in environment variables. Use AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, or HashiCorp Vault. Fetch the secret once at startup and store it in a non-exported in-process variable.
- Short-lived credentials via OIDC/IAM roles. For cloud provider APIs, use workload identity (IRSA on EKS, Workload Identity on GKE, managed identities on AKS) so there is no static key to leak in the first place.
- Automated rotation. If a static key must exist, rotate it on a schedule short enough that a leaked copy expires quickly.
- Scanning as a safety net. Even with the right architecture, human error happens. Automated scanning catches the cases where someone exports a secret as a fallback or a junior developer adds a
console.log(process.env)debug line that gets merged.
Audit Your Current Exposure in Under a Minute
Before you refactor your secret-handling architecture, it's worth understanding your current blast radius. A quick scan of your repos and configuration files will surface any secrets that are already committed or embedded where they shouldn't be. You can run a free GhostCred scan to get a map of exposed keys, tokens, and IAM misconfigurations across your codebase in roughly 60 seconds, with findings already tagged to SOC 2 and HIPAA controls so you know which gaps matter most to your next audit.
Key Takeaways
- Environment variables are a runtime injection mechanism, not a secrets store. Treat them as a transport layer, not a vault.
- Crash reporters, debug endpoints, child processes, image layers, CI logs, and telemetry agents are all realistic leakage paths—each requires its own fix.
- The safest architecture eliminates static credentials entirely in favor of workload identity and short-lived tokens.
- Automated scanning is a necessary backstop even when your architecture is sound, because misconfiguration and human error are inevitable.
See what's exposed in your own code.
Run a free scan