Environment variables and .env files are the most common place that API keys, database passwords and cloud credentials actually live, and they leak far more often than the vaults teams think they use. The pattern is popular because it is simple: configuration stays out of the code, every language can read it, and a developer can start a project with one file. The same properties make it fragile. Environment variables are inherited by child processes, printed by debug pages, captured in crash dumps and CI logs, baked into container images, and read by any build script or package that runs in the same process. This guide explains how environment variables and .env files really behave, where they leak, when they are acceptable, and how to move secrets to safer delivery methods without breaking applications. It is written for developers, platform engineers and security teams who own application configuration.
Key takeaways
- Environment variables are configuration, not a secret store. They are unencrypted, inherited by every child process and readable by anything running as the same user. MITRE classes the pattern as CWE-526, cleartext storage of sensitive information in an environment variable.
- A .env file is a plain text credential file. It has no formal specification, no encryption and no access control beyond file permissions, and it becomes a breach the moment it is committed, copied into an image or served by a web server.
- Prefer files, mounts and runtime fetches over variables for real secrets. OWASP, Kubernetes, Docker and systemd documentation all point to mounted files, secret mounts, credential directories or a secrets manager instead of plain environment variables.
- Short-lived credentials remove most of the risk. A leaked workload identity token that expires in an hour is a much smaller problem than a static cloud access key in a .env file that has been valid for three years.
- Detection has to cover where variables end up. Scan repositories, images, CI logs and web roots for .env content, and treat any exposure as a credential leak that needs rotation, not a file to delete.
How environment variables and .env files actually work
An environment variable is a name and value pair that the operating system passes to a process when it starts. On Linux the kernel copies the environment into the new program at execve, and the proc(5) manual describes /proc/<pid>/environ as containing the initial environment set when the program was started. Access to that file is governed by a ptrace access mode check (PTRACE_MODE_READ_FSCREDS), which in practice means the same user, or root, can read another process's starting environment. Later changes made with putenv are not reflected in the file, so the value a process was launched with stays visible even if the application clears it in memory afterwards.
A .env file is a convention, not a standard. The Node.js documentation says plainly that the format is used across many languages and platforms "but there is no formal specification for it", so Node.js defines its own parsing rules: names matching ^[a-zA-Z_]+[a-zA-Z0-9_]*$, optional quotes, # comments and an ignored export prefix so the file can be sourced by a shell. Node.js can load such files natively with --env-file, --env-file-if-exists or process.loadEnvFile(). Libraries such as the dotenv package for Node.js, python-dotenv and similar loaders in Ruby, PHP and Go read the file at start-up and copy every entry into the process environment. By default dotenv does not override variables already set, and its --override option reverses that, which matters when a stale value in the file silently replaces a correct one injected by the platform.
- Twelve-Factor origin: the Twelve-Factor App methodology recommends storing config in the environment so it can change per deploy without code changes, with the test that the codebase "could be made open source at any moment, without compromising any credentials". That test is about keeping secrets out of code. It does not say environment variables are a secure store.
- Inheritance: every child process (a shell-out, a build tool, a package post-install script, an AI coding agent's terminal) receives a copy of the parent's environment unless the parent clears it.
- No type, no audit: all values are strings, nothing records who read them, and there is no expiry.
Where environment variable secrets leak
Most incidents involving environment variables are not exotic attacks. They are ordinary tools doing what they were designed to do with data that happened to be sensitive. CWE-526 lists real examples: a CMS that displayed server-side environment variables in debug mode, a build plugin that wrote variables into build XML files, and a CI/CD tool that logged password variables.
| Leak path | How it happens | Control |
|---|---|---|
| Committed .env file | .env missing from .gitignore, added in an early commit, or committed as .env.production | Ignore rules, pre-commit and push protection scanning, history rewrite plus rotation |
| Web-served .env | Document root contains the project folder, so https://site/.env returns the file | Keep config outside the web root, deny dotfiles at the server, external scanning |
| Debug and error pages | Framework debug mode prints the full environment on an exception | Disable debug in production, test with a forced error |
| CI logs | A step runs env, printenv or set -x, or a tool prints its configuration | Platform masking, no shell tracing in secret steps, log retention limits |
| Container images | Secrets passed with Dockerfile ENV or ARG, or a .env file copied in by COPY . . | BuildKit secret mounts, .dockerignore, image scanning |
| Orchestrator metadata | Variables visible through docker inspect, a pod spec or service manager IPC | File mounts, RBAC on pod specs, systemd credentials |
| Crash dumps and APM | Core dumps, error trackers and telemetry agents capture process state or environment | Scrub rules in error trackers, restrict dump collection |
| Supply chain code | A dependency, CI script or post-install hook reads the environment and sends it out | Least privilege per job, egress controls, pinned and verified dependencies |
| AI coding tools | An agent reads .env to "understand the project" or echoes values into a terminal or prompt | Deny rules for .env in agent settings, keep real secrets off developer machines |
The supply chain path is the one that turns a local habit into a mass incident. In the Codecov breach, a single modified line in the Bash Uploader sent the output of env from every customer pipeline that ran it to an attacker-controlled server for about two months. Every CI secret exposed as an environment variable in those jobs had to be treated as compromised. See the CI/CD Pipeline Identity Security Guide.
Safer ways to deliver secrets to applications
The goal is not to ban environment variables. Non-sensitive configuration (feature flags, region names, log levels, the address of the secrets manager) belongs there. The goal is to stop using them as the long-term home for credentials. Primary guidance is consistent on this point.
- OWASP Secrets Management Cheat Sheet: for containers it lists mounted volumes, in-memory fetches from a secret store and environment variables, warns that secrets "should never be hardcoded using docker ENV or docker ARG commands", and notes that environment variables are generally accessible to all processes and may be included in logs or system dumps, so they are not recommended unless the other methods are not possible.
- systemd: the systemd.exec manual states that environment variables are not suitable for passing secrets, because unit environment is exposed to unprivileged clients over D-Bus and propagates down the process tree, including across setuid boundaries. It points to
LoadCredential=,LoadCredentialEncrypted=andSetCredentialEncrypted=, which expose the secret as a read-only file under$CREDENTIALS_DIRECTORY. See the systemd Service Hardening Guide. - Docker builds: Docker's build documentation says build arguments and environment variables are inappropriate for passing secrets to a build, because they persist in the final image, and provides secret mounts (
docker build --secretwithRUN --mount=type=secret) and SSH mounts instead. See the Secrets in Container Images and Build Artefacts Guide. - Kubernetes: a Secret can be consumed through
env[].valueFrom.secretKeyRefor as a mounted volume. Kubernetes good practice guidance reminds application owners that once the application reads the value from either source it must avoid logging it or sending it to untrusted parties. Volume mounts can be updated in place when the Secret changes, and they are not visible in the process environment. See the Kubernetes Secrets Security Guide. - Runtime fetch from a secrets manager: the application or a sidecar retrieves the secret over an authenticated call at start-up or on demand, ideally using a platform identity so no bootstrap secret is needed. See the Cloud Secrets Managers Guide.
- Federated, short-lived credentials: cloud workload identity and CI OIDC federation replace static keys entirely. See the Dynamic Secrets and Ephemeral Credentials Guide.
| Delivery method | Visible in process environment | Survives in image or logs | Rotation without redeploy | Best fit |
|---|---|---|---|---|
| Plain .env file in repo | Yes | Often | No | Never for real secrets |
| Environment variable injected by platform | Yes | Sometimes (logs, inspect) | No, restart needed | Low-value secrets, legacy apps, local development |
| Mounted secret file (Kubernetes volume, systemd credential) | No | Rarely | Often, if the app re-reads | Most server workloads |
| Runtime fetch from secrets manager | No | Rarely | Yes | Production services, high-value secrets |
| Federated short-lived token | Sometimes (token path or value) | Expires quickly | Automatic | Cloud APIs, CI/CD, workload to workload |
Choosing an approach: decision guidance
- Local development: a .env file is acceptable if it holds development-only credentials with no production reach, is ignored by Git from the first commit, and a committed
.env.exampledocuments the names with placeholder values. Pull real developer credentials from the secrets manager CLI at run time rather than pasting them into the file. - Legacy or third-party applications that only read variables: keep the variable interface but have a wrapper or init process fetch the value from the secrets manager just before start-up, so the secret never sits in a file, a unit definition or a deployment manifest. Accept that it is still visible to same-user processes, and compensate with least privilege on the credential.
- Containers on Kubernetes: mount Secrets as files by default, use
secretKeyRefonly where the application cannot read a file, and restrict who can read pod specs and Secrets with RBAC. Consider an external secrets operator or CSI driver when the source of truth is a cloud secrets manager. - CI/CD: use platform secrets scoped to environments and protected branches, prefer OIDC federation to cloud providers, and avoid passing secrets on the command line. GitHub's documentation notes that command-line arguments may be visible to other users through
ps, so environment variables or standard input are the lesser evil inside a job. - Encrypted .env tools: projects such as dotenvx encrypt values in the file and require a private key at run time. This removes cleartext from the repository but moves the problem to protecting that key, so treat it as an improvement for small teams, not a replacement for a secrets manager in regulated or multi-team environments.
- Small versus large organisations: a small team can reach most of the benefit with Git ignore rules, push protection, a single managed secrets service and cloud role credentials. A large organisation needs central policy: scanning across every repository and image registry, inventory of which services still read secrets from variables, and an exception process with expiry dates.
Where identity fits
Every value in a .env file that authenticates to something is a non-human identity credential: a database account, a cloud access key, a payment API key, a webhook signing secret. Treating them as "config" is why they escape the lifecycle controls applied to service accounts. Each one needs an owner, a scope, an expiry or rotation schedule and a record of which workloads use it. The OWASP NHI2 Secret Leakage Guide and the OWASP NHI7 Long-Lived Secrets Guide cover the wider risk. The same file often mixes production and non-production values, which breaks environment isolation; see the OWASP NHI8 Environment Isolation Guide.
AI coding assistants add a new reader. An agent with file and shell access in a project directory can open .env, include its values in context sent to a model provider, or print them in a terminal session. Claude Code's settings documentation, for example, shows deny rules such as Read(./.env) and Read(./.env.*) to stop the tool reading those files, and comparable ignore settings exist in other assistants. These rules help, but the stronger control is that developer machines should not hold production secrets at all. See the AI Coding Agents Security Guide.
How to implement: a phased migration
- Inventory. Scan every repository (including history), container registry and web server document root for files named
.env*and for high-entropy values inENVlines. List the variable names each service reads from its deployment manifests, unit files and CI definitions, and mark which ones are secrets. See the Git Secret Scanning Guide. - Stop new leaks first. Add
.envand.env.*(except.env.example) to a shared ignore template, add.envto.dockerignore, turn on push protection or pre-commit secret scanning, and block dotfile requests at web servers and load balancers. These changes are low risk and can be rolled out in days. - Rotate anything found. A secret found in history, an image layer or a public web path is compromised. Revoke and reissue it, then remove it from the source. Deleting the file without rotation leaves the credential valid. See the Leaked Credential Response Playbook.
- Pilot the new delivery method. Pick one service, move its secrets into the secrets manager, and change delivery to a mounted file or runtime fetch. Keep the old variable path available behind a feature flag for one release so rollback is a configuration change, not a code change.
- Replace static cloud keys with federation. Move CI and workload access to cloud APIs onto OIDC federation or platform roles, then delete the access keys. Check CloudTrail or the equivalent activity log for any use of the old key before deletion, and disable before deleting so a missed dependency can be restored quickly.
- Scale by platform default. Update service templates, Helm charts and pipeline libraries so the safe method is the default for new services. Track the remaining variable-based secrets as exceptions with owners and dates.
- Verify. Force an application error in staging to confirm no environment dump appears, run a CI job that would print secrets to confirm masking, and inspect a built image's history and layers for secrets.
Detection and measurement
Monitor the places variables end up, and the use of the credentials they contain.
- External exposure: scan your own internet-facing hosts for
/.env,/.git/configand backup copies, and alert on web server logs showing requests for those paths, which are routine in opportunistic scanning. - Repository and image scanning: secret scanning on every push and on registry images, with alerts routed to the owning team and the security team.
- Credential use: for keys that were ever stored in .env files, alert on use from new IP ranges, new user agents or regions you do not operate in. See the Honeytokens and Canary Credentials Guide for planting decoy keys in .env templates.
- Host telemetry: on sensitive Linux hosts, auditd rules on reads of application .env files and on
/proc/*/environby unexpected processes give early warning of local credential harvesting. See the Linux Logging and auditd for Defenders Guide.
| Metric | What it shows | Direction |
|---|---|---|
| Production secrets delivered as plain environment variables | Migration progress against the inventory | Down towards an agreed exception list |
| .env files found in repositories, images or web roots per month | Whether preventive controls work | Down, ideally zero outside .env.example |
| Median time from secret detection to rotation | Response capability | Down, measured in hours |
| Static cloud access keys in use by workloads and CI | Adoption of federation | Down |
| Repositories with push protection or pre-commit scanning enabled | Coverage of prevention | Up towards 100 percent |
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| MITRE CWE | CWE-526 Cleartext Storage of Sensitive Information in an Environment Variable | The weakness itself; mitigations include using a vault or protected file instead |
| MITRE ATT&CK | T1552.001 Credentials In Files; T1552.007 Container API; T1195.002 Compromise Software Supply Chain | Harvesting .env files, reading container configuration, and malicious build code reading the environment |
| NIST SP 800-53 Rev. 5 | IA-5(7) No Embedded Unencrypted Static Authenticators; IA-5 Authenticator Management; SC-28 Protection of Information at Rest; CM-6 Configuration Settings | No cleartext credentials in code or scripts, lifecycle of authenticators, protecting stored secrets, secure configuration of debug and logging |
| NIST CSF 2.0 | PR.AA-01; PR.DS-01; PR.PS-01; DE.CM-09 | Managing service credentials, protecting data at rest, configuration management, monitoring runtime environments |
| OWASP | Secrets Management Cheat Sheet; Non-Human Identities Top 10 NHI2, NHI7, NHI8 | Delivery patterns, leakage, long-lived secrets and environment isolation |
| CIS Controls v8.1 | Control 3 Data Protection; Control 16 Application Software Security | Protecting sensitive data and secure development practices |
| ISO/IEC 27001:2022 Annex A | 5.17 Authentication information; 8.9 Configuration management; 8.28 Secure coding | Handling of secret authentication information and secure configuration |
Common failure patterns
- .env in the web root. Frameworks that expect the project folder to sit above the public directory get deployed with the whole folder public. In the campaign described on the 230 Million AWS Cloud Environments page, Unit 42 reported attackers harvesting exposed .env files and finding more than 1,185 AWS access keys among other tokens, then using an IAM user's permissions to create an administrator role and extort victims by deleting S3 data.
- Deleting the file instead of rotating. Teams remove a committed .env, rewrite history, and leave the credential valid. Forks, clones, caches and scanners already have it. Rotation is the fix; history cleanup is hygiene.
- Treating .git and .env exposure as separate problems. Researchers in the Indian Government Breach recovered code with hard-coded credentials from exposed .git directories and SMTP, MySQL and Redis secrets from 23 exposed .env files on the same estate. Block all dotfiles and dot-directories, not just one name.
- One file for every environment. A single .env carrying production database credentials on developer laptops means any laptop compromise, infostealer or malicious package reaches production.
- Secrets in Dockerfile ENV or ARG. The value is recorded in image metadata and history and shipped to every registry the image reaches. See the Docker Hub Secrets Leak 2025 for the scale of the problem in public images.
- Debug mode in production. A single unhandled exception with debug enabled publishes every variable in the process. Make debug a deploy-time check that fails the release.
Practitioner checklist
- Add .env and .env.* to shared .gitignore and .dockerignore templates, and commit only a .env.example with placeholder values.
- Enable push protection or pre-commit secret scanning on every repository, and scan history and container images at least once.
- Block requests for dotfiles and dot-directories at every web server, CDN and load balancer, and keep application config outside the document root.
- Rotate every credential ever found in a .env file, image layer, CI log or public path, then remove the source.
- Deliver production secrets as mounted files, systemd credentials or runtime fetches from a secrets manager; keep environment variables for non-sensitive config.
- Replace static cloud keys in CI and workloads with OIDC federation or platform roles.
- Disable framework debug modes in production and verify with a forced error in staging.
- Configure AI coding tools to deny reads of .env files and keep production secrets off developer machines.
- Keep an inventory of remaining variable-based secrets with owners, scopes and rotation dates.
Standards and references
- MITRE: CWE-526 Cleartext Storage of Sensitive Information in an Environment Variable
- OWASP: Secrets Management Cheat Sheet
- systemd: systemd.exec manual (Environment and Credentials)
- Docker: Build secrets
- Kubernetes: Secrets
- Node.js: Environment Variables and .env files
- GitHub Docs: Using secrets in GitHub Actions
- Linux man-pages: proc_pid_environ(5)
Related NHI Mgmt Group resources: Secrets Management Guide · Git Secret Scanning Guide · Cloud Secrets Managers Guide · Secrets in Container Images and Build Artefacts Guide · Leaked Credential Response Playbook