secrets
A tiny wrapper around age for managing a
directory of encrypted secrets. Secrets are stored as individual *.age files
under a single directory ($SECRETS_DIR, default ~/projects/data), and
paths are preserved so you can organize them in a hierarchy and expose one
secret under multiple tags using symlinks.
How it works
- Encryption uses your age identity file to derive the public key; the
identity lives at
~/.config/age/identity.txt(not configurable). - Secret paths are relative to
$SECRETS_DIR. A leading/is optional, a trailing.ageis optional and the suffix is always re-added on disk. - Parent directories are created automatically on encrypt.
..is rejected, so a secret path can never escape$SECRETS_DIR.
Requirements
ageandage-keygenonPATH- an existing
$SECRETS_DIR - an age identity at
~/.config/age/identity.txt, e.g.mkdir -p ~/.config/age age-keygen -o ~/.config/age/identity.txt
Install
With Nix (from the flake):
nix build git+https://git.kilimanjaro.io/secrets
nix run git+https://git.kilimanjaro.io/secrets -- list
Or as a flake input in a dev environment:
inputs.secrets.url = "git+https://git.kilimanjaro.io/secrets";
# ...
buildInputs = [ secrets.packages.${system}.default ];
Without Nix, just put the secrets script on your PATH (e.g. symlink it
into ~/bin). The script only needs age, age-keygen, and coreutils.
Usage
secrets list # list secrets, relative paths, symlinks annotated
secrets encrypt <file|name> # encrypt a file, or stdin, to $SECRETS_DIR/<path>.age
secrets decrypt <secret> # decrypt $SECRETS_DIR/<secret>[.age] to stdout
Encrypt
From stdin, into a nested path (openai/api-key.age is created, along with
the openai/ directory):
echo "sk-abc123" | secrets encrypt openai/api-key
From an existing file. The file’s path is preserved under $SECRETS_DIR:
secrets encrypt config/rclone.conf
# -> $SECRETS_DIR/config/rclone.conf.age
A leading slash is allowed and just means “root of $SECRETS_DIR”:
echo "hunter2" | secrets encrypt /my/fancy/secret.age
# -> $SECRETS_DIR/my/fancy/secret.age
Encrypt refuses to overwrite an existing secret.
Decrypt
Decryption writes to stdout, so redirect it or pipe it where you need:
secrets decrypt openai/api-key # -> sk-abc123
secrets decrypt openai/api-key.age # .age suffix is optional
secrets decrypt /my/fancy/secret.age # leading slash is optional too
secrets decrypt config/rclone.conf > ~/.config/rclone/rclone.conf
List
$ secrets list
config/rclone.conf.age
my/fancy/secret.age
openai/api-key.age
work/openai -> ../openai/api-key.age
Tagging with symlinks
Store a secret once and link to it from as many tag directories as you like.
Symlink names may omit the .age suffix; decrypt falls back to the exact
path when <name>.age doesn’t exist.
ln -s ../openai/api-key.age "$SECRETS_DIR/work/openai"
ln -s ../openai/api-key.age "$SECRETS_DIR/personal/openai"
secrets decrypt work/openai # -> sk-abc123
secrets list # shows "work/openai -> ../openai/api-key.age"
Environment
| Variable | Default | Description |
|---|---|---|
SECRETS_DIR |
~/projects/data |
Directory that holds the *.age files |