Environment variables on Linux are surprisingly insecure by default, often leaking sensitive information like API keys and passwords to unauthorized processes.

Let’s see how a simple web server might expose its database password.

Imagine a Python Flask app. The code might look like this:

import os
from flask import Flask

app = Flask(__name__)

# The sensitive variable
DB_PASSWORD = os.environ.get('DB_PASSWORD')

@app.route('/')
def hello():
    return f"Hello! The database password is: {DB_PASSWORD}"

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')

And we set the environment variable like this:

export DB_PASSWORD='mySuperSecretPassword123!'
python app.py

Now, if another process on the same machine, say a simple curl command or even another user’s shell, can inspect the environment of the running Python process, they can see mySuperSecretPassword123!.

How can they do that? Linux’s /proc filesystem. Every running process has a directory under /proc/<PID>, and within that, a file named environ contains all its environment variables.

A malicious or even a misconfigured process could do something like this:

# Find the PID of the Flask app (e.g., assume it's 12345)
ps aux | grep app.py

# Read the environment of that process
cat /proc/12345/environ | tr '\0' '\n'

This would output a list of all environment variables, including DB_PASSWORD=mySuperSecretPassword123!.

The problem is that the environ file is readable by all users on the system by default. This means any user can potentially snoop on the environment of any other user’s processes.

So, what’s the fix? It’s not about preventing the setting of environment variables, but controlling who can read them. The environ file’s permissions are inherited from the process’s umask at creation. The typical umask 0022 means files are created with rw-r--r-- permissions. We need to make that rw-------.

The most robust solution is to run your sensitive applications within a security context that restricts access to /proc/<PID>/environ. This often involves using systemd with specific security directives.

For systemd services, you can use ProtectSystem=full and PrivateTmp=yes. ProtectSystem=full mounts /usr, /boot, /etc read-only, preventing modification. PrivateTmp=yes creates a private /tmp and /var/tmp for the service, but crucially, it also isolates the process’s view of /proc.

Here’s a sample systemd service file:

[Unit]
Description=My Sensitive App

[Service]
User=appuser
Group=appgroup
WorkingDirectory=/opt/my_app
ExecStart=/usr/bin/python /opt/my_app/app.py
Restart=always

# Security enhancements
ProtectSystem=full
PrivateTmp=yes
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_RAW
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW

When PrivateTmp=yes is set, the systemd service manager creates a private /proc filesystem for the service. In this private /proc, the environ file for the service’s processes will only be readable by the user running the service. Other users on the system will get a permission denied error.

Another approach for applications that don’t run under systemd is to carefully manage the umask within the application’s startup script or the shell session it runs from. Setting umask 0077 before launching the application ensures that files and directories created by that process (including its environment visibility via /proc) are only accessible by the owner.

umask 0077
export DB_PASSWORD='mySuperSecretPassword123!'
python app.py

This umask 0077 will make the /proc/<PID>/environ file have permissions rw-------, preventing other users from reading it.

For containerized applications (Docker, Kubernetes), the isolation is often built-in. Containers typically have their own PID namespace and a restricted view of the host’s /proc filesystem, making this leak much harder to exploit by default. However, it’s still good practice to ensure secrets aren’t logged or exposed within the container itself.

Finally, consider using dedicated secret management tools like HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets. These tools provide more secure mechanisms for storing and injecting secrets, often mounting them as files or injecting them directly into the application’s memory without relying on environment variables.

If you fix the environment variable leak and restart your application, the next error you’ll likely encounter is related to the application failing to start because it can no longer access the database due to missing credentials, which is a good problem to have.

Want structured learning?

Take the full Cdk course →