One of my favorite ways of passing information to a program without leakage of these secrets is environmental variables. The rationale is:
- Reading from a plain text with no permissions set, will lead to any user in the system to be able to read this file which is unnaceptable.
- Reading from a plain text file with permissions only to the user running the process, may lead to the file being included by mistake in the version control system. It can also mean that the whole process may call a function to read the file. Better but still easy to make mistakes with.
- Passing the secrets as command line arguments leads to their complete exposure. Every user is able to access any process’ command lines. This means that even a lowly privileged user can in theory access /proc/<pid>/cmdline and inspect the secrets passed through command line arguments. This is further illustrated in another post: Accessing a process command line without ps. This is unnaceptable.
- The other way is for some environmental variables to be passed to the application. Often the environmental variables are passed from a file or command line which obviously leads to the problems described above. There are safe ways to pass environmental variables besides variables or command line and I will talk about them.
From the above methods, we can extract at the very least 2 requirements:
- The secrets should not be accessible to arbitrary parts of the process, at arbitrary states.
- The secrets should not be accessible by any other means than through the process.
- The secret should not be accessible by any other user in the system, except for the root user. Without security enclaves it is not possible to hide anything from a root user. I will give some of the reasons further down.
- The secret should not be accessible by the process’ user, except through the process that is supposed to use it.
Dockefiles/docker-compose.yml and systemd units are often interpreted in a privileged or sandboxed environment, so they actually provide a medium where you can define and write down your secrets and have them safely passed as environmental variables to the process consuming the secrets. Naturally, you need to make sure that any systemd units or Dockerfiles are not readable by anybody but the root user. This ensures compliance with the second requirement but not with (1).
The non-compliance with the first requirement is where I believe this post has something to offer.
I have seen a lot of support for passing secrets with environmental variables but not much advice on their compartmentalization inside the process. This is especially important because environmental variables are in fact global variables to a process. Any arbitrary process’ state can access environment variables. If for any reason a process is compromised, an attacker can just spawn a process or call the relevant standard library function and get the current process-wide environmental variables set. Most process spawnings pass the current process’ environmental variable so if there is a shell injection issue in your code you will have your secrets exposed.
Similarly, if an attacker can compromise your application so it dumps the current environmental variables your secrets will also be exposed. A common situation is a variable expansion issue like the ones possible in PHP. There
$_ENV can be accessed and your secrets revealed. As stated environmental variables are super-global information.
One might argue that both the process spawn and running of environmental variable-related code requires a compromised process and that in such a situation one has lost any ability to protect itself. That is not true. Defense in depth is a thing, and I propose mitigating actions that can fulfill requirement (1):
- Retrieve the secrets from the environmental variables as early as possible in the startup of your process.
- Remove or set the the environmental variables with the secrets to blank.
- Reduce the availability of secrets throughtout your application, making them available only to the parts that need them. This is may be hard to achieve in terms of architecture.
The above actions mean the shell injection attacks will be unable to inspect secrets from the parent process. It also means that in the case of a local code path allowing inspection of global variables or code execution, an inspection of the environmental variables will not reveal any secrets.
Naturally, if your application’s memory is compromised your secrets are revealable, but that is beside the point of this post, as is the case where the root account is compromised. If an attacker is a root user they can inspect arbitrary parts of the system’s memory. Similarly, if internal program memory is compromised an attacker can just read the secrets from memory directly.
In sum, to use environmental variables effectively for secret sharing:
- Pass secrets as environmental variables from a more priviliged user than the process requiring the secrets.
- Wipe the environmental variables as soon as you capture them into memory.
- Keep availability of secrets as restricted as possible.