A dotfiles repository is usually public, or at least pushed somewhere you do not fully control. statemate keeps secrets out of it in two ways, and both rest on one age key:
#enc files |
Bitwarden references | |
|---|---|---|
| The repository holds | age ciphertext | only a reference, such as {{ bitwarden "github" "field" "token" }} |
| The value lives | in the repository | in your Bitwarden vault |
| Applying needs | your age key | your age key, and the vault for the first fetch |
| Best for | whole files: an SSH config, a kubeconfig, an app's settings | single values inside otherwise ordinary files |
This page sets up both.
mkdir -p ~/.config/statemate
age-keygen -o ~/.config/statemate/key.txt
chmod 600 ~/.config/statemate/key.txtage-keygen prints the public key, which looks like age1ql3z7hjy54…. The file
holds the private key, AGE-SECRET-KEY-1….
The age-keygen command comes with age (brew install age, pacman -S age).
statemate itself has age built in and does not need the age binary.
# mate.yaml
age:
identity: "~/.config/statemate/key.txt"
recipients:
- age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8precipientsis what files are encrypted to. Public keys are safe to commit.identityis the private key used to decrypt. Only the path is committed, so use the same path on every machine.
Without the private key, every #enc file is unrecoverable. Store a copy
somewhere that does not depend on this repository. A password manager works well:
statemate's author keeps it as a custom field of a Bitwarden item, which is also
what makes bootstrapping a new machine a single step.
Instead of a file, you can have statemate read the key from the password manager
on demand with identity_command:
age:
identity_command: "rbw get statemate --field age-key"
recipients: [age1ql3z7hjy54…]The command must print only the AGE-SECRET-KEY-1… line. cat-ing the file
age-keygen wrote does not work, because that file also contains comment lines.
To use a different key for one run, or on a machine whose config names none, set
STATEMATE_KEY_FILE to its path. It overrides identity and identity_command:
STATEMATE_KEY_FILE=~/backup/key.txt mate cat ssh/.ssh/config#encAdd a file encrypted from the start:
mate add --encrypt ~/.ssh/configOr encrypt one that is already managed:
mate encrypt ssh/.ssh/config # becomes ssh/.ssh/config#encThe repository now holds armored ciphertext, while ~/.ssh/config stays plaintext.
To work with encrypted sources:
mate edit ~/.ssh/config # decrypt to a temp file, edit, re-encrypt on save
mate cat ssh/.ssh/config#enc
mate diff # diffs the decrypted contentTo make git diff readable too, see
Tips: readable diffs.
Renaming a file to #enc by hand does not encrypt it. mate doctor reports
any #enc file that holds plaintext, in every source and for every
profile, so it is caught before the secret is committed. To fix it, remove
#enc from the name and run mate encrypt on the file.
Each machine can have its own key; list every public key under recipients.
Files are encrypted to the recipients at the time they are encrypted, so after
adding a key, re-encrypt existing files for the new machine to read them:
mate decrypt path/to/file#enc && mate encrypt path/to/fileOne shared key, kept in a password manager, avoids that bookkeeping.
For values that already live in your vault, reference them from a template instead of committing them at all:
# git/.config/gh/hosts.yml#tmpl
github.com:
oauth_token: {{ bitwarden "github.com" "field" "gh-cli-token" }}
You need the Bitwarden CLI (bw), logged in
once:
bw login
mate secrets fetchmate secrets fetch finds every reference, asks for your master password if the
vault is locked, and stores the values in a local cache encrypted with your age
identity. From then on mate apply, status and diff read the cache and never
touch the vault, and mate apply fetches only references it has not seen before.
See Secrets for the reference syntax and the cache.
A source's .mate.yaml can create files straight from the vault, without a
source file per secret. This one writes one SSH private key per name in a
variable:
# mate.yaml
profiles:
work:
variables:
sshKeys: [laptop-work-ssh-key, github-ssh-key]# ssh/.mate.yaml
generate:
{{- range $key := (var "sshKeys") }}
- target: ".ssh/keys.d/id_{{ $key }}"
mode: "0600"
content: |
{{ indent 6 (bitwarden $key "ssh" "private") }}
{{- end }}Each name is a Bitwarden SSH-key item. indent 6 keeps the multi-line key inside
the YAML block scalar. See generate.
| Operation | Needs |
|---|---|
mate encrypt, mate add --encrypt |
recipients |
Deploying, mate cat, mate eval on an #enc file |
identity or identity_command |
mate edit on an #enc file |
both |
mate secrets fetch, and the secrets cache in general |
identity or identity_command, plus bw |
Importing into an #enc source (#import) |
recipients |
Without the identity, statemate stops with an error instead of deploying ciphertext or writing the secrets cache unencrypted.