The reason is that `getenv()` hands out raw pointers to the variables, so overwriting a variable using `setenv()` is impossible to guard against. Treat with extreme caution.
FreeBSD does the same. See here for discussion: https://freebsd-current.freebsd.narkive.com/NwqZQDWm/fix-for...
For a lot of applications that's the right call given the rest of the posix semantics you're constrained to and the kinds and frequency of data you pass via env vars.
execve seems like the preferable choice on a lot of grounds.
Arguably that's less harmful than the thread-safety issues on Linux, but there's no perfect solution here as long as POSIX stays what it is.
I think that would still be better than env vars, which are more likely to leak somewhere you didn't intend them to.
[0] https://man.netbsd.org/getenv_r.3
[1] https://github.com/freebsd/freebsd-src/commit/873420ca1e6e8a...
> But in reality, not many applications use lowercase. The proper etiquette in software development is to use ALL_UPPERCASE
I always prefer lower case for env variables in scripts. Thanks for pointing out that it can help reduce clashes with standard tools.
Argument list too long
It's absolutely crazy that this isn't a dynamically resizable vector.The environment isn't persistent between sessions. That means you need to make the change in a way that runs on every new session (login or new terminal window).
Depending on how your system is configured:
.bash_profile gets run once on every login
.bashrc gets run once on every non-login new session (i.e. a new terminal window)
It's typical, if using these files, to do something like this:
if [ -f ~/.bashrc ]; then
source ~/.bashrc
fi
in the .bash_profile file, putting most other things in .bashrc, such that you don't have to worry about the distinction.If you're not even using bash or bash-likes at all, but instead something like Zsh, fish, etc you'll need to set things the way they want to be set there.
> They should add a simple env var GUI like Windows has that just works, and isn't terminal-specific
This doesn't exist in linux, because there isn't "one place" that all possible terminals draw from. Conceivably, it's possible to write a GUI tool that reads your .bashrc file, looks for anything that resembles a variable, parses the bash code to look for ways it is set, and then present a GUI tool that could update it in common cases, but... it's way easier to just write the change in a text editor.
What's wrong with an envfile or envdir? The envdir is kind of annoying but at least you can set permissions on the files inside it.
NixOS & home-manager do have functions that can set environment variables across all shells enabled via either of those systems. So I've got `programs.bash.enable = true;` && `programs.fish.enable = true;` && `home.sessionVariables = { EDITOR = "nvim"; };` in my home-manager config for example. A GUI editor for that would certainly be possible.
The application restarting part can't really be fixed, since environment variables aren't ever injected to a running process and can only be changed by the process itself (terms and conditions may apply) and even changes during runtime could be ignored since the program itself may have cached some already computed value based on the variable.
[0]: https://www.freedesktop.org/software/systemd/man/latest/envi...
Situation: There are 14 competing ways to set environment variables on Linux
We should create a universal way that just works and isn't terminal specific to set environment variables!
Situation: There are 15 competing ways to set environment variables on Linux
Contribute to a clean environment! Don't use environment variables for configuration! </rant>
First thing that struck me about the site is how beautiful I found it. I even inspected the font: Iosevka Web, apparently.
pam-config -d --env
[0] Crowded elevator atrium. Multiple elevators running. Elevator wants to close, another one is coming (oh! I heard it "ding"!). Somebody is holding the elevator which wants to depart and trying to wave me in. Why doesn't somebody push them out?[1] I'm at a stop sign. Some complete idiot is trying to turn left onto the street I'm leaving and waving me to turn left in front of them. Fuck no! I turn in front of you, somebody rearends you, you fly forward into me: my fault! You should be able to make this turn, if you can't go around the block! [2]
[2] I could go out of my way and turn right. Or I can just wait and see what happens.
Anything involving vaults where Application reaches out to specific secret vault like Hashicorp Vault/OpenBao/Secrets Manager quickly becomes massive vendor lock in where replacement is very difficult due to library replacement and makes vault uptime extremely important. This puts Ops in extremely difficult place when it becomes time to upgrade or do maintenance.
Config files have problem of you have secrets, how do you get them into config file since config files are generally kept in public systems? Most of the time it's some form of either "Template replacement by privileged system before handing it to application" or "Entire Config File gets loaded into secret vault and passed into application". Templating can be error prone and loading entire config files into secret manager is frustrating as well since someone could screw up the load.
Speaking of config files, since most systems are running containers, and unless you are at Ops discipline company, these config files are never in the right place, it becomes error prone for Ops to screw up the mounting. Also, whatever format you use, JSON/YAML/TOML is ripe for some weird config file bug to emerge like Norway problem in YAML.
Getting secrets from Kubernetes Secrets API I've seen done but lock in again. I'd strongly not recommend this approach unless you are designing a Kubernetes operator or other such type system.
I will say I've seen Subprocess thing bite people but I've seen less and less subprocess generation these days. Most teams go with message bus type system instead of sub processing since it's more robust and allows independent scaling.
Then, on the backend, you can configure etcd to use whatever KMS provider you like for encryption.
Yes, you can mount Secrets as Volumes or Env Var in Kubernetes which is fine but I'm not talking about "How you get env var/secret" but "Methods of dealing with config."
Bit more of an initial hurdle than "just run the docker image"; however.
However, most of time, Devs don't need to develop on Kubernetes since it's just Container Runtime and Networking Layer they don't care about. They run container, they connect to HTTP endpoint to talk to other containers, they are happy. Details are left to us Ops people.
I’ve seen Applications that do direct calls to Kubernetes API and retrieve the secret from it. So they have custom role with bindings and service account and Kubernetes client libraries.
Keep it simple and design your applications so they’re agnostic to that fact.
It’s really not that hard, I’ve been doing this for at least 6 or 7 years. A little bit of good engineering goes a long way!
So Command Line leaks worse than Env Var.
Config file, see original post for problems.
Env Var, see blog for problems.
For example mbsync/isync does this.
String manipulating is one of those "This is easy" until it's not.
I don't mean to hardcode secrets into the config file either. I was suggesting to put a command into the configuration file that the application then calls to get the secret. This way the secret is only ever passed over a file descriptor between two processes.
You can not really rely on that. The initial stack layout is well-defined at least on linux, so digging up argv is not difficult. Or just open /proc/self/cmdline
No, it was very interesting actually!
An excellent deep-dive into the murky area of Unix/Linux environment variables, how they are set, how they are passed, what's really going on behind the scenes.
Basically a must-read for any present or future OS designer...
Observation (if I might!) -- environment variables are to programs (especially chains of programs, parent processes, sub processes and sub-sub processes, etc.) what parameters are to functions -- and/or what command-line parameters are... they're sort of like global variables that get passed around a lot...
They also can influence program behavior -- and thus the determinism of a program -- and thus the determinism of a chain of programs...
Phrased another way -- software that works on one developer's machine might not work on another developer's machine and/or in a production environment because one crucial environment variable was either set or not set, or set to the wrong value...
(NixOS seems to understand this pretty well... that "hermetically sealing" or closing or "performing a closure around" (that's probably slightly the wrong language/terminology in "Nix-speak" but bear with me!) a software environment, including the environment variables is the way to create deterministic software builds that run on any machine... but here I'm digressing...)
But my main point: I complete agree with the article's author -- environment variables are a legacy mess!
Although, if we think about it, environment variables (if we broaden the definition) are a sub-pattern of anything that affects the state of an individual machine or runtime environment -- in other words, things such as the Windows Registry, at least the global aspects of it -- are also in the same category.
Future OS's, when they offer environments for programs or chains of programs to run -- should be completely containerized -- that is, the view of the system -- what data/settings/environment variables/registry variables/files/syscalls/global variables it has access to -- should be completely determinable by the user, completely logabble, completely transparent, and completely able to be compared, one environment to another.
In this way, software that either a) fails to work at all b) works in a non-deterministic way -- can be more easily debugged/diagnosed/fixed (I'm looking at you, future AI's that will assist humans in doing this!) -- then if all of that information is in various different places, broken, and/or opaque...
To reiterate:
>"Wow, I really enjoyed writing this… …and I hope it wasn’t a boring read."
No, I really enjoyed reading it(!), it's a brilliant article, and thank you for writing it! :-)
Upvoted and favorited!
At a past firm, I was trying to debug how a particular ENV var was getting set. I started out thinking it was something simple like the user's .bashrc or equivalent.
I quickly realized that there were roughly 10 "layers" of env var loadings happening where the first couple layers were:
- firm wide
- region
- business unit
- department
- team etc etc
I ended up having to turn on a bash debug flag so that I could see exactly where the var was getting set in the layer stack.
https://nodejs.org/api/cli.html#--trace-env
Since you can set/unset/modify env vars in so many ways with different APIs, this sounds super useful in complex debugging scenarios.
In other languages, check that the var you pulled in isn’t falsey before proceeding.
My guess would be either Jekyll or hand rolled, due to the url structure.
rsyring•2h ago
- On Linux systems, any user process can inspect any other process of that same user for it's environment variables. We can argue about threat model but, especially for a developer's system, there are A LOT of processes running as the same user as the developer.
- IMO, this has become an even more pronounced problem with the popularity of running non-containerized LLM agents in the same user space as the developer's primary OS user. It's a secret exfiltration exploiter's dream.
- Environment variables are usually passed down to other spawned processes instead of being contained to the primary process which is often the only one that needs it.
- Systemd exposes unit environment variables to all system clients through DBUS and warns against using environment variables for secrets[1]. I believe this means non-root users have access to environment variables set for root-only units/services. I could be wrong, I haven't tested it yet. But if this is the case, I bet that's a HUGE surprise to many system admins.
I think ephemeral file sharing between a secret managing process (e.g. 1Password's op cli tool) and the process that needs the secrets (flask, terraform, etc.) is likely the only solution that keeps secrets out of files and also out of environment variables. This is how Systemd's credentials system works. But it's far from widely supported.
Any good solutions for passing secrets around that don't involve environment variables or regular plain text files?
Edit: I think 1Password's op client has a good start in that each new "session" requires my authorization. So I can enable that tool for a cli sessions where I need some secrets but a rogue process that just tries to use the `op` binary isn't going to get to piggyback on that authorization. I'd get a new popup. But this is only step 1. Step 2 is...how to share that secret with the process that needs it and we are now back to the discussion above.
1: https://www.freedesktop.org/software/systemd/man/latest/syst...
robertlagrant•2h ago
This is a very good point I'd never realised! I'm not sure how you get around it, though, as if that program can even find a credential and decrypt a file, if it runs as the user then everything else can go find that credential as well.
rsyring•2h ago
chasil•1h ago
The classical alternative has been to store (FTP) credentials in a .netrc file (also used by curl).
I have some custom code to pull passwords out of a SQLite database.
For people who are really concerned with this, a "secrecy manger" is more appropriate, such as Cyberark conjur and summon, or Hashicorp Vault.
formerly_proven•2h ago
bbkane•2h ago
zdc1•1h ago
That being said, I still use env vars and don't plan on stopping. I just haven't (yet?) seen any exploits or threat models relating to it that would keep me up at night.
maccard•1h ago
I also use env vars.
matthew16550•55m ago
https://github.com/getsops/sops
skylurk•2h ago
https://pypi.org/project/keyring/
cedws•2h ago
throwaway127482•2h ago
cedws•2h ago
ux266478•1h ago
cedws•1h ago
ux266478•29m ago
jcgl•1h ago
ux266478•21m ago
1718627440•1h ago
mhitza•2h ago
memfd_secret comes to mind https://man7.org/linux/man-pages/man2/memfd_secret.2.html
I haven't seen much language support for it, though. On one part maybe because it's Linux only.
People that write in Rust (and maybe Go, depends how easy FFI is) should give it a try.
I wanted for a time to get some support for it in PHP, since wrapping a C function should be easy, but the thought of having to also modify php-fpm put a dent in that. I can't and don't want to hack on C code.
In practice it'd be great if the process manager spawn children after opening a secret file descriptor, and pass those on. Not in visible memory, not in /proc/*/environ
llimllib•1h ago
monocasa•1h ago
You should be able to build up a nice capability model to get access to those memfds from daemon too rather than having to spawn out of a process manager if that model fits your use case a bit better.
bjourne•2h ago
amluto•2h ago
cmdline is a different story.
rsyring•1h ago
I had always assumed getting secrets from a running process' memory was non-trivial and/or required root permissions but maybe that's a bad assumption.
However, reading a process' environment is as trivial as:
`cat /proc/123456/environ`
as long as it's ran by the same user. No ptrace-read required.
intorio•38m ago
By default, on most distributions, a user has PTRACE_ATTACH to all processes owned by it. This can be configured with ptrace_scope:
https://www.kernel.org/doc/Documentation/security/Yama.txt
marcosdumay•1h ago
And that the right thing to do if you want to harden your system is to disallow ptrace-read, and not bother changing software that uses environment variables?
Because I think most people that just try will be able to read the variables of any process on their computer.
nine_k•50m ago
If you want to prevent other processes from peeking into your process, run it under a different uid. Again. that's the point. A bunch of good software does that, running only small privileged bits in separate processes, and running some / bulk of the processes under an uid with severely limited privileges. See e.g. Postfix MTA, or the typical API server + reverse proxy split.
I don't think that this part of Unix is somehow problematic, or needs changing.
amluto•37m ago
Linux has some ways to accomplish this, for example:
- seccomp. It can be done quite securely, but running general purpose software in seccomp is not necessarily a good way to prevent it from acting like the running user.
- Namespaces. Unless user namespaces are in use, only root can set this up. With user namespaces, anyone can do it, but the attack surface against the kernel is huge, mount namespaces are somewhat porous (infamously, /proc/self/exe is a hole), and they can be a bit resource-intensive, especially if you want to use network namespaces for anything interesting. Also, user namespaces have kind of obnoxious interactions with ordinary filesystem uids (if I'm going to run some complex containerized stack with access to a specific directory within my home directory, who should own the files in there, and how do I achieve that without root's help?). And userns without subuid doesn't isolate very strongly, and subuid need's root's help.
- Landlock. Kind of cool, kind of limited.
- Tools like Yama. On Ubuntu, with Yama enabled (the default), programs can't generally ptrace (or read /proc/self/environ) from other programs running as the same user.
In any case, once you've done something to prevent a process from accessing /proc/fd/PID for other pids belonging to the same user (e.g. pidns) and/or restricted ptrace (e.g. PR_SET_DUMPABLE), then environ gets protected.
vendiddy•2h ago
Something like: my_secret = create_secret(value)
Then ideally it's an opaque value from that point on
1718627440•1h ago
:-)
jkrejcha•10m ago
As far as high-level language constructs go, there were similarish things like SecureString (in .NET) or GuardedString (in Java), although as best as I can tell they're relatively unused mostly because the ergonomics around them make them pretty annoying to use.
motorest•1h ago
I think environment variables are recommended to pass configuration parameters, and also secrets, in containerized applications managed by container orchestration systems.
By design, other processes cannot inspect what environment variables are running in a container.
Also, environment variables are passed to child processes because, by design, the goal is to run child processes in the same environment (i.e., same values) as the parent process, or with minor tweaks. Also, the process that spawns child processes is the one responsible for set it's environment variables, which means it already has at least read access to those secrets.
All in all I think all your concerns are based on specious reasoning, but I'll gladly discuss them in finer detail.
rsyring•1h ago
Let's say, as a developer, I need to do some API interactions with GitHub. So, in a terminal, using 1Password's cli tool `op`, I load my GH API token and pass it to my Python script that is doing the API calls.
Presumably, the reason I use that process is because I want to keep that token's exposure isolated to that script and I don't want the token exposed on the filesystem in plaintext. There is no reason for every process running as my user on my laptop to have access to that token.
But, that's not how things work. The Claude agent running in a different CLI session (as an example) also now has access to my GitHub token. Or, any extension I've ever loaded in VS Code also has access. Etc.
It's better than having it in plain text on the file system but not by much.
Additionally, unless I've misunderstood the systemd docs, if you are passing secrets through environment variables using unit's `Environment` config, ANY USER on a server can read those values. So now, we don't even have user isolation in effect.
My reasoning is pretty plain. It could be wrong, but it's hardly specious.
rustyminnow•1h ago
(inb4: container env-vars are isolated from other containers, not from processes on the host system)
jkrejcha•1h ago
Here's the thing though, the security model of most operating systems means that running a process as a user is acting as that user. There are some caveats (FreeBSD has capsicum, Linux has landlock, SELinux, AppArmor, Windows has integrity labels), but in the general case, if you can convince someone to run something, the program has delegated authority to act on behalf of that user. (And some user accounts on many systems may have authority to impersonate others.)
While it's by no means the only security model (there exists fully capability based operating systems out there), it's the security model that is used for most forms of computing, for better or worse. One of the consequences of this is that you can control anything within your domain. I can kill my own processes, put them to sleep, and most importantly for this, debug them. Anything I own that has secrets can grab them my ptrace/process_vm_readv/ReadProcessMemory/etc.
Narushia•1h ago
Honestly, my answer is still systemd-creds. It's a few years old by now, should be available on most distros. It's easy to use and solves the problem. Although credential support for user-level systemd services was added just a few weeks ago.
mixmastamyk•1h ago
If you need to keep secrets from other processes—don't run them under the same user account. Or access them remotely, although that brings other tradeoffs and difficulties.
lelanthran•11m ago
Plain text files are fine, it's the permissions on that file that is a problem.
The best way is to be in control of the source to the program you want to run then you can make a change to always protect against leaking secrets by starting the program as a user that can read the secrets field. After startup the program reads the whole file and immediately drops privileges by switching to a user that cannot read the secrets file.
Works for more than just secrets too.