diff --git a/.chezmoi.toml.tmpl b/.chezmoi.toml.tmpl index 1930458..92e2fa7 100644 --- a/.chezmoi.toml.tmpl +++ b/.chezmoi.toml.tmpl @@ -46,13 +46,14 @@ {{- $tags := $profileData.tags -}} {{- $wantsDecrypt := or (get $profileData "decrypt") false -}} {{- $work := or (get $profileData "work") false -}} +{{- $backup := or (get $profileData "backup") false -}} {{- $de := "" -}} {{- if hasKey $profileData "de" -}}{{- $de = $profileData.de -}} {{- end -}} -{{- /* Enable encryption when the profile declares it. The run_before decrypt - script ensures the key exists (via env var, 1Password, or manual placement) - before chezmoi processes any encrypted files. */ -}} +{{- /* Enable encryption when the profile declares it. run_before_01-decrypt ensures the + age key exists (via env var, 1Password, or manual placement) before chezmoi + processes any encrypted files. */ -}} {{ if $wantsDecrypt -}} encryption = "age" @@ -63,6 +64,7 @@ encryption = "age" tags = "{{ $tags }}" work = {{ $work }} decrypt = {{ $wantsDecrypt }} + backup = {{ $backup }} de = "{{ $de }}" {{ if $wantsDecrypt }} [age] diff --git a/.chezmoidata/packages.yaml b/.chezmoidata/packages.yaml index 653ada4..865b9a9 100644 --- a/.chezmoidata/packages.yaml +++ b/.chezmoidata/packages.yaml @@ -237,6 +237,19 @@ packages: desc: "Multipurpose relay for bidirectional data transfer" brew: false + # restic/resticprofile install ONLY on profiles with `backup: true` (cascade-filter + # backup gate) — keeps servers/containers/Macs (Time Machine) free of the backup stack. + restic: + tags: [core] + desc: "Fast, secure, deduplicating backup program" + backup: true + + resticprofile: + tags: [core] + desc: "Configuration profiles and scheduler for restic" + os: linux + backup: true + # =========================================================================== # macOS GNU Tools # =========================================================================== diff --git a/.chezmoidata/profiles.yaml b/.chezmoidata/profiles.yaml index e4a22c8..cf66042 100644 --- a/.chezmoidata/profiles.yaml +++ b/.chezmoidata/profiles.yaml @@ -8,6 +8,7 @@ # brew/pacman/apt/dnf/rpm_ostree/flatpak/appimage: which package managers to enable # work: work machine (system SSH agent, corporate configs) # decrypt: enable age decryption of private configs +# backup: personal machine — install+schedule restic backups (default false; servers/containers stay off) # # Profile is selected interactively during `chezmoi init`, or via: # DOTFILES_PROFILE=arch chezmoi init @@ -40,6 +41,7 @@ profiles: flatpak: true appimage: true decrypt: true + backup: true # personal machine — runs restic backups to the homelab rest-server # --- Debian/Ubuntu --- debian-server: diff --git a/.chezmoiignore.tmpl b/.chezmoiignore.tmpl index 29ce116..e633ee6 100644 --- a/.chezmoiignore.tmpl +++ b/.chezmoiignore.tmpl @@ -23,6 +23,14 @@ tests/ **/*.age {{ end }} +{{ if not .backup }} +# restic backups only run on personal machines (profile backup: true). +# Skipping the whole config dir keeps the decrypt() in the profile template from +# even running on servers/containers (which lack the age key). +.config/resticprofile +.config/resticprofile/** +{{ end }} + {{ if ne .profile "devpod" }} # Devpod-only tooling dot_local/bin/executable_devpod-linuxbrew-fetch diff --git a/.chezmoitemplates/cascade-filter b/.chezmoitemplates/cascade-filter index a527a37..c0d2d2f 100644 --- a/.chezmoitemplates/cascade-filter +++ b/.chezmoitemplates/cascade-filter @@ -20,10 +20,10 @@ absent = no restriction (installs on all profiles) string = only on profiles where de matches (e.g., hyprland-omarchy) - work/decrypt fields are tri-state: + work/decrypt/backup fields are tri-state: absent = no restriction (installs on all profiles) - true = only on profiles where work/decrypt is true - false = only on profiles where work/decrypt is false + true = only on profiles where work/decrypt/backup is true + false = only on profiles where work/decrypt/backup is false Parameters (via dict): .root - root template context ($) @@ -50,6 +50,7 @@ {{- $de_ok := or (not (hasKey $pkg "de")) (eq (default "" $pkg.de) .root.de) -}} {{- $work_ok := or (not (hasKey $pkg "work")) (eq $pkg.work .root.work) -}} {{- $decrypt_ok := or (not (hasKey $pkg "decrypt")) (eq $pkg.decrypt .root.decrypt) -}} +{{- $backup_ok := or (not (hasKey $pkg "backup")) (eq $pkg.backup .root.backup) -}} {{- /* Opt-in managers: require their field to exist and be truthy */ -}} {{- $field_ok := true -}} @@ -81,7 +82,7 @@ {{- else if $avail_appimage -}}{{- $winner = "appimage" -}} {{- end -}} -{{- if and $enabled $tags_ok $os_ok $de_ok $work_ok $decrypt_ok $field_ok (eq $winner $mgr) -}} +{{- if and $enabled $tags_ok $os_ok $de_ok $work_ok $decrypt_ok $backup_ok $field_ok (eq $winner $mgr) -}} {{- $resolved := $display -}} {{- if and (eq $mgr "brew") (hasKey $pkg "brew") -}} {{- $resolved = index $pkg "brew" -}} diff --git a/.chezmoitemplates/op-cached-secret b/.chezmoitemplates/op-cached-secret new file mode 100644 index 0000000..7ea30d5 --- /dev/null +++ b/.chezmoitemplates/op-cached-secret @@ -0,0 +1,12 @@ +{{- /* Fetch a secret from 1Password ONCE, then serve it from an on-disk cache. Generic — + usable for any cached op secret, not just restic. Returns the cache file's contents when it + exists and is non-empty, otherwise reads from 1Password (the caller writes the result to the + same path, so the next render is an offline cache hit; op is only hit when the cache is missing). + Args (list, positional): + 0 cache path — absolute, or relative to $HOME (the partial prepends $HOME) + 1 full op:// reference */ -}} +{{- $p := index . 0 -}} +{{- $dest := $p -}} +{{- if not (hasPrefix "/" $p) -}}{{- $dest = printf "%s/%s" (env "HOME") $p -}}{{- end -}} +{{- $s := stat $dest -}} +{{- if and $s (gt $s.size 0) -}}{{- output "cat" $dest -}}{{- else -}}{{- onepasswordRead (index . 1) -}}{{- end -}} diff --git a/CLAUDE.md b/CLAUDE.md index fa5e6d5..0f41fc4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,18 +49,19 @@ Each profile (defined in `.chezmoidata/profiles.yaml`) fully specifies: - **Package managers**: `brew`, `pacman`, `apt`, `dnf`, `rpm_ostree`, `flatpak` - **`work`**: work machine (system SSH agent, corporate configs) - **`decrypt`**: enable age decryption of private configs +- **`backup`**: personal machine — install the restic stack and schedule backups (default off; servers/containers/Macs stay off, Macs use Time Machine) If no profile is specified, auto-detects from distro (macOS → `macos-work`, Arch → `arch-desktop`, etc.). **Available profiles:** -| Profile | Tags | Pkg Managers | Work | Decrypt | Description | -|---|---|---|---|---|---| -| `macos-work` | core, container, dev, ai, ui, ui-extra | brew | yes | yes | Work Mac — corporate dev + GUI | -| `arch-desktop` | core, container, dev, ai, hardware, ui, ui-extra, gaming | pacman, flatpak, appimage | | yes | Arch Linux desktop | -| `debian-server` | core | apt | | | Debian/Ubuntu server — CLI only | -| `devpod` | core | brew | yes | no | Debian/Ubuntu dev | -| `silverblue` | core, container, dev, ai, hardware, ui, ui-extra, gaming | brew, rpm_ostree, flatpak, appimage | | yes | Silverblue/Bazzite immutable | +| Profile | Tags | Pkg Managers | Work | Decrypt | Backup | Description | +|---|---|---|---|---|---|---| +| `macos-work` | core, container, dev, ai, ui, ui-extra | brew | yes | yes | | Work Mac — corporate dev + GUI (Time Machine) | +| `arch-desktop` | core, container, dev, ai, hardware, ui, ui-extra, gaming | pacman, flatpak, appimage | | yes | yes | Arch Linux desktop | +| `debian-server` | core | apt | | | | Debian/Ubuntu server — CLI only | +| `devpod` | core | brew | yes | no | | Debian/Ubuntu dev | +| `silverblue` | core, container, dev, ai, hardware, ui, ui-extra, gaming | brew, rpm_ostree, flatpak, appimage | | yes | | Silverblue/Bazzite immutable | **Package categories:** - **core** (all machines): Shell (fish, atuin, zoxide), modern CLI tools (eza, bat, fd, ripgrep, etc.), git, neovim, tmux, essential utils @@ -123,12 +124,13 @@ Scripts use category-based numeric prefixes with gaps for future expansion: 40-49 Custom binaries (e.g. THPM bootstrap) 50-59 Tool configuration (e.g. hyprpm plugin sync) 60-69 Shell configuration -70+ Future custom +70-79 Backup & data services (e.g. restic/resticprofile timers) +80+ Future custom ``` | Script | Description | |---|---| -| `run_before_00-decrypt.sh.tmpl` | Ensures age key exists (1Password or manual) | +| `run_before_01-decrypt.sh.tmpl` | Ensures the age key exists (env/1Password/manual) before chezmoi decrypts; restic secrets are self-caching templates (`op-cached-secret` partial) | | `run_onchange_00-setup-directories.sh` | Creates required dirs (~/.ssh/sockets, etc.) | | `run_onchange_10-install-packages-homebrew.sh.tmpl` | Homebrew formulas (+ Homebrew install on Linux) | | `run_onchange_11-install-packages-cask.sh.tmpl` | Homebrew casks (macOS only) | @@ -142,6 +144,17 @@ Scripts use category-based numeric prefixes with gaps for future expansion: | `run_onchange_50-configure-hyprpm.sh.tmpl` | Adds hyprpm repos and enables declared plugins; omarchy-only | | `run_onchange_60-install-fisher.sh.tmpl` | Installs Fisher and Fish plugins | | `run_onchange_61-configure-tide.sh.tmpl` | Configures Tide prompt | +| `run_onchange_70-configure-restic.sh.tmpl` | Registers resticprofile systemd timers (user `default` + root `system`/etc); `backup` profiles only | + +### Backups (restic + resticprofile) + +`backup: true` profiles run [resticprofile](https://creativeprojects.github.io/resticprofile/) against a REST server on the homelab NAS (`--append-only`, per-host repo `ty/`). Config: `dot_config/resticprofile/private_profiles.yaml.tmpl`. + +- **Two profiles**: `default` (user `$HOME` + a staged manifest of system state → user-scoped `user_logged_on` timers) and `system` (`/etc` as root → a root timer; needs one sudo escalation, see below). +- **Secrets** (never committed): repo encryption key + REST transport key, fetched from `op://dev-keys/restic-/*` once via the `op-cached-secret` partial, then served from on-disk cache (`~/.config/resticprofile/{password,rest-pass}`). +- **Manifest**: `default` stages package/toolchain lists (pacman, AUR, flatpak, AppImage, brew, cargo, npm, pipx, uv, mise, go) and enabled systemd units into `~/.local/state/restic-system/`, captured by the `$HOME` backup — a record of system state without a root timer. +- **Excludes** keep the repo lean (verified ~39 GiB of a 4.9 TB home): caches, container/VM images, toolchain registries, build outputs, Steam installs (saves kept), local `~/Backups`. No `retention` block — pruning is server-side (clients can't forget against `--append-only`). +- **Root `system` timer**: a non-root `chezmoi apply` cannot create system timers. `run_onchange_70` registers it via `sudo -n` if passwordless sudo is available, otherwise it prints the one `sudo … --name system schedule` command to run by hand. The user `default` timers always register without sudo. ## Tests diff --git a/README.md b/README.md index e2491ae..685c0a8 100644 --- a/README.md +++ b/README.md @@ -41,18 +41,15 @@ This will: ## Profiles -Each profile fully specifies a machine setup: tier, package managers, work mode, and decryption. - -| Profile | Tier | Description | -|---------|------|-------------| -| `macos-personal` | 3 | Personal Mac — full dev + GUI apps | -| `macos-work` | 3 | Work Mac — corporate dev + GUI apps | -| `arch` | 3 | Arch Linux desktop — paru + flatpak | -| `debian-server` | 1 | Debian/Ubuntu server — CLI only | -| `debian-dev` | 2 | Debian/Ubuntu dev — apt | -| `debian-brew` | 2 | Debian/Ubuntu dev — Homebrew + flatpak | -| `devpod` | 2 | Work devpod — Homebrew | -| `fedora` | 3 | Fedora Workstation — dnf + flatpak | +Each profile fully specifies a machine setup: category tags, package managers, work mode, decryption, and backups. See `.chezmoidata/profiles.yaml` for the source of truth. + +| Profile | Pkg Managers | Work | Decrypt | Backup | Description | +|---------|--------------|------|---------|--------|-------------| +| `macos-work` | brew | yes | yes | | Work Mac — corporate dev + GUI (Time Machine) | +| `arch-desktop` | pacman, flatpak, appimage | | yes | yes | Arch Linux desktop — paru + flatpak | +| `debian-server` | apt | | | | Debian/Ubuntu server — CLI only | +| `devpod` | brew | yes | | | Debian/Ubuntu dev — Homebrew | +| `silverblue` | brew, rpm-ostree, flatpak, appimage | | yes | | Fedora Silverblue/Bazzite — immutable | | `silverblue` | 3 | Silverblue/Bazzite — Homebrew + flatpak | **Package tiers:** @@ -152,7 +149,7 @@ dotfiles/ ├── .chezmoi.toml.tmpl # Chezmoi config template ├── .chezmoiignore.tmpl # Ignore rules (templated) ├── .age-public-key # Age encryption public key -├── run_before_00-decrypt.sh.tmpl # Age key setup (if decrypt enabled) +├── run_before_01-decrypt.sh.tmpl # Age key from 1Password/env if missing (restic secrets use self-caching templates) ├── run_onchange_01-install-packages.sh.tmpl # brew bundle ├── run_onchange_02-install-fisher.sh.tmpl # Fisher plugins ├── run_onchange_03-configure-tide.sh.tmpl # Tide prompt config diff --git a/dot_config/hypr/looknfeel.conf b/dot_config/hypr/looknfeel.conf index f9e6ec5..be88a12 100644 --- a/dot_config/hypr/looknfeel.conf +++ b/dot_config/hypr/looknfeel.conf @@ -75,38 +75,4 @@ plugin { scale = 0.6 workspace_gap = 100 } - - # hyprglass defaults: default_theme=dark, default_preset=default, - # tint_color=0x8899aa22, dark:brightness=0.82, layers.enabled=false. - hyprglass { - default_preset = clear - - brightness = 0.9 - light:adaptive_boost = 0.5 - - preset = name:clear, glass_opacity:0.8, blur_strength:1.5 - preset = name:clear:dark, brightness:0.7 - preset = name:clear:light, brightness:1.2 - - preset = name:contrasted, inherits:high_contrast, contrast:1.2, adaptive_dim:1.5 - preset = name:contrasted:dark, tint_color:0x02142aa9 - - layers { - enabled = 1 - namespaces = waybar, swaync, quickshell:bezel - exclude_namespaces = debug-panel - preset = subtle - namespace_presets = quickshell:bezel:ui - namespace_mask_thresholds = waybar=0.05, quickshell:bezel=0.3 - } - } - - # dynamic-cursors: only the shaperules diverge from defaults. Top-level - # (enabled=true, mode=tilt, threshold=2) and every subblock (rotate, tilt, - # stretch, shake, hyprcursor) match upstream documented defaults exactly. - dynamic-cursors { - shaperule = text, rotate:offset: 90 - shaperule = grab, stretch, stretch:limit: 2000 - shaperule = clientside, none - } } diff --git a/dot_config/resticprofile/private_password.tmpl b/dot_config/resticprofile/private_password.tmpl new file mode 100644 index 0000000..69854b7 --- /dev/null +++ b/dot_config/resticprofile/private_password.tmpl @@ -0,0 +1,5 @@ +{{- /* restic repo password — fetched from 1Password once, then cached at this target path. + Consumed by restic at runtime via `password-file` (no chezmoi-render dependency). */ -}} +{{- includeTemplate "op-cached-secret" (list + ".config/resticprofile/password" + (printf "op://dev-keys/restic-%s/encryption-key" .chezmoi.hostname)) | trim -}} diff --git a/dot_config/resticprofile/private_profiles.yaml.tmpl b/dot_config/resticprofile/private_profiles.yaml.tmpl new file mode 100644 index 0000000..3649717 --- /dev/null +++ b/dot_config/resticprofile/private_profiles.yaml.tmpl @@ -0,0 +1,192 @@ +{{- /* ============================================================================ + resticprofile — fleet backup config (chezmoi-templated, renders per machine). + + BACKEND: REST (rest-server on the TrueNAS storage LXC), --append-only. + Repo path is ty/ (username/host). "ty" is the rest-server htpasswd user; + --private-repos confines it to /ty/*, so every machine gets /ty/. + + MACHINES: $host namespaces the repo, so each machine has its own repo under + /ty/. Add a machine by giving it a hostname; nothing else changes. + Macs use Time Machine instead and render no profiles (see guard). + + SECRETS (two): self-caching .tmpl files (the op-cached-secret partial) fetch from 1Password + ONLY when the on-disk cache is missing — never committed to this repo. op is hit on the first + apply only; every later render is an offline cache hit. + - repo password (encrypts data): private_password.tmpl -> ~/.config/resticprofile/password (password-file below). + - transport password (REST basic auth): private_rest-pass.tmpl -> ~/.config/resticprofile/rest-pass, + read below and inlined into the repo URL (why THIS file is private_, rendered 0600). + + RETENTION/PRUNE: the server is --append-only, so clients CANNOT forget/prune + (those delete data). Retention runs server-side against the dataset; there is + deliberately no `retention` block here. `check` is read-only and stays. + ============================================================================ */ -}} +{{- $host := .chezmoi.hostname -}} +{{- /* transport password: cached on disk by private_rest-pass.tmpl. The shared partial returns + the cached file, or fetches from 1Password when it's missing (covers the first apply, + before rest-pass is written). Offline on every subsequent render. */ -}} +{{- $rest_pass := includeTemplate "op-cached-secret" (list + ".config/resticprofile/rest-pass" + (printf "op://dev-keys/restic-%s/rest-key" .chezmoi.hostname)) | trim -}} +{{- $repo := printf "rest:https://ty:%s@restic.tysmith.app/ty/%s/" $rest_pass $host -}} +{{- if ne .chezmoi.os "darwin" }} +version: "1" + +global: + default-command: snapshots + initialize: false + priority: low + +# --- $HOME + a staged manifest of system state (user-readable, exits clean) --- +default: + repository: "{{ $repo }}" + password-file: "{{ .chezmoi.homeDir }}/.config/resticprofile/password" + lock: "{{ .chezmoi.homeDir }}/.cache/resticprofile/default.lock" + force-inactive-lock: true + + backup: + run-before: + - "mkdir -p {{ .chezmoi.homeDir }}/.local/state/restic-system" + - "pacman -Qqe > {{ .chezmoi.homeDir }}/.local/state/restic-system/pacman-explicit.txt 2>/dev/null || true" + - "pacman -Qqem > {{ .chezmoi.homeDir }}/.local/state/restic-system/pacman-aur.txt 2>/dev/null || true" + - "flatpak list --app --columns=application,origin > {{ .chezmoi.homeDir }}/.local/state/restic-system/flatpak-apps.txt 2>/dev/null || true" + - "ls -1 {{ .chezmoi.homeDir }}/AppImages > {{ .chezmoi.homeDir }}/.local/state/restic-system/appimage-files.txt 2>/dev/null || true" + # global/user-installed packages from language managers (guarded — absent tools write no file) + - "command -v brew >/dev/null 2>&1 && brew bundle dump --file=- > {{ .chezmoi.homeDir }}/.local/state/restic-system/brew-Brewfile.txt 2>/dev/null || true" + - "command -v cargo >/dev/null 2>&1 && cargo install --list > {{ .chezmoi.homeDir }}/.local/state/restic-system/cargo-global.txt 2>/dev/null || true" + - "command -v npm >/dev/null 2>&1 && npm ls -g --depth=0 > {{ .chezmoi.homeDir }}/.local/state/restic-system/npm-global.txt 2>/dev/null || true" + - "command -v pipx >/dev/null 2>&1 && pipx list --short > {{ .chezmoi.homeDir }}/.local/state/restic-system/pipx.txt 2>/dev/null || true" + - "command -v uv >/dev/null 2>&1 && uv tool list > {{ .chezmoi.homeDir }}/.local/state/restic-system/uv-tools.txt 2>/dev/null || true" + - "command -v mise >/dev/null 2>&1 && mise ls --global > {{ .chezmoi.homeDir }}/.local/state/restic-system/mise-global.txt 2>/dev/null || true" + - "test -d {{ .chezmoi.homeDir }}/go/bin && ls -1 {{ .chezmoi.homeDir }}/go/bin > {{ .chezmoi.homeDir }}/.local/state/restic-system/go-bin.txt 2>/dev/null || true" + - "systemctl list-unit-files --state=enabled --no-legend > {{ .chezmoi.homeDir }}/.local/state/restic-system/systemd-system-enabled.txt 2>/dev/null || true" + - "systemctl --user list-unit-files --state=enabled --no-legend > {{ .chezmoi.homeDir }}/.local/state/restic-system/systemd-user-enabled.txt 2>/dev/null || true" + source: + - "{{ .chezmoi.homeDir }}" + exclude-caches: true + tag: + - "{{ $host }}" + - home + exclude: + # --- re-fetchable / huge (Dropbox is mirrored to the NAS separately) --- + - "{{ .chezmoi.homeDir }}/.cache" + - "{{ .chezmoi.homeDir }}/Dropbox" + - "{{ .chezmoi.homeDir }}/.local/share/Trash" + - "{{ .chezmoi.homeDir }}/Downloads" + - "{{ .chezmoi.homeDir }}/Backups" + - "{{ .chezmoi.homeDir }}/.local/share/containers" + - "{{ .chezmoi.homeDir }}/.var/app/*/cache" + - "{{ .chezmoi.homeDir }}/.var/app/*/.cache" + # --- dev toolchains / registries (reproducible) --- + - "{{ .chezmoi.homeDir }}/.rustup" + - "{{ .chezmoi.homeDir }}/.cargo/registry" + - "{{ .chezmoi.homeDir }}/.cargo/git" + - "{{ .chezmoi.homeDir }}/.npm" + - "{{ .chezmoi.homeDir }}/go/pkg" + - "{{ .chezmoi.homeDir }}/.local/share/mise/installs" + - "{{ .chezmoi.homeDir }}/.local/share/mise/downloads" + - "{{ .chezmoi.homeDir }}/.local/share/mise/shims" + - "{{ .chezmoi.homeDir }}/.m2" + - "{{ .chezmoi.homeDir }}/.gradle" + - "**/node_modules" + - "**/.venv" + - "**/__pycache__" + - "**/target/debug" + - "**/target/release" + # --- build outputs (sources live in git) --- + - "**/.gradle" + - "**/dist" + - "**/build" + - "**/.next" + - "**/.nuxt" + - "**/.svelte-kit" + - "**/.turbo" + - "**/.parcel-cache" + # --- Steam: drop installs/caches, KEEP saves (steamapps/compatdata + userdata) --- + - "{{ .chezmoi.homeDir }}/.local/share/Steam/steamapps/common" + - "{{ .chezmoi.homeDir }}/.local/share/Steam/steamapps/workshop" + - "{{ .chezmoi.homeDir }}/.local/share/Steam/steamapps/shadercache" + - "{{ .chezmoi.homeDir }}/.local/share/Steam/steamapps/downloading" + - "{{ .chezmoi.homeDir }}/.local/share/Steam/steamapps/temp" + - "{{ .chezmoi.homeDir }}/.local/share/Steam/compatibilitytools.d" + - "{{ .chezmoi.homeDir }}/.local/share/Steam/appcache" + - "{{ .chezmoi.homeDir }}/.local/share/Steam/depotcache" + # --- app caches / indexes (state kept, rebuildables dropped) --- + - "{{ .chezmoi.homeDir }}/.local/share/voxtype/models" + - "{{ .chezmoi.homeDir }}/.local/share/vicinae/favicon-data" + - "{{ .chezmoi.homeDir }}/.local/share/vicinae/file-indexer.db*" + # --- Chromium/Electron caches inside .config (not under ~/.cache) --- + - "**/Cache" + - "**/Code Cache" + - "**/GPUCache" + - "**/GPUPersistentCache" + - "**/GrShaderCache" + - "**/DawnWebGPUCache" + - "**/DawnGraphiteCache" + - "**/CachedData" + - "**/Service Worker/CacheStorage" + - "**/Service Worker/ScriptCache" + # --- editor toolchains (reinstalled on first launch) --- + - "{{ .chezmoi.homeDir }}/.local/share/zed/node" + - "{{ .chezmoi.homeDir }}/.local/share/zed/copilot" + # --- GPU/compute caches --- + - "{{ .chezmoi.homeDir }}/.nv" + # --- VMs & disk images: scoped to VM dirs, NOT a *.img glob (which matches LUKS header backups) --- + - "{{ .chezmoi.homeDir }}/.windows" + - "{{ .chezmoi.homeDir }}/vms" + - "**/*.qcow2" + - "**/*.iso" + schedule: "*-*-* 13:00" + # `user` (run while logged out) needs systemd lingering; without it resticprofile + # falls back to a system job and a non-root `chezmoi apply` can't create it. + # `user_logged_on` is a pure --user timer (Persistent=true catches up missed runs). + schedule-permission: user_logged_on + schedule-lock-wait: 30m + + # NOTE: no `retention` block — server is --append-only (clients can't forget/prune). + # Retention is enforced server-side against tank/backups/machines/ty/. + + check: + schedule: "*-*-01 04:00" + schedule-permission: user_logged_on # see backup note above + schedule-lock-wait: 1h + read-data-subset: "1/12" + +# --- /etc, run as ROOT (captures root-only files the user HOME backup can't read: +# /etc/shadow, NetworkManager system-connections, sudoers.d, etc.) --- +# schedule-permission: system → a root systemd timer. A non-root `chezmoi apply` cannot +# create system timers, so run_onchange_70-configure-restic.sh schedules this via +# `sudo resticprofile --name system schedule` (it prints the one command to run by hand +# if passwordless sudo isn't available). Same repo as `default`, distinguished by the +# `system` tag; the server-side --append-only retention covers both. +# (The `default` run-before still stages package/unit manifests into ~/.local/state/restic-system, +# which the user HOME backup captures — that record is independent of this profile.) +system: + repository: "{{ $repo }}" + password-file: "{{ .chezmoi.homeDir }}/.config/resticprofile/password" + lock: "{{ .chezmoi.homeDir }}/.cache/resticprofile/system.lock" + force-inactive-lock: true + + backup: + source: + - /etc + exclude-caches: true + tag: + - "{{ $host }}" + - system + exclude: + - "/etc/pacman.d/gnupg" # pacman keyring — large & rebuildable (pacman-key --init && --populate) + - "/etc/ssl/certs" # CA bundle symlinks — regenerated by the ca-certificates package + schedule: "*-*-* 13:30" # 30m after the user HOME backup + schedule-permission: system + schedule-lock-wait: 30m + + # No `retention` — server is --append-only; retention runs server-side (see default). + # No `check` either — `restic check` verifies the whole REPOSITORY (not a tag/source), and + # `system` shares the repo with `default`, so default/check already validates the system-tagged + # /etc snapshots. A second check here would just re-verify the same repo. One check is enough. +{{- else }} +# macOS ({{ $host }}): no resticprofile — this machine uses Time Machine to the NAS. +version: "1" +global: + default-command: version +{{- end }} diff --git a/dot_config/resticprofile/private_rest-pass.tmpl b/dot_config/resticprofile/private_rest-pass.tmpl new file mode 100644 index 0000000..921dae8 --- /dev/null +++ b/dot_config/resticprofile/private_rest-pass.tmpl @@ -0,0 +1,5 @@ +{{- /* restic transport password (REST basic auth) — fetched from 1Password once, then cached + here. profiles.yaml reads this file to inline it into the repo URL (offline after first apply). */ -}} +{{- includeTemplate "op-cached-secret" (list + ".config/resticprofile/rest-pass" + (printf "op://dev-keys/restic-%s/rest-key" .chezmoi.hostname)) | trim -}} diff --git a/run_before_01-decrypt.sh.tmpl b/run_before_01-decrypt.sh.tmpl index ca1de19..32a4feb 100644 --- a/run_before_01-decrypt.sh.tmpl +++ b/run_before_01-decrypt.sh.tmpl @@ -1,63 +1,45 @@ {{- if .decrypt -}} #!/usr/bin/env bash -# Ensure age decryption key exists for chezmoi encrypted files -# Key sources (in order): local file, DOTFILES_AGE_KEY env var, 1Password -# Fails if no key can be obtained — profile requires decryption. +# Ensure the age decryption key is on disk BEFORE chezmoi processes any encrypted (.age) files. +# Fetched from 1Password only when missing; once present it is reused (delete it to re-fetch). +# This must be a run_before (chezmoi needs the key to decrypt) — restic secrets do NOT live here; +# they are self-caching templates under dot_config/resticprofile/ (op-cached-secret partial). +# age key <- op://dev-keys/dotfiles-age-key/key (or $DOTFILES_AGE_KEY, e.g. install.sh --age-key) set -euo pipefail # shellcheck source=/dev/null source "${CHEZMOI_SOURCE_DIR:-$(chezmoi source-path)}/scripts/lib/common.sh" -ensure_pkg age +# ensure_secret