Files

78 lines
5.5 KiB
Markdown

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