keenv
keenv puts secrets from a KeePass database into the
environment of one command and nowhere else. The values never reach a file, an
export, or the shell history: they exist between unlocking the database and
execing the command, and the process that held them is replaced.
It is the tool the Oberon Systems keyring policy names for local secrets, and the way indech hands the Cloudflare R2 keys to OpenTofu.
Contents
Why
The usual ways of getting a secret into a process all leave it somewhere:
export puts it in every child of the shell for the rest of the session, a
.env full of plaintext puts it on disk, and eval $(something) puts it in
the history file as well.
keenv reads the value out of the database at the moment it is needed, builds
the environment for exactly one command, and replaces itself with that command.
There is no keenv export and no keenv eval, on purpose.
Installation
pip install keenv
The database itself is read in process through pykeepass, so KeePassXC does not have to be installed.
Configuration
keenv reads two files, both optional. keenv.yaml names the database and
maps environment variables onto entries:
vault: ~/Dropbox/oberon.kdbx
keyfile: ~/.keys/oberon.keyx
env:
AWS_ACCESS_KEY_ID:
entry: Oberon/R2/indech-state
field: username
AWS_SECRET_ACCESS_KEY:
entry: Oberon/R2/indech-state
field: password
.env is the same mapping in the shape people already write, where a value may
be a keenv:// reference instead of a literal:
AWS_ACCESS_KEY_ID=keenv://Oberon/R2/indech-state/username
AWS_SECRET_ACCESS_KEY=keenv://Oberon/R2/indech-state/password
TF_LOG=INFO
A reference is keenv://<entry path>/<field>. The last segment is the field
and everything before it is the path to the entry, so a field whose name
contains a slash cannot be addressed. username, password, url, notes
and title are matched case-insensitively; any other name is looked up as a
custom attribute, with its spelling preserved.
The entry path starts at the top-level groups, one level below the root group
KeePass shows at the top of its tree, so Oberon/R2/indech-state and not
Root/Oberon/R2/indech-state. A path that does name the root group first is
accepted too, under either the group's real name or a plain root, so a path
copied straight out of KeePass works as it stands.
Values that are not references pass through literally. A # only starts a
comment at the beginning of a line, never in the middle of one, because a
secret may contain it.
The layers apply in this order, each one overriding the last:
| Layer | Set by |
|---|---|
keenv.yaml |
-c, default ./keenv.yaml |
.env |
-e, default ./.env |
| the database path | vault:, then KEENV_VAULT, then --vault |
| the key file path | keyfile:, then KEENV_KEYFILE, then --keyfile |
A file that is not there is an empty layer, not an error. Having nothing to resolve after both layers is an error.
keenv.yaml is validated against a
pydantic model that rejects keys it does not
know, so vualt: is reported as a mistake rather than quietly ignored.
Usage
Run a command with the resolved environment:
keenv run -- tofu -chdir=circuits/live/ramnode/compute plan
Check that every reference still points at something, without printing any value:
keenv check
Expected output:
AWS_ACCESS_KEY_ID keenv://Oberon/R2/indech-state/UserName 20 chars keenv.yaml
AWS_SECRET_ACCESS_KEY keenv://Oberon/R2/indech-state/Password 40 chars keenv.yaml
TF_LOG literal 4 chars .env
The last column is the file the variable came from, which is the quickest
way to see that a .env is overriding keenv.yaml.
Point at another database and another mapping:
keenv run --vault ~/other.kdbx -e deploy.env -- ./deploy.sh
Exit codes are 0 on success, 1 for anything keenv can explain, 2 for a
usage mistake, and 127 when the command does not exist. Otherwise the exit
code is the command's own, because the command replaces keenv.
How it works
- Both layers are read and merged into one list of variables. A name defined
in both takes its value from
.env, andkeenv checknames the file each variable came from. - If any of them is a
keenv://reference, the database is opened once. The master password is asked for on/dev/tty, never on stdin, so a password prompt can never swallow the first line of a pipe. A key file replaces the prompt. - Every reference is resolved from that one open database.
- The resolved variables are laid over a copy of the current environment and
handed to
os.execvpe.
Step 4 is what keeps the secrets contained: execvpe replaces the process
image, so nothing that held the values is still running once the command
starts, and the shell that invoked keenv never saw them.
Development
make init
make test
make lint
make init creates .venv, installs the package in editable mode and wires up
the pre-commit hooks. The test suite builds its own
throwaway database in a temporary directory; no test opens a real vault.
Commits go through commitizen:
.venv/bin/cz commit
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keenv-0.1.2.tar.gz.
File metadata
- Download URL: keenv-0.1.2.tar.gz
- Upload date:
- Size: 8.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.8.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
271677efa99d675ac5bdc3ba3ec2a550482aa3ad4e6ef2cb3aa376a60174a85c
|
|
| MD5 |
24f0ba7ec11996907ef767f6d5f04ce9
|
|
| BLAKE2b-256 |
30e12971c682a9b2a41c2ae288018fe9c6e52f8f3f84ad69c85ace65599a6d32
|
File details
Details for the file keenv-0.1.2-py3-none-any.whl.
File metadata
- Download URL: keenv-0.1.2-py3-none-any.whl
- Upload date:
- Size: 10.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.8.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
823ce29569894a6d9c8cb7fbac76bb7d005dde00a6876cbc23e5c4e080e19d5f
|
|
| MD5 |
3feca49c6f31223816b0644f06dae132
|
|
| BLAKE2b-256 |
356ee618eb1043cd01433652636d4dfbe0916b169e8eef81cbef59b3eff474d3
|