As enhancing deployment is part of the 12-factor principles, many programming languages and frameworks include helpers and external libraries to ease the use of environment variables.
However, some practices can introduce some risks. Let's see how ENV vars work and how to secure their usage.
Loading configuration files
Modern webapps usually require ENV vars. It's not uncommon to use .env
files where you can write a series of key/value pairs. Each environment (like staging, dev, or production) gets its own .env
with its own values.
Only the keys remain the same across all environments.
N.B.: Note that some environment might also use additional vars, for example, to configure the debug
The app loads the environment from the configuration file, which often contains secrets like API keys and DB credentials. It might be handy, but it has some risks and inconvenient:
- the
.env
files are often committed in the versioning system (e.g. git) - the
.env
files are sometimes deployed on a public folder in production without any restriction (I've seen it multiple times) - maintaining these files can quickly become tedious
Attackers will enumerate these files at the speed of light, using free tools like Gobuster or Nikto (there are many other scanners available). There are some workarounds:
- give the
.env
files specific permissions likechmod 600
to restrict access - forbid access in your
.htaccess
, nginx config, or Web.Config to all.env
files - store these files in a specific folder that is not publicly accessible
Let's cut to the chase: none of these workarounds is completely safe. Configurations can be modified on purpose or accidentally, resulting in a public disclosure. At least, it would add a thin layer of security, in addition, but it would not be sufficient in the long term.
In the last scenario, the server points the webapp to a specific subfolder like Public/
instead of exposing the whole installation:
│
├── .gitignore
├── Public/
│ ├── style.css
│ └── index.php
│
├── Config/
│ ├── .env
│ └── config.json
│
└── README.md
This above imaginary structure keeps the .env
file outside the web root. A more concrete example would be WordPress and its wp-config.php
file that contains database credentials.
Alternative approaches such as Bedrock can provide more protection. The framework leverages ENV vars to store information and provides a convenient way to override configurations according to the environment.
Still, it can have major drawbacks:
- the framework might not let you do whatever you want
- you might get unwanted side effects and additional bugs when migrating from a classic installation
- depending on your approach of versioning, some creds can still be in your git repo in plain text (e.g., dev config)
How these ENV vars work?
Getting started
Each ENV var has a name and an associated value, and the name will likely be uppercase:
MY_ENV_VAR="the value"
Technically speaking, you don't have to use uppercase, but it's a naming convention. On a Linux machine, you can print ENV vars with the env
command (no option).
These vars are used by many processes (~ programs), and the values can deeply modify their behavior. For example, $PATH
, the ENV var that contains... paths, allows using various tools and commands without entering the path to the binary again and again.
Roughly speaking, this is how most systems work, with processes that fork other processes. During these operations, ENV vars are implicitly available.
On your machine, you may use the export
command to set a new ENV var:
export MY_ENV_VAR="the value"
How to make ENV vars persistent
The variable is only available for child sessions and will die with the current shell. In other words, if you exit or close the window, your variable is lost.
To create a persistent var, you can use the same export
command and save it in the .bash_profile
or the .bashrc
file.
It's pretty cool, but never use such files to store secrets, as many programs will be able to read them.
ENV vars vs. shell vars
MY_ENV_VAR="the value"
and export MY_ENV_VAR="the value"
are not the same commands at all. The first one defines a local var.
To transmit this var to every new child process automatically, you must use export
, which converts the local var into an ENV var.
Otherwise, it won't be available.
Don't store secrets in ENV vars
Due to the very nature of ENV vars, using them to store secrets is not the best idea:
- for any given process, all child processes will inherit ENV vars, exposing the secrets to way more processes than necessary
- the system will log events containing ENV values in plain text (e.g., debugging, error logs, third-party tools)
- hackers could use the
env
orprintenv
command to print ENV vars after exploiting a vulnerability
Good practices and alternatives
- don't consider all ENV vars are equal
- don't use the same API keys for all environments
- be extra vigilant with permissions and accessible secrets, especially in cloud-based architectures
- use encrypted secrets. For example, GitHub provides very convenient interface to encrypt your secrets and manage them with granularity
- use CI/CD solutions that abstract ENV vars away and allow you to update values via secured APIs
- if you use Kubernetes, use the feature called "secrets", but be aware that secrets are stored unencrypted by default
Wrap up
ENV vars are accessible pretty much everywhere, making them particularly convenient for web development, but sometimes exposing projects to severe leaks and escalations.
The alternative solutions are not always the easiest to implement, and you won't solve all security issues just by skipping ENV vars, but storing secrets in plain text is a bad practice.
Top comments (0)