EnvFolio 1.0: Keep Your Secrets Out of Plain Sight
One store, one vars file per project, and an encrypted export for machines with no store.
EnvFolio keeps your texts and secrets in one store, and hands them to a command only when you ask. Version 1.0 is out.
Quick question: where are your application secrets right now? In a .env file. In an export line in your ~/.bashrc. In your shell history, from the day you
pasted a token into a curl. Plain text, in all of those, on every machine you work
on.
And they travel. You work in containers, in cloud dev environments, next to AI agents that read every file in the project. Plain-text secrets go wherever your files go.
Meet EnvFolio
EnvFolio is a small
command-line tool, folio, that keeps your texts and your secrets in one store. It hands them to a program directly, as environment
variables, and only when you ask. That is the whole idea:
folio execonly when you askNot on a command line. Not in a file. Not on the screen.
With 1.0, its commands, options and file formats (the store, vars files, exports and backups) are stable: a change that breaks any of them will come with a new major version.
Built on pass
EnvFolio does not invent its own vault. It is built on pass, the standard Unix
password manager. Your secrets are encrypted with your own GPG key, and every change goes into the
store's git history. The store lives in ~/.folio, apart from your own ~/.password-store, but it has the same layout, so plain pass still opens
every secret in it:
PASSWORD_STORE_DIR=~/.folio pass show gh/tokenTexts and secrets
EnvFolio stores two kinds of items. A text, like a user name or a host, is
kept plain, and signed with your key, so you can tell if someone changed it. A secret is encrypted, as it should be. The two never blur: every command that
touches a secret says secret out loud, and a secret is never taken as an argument.
folio text insert -t "demo@example.com" gh/user
folio secret insert gh/tokenfolio ls shows both, side by side:
$ folio ls EnvFolio Store βββ aws β βββ πregion βββ db β βββ πpass β βββ πuser βββ gh β βββ πtoken β βββ πuser βββ git βββ πuser_name
Hand them to a command
To use the items, run your command through folio exec. Each item becomes an
environment variable for that one command, named after its path: / becomes _, then it is uppercased. So gh/token becomes GH_TOKEN. Want another name? Put it in front: GITHUB_TOKEN=gh/token.
Texts load by default; secrets come along only with -s (--secrets).
Every item is checked and read before the command starts. If one fails, the command does not run.
The echo below is only there to prove the value arrived; a real command just uses it.
# Use an item as a variable $ folio exec -s gh/token -- sh -c 'echo ${GH_TOKEN}' ghp_DEMO0000fake0000token # Or pick its name $ folio exec -s GITHUB_TOKEN=gh/token -- sh -c 'echo ${GITHUB_TOKEN}' ghp_DEMO0000fake0000token
Name a folder, and every item under it comes along. Not sure what you will get? Add --names: it shows which variable gets which item, and stops. Nothing is read or
decrypted, and the command does not run.
# Use every items in a folder $ folio exec -s gh -- sh -c 'env | grep GH_' GH_TOKEN=ghp_DEMO0000fake0000token GH_USER=demo@example.com # Use `--names` to show which var get value from which item # Note that `--names` will stop the command from running -- make this a dryrun. $ folio exec --names -s gh -- gh repo list GH_TOKEN <- πgh/token GH_USER <- πgh/user
Need the items for a whole session instead of one command? folio shell does the same,
but starts your shell. It prints the names it loaded, never their values.
Write it down once: vars files
Typing item names gets old. For a project, write down once what its command needs, and where each value comes from, in a vars file:
GH_TOKEN=@{gh/token}
DB_URL=pg://@{db/user}:@{db/pass}@${DB_HOST}/app
LOG_LEVEL=info@{name} | a text or secret from the store |
${NAME} | from your environment |
β¦:-fallback | if unset or missing: @{gh/token:-none} |
@{folder/*} | every item under a folder |
LOG_LEVEL=info | a constant |
The file holds where the values are, not the values, so commit it with the code. Nothing in it
ever runs: EnvFolio reads it a character at a time and never hands it to a shell, so $(β¦) and backticks are refused. folio vars check checks the file without
a store, in CI for example. Then one line runs the project's command. Here, deploy.sh is a stand-in that reports what it got without showing a secret:
$ folio exec -s --vars app.vars -- deploy.sh ==| Super Cool App -- Deployment |== Parameter check: GH_TOKEN : β Given DB_URL : β pg://app:###@12.34.56.7/app LOG_LEVEL: info Deployment will now start ... ... --| Deploying is completed |--
Take it anywhere
Now the part that started it all: running that command on a machine that has no store, like a
remote server: a CI/CD server, say. folio export packs just the items your vars file
names into one encrypted file. It makes a new key for this export only, so your store's key never
leaves your machine, and it always locks the file: with a passphrase you choose, or with --to, to the public key of the machine it is going to.
$ folio export -s --vars app.vars -o app.folio β Items: 3 (texts: 1, secrets: 2) β File: app.folio
On the other side, folio exec --from runs the command with the items from that file.
No store is needed, and none is made. The file is unpacked into a private temporary folder, and
that folder is removed before the command starts: nothing is left behind.
$ folio exec -s --from app.folio --vars app.vars -- deploy.sh ==| Super Cool App -- Deployment |== Parameter check: GH_TOKEN : β Given DB_URL : β pg://app:###@12.34.56.7/app LOG_LEVEL: info Deployment will now start ... ... --| Deploying is completed |--
So what may travel in the open? The vars file holds no secret, and the export file is locked: both
can sit next to the code, or on the server. What opens the file is the one thing to keep to
yourself: the passphrase, kept out of the repository and never next to the file; or, with --to, the secret key on the machine it is going to, which never travels at all. Your
secrets get to a machine you do not own, without its provider ever touching them.
Why trust it
EnvFolio is one bash script, and it runs on the bash 3.2 that macOS ships. It needs little more
than pass, gpg and git. You can read every line, and see for
yourself that no secret goes where it shouldn't.
folio backup saves the whole store, its key included (still under its passphrase),
into one file, and folio restore brings it back on a new machine. And if you ever stop
using EnvFolio, nothing is locked in: your secrets are plain pass entries, and your
texts are plain files.
Get it
Download folio from the latest release,
and check it against SHA256SUMS. It is the script that sees your secrets, so mind its
permissions: anyone who can change it can read them. Make it executable but writable by you only,
and put it on your PATH in a folder that only you can write to:
sha256sum -c SHA256SUMS # shasum -a 256 -c SHA256SUMS on macOS
chmod 755 folio # everyone may run it, only you may change it
mkdir -p ~/.local/bin && mv folio ~/.local/bin/
folio versionOr with Homebrew, which checks the download against its checksum and installs it for you:
brew install nawaman/tap/folioThen folio init makes your store, in a folder only you can get into
(700), and folio help lists the rest. For Tab completion, add eval "$(folio completion bash)" to your ~/.bashrc, or the zsh one to your ~/.zshrc.
Bottom line
Your secrets don't belong in .env files, shell profiles or history. Put them in one pass store, write down once what each project needs, and let folio exec hand the values to the command that needs them, and to nothing else. When the work moves to another
machine, an encrypted export goes with it, and leaves nothing behind.
Keep your secrets out of plain sight.
Links
- EnvFolio on GitHub
- The latest release
- The manual: every command, with examples
pass, the standard Unix password manager- This article on the EnvFolio site
Comments
Thank you for keeping the comment section positive, constructive and respectful. I appreciate constructive criticism & respectful disagreement!