Direnv: For Secure Coding and Kind Documentation
What environment variables are
Introduction to /usr/bin/env
/usr/bin/env (or just env) echoes out all of the currently set environment variables.
Shebangs at the top of files in Unix-based OS’s determine how to execute instead of by file extension.
Might see #!/usr/bin/env python or #!/usr/bin/env ruby as a nicer way of not hardcoding the path to ruby or python binary.
Why not #!ruby or #!python? Environment variables such as GEM_HOME.
Let’s take a look into what is set in your terminal environment: (Incoming!)
| |
Accessing from the language
Ruby:
| |
Python:
| |
PowerShell:
| |
Following good practices
3 basic ways, depending on how framework-y you want to get:
- Just-in-time environment variables
- Shell script setting environment variables
dotenv/direnvframeworks
Just-in-time
Setup: N/a
Invoke:
| |
Shell script
Setup:
| |
Invoke:
| |
Frameworks
Depending on the framework, it auto-loads (a la source) the file when entering the directory. It is hidden by default, hence filename starting with ., so it stays out of your way most times. The file evaluated depends on the framework (.env for dotenv, .envrc for direnv), and evaluates all other files by that name up the directory tree. Might have seen this approach already with other tools (rspec, git, etc.)
Setup:
| |
Invoke:
| |
Documentation
Document it.
No. Seriously.
When
Always. As soon as it is introduced, even in a branch.
Make the code fail spectacularly (throw unhandled exception with explanation in message) if the environment varaible is not set.
Where
Possible locations:
.env.example.envrc.example- within
README.md - within other file that is versioned with the project
How (Development Practices)
Documenting environment variables can be done in a README.md (best), but the quickest notes for later reference can be as comments in a .env.example file itself.
JIT environment variable setting
Document example usage in README.md, or other docs
Benefits
- Consistent – Same approach as with
/etc/init.d/*scripts - Dependencies – No additional tooling
- Power – Access to everything the shell has to offer
- Transitive – Switching projects? Variables do not persist to shell session
Tradeoffs
- Forgetting – Must remember to include variables on every invocation
- Human Error – Grabbing what variables are needed requires parsing the docs intended for humans (which is less fault-tolerant)
- Complicated – Must be a one-liner, or else it falls under the “sourced files” option by definition
Source files manually
Center on the .env convention and include a file (committed to the repository) named .env.example, containing sample values for all variables.
This allows people to optionally use a framework like dotenv. Do not allow yourself to commit .env to the repo, so add it to global gitignore for your workstation.
Benefits
- Simplicity – Includes example, machine-parseable values with the project source code
- Dependencies – No additional tooling
- Power – Access to everything the shell has to offer
Tradeoffs
- Forgetting – Must remember to
source ./.envin every terminal running the application- Designed well: Blows up spectacularly at earliest possible phase
- Designed poorly: Partially executes, then bombs out
- Dirty Environment – Must open new or restart terminal when switching projects
Frameworks
I use direnv because I like the set-and-forget approach. For this section, I’m going to focus on direnv implementation.
- Create a
.envrcfile - Globally gitignore
.envrc - Create a
.envrc.examplefile - Store sanitized, dummy examples in
.envrc.example
Yes, I store credentials on local machine in plaintext. Shame me. If you can think of a way around this, I’m very open to improvement.
Direnv
Found at https://direnv.net/
Benefits
- Secure – Required to manually whitelist versions of
.envrcevery time it changes (direnv allow) - Automatic – Sets variables when
cdinto directory - Self-Contained – No hooks to additional libraries inside of program (like with
dotenv) - Inheiritance – PWD containing
.envrcoverwrites ones in parent dirs, on up the chain
Tradeoffs
- Execution – Execute from outside of project directory (
./project-dir/some_script.shwill not automatically set environment variables in./project-dir/.envrc) - Lag – Additional program running on every
cdin the shell - Shell – Limits to one of several popular shells
- Variables – Only handles shell variables (e.g.,
export SOME_VAR='foo', notsome_var() { echo 'foo' })