
September 1, 2026
Environment variables are one of the simplest ways to configure software, which is exactly why they show up everywhere: local development, CI pipelines, containers, cloud platforms, and production services. But simplicity can become risk very quickly. When the same variable names, values, and workflows are reused across environments without discipline, teams end up with configuration drift, broken deployments, leaked secrets, and “works on my machine” behavior. OWASP recommends keeping secrets out of source code, config files, and environment variables when possible, and using centralized secrets management with least-privilege access instead. (cheatsheetseries.owasp.org)
This post walks through a practical operating model for managing environment variables across development, staging, and production. The goal is not to eliminate environment variables entirely, but to use them where they fit best: non-sensitive runtime configuration, environment-specific toggles, and integration points that need to vary by deployment. For sensitive credentials, dedicated secret storage is usually the safer default. Kubernetes, Docker, and Vault all provide patterns that can help, but the right design depends on your app architecture, deployment model, and security requirements. (kubernetes.io)

A healthy configuration strategy gives each environment exactly what it needs, and nothing more. Development needs speed and flexibility. Staging needs to behave like production without exposing real production secrets or production data. Production needs strong controls, traceability, and safe recovery paths. When configuration is scattered across .env files, shell exports, CI settings, cloud consoles, and image build arguments, the result is inconsistency. A feature might work in a local container but fail in staging because a variable was named differently, omitted, or set to an outdated value. That kind of drift is expensive because it hides until the latest possible moment.
Environment variables matter because they sit at the boundary between code and deployment. They make twelve-factor-style applications easier to deploy, but they also create a temptation to stuff everything into a flat list of key-value pairs. That temptation is especially dangerous for secrets. OWASP explicitly warns against storing secrets in code, config files, or environment variables as a general rule, and recommends a designated secrets management solution instead. The reason is practical: environment variables are often broadly accessible within a process context and can leak into logs, debug output, shell history, crash dumps, or inspection tools. (cheatsheetseries.owasp.org)
The best teams treat configuration as part of the software delivery system. They define what belongs in source control, what belongs in environment-specific deployment manifests, what must come from a secret manager, and how values are validated before release. That separation reduces surprises and makes incidents easier to resolve because operators know where to look when something breaks. In short: if you manage environment variables well, you reduce both operational friction and security risk. If you manage them poorly, they become a source of hidden coupling across every environment. (cheatsheetseries.owasp.org)
The first principle is separation of environments. Development, staging, and production should not share the same runtime assumptions unless there is a deliberate, documented reason. Production-like behavior in staging helps reveal deployment and integration issues before customers are affected, while still keeping production credentials and production data isolated. Kubernetes documentation notes that Secrets can be used to set environment variables for containers, but it also highlights that Kubernetes Secrets are stored in the control plane’s underlying data store by default unless additional protection is configured. That means the environment boundary is not just organizational; it is also technical. (kubernetes.io)
The second principle is least privilege. Only the people and services that need access to a configuration value should be able to read or change it. OWASP’s secrets guidance is explicit that engineers should not have access to all secrets in a secret management system and that access control should be fine-grained. This applies to environment variables too. If every service, build job, and developer shell can see every variable, then one compromise becomes a platform-wide compromise. The less widely a variable is distributed, the smaller the blast radius. (cheatsheetseries.owasp.org)
The third principle is to avoid secrets in source control. Once a secret is committed, it is effectively out in the open, because git history and code hosting platforms can preserve it long after the file is “deleted.” OWASP’s DevSecOps guidance recommends preventing secrets from entering the repository in the first place, ideally with pre-commit and pre-receive controls, and scanning the repository continuously as a backup measure. If a credential is exposed, the right response is to invalidate and rotate it, not simply remove the line from the file. (owasp.org)
A good rule of thumb is this: configuration is okay in source control if it is non-sensitive and the same for everyone; environment-specific but non-secret settings can live in deployment manifests; secrets belong in a dedicated secret store or equivalent managed system. That framework keeps the boundaries understandable and auditable. (cheatsheetseries.owasp.org)
Environment variables are best for non-sensitive configuration that changes by environment: feature flags, service URLs, log levels, timeouts, public API endpoints, and region selectors. They are also useful for wiring containers and processes together in a simple way. Docker supports -e, --env, and --env-file for setting or overriding container environment variables, which is convenient for ordinary configuration. (docs.docker.com)
But environment variables are a weaker fit for secrets such as database passwords, private keys, API tokens, and certificates. OWASP’s Secrets Management Cheat Sheet recommends using a designated secret management solution in any environment. It also notes that environment variables can be visible to processes and may be exposed through system inspection or dumps. Kubernetes and Docker both support secret-oriented patterns, but they also show why you should prefer purpose-built mechanisms: Kubernetes Secrets are first-class objects; Docker has docker secret and secret-oriented tooling; Vault provides a dedicated system for secret lifecycle management. (cheatsheetseries.owasp.org)
A simple decision model works well:
Use environment variables for non-sensitive runtime configuration.
Use a secret manager for credentials, keys, and tokens.
If a value is both sensitive and frequently rotated, prefer short-lived or dynamically issued secrets.
If a value is sensitive but an app only needs it at startup, inject it securely at runtime rather than baking it into the image or repo. (cheatsheetseries.owasp.org)
This distinction matters because the lifecycle is different. Environment variables are usually static key-value settings, while secret managers often provide access control, audit logs, rotation, revocation, and sometimes dynamic secret issuance. That extra machinery is worth it whenever the value has meaningful security impact or needs operational oversight. (cheatsheetseries.owasp.org)
Local development should be easy without being reckless. The standard pattern is a checked-in example file such as .env.example plus a developer-owned .env file that is excluded from source control. The example file documents required variables and safe defaults, while the local file contains personal overrides or machine-specific settings. Docker supports loading environment values via --env-file, which makes this pattern easy to use in containerized workflows. (docs.docker.com)
The key is to keep local values safe and disposable. Developers should be able to spin up the app without real production credentials, and test data should be synthetic or isolated. If the app depends on external services, local defaults should point to sandbox endpoints or mocks rather than production. A good local workflow also lets each developer override variables without changing shared files. That prevents accidental conflict in the repository and avoids the trap of one person’s machine settings becoming the team’s unofficial standard.
A robust local setup usually includes:
.env.example committed to the repo with required keys and sample values.
.env ignored by git.
Optional per-developer override files such as .env.local.
A startup check that fails fast if required variables are missing.
A validation layer that warns when a value looks unsafe, such as a real production host in a development context. (owasp.org)

One practical habit is to make local configuration boring. If developers need to manually request secrets every day, they will work around the system. If the local workflow is clear, repeatable, and well documented, people are more likely to follow it. That is one reason many teams pair .env files with a secrets CLI, a local keychain integration, or a developer portal that can fetch ephemeral values without hardcoding them. Docker’s docker pass and Vault’s CLI both show how tooling can bridge secure storage and developer ergonomics. (docs.docker.com)
Staging should be close enough to production to catch configuration bugs, but far enough away to prevent real damage. The most important discipline here is realism without risk. That means using the same classes of variables, the same service topology, and the same deployment mechanism as production, while replacing real credentials and sensitive data with fake, masked, or scoped-down equivalents. OWASP’s guidance on secrets emphasizes centralization, automation, and fine-grained access control; staging should follow the same pattern, just with non-production values. (cheatsheetseries.owasp.org)
A staging environment is where configuration validation earns its keep. Before release, the system should verify that all required variables exist, that types and formats are correct, that feature flags are set intentionally, and that the app can connect to its dependencies. If a deployment uses container orchestrators, the platform can enforce some of this at admission time or startup time. Kubernetes Secrets can be mounted or injected as environment variables, but because they are part of the cluster’s secret object model, they should still be treated carefully and separated by environment. (kubernetes.io)
Staging secrets should usually be one of three things: fake, masked, or tightly scoped. Fake secrets are placeholders for systems that do not need real auth. Masked secrets are redacted surrogates that look structurally similar to real values but cannot be used against production systems. Scoped secrets are real credentials, but only for staging-only dependencies. The important thing is that staging should never be able to affect production, even if something in staging misbehaves.
This is also the place to catch drift. If staging runs on a different set of variables than production, that difference should be explicit and documented, not accidental. A release gate can compare expected variable names and required values across environments, ensuring that the app ships with the same assumptions it will have after deployment. That approach reduces surprises and makes rollback safer because operators know which configuration changed and why. (cheatsheetseries.owasp.org)
Production is where configuration mistakes become incidents, so the rules get stricter. Secret rotation should be routine, not heroic. OWASP recommends automating rotation where possible and reducing human interaction with secrets. The point is to shrink the window of exposure if a credential leaks. For static secrets, automate replacement and revocation. For dynamic secrets, use short-lived credentials that naturally expire. (cheatsheetseries.owasp.org)
Version pinning matters because “latest” is not a stable production strategy. Configuration files, secret references, and deployment manifests should point to known versions or versioned identifiers whenever possible. If a secret store supports versions or aliases, use them intentionally. That makes it easier to roll forward or roll back without guessing which value was active at a given time. It also improves auditability because you can trace which secret version a deployment consumed. Vault’s documentation highlights environment-driven access patterns and configuration controls, while Kubernetes provides a native secret object model; both can be used in a version-aware operational process. (developer.hashicorp.com)
Auditability is essential. You should be able to answer: who changed a variable, when was it changed, what system consumed it, and was it rotated afterward? Secret managers are especially valuable here because they often record access and mutation events. That history becomes critical during incident response and compliance reviews. (cheatsheetseries.owasp.org)
Rollback safety is the final piece. A rollback is not just a code rollback; it is also a configuration rollback. If a new release requires a new secret version or a new environment variable shape, you need a plan for reverting safely. Good teams keep backward-compatible configuration during a transition period, rotate credentials in overlapping windows, and avoid destructive changes that make older versions impossible to run. That reduces the chance that a rollback becomes a second outage. OWASP’s guidance on gradual or scheduled rotation is especially relevant here. (cheatsheetseries.owasp.org)
Different platforms solve the same problem in different ways, but the underlying ideas are similar: inject configuration at runtime, separate secrets from plain variables, and make access controllable. Docker supports environment variables directly through CLI flags and also offers docker secret for Swarm-managed secrets. The docs also describe secret-focused helpers such as Docker Pass, which retrieves secrets from backends and injects them when needed. That distinction is useful: ordinary runtime config can be simple, while sensitive values deserve stronger handling. (docs.docker.com)
Kubernetes treats Secrets as native objects and can expose them as environment variables or mounted volumes. The official docs note, however, that Secrets are stored unencrypted in etcd by default unless you add encryption and other protective measures. That means Kubernetes gives you primitives, not a complete security posture. You still need cluster-level encryption, RBAC, namespace boundaries, and careful access policies. (kubernetes.io)
Vault is a dedicated secrets platform designed for managing sensitive values and operational access. Its documentation shows environment variable-based configuration for connecting clients to Vault, which is common in automation and CLI workflows. The broader pattern is that environment variables remain useful as transport for non-sensitive connection settings, while the actual secret data lives in the vault. (developer.hashicorp.com)
Across cloud providers and deployment tools, the mature pattern looks like this:
Store secret values in a managed secret service.
Inject them into workloads at runtime.
Use environment variables for wiring and configuration, not as the primary secrets store.
Limit human access through RBAC and audited workflows.
Prefer ephemeral or short-lived secrets where possible. (cheatsheetseries.owasp.org)
The tooling may differ, but the operational posture should not. Whether you use Kubernetes, Docker, Vault, or a cloud-native secret manager, the design goal is the same: configuration should be predictable, secrets should be controlled, and access should be narrow and observable. (cheatsheetseries.owasp.org)
CI/CD pipelines are powerful because they can deploy code, run tests, and provision environments automatically. They are also dangerous because they often need access to high-privilege credentials. OWASP advises treating CI/CD tooling like production infrastructure: harden it, patch it, monitor it, and apply least privilege. It also warns that secrets in CI/CD systems can leak through logs, web interfaces, forks, or misconfigured jobs. (cheatsheetseries.owasp.org)
A secure pipeline should inject variables at runtime rather than hardcode them into the build definition. Whenever possible, use a secret manager or an official platform secret store, and only expose the value to the job that needs it for the shortest possible time. Make sure secrets are masked in logs and that scripts do not echo them accidentally. The pipeline should fail if a variable is missing or malformed, but it should not dump its contents for debugging. (cheatsheetseries.owasp.org)
There are a few especially important controls:
Mask secret values in build logs.
Avoid printing entire environment dumps.
Prevent forked pull requests from accessing sensitive variables.
Restrict who can modify pipeline definitions and secret settings.
Rotate credentials regularly and after any suspected exposure. (cheatsheetseries.owasp.org)
The “forking should not leak” warning from OWASP is worth emphasizing: if a CI job or repository fork can inherit secret access, you may accidentally expose production credentials to untrusted code. This is a common weak point in automated systems because pipelines are often optimized for convenience, not trust boundaries. The fix is to separate trust levels and make secret access conditional on the source, branch, or approval state of the job. (cheatsheetseries.owasp.org)
Finally, remember that CI/CD itself should be part of your configuration governance. If pipelines inject environment variables, that fact should be documented and reviewed just like application code. Secret sprawl often starts in automation, so the pipeline is a natural place to enforce policy early. (cheatsheetseries.owasp.org)
Naming matters more than many teams expect. A consistent naming convention tells developers whether a value is secret, optional, environment-specific, or feature-related. For example, a prefix like APP_ or SERVICE_ can reduce collisions, while suffixes like _URL, _TIMEOUT_MS, or _ENABLED can make intent obvious. The goal is not just tidiness; it is operational clarity. If variable names are inconsistent, teams will make mistakes when copying settings across environments or services.
Schema validation is the next step. Your application should refuse to start if required variables are missing or malformed. That can be done in code, via a startup script, or through deployment validation. The important thing is to fail fast and explain the problem clearly. This is especially valuable in staging, where validation can catch issues before release. OWASP’s guidance on secrets management and DevSecOps aligns well with this approach because it emphasizes preventing exposure before secrets enter the repository and ensuring that errors are caught early in the delivery process. (cheatsheetseries.owasp.org)
Documentation is what keeps the system usable. Every variable should have a description, expected type, default value if appropriate, owner, source of truth, and environment scope. A simple configuration registry or README table is often enough for small systems, while larger organizations may want a dedicated service catalog or internal developer portal. If people cannot tell where a value comes from, they will guess, and guessing is how drift begins.
A good documentation standard should answer:
What is this variable for?
Is it secret or non-secret?
Which environments use it?
Who owns it?
Where is it stored?
How is it rotated or updated?
What happens if it is missing? (cheatsheetseries.owasp.org)
Over time, schema validation and documentation become part of the team’s contract with the system. They make onboarding easier, reduce ad hoc fixes, and help future maintainers understand whether a given variable is safe to change. In a multi-service environment, that is the difference between controlled evolution and configuration chaos. (cheatsheetseries.owasp.org)
One common mistake is sharing variables too broadly. A single “global” secret used by many services makes incident response harder because one compromise affects everything. OWASP explicitly calls out the problem of shared secrets and recommends fine-grained access control and centralized management instead. Every additional consumer of a secret increases the blast radius and complicates rotation. (cheatsheetseries.owasp.org)
Another mistake is overusing environment variables for secrets simply because they are easy. Easy is not the same as safe. As OWASP notes, environment variables can be exposed through inspection tools, logs, or memory dumps, and they are generally accessible to processes in ways that make them a weaker secret boundary than a dedicated vault. Use them as a transport or runtime injection mechanism when needed, but do not treat them as your only security control. (cheatsheetseries.owasp.org)
Configuration drift is another frequent failure mode. Development, staging, and production slowly diverge until nobody knows which values are authoritative. This can happen when one environment is manually patched, another uses stale defaults, or a release process updates some values but not others. The answer is to define one source of truth for each variable, validate parity between environments, and version changes intentionally. (cheatsheetseries.owasp.org)
Debug and log exposure is the final major anti-pattern. It is very common for teams to add temporary logging that accidentally prints environment variables or request context containing tokens. Because logs are often centralized and retained for long periods, one accidental print can turn into a long-lived exposure. Build your observability and debugging practices so they can solve problems without revealing secrets. Mask sensitive values, redact payloads, and use targeted diagnostics instead of full dumps. OWASP’s CI/CD and secrets guidance repeatedly stresses logging controls and monitoring for misuse. (cheatsheetseries.owasp.org)
The safest pattern is to assume that anything placed in a broadly accessible runtime context can leak eventually. That assumption encourages better design: fewer shared secrets, narrower permissions, cleaner logs, and explicit ownership. (cheatsheetseries.owasp.org)
A practical operating model starts with clear ownership. Platform or DevOps teams should own the configuration framework, security should define the control requirements, and application teams should own the variable definitions and usage in code. That division keeps the system coherent without making every change a centralized bottleneck.
Here is a straightforward implementation checklist:
Classify every variable as secret or non-secret.
Define the source of truth for each variable.
Keep non-secret defaults in source-controlled example files or deployment manifests.
Store secrets in a dedicated secret manager or platform secret store.
Use environment variables primarily for runtime injection, not long-term secret storage.
Enforce least privilege on read and write access.
Validate required variables at build, deploy, or startup time.
Mask secrets in CI/CD logs and block access from untrusted forks or jobs.
Rotate secrets regularly and immediately after exposure.
Document ownership, rotation, and rollback procedures. (cheatsheetseries.owasp.org)
A strong team operating model usually looks like this:
Developers manage local .env files and can override safe settings.
Staging mirrors production structure but uses fake, masked, or scoped secrets.
Production secrets are provisioned by automation and audited.
CI/CD retrieves secrets just in time and never prints them.
Changes to configuration follow the same review and approval discipline as code. (cheatsheetseries.owasp.org)
The most important habit is consistency. If every team invents its own approach, the platform becomes impossible to reason about. If the organization adopts one clear model and applies it everywhere, people can move faster with fewer surprises. That is the real payoff of good environment variable management: not just security, but operational calm.
Managing environment variables well is about more than keeping a few keys out of a file. It is a discipline for separating environments, reducing blast radius, and making deployments predictable. Development should be flexible, staging should be production-like but safe, and production should be controlled, auditable, and recoverable. OWASP’s guidance strongly favors centralized secret management, least privilege, automation, and early detection of leaks, while Docker, Kubernetes, and Vault show the practical mechanisms teams can use to implement those ideas. (cheatsheetseries.owasp.org)
If you take only a few ideas from this guide, make them these: keep secrets out of source control, use environment variables for non-sensitive configuration, store sensitive values in a dedicated secret system, validate configuration early, and design for rotation and rollback from the beginning. Those habits will save time in development, prevent surprises in staging, and reduce risk in production.