Skip to content

Encryption and Secrets

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:

#encrypted 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.

1. Create an age key

mkdir -p ~/.config/statemate
age-keygen -o ~/.config/statemate/key.txt
chmod 600 ~/.config/statemate/key.txt

age-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.

2. Configure it

# mate.yaml
age:
  identity: "~/.config/statemate/key.txt"
  recipients:
    - age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
  • recipients is what files are encrypted to. Public keys are safe to commit.
  • identity is the private key used to decrypt. Only the path is committed, so use the same path on every machine.

3. Back up the private key

Without the private key, every #encrypted 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.

4. Encrypt files

Add a file encrypted from the start:

mate add --encrypt ~/.ssh/config

Or encrypt one that is already managed:

mate encrypt ssh/.ssh/config          # becomes ssh/.ssh/config#encrypted

The 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#encrypted
mate diff                   # diffs the decrypted content

To make git diff readable too, see Tips: readable diffs.

More than one key

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#encrypted && mate encrypt path/to/file

One shared key, kept in a password manager, avoids that bookkeeping.

5. Secrets from Bitwarden

For values that already live in your vault, reference them from a template instead of committing them at all:

# git/.config/gh/hosts.yml#template
github.com:
  oauth_token: {{ bitwarden "github.com" "field" "gh-cli-token" }}

You need the Bitwarden CLI (bw), logged in once:

bw login
mate secrets fetch

mate 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.

Generating files from secrets

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.

What needs what

Operation Needs
mate encrypt, mate add --encrypt recipients
Deploying, mate cat, mate eval on an #encrypted file identity or identity_command
mate edit on an #encrypted file both
mate secrets fetch, and the secrets cache in general identity or identity_command, plus bw
Importing into an #encrypted source (#import) recipients

Without the identity, statemate stops with an error instead of deploying ciphertext or writing the secrets cache unencrypted.