# Security model What's encrypted, what isn't, and where each line is drawn. Encryption uses [age](https://github.com/FiloSottile/age) ([format spec](https://age-encryption.org/v1)). [chezmoi's age integration docs](https://www.chezmoi.io/user-guide/encryption/age/) 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. |