- Go 98.1%
- Just 1.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci/woodpecker/push/build Pipeline was successful
.woodpecker/release.yml |
||
| .just | ||
| .woodpecker | ||
| internal/stash | ||
| .gitignore | ||
| CHANGELOG.md | ||
| config.go | ||
| go.mod | ||
| go.sum | ||
| justfile | ||
| main.go | ||
| main_test.go | ||
| README.md | ||
| sage.go | ||
| sage_test.go | ||
locker
A token stash that follows you between machines. Each stash is a dotenv file
sealed with age, kept in a directory you already
sync — a dotfiles repo does nicely — and opened only by the machines enrolled
for it. The plaintext never touches disk: locker env prints it as exports for
a shell to eval, which is how moor loads a
domain's tokens on every switch.
Install
Requires Go 1.26+ and just.
just install # builds and copies to ~/.local/bin ($XDG_BIN_HOME)
Usage
locker init # mint this machine's identity and list it in the store
locker pubkey # print its public key
locker machines # the machines listed, and their keys (--json)
locker enroll amedia laptop # let a listed machine open a stash (run on an enrolled one)
locker enroll amedia laptop age1… # or name its key, for a machine not listed
locker edit amedia # decrypt into $EDITOR, re-seal what you save
# (a new stash is created, this machine enrolled)
locker edit amedia < tokens.env # or pipe the whole dotenv in
locker revoke amedia laptop # re-seal without it — then rotate the values
eval "$(locker env amedia)" # global's exports, then amedia's
token=$(locker get global GITHUB_TOKEN) # one value, for scripts (amedia's, else global's)
locker show amedia # one stash's values (--json)
locker list # stashes, their state here, who may open them (--json)
locker # status: identity, store, stashes (--json)
env is built to run unattended: a machine with no identity, a stash that does
not exist, or one this machine is not enrolled for prints nothing and exits 0,
so a shell switching into a domain never breaks over it. Only a stash this
machine should open and cannot read is an error.
get is the opposite: asked for one value by name, it fails out loud — a
stash or key not there, or a stash this machine is not enrolled for, is exit 1
— so $(locker get …) never quietly hands a script an empty string. It answers
what the stash's shell would hold: the stash's own value, else global's.
revoke is forward-only. The stash as sealed before stays in whatever history
synced it, readable by the dropped key; rotating the values and sealing the new
ones with locker edit is what actually revokes.
Layout
The store is a directory of pairs — the layout the secrets script used, so a
store it wrote is one locker reads as is:
<stash>.recipients age public keys, each under a "# <machine>" line
<stash>.env.age the sealed dotenv
machines every machine's public key, in the same format
machines is the store's list of who can be enrolled. locker init adds the
machine it runs on, by its short hostname, so once the store has synced a
machine already enrolled lets it in by name: locker enroll amedia laptop.
Running init again on a machine with an identity mints nothing and only
lists it — which is how a machine that predates the list joins it. A machine
that lists itself with a new key replaces its old one, and must be enrolled
again for the stashes sealed to that.
The stash named global is layered under every other: locker env amedia
prints global's exports first, so a key set in both takes amedia's. It is for
what every context should have — a personal token, an API key used
everywhere — and is listed even before it holds anything; locker edit global
starts it.
Each machine has one identity, ~/.config/locker/identity.txt (0600, never
synced). It is unencrypted on purpose: env runs inside a shell hook, where
nothing can ask for a passphrase.
Identity in sage's keyring
With sage, the identity can live in its keyring instead, so it is not on disk unencrypted:
sage keyring locker add # from ~/.config/locker/identity.txt
locker status # identity: sage's keyring, once the file is gone
rm ~/.config/locker/identity.txt
When the identity file is absent, locker asks sage for it; sage hands it only
to the installed locker binary. At a terminal, a locked keyring
asks for the keyring passphrase. env never asks — it runs in a shell hook —
so while the keyring is locked it prints nothing, as on a machine with no
identity; status and the other commands say the keyring did not hand it
over. init refuses while the keyring holds an identity, since a new one
would shadow the one the stashes are sealed to.
Config
~/.config/locker/config.toml ($XDG_CONFIG_HOME), every key optional:
store = "~/.local/share/chezmoi/secrets" # default ~/.local/share/locker
identity = "~/.config/locker/identity.txt" # the default
--store, --identity and --config override them per run.
Moving from secrets
Nothing is re-sealed. Per machine:
mkdir -p ~/.config/locker && chmod 700 ~/.config/locker
mv ~/.config/secrets/identity.txt ~/.config/locker/identity.txt
printf 'store = "~/.local/share/chezmoi/secrets"\n' > ~/.config/locker/config.toml
then secrets env <stash> becomes locker env <stash>, byte for byte.