ebccdda936
A chezmoi-based fleet-dotfiles template for macOS workstations: - Two-way auto-sync via launchd watcher + 5-min puller - Mesh SSH via modify_authorized_keys driven by .chezmoidata/fleet.yaml - age-encrypted secrets file - Bundled Claude Code agentic team (11 agents) + /lite + /lite-sub commands - Verify-before-claiming Stop hook - Generic statusline + project-boundary validate-path hook - Reference launchd plist for cross-fleet task-durations aggregation (companion repo: gitea.tojo.team/cardinale/task-durations) - AGENTS.md walks an agent through the entire setup Q&A interactively - docs/ covers architecture, security model, fleet onboarding
5.2 KiB
5.2 KiB
Security model
What's encrypted, what isn't, and where each line is drawn.
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.shand 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.envand 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
echosecrets, 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_*andprivate_dot_*directories with cleartext config (paths, hostnames, settings).encrypted_*.agefiles for anything containing secrets.*.tmplfiles (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:
- New machine generates
~/.ssh/id_ed25519locally. - You add its pubkey to
.chezmoidata/fleet.yaml(committed via the watcher). - Within ~7 minutes, every existing fleet machine pulls the change and the
modify_script appends the new pubkey to itsauthorized_keys. - 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. |