Files

5.5 KiB

Security model

What's encrypted, what isn't, and where each line is drawn.

Encryption uses age (format spec). chezmoi's age integration docs describe how chezmoi add --encrypt works and how the recipient public key is configured.

Encryption boundaries

Lives where Encrypted at rest? Why
~/.config/fleet-dotfiles/secrets.env (live, on disk) No — plaintext, mode 600 Sourced by .zshrc on shell start; encryption would block that.
dot_config/private_fleet-dotfiles/encrypted_private_secrets.env.age (chezmoi source) Yes — age Pushed to a forge that may be public-readable.
private_dot_ssh/encrypted_private_id_ed25519_*.age (chezmoi source) Yes — age Same.
~/.ssh/id_ed25519_* (live, on disk) No — plaintext, mode 600 OpenSSH reads it directly; no decryption hook.
~/.ssh/id_ed25519 (machine identity) n/a — never enters chezmoi Each machine generates its own; this key never travels.
~/.ssh/authorized_keys n/a — plaintext is the format Composed by a modify_ script that appends fleet pubkeys; pubkeys aren't secret.
~/.config/chezmoi/key.txt (age private key) n/a — never enters chezmoi Distributed via secure side-channel only.

Threat model

This template is designed for a fleet of personal machines you control, syncing through a forge you trust (your own gitea, a private GitHub repo, etc.).

It assumes:

  • The forge can read the encrypted artifacts but can't decrypt them.
  • A network observer can read the forge HTTPS traffic (just encrypted blobs).
  • The age private key is distributed out-of-band (USB, password manager, secure messaging).
  • Each machine's local disk is encrypted at the OS level (FileVault on macOS).

It does NOT defend against:

  • A forge admin running arbitrary code on the server (they could swap chezmoi-auto-sync.sh and exfiltrate plaintext secrets the next time the watcher fires). Mitigation: only use forges you control or fully trust.
  • A compromised local user account (they'd have read access to the cleartext secrets.env and the age private key). Mitigation: standard endpoint hygiene; rotate aggressively if a machine is lost.
  • Side-channel disclosure of secrets via shell history, tmux scrollback, or process arg lists. Mitigation: never echo secrets, never pass them as command-line args, prefer env-var sourcing.

Age key handling

  • The fleet uses a single age recipient public key for encryption, stored in .chezmoi.toml.tmpl. It's safe to commit publicly.
  • Every fleet machine carries the matching private key at ~/.config/chezmoi/key.txt. This is what decrypts the encrypted source files on apply. Never commit this file. Never email it. Never drop it in cloud storage. Move it via secure messaging or a USB drive.
  • If the private key is lost, you can still decrypt the source files on any machine that still has its copy of the key. Re-encrypt with a new keypair, distribute the new private key, and rotate keys-of-keys (any tokens that lived inside the encrypted secrets) since you'd want to assume any machine that lost track of its key may have leaked it.

What gets pushed to the forge

After a successful watcher fire, the chezmoi source repo on the forge contains:

  • dot_* and private_dot_* directories with cleartext config (paths, hostnames, settings).
  • encrypted_*.age files for anything containing secrets.
  • *.tmpl files (chezmoi templates with cleartext placeholders).
  • The chezmoi-auto-sync.sh script (cleartext bash).

It does NOT contain:

  • The age private key.
  • The cleartext secrets.env.
  • The machine-identity SSH private key (~/.ssh/id_ed25519).
  • Any ~/.claude/projects/ transcripts (these are local-only).
  • Any task-durations parquet output (lives at ~/.local/share/task-durations/, outside the watch tree).

SSH between fleet machines

Mesh access is set up by private_dot_ssh/modify_private_authorized_keys.tmpl. On every chezmoi apply, it reads .chezmoidata/fleet.yaml, walks every machine's pubkey, and appends any that aren't already in ~/.ssh/authorized_keys. Existing entries (e.g., a GitHub deploy key, a colleague's pubkey) are preserved.

The flow when a new machine joins:

  1. New machine generates ~/.ssh/id_ed25519 locally.
  2. You add its pubkey to .chezmoidata/fleet.yaml (committed via the watcher).
  3. Within ~7 minutes, every existing fleet machine pulls the change and the modify_ script appends the new pubkey to its authorized_keys.
  4. Now the new machine can SSH any peer.

Rotation playbook

Compromise Action
One token leaked Edit ~/.config/fleet-dotfiles/secrets.env with the new value → save → watcher commits + pushes encrypted form → puller fans it out. Rotate the token at the issuing service.
Age private key leaked Generate new keypair → decrypt all *.age files in source → re-encrypt with new public key (chezmoi re-add --encrypt) → distribute new private key out-of-band → rotate every token in secrets.env (assume they leaked).
One machine lost / stolen Remove its entry from .chezmoidata/fleet.yaml → commit → on next puller cycle, remaining machines no longer accept the lost machine's pubkey. Revoke any tokens the lost machine had local cleartext access to.
Forge compromised Treat as if all encrypted secrets leaked: rotate every token. Move the fleet to a new forge.