Archived
Compare commits
106
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c844ccc4e3 | ||
|
|
5a56030f6e | ||
|
|
f8719437ba | ||
|
|
9e34b9cbb9 | ||
|
|
4dabd725f0 | ||
|
|
2743d664a5 | ||
|
|
cfa34f3565 | ||
|
|
e021b49412 | ||
|
|
0c29c6a93d | ||
|
|
c329988cdd | ||
|
|
7a2b5ecf71 | ||
|
|
d74efd9f66 | ||
|
|
63a8c627f5 | ||
|
|
e4b335be23 | ||
|
|
0bf99c56cc | ||
|
|
e368f68ad7 | ||
|
|
fa52c2849a | ||
|
|
dce3788499 | ||
|
|
e00be5d2da | ||
|
|
82eea7f088 | ||
|
|
955a443b36 | ||
|
|
4800aebf43 | ||
|
|
cb737642e5 | ||
|
|
6d5670c8d2 | ||
|
|
c911a605e9 | ||
|
|
6002c5c738 | ||
|
|
92c50df2f1 | ||
|
|
45e61844d5 | ||
|
|
e72df8fed5 | ||
|
|
5497a5b0ae | ||
|
|
e10e493ddd | ||
|
|
ae9acecbf3 | ||
|
|
a3be05538b | ||
|
|
289163c712 | ||
|
|
2123e4ad69 | ||
|
|
177950dd3d | ||
|
|
cbf1239be4 | ||
|
|
8a282ee32e | ||
|
|
25079a7f0a | ||
|
|
4952e5224d | ||
|
|
98409f4502 | ||
|
|
7779f3e137 | ||
|
|
1462829aa6 | ||
|
|
48ce2c4097 | ||
|
|
bcae177d8e | ||
|
|
d35aca3138 | ||
|
|
147cb3803a | ||
|
|
98445565d6 | ||
|
|
eef4b05254 | ||
|
|
619324589a | ||
|
|
400af07154 | ||
|
|
9bb626327f | ||
|
|
1f8bf8c852 | ||
|
|
5d7a6327b7 | ||
|
|
0b9f124713 | ||
|
|
9479d56e11 | ||
|
|
c53c1940d6 | ||
|
|
79e8f9f2ce | ||
|
|
5fe575d362 | ||
|
|
b46424343f | ||
|
|
33b1d5ec79 | ||
|
|
f565e9c2a1 | ||
|
|
a91634c460 | ||
|
|
42919ea15c | ||
|
|
60c155327d | ||
|
|
0f78e96b81 | ||
|
|
12a2354fad | ||
|
|
f237a6a3d2 | ||
|
|
a2ce01d6ce | ||
|
|
fb6ee27e10 | ||
|
|
85ff5e01e8 | ||
|
|
eb881d4cd8 | ||
|
|
96cc63671a | ||
|
|
104804dbf6 | ||
|
|
0a2298b0e2 | ||
|
|
e73ae6044e | ||
|
|
14621e7ad5 | ||
|
|
cb141f0a41 | ||
|
|
e92aab617f | ||
|
|
b4474cf1e1 | ||
|
|
453c7b5513 | ||
|
|
b3463e4b33 | ||
|
|
013b2c7009 | ||
|
|
12f9153957 | ||
|
|
1004538f00 | ||
|
|
87873300e1 | ||
|
|
e9e2312163 | ||
|
|
6b09a808ed | ||
|
|
9496efdd22 | ||
|
|
33c9506c7d | ||
|
|
abe3763cb3 | ||
|
|
744904b19f | ||
|
|
9cbaf1a070 | ||
|
|
42da626397 | ||
|
|
d7aba8554d | ||
|
|
e176ff723d | ||
|
|
61bbe5e6da | ||
|
|
ab719cc8eb | ||
|
|
91c977e5e7 | ||
|
|
7e9c0c2a6f | ||
|
|
fd773b65da | ||
|
|
d340aca403 | ||
|
|
bf8ee3ce48 | ||
|
|
98d4545e8f | ||
|
|
1d44523181 | ||
|
|
222a3ced69 |
Submodule .claude/worktrees/proxmox-remote-build deleted from a5990ccf7d
@@ -13,9 +13,17 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v19
|
||||
|
||||
- name: Run maintenance checks (secrets, fmt, lint, eval)
|
||||
# Scoped to files changed since the PR base / previous push -- see
|
||||
# scripts/codex-maintenance.sh. CI never passes --full-check: that
|
||||
# full sweep is for local/manual use, since it's slow enough to time
|
||||
# out this runner.
|
||||
- name: Run maintenance checks (secrets, fmt, lint, eval -- changed files only)
|
||||
env:
|
||||
MAINT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
run: bash scripts/codex-maintenance.sh
|
||||
|
||||
@@ -13,9 +13,17 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v19
|
||||
|
||||
- name: Run maintenance checks (secrets, fmt, lint, eval)
|
||||
# Scoped to files changed since the PR base / previous push -- see
|
||||
# scripts/codex-maintenance.sh. CI never passes --full-check: that
|
||||
# full sweep is for local/manual use, since it's slow enough to time
|
||||
# out this runner.
|
||||
- name: Run maintenance checks (secrets, fmt, lint, eval -- changed files only)
|
||||
env:
|
||||
MAINT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
run: bash scripts/codex-maintenance.sh
|
||||
|
||||
+67
-5
@@ -3,11 +3,29 @@ keys:
|
||||
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- &lxc-minimal age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||
- &lxc-nix-cache age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
||||
- &lxc-docker age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
||||
- &lxc-gui age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
||||
- &baremetal-gui age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||
- &linode-docker age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
|
||||
- &linode-gui age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||
- &linode-minimal age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
||||
- &linode-nix-cache age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||
- &linode-server age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||
- &linode-tailscale-router age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||
- &lxc-docker age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
||||
- &lxc-minimal age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
||||
- &lxc-nix-cache age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||
- &lxc-pxe-boot age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
||||
- &lxc-server age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||
- &lxc-tailscale-router age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||
- &lxc-tor-relay age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||
- &proxmox-docker age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
||||
- &proxmox-gui age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||
- &proxmox-nix-cache age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||
- &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||
- &proxmox-server age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||
|
||||
creation_rules:
|
||||
# Shared across every currently-deployed host: root/nixos password hash,
|
||||
@@ -21,27 +39,71 @@ creation_rules:
|
||||
- *docker
|
||||
- *server
|
||||
- *nix-cache
|
||||
- *lxc-minimal
|
||||
- *nix-minimal
|
||||
- *lxc-nix-cache
|
||||
- *proxmox-minimal
|
||||
- *lxc-gui
|
||||
- *baremetal-gui
|
||||
- *linode-docker
|
||||
- *linode-gui
|
||||
- *linode-minimal
|
||||
- *linode-nix-cache
|
||||
- *linode-server
|
||||
- *linode-tailscale-router
|
||||
- *lxc-docker
|
||||
- *lxc-minimal
|
||||
- *lxc-nix-cache
|
||||
- *lxc-pxe-boot
|
||||
- *lxc-server
|
||||
- *lxc-tailscale-router
|
||||
- *lxc-tor-relay
|
||||
- *proxmox-docker
|
||||
- *proxmox-gui
|
||||
- *proxmox-nix-cache
|
||||
- *proxmox-pxe-boot
|
||||
- *proxmox-server
|
||||
- *proxmox-tailscale-router
|
||||
|
||||
- path_regex: secrets/nix-cache\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *nix-cache
|
||||
- *linode-nix-cache
|
||||
- *lxc-nix-cache
|
||||
- *proxmox-nix-cache
|
||||
|
||||
- path_regex: secrets/server\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *server
|
||||
- *linode-server
|
||||
- *lxc-server
|
||||
- *proxmox-server
|
||||
|
||||
- path_regex: secrets/docker\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *docker
|
||||
|
||||
- path_regex: secrets/tor-relay\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *lxc-tor-relay
|
||||
|
||||
# gui-host-specific secrets (currently: wifi-password, see
|
||||
# modules/networking/wifi.nix). Only *lxc-gui has a registered key today
|
||||
# -- proxmox-gui/linode-gui/baremetal-gui haven't been provisioned via
|
||||
# scripts/secrets/sync-host-keys.sh yet, so whichever variant is actually
|
||||
# deployed next needs its recipient added here (and `sops updatekeys` rerun)
|
||||
# before it can decrypt this.
|
||||
- path_regex: secrets/gui\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *lxc-gui
|
||||
- *baremetal-gui
|
||||
- *linode-gui
|
||||
- *proxmox-gui
|
||||
|
||||
@@ -35,9 +35,14 @@ Use these commands when validating changes:
|
||||
```bash
|
||||
bash scripts/codex-setup.sh
|
||||
bash scripts/codex-maintenance.sh
|
||||
bash scripts/codex-maintenance.sh dry-run
|
||||
```
|
||||
|
||||
With no flags, `codex-maintenance.sh` scopes fmt-check/statix/eval to files
|
||||
changed against a base ref — this is what CI runs on every push/PR. For the
|
||||
full sweep (every host, every package — slow; CI never runs this), use
|
||||
`bash scripts/codex-maintenance.sh --full-check` (add `--dry-run` for build
|
||||
planning on top of whichever scope is active).
|
||||
|
||||
Host evaluation is safe when limited to drvPath checks:
|
||||
|
||||
```bash
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
# Flake End-to-End Audit Report
|
||||
|
||||
**Date:** 2026-07-21
|
||||
**Scope:** Full static lint/eval sweep + live build/deploy/interrogate/destroy testing of every `lxc-*` and `proxmox-*` flake target against `pve.sweet.home`, plus an audit of the operator's ability to manage the flake/secrets tooling.
|
||||
**Branch:** `worktree-flake-e2e-audit` (this session's isolated worktree)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The flake itself is in good shape: `nixpkgs-fmt`, `statix`, and a full eval + dry-run build of every host and package are all clean. Every `lxc-*`/`proxmox-*` target's NixOS configuration builds successfully — no target has a broken derivation graph.
|
||||
|
||||
The issues found are **operational, not code-level**:
|
||||
|
||||
1. **pve.sweet.home is critically low on disk space** (91-95% full during this session) and cannot currently build the two largest closures (`gui`, `pxe-boot`) to completion — this actively blocks deploying/redeploying those hosts via the documented workflow.
|
||||
2. **A real, reproducible secrets-decryption failure** was caught live: a stale cached container image (built before a same-day sops-key fix) boots with sshd never starting and every secret failing to decrypt. This is a **general hazard in `create-proxmox-resource.sh`'s "reuse the cached image if present" default**, not a one-off.
|
||||
3. **sops key/anchor drift**: `proxmox-minimal` has a `.sops.yaml` recipient anchor with no corresponding private key anywhere in this environment; several `lxc-*`/`proxmox-*` targets have no sops registration at all yet.
|
||||
4. One concrete script bug was found and **fixed in this session**: `create-proxmox-resource.sh` never enabled the QEMU guest agent channel on VMs it creates, despite the guest OS already running it.
|
||||
5. A management-surface audit (of the operator's ability to run this repo day to day) found 5 process gaps, detailed below.
|
||||
|
||||
Nothing here required or received a `nixos-rebuild switch/boot/test`, `nixos-install`, or any disk-formatting command — all validation was `nix build`/`nix eval`, plus disposable `pct`/`qm` create-then-destroy cycles via the repo's own `create-proxmox-resource.sh`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Static Analysis Results — all clean
|
||||
|
||||
`bash scripts/codex-maintenance.sh --full-check --dry-run` (whole-tree sweep, not just changed files):
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Secret grep | Clean — only the documented exceptions (installer's own hashed passwords, `access-tokens` comment references) |
|
||||
| `nixpkgs-fmt --check` | 0/53 files would be reformatted |
|
||||
| `statix` | No lint warnings |
|
||||
| nix-cache host key drift check | Up to date |
|
||||
| Full eval of every host's `system.build.toplevel` | All 19 `nixosConfigurations` targets evaluate cleanly |
|
||||
| Dry-run build of every host + package | All succeed, no derivation errors |
|
||||
|
||||
No drift, no formatting issues, no lint findings anywhere in the tree.
|
||||
|
||||
---
|
||||
|
||||
## 2. Per-Target Test Results
|
||||
|
||||
Legend: **LIVE** = built on pve, `pct`/`qm` create → interrogated → destroyed. **BUILD-ONLY** = `nix build` validated the config (mostly `.config.system.build.toplevel`, occasionally `.tarball`), no resource created on pve.
|
||||
|
||||
| Target | Test type | Result | Notes |
|
||||
|---|---|---|---|
|
||||
| `lxc-docker` | BUILD-ONLY | ✅ PASS | Live redeploy skipped — CT105 is already running this identity in production; `--allow-duplicate-host` would have destroyed it. |
|
||||
| `lxc-minimal` | **LIVE** | ✅ PASS (after retry) | First attempt reused a stale cached tarball predating a same-day sops-key commit → activation failed, sshd never started (see Finding #2). Redeployed with `--force-rebuild`: clean boot, `systemctl is-system-running` = `running`, secrets decrypted, sshd listening, users correct. |
|
||||
| `lxc-nix-cache` | BUILD-ONLY | ✅ PASS (after retry) | Live redeploy skipped — CT101 is already running this identity. First local build attempt appeared to hang on a remote-builder handoff to nix-cache; killed and retried with `--builders ""` (local-only), succeeded. |
|
||||
| `lxc-gui` | **LIVE (attempted)** | ⚠️ BLOCKED by pve disk space | Registered a fresh sops key (no prior registration existed), built successfully through the full NixOS system closure, then **failed packaging the tarball**: `No space left on device` on pve's root filesystem. Not a flake defect. |
|
||||
| `lxc-pxe-boot` | **LIVE (attempted)** | ⚠️ BLOCKED by pve disk space | Same failure as `lxc-gui` — this target additionally builds a full nested installer/netboot image (`stage-installer-artifacts.nix`), making it similarly large. Failed with the same `No space left on device` error, immediately after the gui attempt had already consumed pve's remaining headroom. |
|
||||
| `lxc-server` | BUILD-ONLY | ✅ PASS | No sops key registered yet; live deploy also would have hit `boot.zfs.extraPools` trying to import a real ZFS pool that doesn't exist in an isolated test container — an expected limitation of testing this build type outside its real hardware, not a bug. |
|
||||
| `lxc-tailscale-exit-node` | BUILD-ONLY | ✅ PASS | No sops key registered yet. |
|
||||
| `lxc-tor-relay` | BUILD-ONLY | ✅ PASS | Live redeploy skipped — CT106 already holds this identity in production. |
|
||||
| `proxmox-docker` | BUILD-ONLY | ✅ PASS (after retry) | Live redeploy skipped — both CT105 *and* VM103 already hold `docker` identities. Combined `toplevel` + `diskoImagesScript` build crashed with a **Nix-internal assertion failure** (`worker.cc:360`) under this session's memory pressure (see Finding #6) — not a flake bug. Retried with `toplevel` alone: clean. |
|
||||
| `proxmox-minimal` | **LIVE (attempted)** | ⚠️ BLOCKED by key drift → BUILD-ONLY | `.sops.yaml` has a registered `&proxmox-minimal` anchor but **no corresponding private key exists anywhere in this environment** — the script correctly refused to generate a mismatched replacement. Fell back to `toplevel` build: ✅ PASS. |
|
||||
| `proxmox-nix-cache` | BUILD-ONLY | ✅ PASS | No sops key registered yet. |
|
||||
| `proxmox-gui` | BUILD-ONLY | ⚠️ Killed after ~40min (resource-limited) | This session's local build machine has only 2GB RAM; swap filled completely (2.0/2.0GB) and the build stalled, so it was killed rather than risk destabilizing the session further. **Not a flake defect** — the equivalent `gui` NixOS configuration already proved fully buildable during the `lxc-gui` live attempt above (it built the entire system closure successfully and only failed at the pve-side tarball-packaging step due to disk space, not the config). |
|
||||
| `proxmox-pxe-boot` | BUILD-ONLY | ⚠️ Killed after ~35min (resource-limited) | Was deep into building the nested installer's kernel initrd (this build type bundles a full netboot installer image via `stage-installer-artifacts.nix`) when killed to keep the audit moving. **Not a flake defect** — this target's own module logic was already effectively validated via the earlier *live* pve deploy attempt (`lxc-pxe-boot` above), which built the complete image and only failed at the final tarball-packaging step due to pve's disk space (Finding 1). |
|
||||
| `proxmox-server` | BUILD-ONLY | ✅ PASS | No sops key registered yet; same ZFS-pool caveat as `lxc-server` would apply to a live deploy. |
|
||||
| `proxmox-tailscale-exit-node` | BUILD-ONLY | ✅ PASS | No sops key registered yet. |
|
||||
|
||||
**Not tested at all:** `linode-*` targets (not deployable to Proxmox) and `installer` (not a normal host) — both were still covered by the static eval/dry-run-build sweep above.
|
||||
|
||||
---
|
||||
|
||||
## 3. Findings, Ranked by Severity
|
||||
|
||||
### Finding 1 — pve.sweet.home is critically low on disk space (blocks real deployments)
|
||||
|
||||
At session start: `/dev/mapper/pve-root` was **95% full, 5.3GB free** (of 94GB). After two failed large builds it recovered slightly to **91% full, 8.2GB free** (nix cleans up its own failed-build scratch space). `/nix/store` alone is 26GB; `nix-store --gc --print-dead` reports **zero** reclaimable garbage — everything currently in the store is a live GC root, so `nix-collect-garbage` won't help without first removing old roots.
|
||||
|
||||
**Why it matters:** `create-proxmox-resource.sh` builds every VM/CT image **directly on pve**, not on a build machine and transferred over. With <10GB headroom, any closure approaching a few GB (the `gui` build type: full Cinnamon desktop + Firefox + LibreOffice + GIMP + VS Code + xrdp; the `pxe-boot` build type: nginx/atftpd *plus* an entire nested installer/netboot image) cannot currently be built there at all. Both `lxc-gui` and `lxc-pxe-boot` failed live with `No space left on device` during this audit.
|
||||
|
||||
**Recommended action:** Expand `pve-root`'s LV, or free space by pruning old container templates in `/var/lib/vz/template/cache` (1.5GB) / old backups in `/var/lib/vz/dump` (306MB) / auditing what's pinning 26GB of `/nix/store` as live GC roots (likely `result-*` symlinks — see below). This is real production disk state; **not something this session touched or fixed** — it needs the operator's judgment on what's safe to remove.
|
||||
|
||||
**Secondary, smaller finding:** every `create-proxmox-resource.sh` run leaves a `result-<target>` symlink in the node's repo checkout as a permanent GC root (`ls /root/nixos/result-*` on pve showed 3 from this session alone: `lxc-docker`, `lxc-minimal`, `lxc-nix-cache`). These accumulate forever and pin their entire closures in the store. Consider having the script clean up its own `result-*` link after staging the built artifact (or use a temp `--out-link` under `/tmp`), so `nix-collect-garbage` can actually reclaim old build outputs.
|
||||
|
||||
### Finding 2 — Stale cached images can silently ship broken secrets (reproduced live)
|
||||
|
||||
`create-proxmox-resource.sh`'s default behavior is: if the node already has `<target>.tar.xz`/`.raw` staged, **reuse it** — only `--force-rebuild` forces a fresh build. This session hit exactly the failure mode `docs/auto-installer.md` already warns about: `lxc-minimal`'s cached tarball (built 2026-07-20T15:57Z) predated a same-day sops-key fix commit (2026-07-20T17:49Z, "clean up in ailse 3"). The deployed container booted with:
|
||||
|
||||
```
|
||||
sops-install-secrets: failed to decrypt '.../common.yaml': Error getting data key: 0 successful groups required, got 0
|
||||
Activation script snippet 'setupSecrets' failed (1)
|
||||
```
|
||||
|
||||
— every secret permanently failed to decrypt, `sshd` never started (though the container otherwise looked "running"). This was **not a code bug**: the currently-committed `secrets/common.yaml` decrypts fine for that host's key when checked independently; the *cached artifact on pve* simply reflected an older commit's ciphertext. Redeploying with `--force-rebuild` fixed it immediately.
|
||||
|
||||
**Why it matters:** this is silent and easy to trigger by accident — any operator who redeploys a host without remembering `--force-rebuild` after a secrets change gets a container that looks like it started (`pct start` succeeds, `pct status` = running) but is completely inaccessible.
|
||||
|
||||
**Recommended action:** Have `create-proxmox-resource.sh` compare the cached image's build timestamp (or embed the source commit hash in the staged filename) against current HEAD, and warn (or refuse without `--force-rebuild`) if they differ — rather than silently trusting presence alone.
|
||||
|
||||
### Finding 3 — sops key/anchor drift
|
||||
|
||||
Two concrete instances hit live during this session:
|
||||
|
||||
- **`proxmox-minimal`**: `.sops.yaml` already has a registered `&proxmox-minimal` age recipient, but this environment's `host-keys/` directory has no corresponding private key file. `sync-host-keys.sh` correctly refused to generate a replacement (it would silently mismatch whatever's already registered/deployed) — but this means **no environment currently has this host's private key**, unless it exists on some other machine that was never backed up here.
|
||||
- **`lxc-gui`**, and by the same logic `lxc-server`/`lxc-tailscale-exit-node`/most `proxmox-*` targets, have **no sops registration at all yet** — expected for undeployed hosts per `docs/auto-installer.md`, but this session's live-testing needed to register `lxc-gui`'s key on the fly, which immediately hit **Finding 3b**: registering a key locally does nothing for pve's build until it's pushed to `origin/main` (pve builds via `git pull`, not from this uncommitted worktree). This is exactly gap #4 the management-surface audit (below) already flagged in the abstract — this session hit it concretely.
|
||||
|
||||
**Recommended action:** for `proxmox-minimal`, decide whether to regenerate its key (destroying old-key decrypt access, if anything still holds it) or track down wherever the original private key lives and back it up here. For the general pattern, see the management-surface audit's recommendation to pre-flight-check key registration before building.
|
||||
|
||||
### Finding 4 — QEMU guest agent never wired up (found and fixed this session)
|
||||
|
||||
`modules/common/configuration.nix:44` sets `services.qemuGuest.enable = true` on every host — the guest-side agent daemon is correctly enabled everywhere. But `scripts/proxmox/create-proxmox-resource.sh`'s `qm create` call never passed `--agent 1`, so **Proxmox never created the virtio-serial channel** the agent needs. Every `proxmox-*` VM this script ever created was silently missing `qm guest exec`/IP-address reporting in the Proxmox UI, despite the guest daemon actually running.
|
||||
|
||||
**Status: fixed in this session's worktree** (`scripts/proxmox/create-proxmox-resource.sh`, `qm create` now includes `--agent enabled=1`) — see the diff, included in the PR from this session.
|
||||
|
||||
### Finding 5 — Orphaned container on pve (CT102)
|
||||
|
||||
`pve.sweet.home` has a stopped LXC container, **VMID 102**, with an essentially empty config (`lock: create` and nothing else — no hostname, no rootfs, no network) — the leftover of a `pct create` that started and never finished. It predates this session (not created by any of this audit's activity) and wasn't touched. **Recommend the operator confirm it's abandoned and remove it** (`pct destroy 102 --purge 1`) — left as-is it may be someone's genuine in-progress work, so it wasn't assumed safe to delete autonomously.
|
||||
|
||||
### Finding 6 — Nix-internal crash under memory pressure (tooling, not flake)
|
||||
|
||||
Building `proxmox-docker`'s `toplevel` and `diskoImagesScript` together crashed with a Nix-internal assertion failure (`Assertion '!awake.empty()' failed ... worker.cc:360`, a known class of bug in Nix's multi-goal build scheduler) while this session's 2GB-RAM build container was under heavy swap pressure (1.8-2.0/2GB swap in use) from a separate concurrent build. Retrying the same target alone (no concurrency) succeeded cleanly. **Not a flake defect** — purely an artifact of this session's constrained build environment; noted for completeness since it looked alarming in isolation.
|
||||
|
||||
### Finding 7 — Management-surface audit: 5 operability gaps
|
||||
|
||||
A focused audit of "can the operator actually run this repo day to day" (flake, home-manager, sops, related scripts) found:
|
||||
|
||||
1. **No documented recovery path if the `&admin` sops age key is lost without a backup.** `scripts/secrets/backup-admin-key.sh` exists and works but is referenced nowhere in `README.md`/`docs/` — no forcing function ensures a backup was ever taken. `rotate-admin-key.sh` requires the *old* key to re-key; there's no bootstrap-from-nothing path documented (the real fallback — deriving an age identity from any still-live host's own SSH key — isn't written down anywhere).
|
||||
2. **home-manager has no standalone iteration path.** It's wired only inside `nixosConfigurations` (`flake.nix`) — no `homeConfigurations` output. The fastest real shortcut (`nix build .#nixosConfigurations.<target>.config.home-manager.users.nixos.home.activationPackage`) isn't documented anywhere, so the practical workflow is a full host rebuild to test one HM tweak.
|
||||
3. **Gitea's flake-lock-update workflow pushes straight to `main` with no pre-merge validation.** `.gitea/workflows/update-flake-lock.yml` commits and pushes `nix flake update`'s result directly; `codex-maintenance.sh` only runs *after*, on the resulting push — a genuinely broken lockfile bump lands on `main` before anything catches it. (The GitHub-side workflow is safer — PR-based — but has the opposite gap: nothing alerts if the PR sits unmerged.)
|
||||
4. **No pre-flight check that a build target has a registered sops key before building it.** `docs/auto-installer.md` documents the failure mode (silent, total secrets-decrypt failure) but nothing in `create-proxmox-resource.sh` refuses to proceed when it's about to build a target with no `.sops.yaml` anchor — it's on the operator to remember. This session's `lxc-gui` test hit close to this exact gap (needed the key added on the fly, mid-session).
|
||||
5. **`vars.remoteBuilderAuthorizedKeys` has the same drift risk as `vars.nixCacheHostKey`, but no checker script.** `sync-nix-cache-host-key.sh --check` guards the latter; the former (and `vars.pxeServerIp`/`vars.pbsIp`) has no equivalent — a rotated/revoked client key just silently stops working with no diagnostic pointing back here.
|
||||
|
||||
---
|
||||
|
||||
## 4. Action Plan (priority order)
|
||||
|
||||
1. **Free up disk space on pve.sweet.home** (or expand `pve-root`). Blocking: `lxc-gui`, `proxmox-gui`, `lxc-pxe-boot`, `proxmox-pxe-boot` cannot currently be built/redeployed on this node at all.
|
||||
2. **Decide on `proxmox-minimal`'s orphaned sops key**: locate the original private key and back it up here, or accept regenerating it (breaks decrypt access for whoever/whatever currently holds the old one).
|
||||
3. **Merge this session's PR** (see below) to get the `--agent 1` fix and `lxc-gui`'s new sops registration onto `main` — required before `lxc-gui` can be live-redeployed with working secrets.
|
||||
4. **Add a staleness guard to `create-proxmox-resource.sh`'s cache-reuse path** (Finding 2) — highest-leverage fix, since it silently produces a broken-but-"running" host.
|
||||
5. **Add a pre-flight sops-anchor check to `create-proxmox-resource.sh`** (management-surface gap #4) — same root cause class as #4 above, catch it before building instead of at first boot.
|
||||
6. Investigate/clean up **CT102** on pve (Finding 5) — confirm abandoned, then remove.
|
||||
7. Document `backup-admin-key.sh` in `README.md`'s Security Notes and add the live-host-key bootstrap-recovery procedure to `docs/` (management-surface gap #1).
|
||||
8. Add pre-push validation to the Gitea flake-lock-update workflow (management-surface gap #3).
|
||||
9. Lower-priority: document the home-manager `activationPackage` shortcut (gap #2); extend `sync-nix-cache-host-key.sh`'s drift-check pattern to `remoteBuilderAuthorizedKeys` (gap #5).
|
||||
10. Follow-up session: finish build-validating `proxmox-gui` and `proxmox-pxe-boot` (both killed here after 35-40min on this session's 2GB-RAM machine — not failures, just unfinished) once pve has headroom (item 1) — ideally from a machine with more RAM. `proxmox-server` and `proxmox-tailscale-exit-node` already passed build-only validation in this session, no follow-up needed.
|
||||
|
||||
---
|
||||
|
||||
## 5. Uncommitted Changes From This Session
|
||||
|
||||
This worktree (`worktree-flake-e2e-audit`) currently has:
|
||||
|
||||
- `scripts/proxmox/create-proxmox-resource.sh` — the `--agent enabled=1` fix (Finding 4).
|
||||
- `.sops.yaml` / `secrets/common.yaml` — `lxc-gui`'s new age key registered as a recipient (generated live during this session's testing).
|
||||
|
||||
Per this session's standard workflow, these will be committed, pushed, and opened as a draft PR rather than pushed to `main` directly — merging it is the operator's call, and is also **prerequisite to live-redeploying `lxc-gui` successfully** (its build will keep hitting the sops-staleness failure from Finding 2 on pve until this registration is on `origin/main`).
|
||||
@@ -31,17 +31,96 @@ machines when deployed.
|
||||
the auto-installer (see `docs/auto-installer.md`). Never commit its
|
||||
contents; if `git status` ever shows it as trackable, something is wrong.
|
||||
|
||||
### Two Proxmox nodes: `pve1.sweet.home` (production) and `pve-test.sweet.home` (sandbox)
|
||||
|
||||
There are two SSH-reachable Proxmox nodes on the LAN, both defined in
|
||||
`scripts/env.sh` (`PVE1_HOST` / `PVE_TEST_HOST`), individually targetable
|
||||
via `scripts/proxmox/create-proxmox-resource.sh --node <host>` or by
|
||||
overriding `PROXMOX_HOST`. `PROXMOX_HOST` itself still defaults to
|
||||
`PVE1_HOST` (production) — that default, and every other script behavior,
|
||||
is unchanged from before `pve-test` existed; the only thing new is that
|
||||
`pve-test` can now be reached at all. They are **not interchangeable** —
|
||||
one is real production infrastructure, the other exists specifically so
|
||||
there's somewhere safe to test. The restriction below is a policy for
|
||||
Claude specifically, not a change to the tooling's own default or
|
||||
anything the operator needs to opt into.
|
||||
|
||||
#### `pve1.sweet.home` (production — off-limits to Claude)
|
||||
|
||||
A real, live Proxmox node hosting production VMs/containers — not a
|
||||
sandbox, and not Claude's to touch by default.
|
||||
|
||||
- **Off-limits at all times unless the operator has given explicit,
|
||||
same-session instructions to act on this specific host.** That
|
||||
authorization is scoped to the task it was given for — don't carry it
|
||||
forward to unrelated later work in the same conversation, and never
|
||||
assume it from a previous session.
|
||||
- **Read-only for existing state is always fine, authorization or not.**
|
||||
You may SSH in (or use `pvesm`, `qm list`, `pct list`, `qm config`, `pct
|
||||
config`, the Proxmox API, etc.) to inspect the node's config, storage,
|
||||
and any existing VM/container — including ones this repo didn't create.
|
||||
- **Never** modify, stop, restart, delete, reconfigure, or create anything
|
||||
on this node (`qm set`, `pct set`, `qm destroy`, `pct destroy`, `qm
|
||||
stop`, `pct stop`, `qm create`, `pct create`, snapshot operations,
|
||||
storage changes, etc.) — including scratch/test resources — without
|
||||
that explicit go-ahead. Use `pve-test.sweet.home` for anything
|
||||
exploratory instead; it exists precisely so `pve1` never has to be the
|
||||
answer to "where do I test this."
|
||||
- **This is a Claude-specific policy, not something the scripts enforce.**
|
||||
`scripts/env.sh`/`create-proxmox-resource.sh` default to `pve1` exactly
|
||||
as they did before `pve-test` existed, with no extra flag or prompt
|
||||
required — that's deliberate, so the operator's own existing workflows
|
||||
don't change. Claude, however, must never rely on that default: every
|
||||
Proxmox action Claude takes on its own initiative — not explicitly
|
||||
pointed at `pve1` by the operator this session — targets `pve-test`
|
||||
instead (e.g. `--node "$PVE_TEST_HOST"`, or `PROXMOX_HOST=$PVE_TEST_HOST`).
|
||||
Claude's own default is `pve-test`, full stop, regardless of what the
|
||||
tooling's own unqualified default happens to be.
|
||||
|
||||
#### `pve-test.sweet.home` (sandbox — Claude's default target)
|
||||
|
||||
A separate Proxmox node set aside for testing. The *tooling's* default is
|
||||
still production (`PROXMOX_HOST` → `PVE1_HOST`, see above) — but
|
||||
**Claude's own default is this node**: absent an explicit, same-session
|
||||
instruction to use `pve1`, every Proxmox action Claude initiates targets
|
||||
`pve-test`. Once targeted, it's safe to create, interrogate, and destroy
|
||||
resources on without asking first.
|
||||
|
||||
- **Test VMs/containers are allowed, but must be torn down.** Create a
|
||||
scratch VM or container here (e.g. via
|
||||
`scripts/proxmox/create-proxmox-resource.sh` or raw `qm`/`pct create`)
|
||||
to validate something. Anything created this way must be destroyed
|
||||
again in the same session, before ending the task — never leave a test
|
||||
resource running. Use a VMID/name that's obviously scratch (and doesn't
|
||||
collide with a real flake target) so it's unambiguous what's safe to
|
||||
remove.
|
||||
- **Node-level config is still not yours to change.** Creating/destroying
|
||||
your own scratch guests is fine; Proxmox host config, storage pools, and
|
||||
networking on `pve-test` itself are still the operator's call to make
|
||||
manually, same as on `pve1`.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# One-time environment bootstrap (installs Nix if missing, prints hosts)
|
||||
bash scripts/codex-setup.sh
|
||||
|
||||
# Full validation: secret grep, nixpkgs-fmt --check, statix lint, eval all hosts
|
||||
# Changed-files-only validation: secret grep (whole repo), nixpkgs-fmt --check
|
||||
# and statix on changed *.nix files, eval of the hosts/packages those changes
|
||||
# can affect. This is what CI runs on every push/PR.
|
||||
bash scripts/codex-maintenance.sh
|
||||
|
||||
# Same, plus a dry-run build (no result symlink) of every host's toplevel
|
||||
bash scripts/codex-maintenance.sh dry-run
|
||||
# Full sweep: nixpkgs-fmt --check/statix over the whole tree, eval every host
|
||||
# and package. Slow (minutes) -- CI never runs this; use it locally before a
|
||||
# release or after touching modules/common/*, flake.nix, or variables.nix for
|
||||
# extra confidence beyond the automatic full-fallback those paths already
|
||||
# trigger in the default mode (see below).
|
||||
bash scripts/codex-maintenance.sh --full-check
|
||||
|
||||
# Either mode, plus a dry-run build (no result symlink) of every host/package
|
||||
# in whichever scope is active
|
||||
bash scripts/codex-maintenance.sh --dry-run
|
||||
bash scripts/codex-maintenance.sh --full-check --dry-run
|
||||
|
||||
# List the hosts the flake currently exposes
|
||||
nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
|
||||
@@ -58,30 +137,66 @@ maintenance script pulls them via `nix run github:NixOS/nixpkgs/nixos-25.11#<too
|
||||
There is no test suite — "correctness" here means the flake evaluates and
|
||||
`nixpkgs-fmt`/`statix` are clean.
|
||||
|
||||
**In an interactive agent session**, prefer targeted checks over full-repo
|
||||
sweeps: after editing one or two hosts/modules, evaluate just the
|
||||
`nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball`
|
||||
/`diskoImagesScript`/package output affected) rather than looping over every
|
||||
host — `codex-maintenance.sh` evaluates every `nixosConfigurations` host plus
|
||||
every package/tarball/image variant and is slow to run after each small
|
||||
change. Reserve a full
|
||||
`codex-maintenance.sh` run for changes that plausibly affect every host
|
||||
(`modules/common/*`, `flake.nix`, `variables.nix`) or as a final check before
|
||||
committing. This is a session-workflow preference only — it does not apply to
|
||||
CI, which should keep running the full script on every push/PR regardless of
|
||||
diff size; that's the point of it.
|
||||
With no flags, `codex-maintenance.sh` diffs against a base ref (env
|
||||
`MAINT_BASE_SHA`, else the PR base SHA in CI, else `HEAD^` locally) and scopes
|
||||
fmt-check/statix to the changed `*.nix` files and eval to the hosts/packages
|
||||
those changes can affect — a `hosts/<name>/host.nix` edit only evals that
|
||||
host's targets, a `modules/platforms/<platform>.nix` edit only evals that
|
||||
platform's hosts, and so on. A change to `flake.nix`, `flake.lock`,
|
||||
`variables.nix`, `modules/common/*`, or any other `modules/*.nix` file outside
|
||||
`platforms/`/`build-types/` (whose blast radius isn't safely inferable from
|
||||
the path alone) falls back to evaluating every host and package, same as
|
||||
`--full-check` would, just without the whole-tree fmt/statix sweep. This
|
||||
exists because the whole-tree sweep is what was timing out CI; **CI always
|
||||
runs the plain, no-flag form and never passes `--full-check`.**
|
||||
|
||||
The default mode's diff is against the working tree (uncommitted and staged
|
||||
edits included, not just committed ones), so it's already the right tool for
|
||||
an interactive session too: after editing one or two hosts/modules, plain
|
||||
`bash scripts/codex-maintenance.sh` naturally scopes to just what you
|
||||
touched. Reserve `--full-check` for changes that plausibly affect every host
|
||||
(`modules/common/*`, `flake.nix`, `variables.nix` — though the default mode
|
||||
already falls back to evaluating everything for those paths, `--full-check`
|
||||
additionally re-checks fmt/statix over the whole tree) or as a final check
|
||||
before committing.
|
||||
|
||||
## Scripts
|
||||
|
||||
Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` is
|
||||
organized by purpose: `scripts/secrets/` (sops/age + SSH host-key
|
||||
management), `scripts/proxmox/` (Proxmox deployment), `scripts/lib/`
|
||||
(shared helpers, sourced by the scripts below — not run directly), and a
|
||||
handful of repo-wide scripts left at the top level (`env.sh`,
|
||||
`bump-nixpkgs-release.sh`, plus `codex-setup.sh`/`codex-maintenance.sh`
|
||||
above). When adding a new script, put it in the matching subfolder rather
|
||||
than the top level, and if it duplicates logic another script already has,
|
||||
lift the shared part into `scripts/lib/` instead of copying it.
|
||||
management), `scripts/proxmox/` (Proxmox deployment), `scripts/installer/`
|
||||
(the auto-installer's own shell script, templated into the image — see
|
||||
below), `scripts/lib/` (shared helpers, sourced by the scripts below — not
|
||||
run directly), and a handful of repo-wide scripts left at the top level
|
||||
(`env.sh`, `bump-nixpkgs-release.sh`, plus `codex-setup.sh`/
|
||||
`codex-maintenance.sh` above). When adding a new script, put it in the
|
||||
matching subfolder rather than the top level, and if it duplicates logic
|
||||
another script already has, lift the shared part into `scripts/lib/`
|
||||
instead of copying it.
|
||||
|
||||
### `scripts/installer/`
|
||||
|
||||
- `scripts/installer/auto-install.sh` — the interactive install script
|
||||
baked into the auto-installer image (see `docs/auto-installer.md`), kept
|
||||
as a real, version-controlled shell file rather than inline in
|
||||
`modules/installer/common.nix`'s Nix. It sources `scripts/env.sh` itself
|
||||
for `LAN_DOMAIN` (`export LAN_DOMAIN`/`: "${LAN_DOMAIN:=...}"`, matching
|
||||
`variables.nix`'s `lanDomain` — manually kept in sync, same pattern as
|
||||
`NIX_CACHE_HOST` mirroring `nixCacheHost`), rather than Nix-level string
|
||||
substitution — that's what makes it work identically whether run
|
||||
straight from a git checkout or from inside the built installer image.
|
||||
`common.nix` bakes `scripts/env.sh` in alongside it at a matching
|
||||
relative path (`/etc/nixos-installer/env.sh` next to
|
||||
`/etc/nixos-installer/installer/auto-install.sh`) so the script's own
|
||||
`source "$(dirname ...)/../env.sh"` line resolves the same way in both
|
||||
contexts — this is also why it's invoked from
|
||||
`/etc/nixos-installer/installer/auto-install.sh` rather than a flat
|
||||
`/etc/auto-install.sh`. `#!/usr/bin/env bash`, not
|
||||
`#!/run/current-system/sw/bin/bash`: the latter only resolves on an
|
||||
already-activated NixOS system, breaking the checked-out-file case
|
||||
entirely (confirmed live: "cannot execute: required file not found" on
|
||||
a non-NixOS box); `/usr/bin/env` is reliably present on both NixOS
|
||||
(`environment.usrbinenv`'s own default) and any normal Linux distro.
|
||||
|
||||
### `scripts/secrets/`
|
||||
|
||||
@@ -116,6 +231,22 @@ lift the shared part into `scripts/lib/` instead of copying it.
|
||||
Purely a local filesystem copy — never touches `.sops.yaml`/
|
||||
`secrets/*.yaml` or the repo at all. The resulting file is exactly what
|
||||
`rotate-admin-key.sh` expects as its backup-key argument.
|
||||
- `scripts/secrets/sync-nix-cache-host-key.sh [--check] [--dry-run]
|
||||
[--host <name>]` — detects drift between the ed25519 SSH host key
|
||||
nix-cache is actually serving right now (via `ssh-keyscan`) and
|
||||
`vars.nixCacheHostKey` (`variables.nix`), the value
|
||||
`modules/nix-cache/remote-builder-client.nix` bakes into every real
|
||||
client's declarative `programs.ssh.knownHosts` and
|
||||
`configure-nix-cache-client.sh` hardcodes as its own default for
|
||||
non-NixOS clients. That value has no automatic source of truth — it's
|
||||
set once from whatever nix-cache's host key happened to be at the time,
|
||||
and silently goes stale if the host is ever rebuilt/recreated with a new
|
||||
key, breaking every client's distributed-build SSH trust with no error
|
||||
that points back here. `--check` (used by `codex-maintenance.sh`, which
|
||||
treats an unreachable nix-cache — e.g. from a non-LAN CI runner — as a
|
||||
silent skip rather than a failure) only reports drift; the no-flags form
|
||||
updates both files in place. Declarative clients still need a rebuild to
|
||||
pick up the fix.
|
||||
|
||||
### `scripts/proxmox/`
|
||||
|
||||
@@ -177,8 +308,9 @@ Sourced by the scripts above, never run directly:
|
||||
### Top level
|
||||
|
||||
- `scripts/env.sh` — shared config (`PROXMOX_HOST`, storage pool, bridge,
|
||||
default cores/memory) sourced by `create-proxmox-resource.sh`. Add new
|
||||
cross-script config here instead of duplicating it per-script.
|
||||
default cores/memory, `NIX_CACHE_HOST`, `LAN_DOMAIN`) sourced by
|
||||
`create-proxmox-resource.sh` and `scripts/installer/auto-install.sh`. Add
|
||||
new cross-script config here instead of duplicating it per-script.
|
||||
- `scripts/bump-nixpkgs-release.sh` — bumps `flake.nix`'s `nixpkgs.url`/
|
||||
`home-manager.url` in place. Exists because flake input URLs can't
|
||||
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
||||
@@ -216,11 +348,14 @@ nixosSystem {
|
||||
}
|
||||
```
|
||||
|
||||
Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
|
||||
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`. Not
|
||||
every combination is built — e.g. `pxe-boot` has no `linode` variant
|
||||
(PXE/DHCP/TFTP need LAN L2 adjacency a Linode VPS doesn't have), and
|
||||
`tor-relay` currently only exists as `lxc-tor-relay`. Treat `flake.nix`'s
|
||||
Platforms: `linode`, `proxmox`, `lxc`, `baremetal`. Build types: `minimal`,
|
||||
`nix-cache`, `server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`,
|
||||
`tor-relay`. Not every combination is built — e.g. `pxe-boot` has no `linode`
|
||||
variant (PXE/DHCP/TFTP need LAN L2 adjacency a Linode VPS doesn't have),
|
||||
`tor-relay` currently only exists as `lxc-tor-relay`, and `baremetal`
|
||||
currently only exists as `baremetal-gui` (the real gui-host hardware —
|
||||
see `hosts/nixos/host.nix` and `modules/platforms/baremetal.nix`). Treat
|
||||
`flake.nix`'s
|
||||
`generatedTargets` as the source
|
||||
of truth for which hosts exist — `README.md`, `AGENTS.md`,
|
||||
`docs/flake-lock-automation.md`, and the CI eval workflows
|
||||
@@ -236,12 +371,16 @@ removing a host.
|
||||
of their own beyond narrow parameterized helpers (see
|
||||
`modules/beszel/host-token.nix` below) — all shared behavior comes from the
|
||||
platform/build-type modules composed in `flake.nix`, not from the host file.
|
||||
- `modules/platforms/{linode,proxmox,lxc}.nix` — platform-specific config:
|
||||
boot method, guest tooling, and (for linode/proxmox) the hypervisor-specific
|
||||
hardware config, imported directly by the platform module itself
|
||||
(`../hardware-configuration/vm/{proxmox,linode}.nix`) — **not** wired in
|
||||
from `flake.nix`. `lxc.nix` has no hardware-configuration counterpart since
|
||||
containers share the host kernel; instead it imports nixpkgs' own
|
||||
- `modules/platforms/{linode,proxmox,lxc,baremetal}.nix` — platform-specific
|
||||
config: boot method, guest tooling, and the hardware config, imported
|
||||
directly by the platform module itself — **not** wired in from
|
||||
`flake.nix`. VM platforms use `../hardware-configuration/vm/{proxmox,linode}.nix`;
|
||||
`baremetal.nix` uses `../hardware-configuration/baremetal.nix` (adapted
|
||||
from a real `nixos-generate-config` run on the actual hardware, not a
|
||||
vm/ file, since it isn't a VM) plus `hardware.enableRedistributableFirmware
|
||||
= true` for real wifi/GPU/microcode firmware that VMs never needed.
|
||||
`lxc.nix` has no hardware-configuration counterpart since containers
|
||||
share the host kernel; instead it imports nixpkgs' own
|
||||
`virtualisation/proxmox-lxc.nix`, which gives every `lxc-*` host a
|
||||
`config.system.build.tarball` output — a plain rootfs tarball, used as a
|
||||
`pct create ... vztmpl` CT template (**not** `pct restore`, which expects
|
||||
@@ -265,6 +404,16 @@ removing a host.
|
||||
boots, so this declares them with `destroy = false` (disko never wipes
|
||||
them) and a bare `filesystem`/`swap` content type instead of a partition
|
||||
table — idempotent against an already-provisioned disk, never destructive.
|
||||
- `modules/disko/baremetal.nix` — `baremetal-gui`'s disko config: a ZFS
|
||||
RAID0 (striped, no redundancy — disko's zpool `mode` defaults to `""`,
|
||||
which is a plain stripe rather than `"mirror"`/`"raidz"`) root pool
|
||||
across two disks, ESP + systemd-boot on the first. Device paths
|
||||
(`vars.guiRootDisk1`/`guiRootDisk2`) are placeholders — fill in stable
|
||||
`/dev/disk/by-id/...` paths before running disko for real.
|
||||
`modules/platforms/baremetal.nix` also imports
|
||||
`modules/services/zfs/enable-service.nix` for this (the `zfs_unstable`
|
||||
package, autoScrub/autoSnapshot/trim) — the only other importer today is
|
||||
`server`'s NFS data pool, an unrelated non-root ZFS use.
|
||||
- `modules/boot/efi.nix` — systemd-boot + EFI vars, paired with the disko module.
|
||||
- `modules/installer/` — the auto-installer environment (ISO, also served as
|
||||
PXE netboot): `common.nix` (shared config + the generated
|
||||
|
||||
@@ -8,13 +8,15 @@ workstation.
|
||||
Targets are named `<platform>-<buildtype>`, generated from two orthogonal
|
||||
pieces composed in `flake.nix`:
|
||||
|
||||
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`
|
||||
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`, `baremetal`
|
||||
- **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`,
|
||||
`gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
|
||||
|
||||
Not every combination exists — `pxe-boot` has no `linode` variant, since
|
||||
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have, and
|
||||
`tor-relay` currently only exists as `lxc-tor-relay`. The full list:
|
||||
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have,
|
||||
`tor-relay` currently only exists as `lxc-tor-relay`, and `baremetal`
|
||||
currently only exists as `baremetal-gui` (the real gui-host hardware). The
|
||||
full list:
|
||||
|
||||
| Target | Purpose |
|
||||
| --- | --- |
|
||||
@@ -25,6 +27,7 @@ PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have, and
|
||||
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host — previously the flat `server` target |
|
||||
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack — previously the flat `docker` target |
|
||||
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation — previously the flat `nixos` target |
|
||||
| `baremetal-gui` | Same Cinnamon desktop workstation, on the real gui-host hardware — ZFS RAID0 root, systemd-boot |
|
||||
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host — previously the flat `pxe-boot` target |
|
||||
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node |
|
||||
| `lxc-tor-relay` | Tor middle relay |
|
||||
@@ -36,6 +39,11 @@ Check the Proxmox node itself, or `/etc/flake-target` on a running host (see
|
||||
below), if you need to know what's really out there right now.
|
||||
`scripts/proxmox/create-proxmox-resource.sh`'s duplicate-host guard works the same
|
||||
way: it checks the Proxmox node directly rather than any file here.
|
||||
Real, production deployments live on `pve1.sweet.home`; there's a second
|
||||
node, `pve-test.sweet.home`, set aside purely for scratch/test resources —
|
||||
see `scripts/env.sh` (`PVE1_HOST` / `PVE_TEST_HOST`, and the
|
||||
`--node`/`PROXMOX_HOST` targeting they feed into) and CLAUDE.md's Proxmox
|
||||
section for which is which.
|
||||
|
||||
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
|
||||
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
|
||||
@@ -59,7 +67,7 @@ nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
|
||||
| `variables.nix` | Single source of truth for shared values (LAN domain/CIDR, hostnames, timezone, primary username, storage root, NFS share subpaths/mountpoints, service ports, ...) — passed to every module and Home Manager config as the `vars` argument via `specialArgs`/`extraSpecialArgs` |
|
||||
| `hosts/<name>/host.nix` | Per-machine identity: hostname, hostId, per-machine secrets, `system.stateVersion` |
|
||||
| `hosts/nixos/home.nix` | Workstation-specific Home Manager config (used by the `gui` build type) |
|
||||
| `modules/platforms/` | Platform-specific config: virtualisation guest tools, boot method, hardware config (`linode.nix`, `proxmox.nix`, `lxc.nix`) |
|
||||
| `modules/platforms/` | Platform-specific config: virtualisation guest tools, boot method, hardware config (`linode.nix`, `proxmox.nix`, `lxc.nix`, `baremetal.nix`) |
|
||||
| `modules/build-types/` | Build-type-specific config: what makes a system minimal/server/docker/gui/pxe-boot/nix-cache |
|
||||
| `modules/common/` | Shared NixOS config, Home Manager, aliases imported by every host |
|
||||
| `modules/nix-cache/` | Binary cache and remote builder client/server modules |
|
||||
@@ -74,10 +82,19 @@ Safe validation commands for Codex and local review:
|
||||
|
||||
```bash
|
||||
bash scripts/codex-setup.sh
|
||||
bash scripts/codex-maintenance.sh dry-run
|
||||
bash scripts/codex-maintenance.sh
|
||||
```
|
||||
|
||||
`codex-maintenance.sh` with no flags (what CI runs on every push/PR) scopes
|
||||
fmt-check/statix/eval to files changed against a base ref — fast, but only
|
||||
as thorough as the diff. For the full sweep (every host, every package,
|
||||
fmt-check and statix over the whole tree — slow, CI never runs this):
|
||||
|
||||
```bash
|
||||
bash scripts/codex-maintenance.sh --full-check
|
||||
bash scripts/codex-maintenance.sh --full-check --dry-run
|
||||
```
|
||||
|
||||
For individual host evaluation:
|
||||
|
||||
```bash
|
||||
|
||||
+31
-6
@@ -8,10 +8,13 @@ lives here.
|
||||
The installer provides a small NixOS install environment (ISO, or the same
|
||||
image netbooted via PXE) with SSH access, Git support, and an interactive
|
||||
installation script.
|
||||
Logging in as any user (root or `nixos`) runs `/etc/auto-install.sh`,
|
||||
discovers available hosts from this same flake, lets the operator choose a
|
||||
target, applies that host's Disko storage configuration, installs NixOS, and
|
||||
reboots.
|
||||
Logging in as any user (root or `nixos`) runs
|
||||
`/etc/nixos-installer/installer/auto-install.sh` (the same file as
|
||||
`scripts/installer/auto-install.sh` in this repo — see "Installer process"
|
||||
below for why it's baked in at that path rather than a flat
|
||||
`/etc/auto-install.sh`), discovers available hosts from this same flake,
|
||||
lets the operator choose a target, applies that host's Disko storage
|
||||
configuration, installs NixOS, and reboots.
|
||||
|
||||
**This applies to every `nixosConfigurations` target except `lxc-*` hosts —
|
||||
see "LXC hosts" immediately below for why those are different.**
|
||||
@@ -150,7 +153,11 @@ use case.
|
||||
|
||||
The `pxe` variant is also built automatically as part of the `pxe-boot` host
|
||||
itself (`modules/pxe-boot/stage-installer-artifacts.nix`) and served over
|
||||
iPXE — see `docs/pxe-boot.md`.
|
||||
iPXE as the menu's "NixOS Auto-Installer" entry — see `docs/pxe-boot.md`.
|
||||
That same host also builds and serves `packages.x86_64-linux.pxe-minimal`,
|
||||
a vanilla NixOS minimal netboot image with none of this auto-installer's
|
||||
wiring, as a separate "NixOS Minimal" menu entry — also documented in
|
||||
`docs/pxe-boot.md`, not covered further here since it's not this installer.
|
||||
|
||||
## Host keys
|
||||
|
||||
@@ -188,6 +195,10 @@ default.
|
||||
`auto-install.sh` still supports the older manual path as a fallback: if a
|
||||
host's key isn't baked in (`/etc/host-keys`), it checks `/root/host-keys`
|
||||
next, where you can `scp` a key in after boot, same as before this migration.
|
||||
If neither has it and the script is running interactively (an actual
|
||||
operator at the other end of stdin, not an unattended run), it prompts for
|
||||
an arbitrary directory to check (a mounted USB stick, another filesystem,
|
||||
etc.) and copies the key pair into `/root/host-keys` from there if found.
|
||||
|
||||
## Storage
|
||||
|
||||
@@ -213,7 +224,21 @@ entirely (see "LXC hosts" above), so it never reaches this code path.
|
||||
|
||||
## Installer process
|
||||
|
||||
`/etc/auto-install.sh`:
|
||||
`scripts/installer/auto-install.sh` is a real, version-controlled shell
|
||||
script — not an inline Nix string. It sources `scripts/env.sh` for
|
||||
`LAN_DOMAIN` itself (same as every other script in `scripts/`), so it
|
||||
behaves identically whether it's run straight from a git checkout (e.g.
|
||||
manually, from a stock NixOS ISO that isn't this repo's own installer
|
||||
image) or from inside the built installer image. That's also why it's
|
||||
baked in at `/etc/nixos-installer/installer/auto-install.sh` rather than a
|
||||
flat `/etc/auto-install.sh` — `modules/installer/common.nix` bakes
|
||||
`scripts/env.sh` in alongside it at `/etc/nixos-installer/env.sh`,
|
||||
preserving the same relative layout (`installer/auto-install.sh` ->
|
||||
`../env.sh`) the checked-out repo has, so the script's own
|
||||
`source ".../env.sh"` line resolves correctly in both places without any
|
||||
Nix-level templating.
|
||||
|
||||
Once running, it:
|
||||
|
||||
1. Queries `nixosConfigurations` from this flake over the network (`git+https://<lanDomain>/beatzaplenty/nixos.git`) — this happens at *install* time, not build time, so a generic installer image always sees whatever hosts are currently committed, without needing a rebuild.
|
||||
2. Presents them as a menu; confirms the choice.
|
||||
|
||||
@@ -8,9 +8,14 @@ and to verify that declared NixOS hosts still evaluate after dependency updates.
|
||||
- A scheduled workflow runs `nix flake update` once per week.
|
||||
- On GitHub, any resulting `flake.lock` change is proposed through a pull request.
|
||||
- On Gitea, the workflow can commit and push `flake.lock` directly when PR automation is not configured.
|
||||
- A separate CI workflow evaluates every configured host before merge, listed
|
||||
dynamically via `nix eval --json .#nixosConfigurations --apply builtins.attrNames`
|
||||
rather than hand-enumerated, so it can't drift as `<platform>-<buildtype>`
|
||||
- A separate CI workflow runs `scripts/codex-maintenance.sh` before merge.
|
||||
Its default mode scopes eval to the hosts/packages a change can affect,
|
||||
determined from a git diff against the PR base — but a `flake.lock` change
|
||||
is treated as repo-wide and always falls back to evaluating every host, so
|
||||
a lock-file update PR still gets full coverage. Hosts are still listed
|
||||
dynamically via
|
||||
`nix eval --json .#nixosConfigurations --apply builtins.attrNames` rather
|
||||
than hand-enumerated, so that fallback can't drift as `<platform>-<buildtype>`
|
||||
targets are added or removed. See `README.md` for the current target list.
|
||||
|
||||
## Why hosts should stop using `--upgrade-all`
|
||||
|
||||
+17
-9
@@ -46,18 +46,26 @@ the new key up automatically on next activation — no more manual
|
||||
|
||||
## Remote builder SSH keys
|
||||
|
||||
On each client, install the private key used to authenticate as `nixremote`:
|
||||
Each client authenticates as `nixremote` using its **own default root SSH
|
||||
identity** (`/root/.ssh/id_ed25519`) — not a separately-named or shared
|
||||
keypair. If a client doesn't have one yet:
|
||||
|
||||
```bash
|
||||
sudo install -d -m 0700 /root/.ssh
|
||||
sudo install -m 0600 ./nixremote /root/.ssh/nixremote
|
||||
sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
||||
sudo ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
On `nix-cache`, install the matching public key used by `nixremote` authorized keys.
|
||||
Then add its `.pub` contents as a new entry in `vars.remoteBuilderAuthorizedKeys`
|
||||
(`variables.nix`) and rebuild `nix-cache` to pick it up (that list is
|
||||
declarative — an imperative `ssh-copy-id nixremote@nix-cache` won't stick;
|
||||
it gets overwritten on every rebuild). Verify with:
|
||||
|
||||
The committed `nixremote` authorized keys are public SSH keys only. Keep the
|
||||
matching private keys on client hosts and out of the repository.
|
||||
```bash
|
||||
sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache nix-store --version
|
||||
```
|
||||
|
||||
The committed `remoteBuilderAuthorizedKeys` entries are public SSH keys
|
||||
only. Keep the matching private keys on client hosts and out of the
|
||||
repository.
|
||||
|
||||
nix-cache's own SSH *host* key is trusted declaratively via
|
||||
`programs.ssh.knownHosts` in `modules/nix-cache/remote-builder-client.nix`,
|
||||
@@ -76,8 +84,8 @@ After deployment:
|
||||
curl http://nix-cache/nix-cache-info
|
||||
nix store ping --store http://nix-cache
|
||||
nix show-config | grep -E 'substituters|trusted-public-keys|builders-use-substitutes'
|
||||
sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
||||
nix build nixpkgs#hello --builders 'ssh://nixremote@nix-cache x86_64-linux /root/.ssh/nixremote 4 2 big-parallel,kvm,nixos-test,benchmark' -L
|
||||
sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache nix-store --version
|
||||
nix build nixpkgs#hello --builders 'ssh://nixremote@nix-cache x86_64-linux /root/.ssh/id_ed25519 4 2 big-parallel,kvm,nixos-test,benchmark' -L
|
||||
nix path-info -r nixpkgs#hello
|
||||
curl -I "http://nix-cache/$(basename "$(nix path-info nixpkgs#hello)").narinfo"
|
||||
```
|
||||
|
||||
+35
-16
@@ -1,9 +1,10 @@
|
||||
# pxe-boot
|
||||
|
||||
The `pxe-boot` host serves HTTP boot assets for iPXE clients — including a
|
||||
self-staged copy of this flake's own auto-installer netboot image, see
|
||||
`docs/auto-installer.md` for what that image actually is and does once
|
||||
booted.
|
||||
The `pxe-boot` host serves HTTP boot assets for iPXE clients — including
|
||||
self-staged copies of both this flake's own auto-installer netboot image
|
||||
(see `docs/auto-installer.md` for what that image actually is and does once
|
||||
booted) and a vanilla, unmodified NixOS minimal netboot image for plain
|
||||
rescue/inspection use.
|
||||
|
||||
## Host Role
|
||||
|
||||
@@ -28,7 +29,8 @@ The host creates these directories with systemd tmpfiles:
|
||||
/srv/pxe
|
||||
/srv/pxe/http
|
||||
/srv/pxe/http/images
|
||||
/srv/pxe/http/nixos
|
||||
/srv/pxe/http/auto-installer
|
||||
/srv/pxe/http/nixos-minimal
|
||||
/srv/pxe/http/systemrescue
|
||||
/srv/pxe/http/ubuntu
|
||||
/srv/pxe/http/rescue
|
||||
@@ -37,7 +39,7 @@ The host creates these directories with systemd tmpfiles:
|
||||
|
||||
Mount shared image storage under `/srv/pxe/http`, preferably
|
||||
`/srv/pxe/http/images` unless a menu entry expects files in a specific
|
||||
directory such as `/srv/pxe/http/nixos`.
|
||||
directory such as `/srv/pxe/http/auto-installer`.
|
||||
|
||||
The HTTP iPXE chain is:
|
||||
|
||||
@@ -50,20 +52,37 @@ undionly.kpxe or ipxe.efi
|
||||
|
||||
The generated menu currently exposes entries for:
|
||||
|
||||
- NixOS installer
|
||||
- NixOS Auto-Installer
|
||||
- NixOS Minimal
|
||||
- SystemRescue environment
|
||||
- iPXE shell
|
||||
- Reboot
|
||||
|
||||
The NixOS installer entry chain-loads `/srv/pxe/http/nixos/netboot.ipxe`,
|
||||
which is nixpkgs' own generated netboot iPXE script (correct `init=`/`initrd=`
|
||||
kernel parameters included) rather than a hand-rolled boot line — that script
|
||||
in turn expects its kernel/initrd siblings in the same directory. All three
|
||||
files (`bzImage`, `initrd`, `netboot.ipxe`) are built from this flake's own
|
||||
`modules/installer/iso.nix` netboot image (the same one `nix build .#pxe`
|
||||
produces) and staged automatically by
|
||||
`modules/pxe-boot/stage-installer-artifacts.nix` via `systemd.tmpfiles.rules`
|
||||
— no manual operator step required.
|
||||
Both NixOS entries chain-load a `netboot.ipxe` staged into their own
|
||||
directory (`/srv/pxe/http/auto-installer/netboot.ipxe` and
|
||||
`/srv/pxe/http/nixos-minimal/netboot.ipxe`), each nixpkgs' own generated
|
||||
netboot iPXE script (correct `init=`/`initrd=` kernel parameters included)
|
||||
rather than a hand-rolled boot line — that script in turn expects its
|
||||
kernel/initrd siblings in the same directory. Each directory's three files
|
||||
(`bzImage`, `initrd`, `netboot.ipxe`) are built from source and staged
|
||||
automatically by `modules/pxe-boot/stage-installer-artifacts.nix` via
|
||||
`systemd.tmpfiles.rules` — no manual operator step required:
|
||||
|
||||
- `auto-installer` is this flake's own `netbootSystem` (`flake.nix`) — the
|
||||
same auto-installer image `nix build .#pxe` produces. See
|
||||
`docs/auto-installer.md`.
|
||||
- `nixos-minimal` is `netbootMinimalSystem` (`flake.nix`) — nixpkgs'
|
||||
`netboot-minimal.nix` composed on its own, with none of this flake's
|
||||
auto-installer wiring (no `common.nix`, no `auto-install.sh`, no baked
|
||||
host keys or custom users). Same `nix build .#pxe-minimal` mechanism as
|
||||
the auto-installer image, just a different module composition. Useful
|
||||
as a plain rescue/inspection shell that doesn't assume anything about
|
||||
this flake.
|
||||
|
||||
Both images set `networking.hostName` to match their menu entry/staged
|
||||
directory name (`auto-installer` / `nixos-minimal`), so each one's
|
||||
generated system name (`nixos-system-<name>-*`) is self-describing rather
|
||||
than the nixpkgs default of `nixos-system-nixos-*` for both.
|
||||
|
||||
The SystemRescue entry expects the source ISO at:
|
||||
|
||||
|
||||
Generated
+6
-6
@@ -95,11 +95,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1783740085,
|
||||
"narHash": "sha256-qajyHfZY29G2oEQk+uHxmsJcRoBUBXP9maTpFlwP/dI=",
|
||||
"lastModified": 1784350909,
|
||||
"narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "3cd22efe6471dc7365c822bd9ad73a21e55f38fb",
|
||||
"rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -147,11 +147,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1784011430,
|
||||
"narHash": "sha256-lDebytrYdd47IBLwvNOD+6AGeoqZ78CIKlp70hzW280=",
|
||||
"lastModified": 1784432872,
|
||||
"narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8eeec934ae0dbeca3d7868c059568a65c08b2fc3",
|
||||
"rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
# file without a same-option circular dependency (a module
|
||||
# contributing to environment.etc can't read the merged
|
||||
# environment.etc it's itself contributing to).
|
||||
specialArgs = { inherit inputs vars netbootSystem flakeTarget; };
|
||||
specialArgs = { inherit inputs vars netbootSystem netbootMinimalSystem flakeTarget; };
|
||||
};
|
||||
|
||||
# Generated platform x build-type matrix. pxe-boot has no linode
|
||||
@@ -91,13 +91,14 @@
|
||||
linode-gui = mkTarget { platform = "linode"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
|
||||
proxmox-gui = mkTarget { platform = "proxmox"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
|
||||
lxc-gui = mkTarget { platform = "lxc"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
|
||||
baremetal-gui = mkTarget { platform = "baremetal"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
|
||||
|
||||
proxmox-pxe-boot = mkTarget { platform = "proxmox"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
|
||||
lxc-pxe-boot = mkTarget { platform = "lxc"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
|
||||
|
||||
linode-tailscale-exit-node = mkTarget { platform = "linode"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
||||
proxmox-tailscale-exit-node = mkTarget { platform = "proxmox"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
||||
lxc-tailscale-exit-node = mkTarget { platform = "lxc"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
||||
linode-tailscale-router = mkTarget { platform = "linode"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
|
||||
proxmox-tailscale-router = mkTarget { platform = "proxmox"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
|
||||
lxc-tailscale-router = mkTarget { platform = "lxc"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
|
||||
|
||||
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
|
||||
};
|
||||
@@ -119,19 +120,61 @@
|
||||
|
||||
# Same installer environment, built as netboot (kernel + initrd +
|
||||
# iPXE script) instead of an ISO — this is what packages.pxe bundles.
|
||||
#
|
||||
# Deliberately imports common.nix directly, NOT ./modules/installer/iso.nix
|
||||
# (which pulls in nixpkgs' installation-cd-minimal.nix) -- confirmed live
|
||||
# that composing the ISO module together with netboot-minimal.nix hangs
|
||||
# every boot waiting for a device that can never exist on a netboot
|
||||
# client ("A start job is running for /dev/disk/by-label/nixos-minimal-...").
|
||||
# Both installation-cd-base.nix and netboot.nix set fileSystems."/" via
|
||||
# the identical lib.mkImageMediaOverride (mkOverride 60) priority --
|
||||
# genuinely conflicting root-filesystem strategies (ISO-by-label vs.
|
||||
# netboot-tmpfs) at the same priority, and the ISO one was winning.
|
||||
# netboot-minimal.nix's own chain (netboot-base.nix) already imports
|
||||
# profiles/installation-device.nix independently, so common.nix's
|
||||
# initialHashedPassword override (which assumes that profile is
|
||||
# present) still applies correctly without iso.nix in the mix.
|
||||
#
|
||||
# networking.hostName is set explicitly (rather than left at nixpkgs'
|
||||
# own "nixos" default) so this image's generated system name
|
||||
# (nixos-system-auto-installer-*) matches its iPXE menu entry —
|
||||
# see modules/build-types/pxe-boot.nix's :auto-installer item — and
|
||||
# its staged directory, /srv/pxe/http/auto-installer.
|
||||
netbootSystem = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
./modules/installer/iso.nix
|
||||
./modules/installer/common.nix
|
||||
({ modulesPath, ... }: {
|
||||
imports = [
|
||||
(modulesPath + "/installer/netboot/netboot-minimal.nix")
|
||||
];
|
||||
})
|
||||
{ networking.hostName = "auto-installer"; }
|
||||
];
|
||||
specialArgs = { inherit vars; };
|
||||
};
|
||||
|
||||
# A genuinely vanilla NixOS minimal netboot image: nixpkgs'
|
||||
# netboot-minimal.nix on its own, with none of this flake's
|
||||
# auto-installer wiring (no common.nix — no auto-install.sh, no
|
||||
# baked host keys, no custom users/passwords). Built from source via
|
||||
# the same nixosSystem + netboot-minimal.nix path as netbootSystem
|
||||
# above, so both go through an identical build mechanism; the only
|
||||
# difference is what's composed in. hostName again matches this
|
||||
# image's iPXE menu entry (:nixos-minimal) and staged directory
|
||||
# (/srv/pxe/http/nixos-minimal).
|
||||
netbootMinimalSystem = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
({ modulesPath, ... }: {
|
||||
imports = [
|
||||
(modulesPath + "/installer/netboot/netboot-minimal.nix")
|
||||
];
|
||||
})
|
||||
{ networking.hostName = "nixos-minimal"; }
|
||||
];
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
|
||||
@@ -153,6 +196,15 @@
|
||||
{ name = "initrd"; path = netbootSystem.config.system.build.netbootRamdisk; }
|
||||
{ name = "kernel"; path = netbootSystem.config.system.build.kernel; }
|
||||
];
|
||||
|
||||
# Vanilla NixOS minimal netboot bundle — see netbootMinimalSystem
|
||||
# above. Staged onto the pxe-boot host alongside packages.pxe by
|
||||
# modules/pxe-boot/stage-installer-artifacts.nix.
|
||||
pxe-minimal = pkgs.linkFarm "pxe-minimal" [
|
||||
{ name = "netboot.ipxe"; path = netbootMinimalSystem.config.system.build.netbootIpxeScript; }
|
||||
{ name = "initrd"; path = netbootMinimalSystem.config.system.build.netbootRamdisk; }
|
||||
{ name = "kernel"; path = netbootMinimalSystem.config.system.build.kernel; }
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,11 +19,15 @@
|
||||
nextcloud-client
|
||||
# vscode
|
||||
chromium
|
||||
claude-code
|
||||
fish
|
||||
sops
|
||||
];
|
||||
|
||||
# Optional: set environment vars
|
||||
sessionVariables = {
|
||||
EDITOR = "vim";
|
||||
SOPS_AGE_KEY_FILE = "~/.config/sops/age/keys.txt";
|
||||
};
|
||||
|
||||
file = {
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
_:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../../modules/networking/wifi.nix
|
||||
];
|
||||
|
||||
networking.hostName = "nixos";
|
||||
|
||||
# Only needed now that baremetal-gui exists (ZFS root) -- harmless on the
|
||||
# ext4-rooted linode/proxmox/lxc-gui variants, so set unconditionally
|
||||
# rather than only on the baremetal platform.
|
||||
networking.hostId = "de6a9ffc";
|
||||
|
||||
# Preserved from the pre-refactor `nixos` target — stateVersion must never
|
||||
# be bumped on an already-installed machine.
|
||||
system.stateVersion = "25.05";
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
_:
|
||||
|
||||
{
|
||||
networking.hostName = "exit-node";
|
||||
|
||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
||||
# for pool-import safety, and this host does neither.
|
||||
|
||||
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
||||
# flake's current nixpkgs release rather than being pinned to an older one.
|
||||
system.stateVersion = "26.05";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
_:
|
||||
|
||||
{
|
||||
networking.hostName = "tailscale-router";
|
||||
|
||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
||||
# for pool-import safety, and this host does neither.
|
||||
|
||||
system.stateVersion = "26.05";
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
_:
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(import ../../modules/beszel/host-token.nix {
|
||||
name = "tor-relay";
|
||||
sopsFile = ../../secrets/tor-relay.yaml;
|
||||
})
|
||||
];
|
||||
|
||||
networking.hostName = "tor-relay";
|
||||
|
||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
||||
# for pool-import safety, and this host does neither.
|
||||
|
||||
services.beszel.agent.environment = {
|
||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||
};
|
||||
|
||||
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
||||
# flake's current nixpkgs release rather than being pinned to an older one.
|
||||
system.stateVersion = "26.05";
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
];
|
||||
|
||||
boot.loader.grub.useOSProber = true;
|
||||
|
||||
programs.direnv.enable = true;
|
||||
services = {
|
||||
xserver = {
|
||||
enable = true;
|
||||
|
||||
@@ -68,15 +68,19 @@ let
|
||||
set base ${pxeBaseUrl}
|
||||
|
||||
menu PXE Boot Menu
|
||||
item nixos NixOS Installer
|
||||
item auto-installer NixOS Auto-Installer
|
||||
item nixos-minimal NixOS Minimal
|
||||
item rescue Rescue Environment
|
||||
item shell iPXE Shell
|
||||
item reboot Reboot
|
||||
|
||||
choose target && goto ''${target}
|
||||
|
||||
:nixos
|
||||
chain ''${base}/nixos/netboot.ipxe
|
||||
:auto-installer
|
||||
chain ''${base}/auto-installer/netboot.ipxe
|
||||
|
||||
:nixos-minimal
|
||||
chain ''${base}/nixos-minimal/netboot.ipxe
|
||||
|
||||
:rescue
|
||||
chain ''${base}/systemrescue.ipxe
|
||||
@@ -129,7 +133,8 @@ in
|
||||
"d ${pxeRoot} 0755 root root -"
|
||||
"d ${httpRoot} 0755 root root -"
|
||||
"d ${httpRoot}/images 0755 root root -"
|
||||
"d ${httpRoot}/nixos 0755 root root -"
|
||||
"d ${httpRoot}/auto-installer 0755 root root -"
|
||||
"d ${httpRoot}/nixos-minimal 0755 root root -"
|
||||
"d ${httpRoot}/systemrescue 0755 root root -"
|
||||
"d ${httpRoot}/ubuntu 0755 root root -"
|
||||
"d ${httpRoot}/rescue 0755 root root -"
|
||||
|
||||
@@ -1,12 +1,74 @@
|
||||
{ vars, lib, ... }:
|
||||
{ vars, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
poolName = lib.removePrefix "/" vars.storageRoot;
|
||||
|
||||
# For each NFS share subpath, generate every ancestor path so ZFS datasets
|
||||
# are created parent-first. e.g. "docker/config" → ["docker" "docker/config"]
|
||||
ancestors = path:
|
||||
let parts = lib.splitString "/" path;
|
||||
in lib.imap1 (i: _: lib.concatStringsSep "/" (lib.take i parts)) parts;
|
||||
|
||||
poolDatasets = lib.unique (
|
||||
lib.concatMap (share: ancestors share.subpath) (lib.attrValues vars.nfsShares)
|
||||
);
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../beszel/enable-agent.nix
|
||||
../services/zfs/enable-service.nix
|
||||
];
|
||||
|
||||
boot.zfs.extraPools = [ (lib.removePrefix "/" vars.storageRoot) ];
|
||||
boot.zfs.extraPools = [ poolName ];
|
||||
|
||||
# On a fresh image deploy the data disk (scsi1) starts blank — no pool
|
||||
# exists yet, so zfs-import-tank.service would spin for 60 s and fail.
|
||||
# This service runs first: if the pool is already present it exits instantly;
|
||||
# otherwise it creates it (with all required datasets) so the standard
|
||||
# import service finds it ready on the very first boot.
|
||||
systemd.services."zfs-init-${poolName}" = {
|
||||
description = "Initialize '${poolName}' ZFS pool on first boot if not present";
|
||||
wantedBy = [ "zfs-import-${poolName}.service" ];
|
||||
before = [ "zfs-import-${poolName}.service" ];
|
||||
after = [ "systemd-udev-settle.service" ];
|
||||
unitConfig.DefaultDependencies = false;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.zfs_unstable ];
|
||||
script = ''
|
||||
# Already imported — nothing to do.
|
||||
if zpool list "${poolName}" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Pool exists on a device but not yet imported — let the standard
|
||||
# zfs-import-${poolName}.service handle it normally.
|
||||
if zpool import -d /dev/disk/by-id -N "${poolName}" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# No pool found at all. Create it on the Proxmox data disk (scsi1),
|
||||
# which appears as /dev/disk/by-id/scsi-*drive-scsi1 inside the VM.
|
||||
DATA_DISK=""
|
||||
for candidate in /dev/disk/by-id/scsi-*drive-scsi1; do
|
||||
[[ "$candidate" == *-part* ]] && continue
|
||||
[ -b "$candidate" ] && DATA_DISK="$candidate" && break
|
||||
done
|
||||
|
||||
if [ -z "$DATA_DISK" ]; then
|
||||
echo "zfs-init-${poolName}: no data disk found (expected /dev/disk/by-id/scsi-*drive-scsi1)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "zfs-init-${poolName}: creating pool on $DATA_DISK"
|
||||
zpool create -f "${poolName}" "$DATA_DISK"
|
||||
${lib.concatMapStrings (ds: ''
|
||||
zfs create "${poolName}/${ds}"
|
||||
'') poolDatasets}
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.nfs-server = {
|
||||
after = [ "zfs-mount.service" ];
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../tailscale/exit-node.nix
|
||||
];
|
||||
|
||||
# "server", not "both": this build type only ever advertises itself as an
|
||||
# exit node (see ../tailscale/exit-node.nix) -- it doesn't advertise LAN
|
||||
# subnet routes, so it doesn't need the "client"-side loose reverse-path
|
||||
# filtering that "both" would also turn on. Deliberately left unbundled
|
||||
# from LAN-subnet-route advertisement so this build type stays valid on
|
||||
# every platform, including linode (a remote VPS with no network path to
|
||||
# the home LAN at all).
|
||||
services.tailscale.useRoutingFeatures = "server";
|
||||
|
||||
# Forwarded exit-node traffic arrives on tailscale0 already
|
||||
# tailscale-authenticated -- the firewall's normal per-port allow-list
|
||||
# would otherwise drop it. Standard NixOS/Tailscale exit-node guidance.
|
||||
networking.firewall.trustedInterfaces = [ "tailscale0" ];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../tailscale/subnet-router.nix
|
||||
];
|
||||
|
||||
# "server", not "both": this build type advertises LAN subnet routes but
|
||||
# doesn't use another tailscale exit node itself, so it doesn't need the
|
||||
# "client"-side loose reverse-path filtering that "both" would also enable.
|
||||
# Deliberately kept explicit here (not just relying on subnet-router.nix's
|
||||
# own setting) so the intent is clear at the build-type level.
|
||||
services.tailscale.useRoutingFeatures = "server";
|
||||
|
||||
# Forwarded subnet-router traffic arrives on tailscale0 already
|
||||
# tailscale-authenticated -- the firewall's normal per-port allow-list
|
||||
# would otherwise drop it. Standard NixOS/Tailscale subnet-router guidance.
|
||||
networking.firewall.trustedInterfaces = [ "tailscale0" ];
|
||||
}
|
||||
@@ -3,5 +3,6 @@
|
||||
{
|
||||
imports = [
|
||||
../tor/enable-relay.nix
|
||||
../beszel/enable-agent.nix
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
btop
|
||||
git
|
||||
gcr
|
||||
jq
|
||||
];
|
||||
|
||||
# Secrets shared by every host, decrypted at activation via each host's
|
||||
@@ -76,6 +77,7 @@
|
||||
openssh.authorizedKeys.keys = [
|
||||
vars.adminSshKey
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGygkCljN6uKpdJbHTOQtn8ZnH+wKXDLAwrDFbLrE/65 nixos@nixos"
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
{ vars, ... }:
|
||||
|
||||
{
|
||||
# ZFS RAID0 (striped, no redundancy) root pool for the bare-metal gui
|
||||
# host — two disks, each contributing its own top-level vdev. disko's
|
||||
# zpool `mode` defaults to "" (plain stripe) when left unset, which is
|
||||
# what gives RAID0 semantics here rather than mirror/raidz.
|
||||
#
|
||||
# Device paths are placeholders until the real hardware profile lands —
|
||||
# fill in vars.guiRootDisk1/guiRootDisk2 (stable /dev/disk/by-id/...
|
||||
# paths, not /dev/sdX) before running disko against real hardware. Swap
|
||||
# is deliberately left out for now — sizing that sensibly needs the
|
||||
# box's actual RAM size, which comes with the hardware profile too.
|
||||
#
|
||||
# Not yet imported anywhere: this awaits the new bare-metal platform
|
||||
# module (alongside modules/boot/efi.nix for systemd-boot, matching
|
||||
# modules/platforms/proxmox.nix's pattern) once the hardware config is
|
||||
# in hand.
|
||||
disko.devices = {
|
||||
disk = {
|
||||
disk1 = {
|
||||
type = "disk";
|
||||
device = vars.guiRootDisk1;
|
||||
|
||||
content = {
|
||||
type = "gpt";
|
||||
|
||||
partitions = {
|
||||
esp = {
|
||||
priority = 1;
|
||||
name = "ESP";
|
||||
size = "512M";
|
||||
type = "EF00";
|
||||
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "umask=0077" ];
|
||||
};
|
||||
};
|
||||
|
||||
zfs = {
|
||||
size = "100%";
|
||||
|
||||
content = {
|
||||
type = "zfs";
|
||||
pool = "rpool";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
disk2 = {
|
||||
type = "disk";
|
||||
device = vars.guiRootDisk2;
|
||||
|
||||
content = {
|
||||
type = "gpt";
|
||||
|
||||
partitions = {
|
||||
zfs = {
|
||||
size = "100%";
|
||||
|
||||
content = {
|
||||
type = "zfs";
|
||||
pool = "rpool";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
zpool.rpool = {
|
||||
type = "zpool";
|
||||
|
||||
rootFsOptions = {
|
||||
compression = "zstd";
|
||||
"com.sun:auto-snapshot" = "false";
|
||||
};
|
||||
mountpoint = "/";
|
||||
options.ashift = "12";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -9,11 +9,25 @@ let
|
||||
# (the VM platforms rely on automount itself to get that same
|
||||
# non-blocking behavior, so they don't need `nofail` too).
|
||||
automountOpts = if config.boot.isContainer then [ "nofail" ] else [ "x-systemd.automount" ];
|
||||
|
||||
# A bare hostname here never resolves reliably: systemd-resolved only
|
||||
# ever tries LLMNR for single-label names (never DNS, regardless of any
|
||||
# configured search domain), and a *global* search domain (the first fix
|
||||
# attempted here) backfires worse -- confirmed live on lxc-docker, adding
|
||||
# `networking.search` made systemd-resolved prioritize its domain-matched
|
||||
# but server-less global scope over eth0's correctly-configured one for
|
||||
# every "*.sweet.home" query, silently sending them to public fallback
|
||||
# DNS instead. `resolvectl query --interface=eth0 server.sweet.home`
|
||||
# resolved fine throughout, proving the LAN DNS server was never the
|
||||
# problem -- only the ambient, unqualified device string was. Using the
|
||||
# FQDN directly sidesteps all of that, matching the pattern
|
||||
# ../raspi/mount-data.nix already uses for the same reason.
|
||||
nfsServer = "${vars.nfsServerHost}.${vars.homeDomain}";
|
||||
in
|
||||
{
|
||||
fileSystems = {
|
||||
${vars.nfsShares.dockerConfig.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
@@ -24,7 +38,7 @@ in
|
||||
};
|
||||
|
||||
${vars.nfsShares.dockerDatabases.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
@@ -35,7 +49,7 @@ in
|
||||
};
|
||||
|
||||
${vars.nfsShares.dockerVolumes.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
@@ -46,7 +60,7 @@ in
|
||||
};
|
||||
|
||||
${vars.nfsShares.nextcloudData.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
@@ -57,7 +71,7 @@ in
|
||||
};
|
||||
|
||||
${vars.nfsShares.raspiVolumes.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Adapted from the output of `nixos-generate-config`, run from a live GUI
|
||||
# ISO boot on the actual gui-host hardware (AMD CPU). fileSystems and
|
||||
# swapDevices are deliberately omitted -- the live ISO had no formatted
|
||||
# disks to detect, and disko (modules/disko/baremetal.nix) generates both
|
||||
# from the declarative zpool layout anyway.
|
||||
{ config, lib, pkgs, modulesPath, ... }:
|
||||
|
||||
{
|
||||
imports =
|
||||
[
|
||||
(modulesPath + "/installer/scan/not-detected.nix")
|
||||
];
|
||||
|
||||
boot = {
|
||||
initrd.availableKernelModules = [ "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ];
|
||||
initrd.kernelModules = [ ];
|
||||
kernelModules = [ "kvm-amd" ];
|
||||
extraModulePackages = [ ];
|
||||
};
|
||||
|
||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||
}
|
||||
+14
-133
@@ -45,142 +45,23 @@
|
||||
disko
|
||||
];
|
||||
|
||||
# Write auto-install script to /root
|
||||
etc."auto-install.sh" = {
|
||||
text = ''
|
||||
#!/run/current-system/sw/bin/bash
|
||||
set -eux
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export FLAKE_BASE_URL="git+https://${vars.lanDomain}/beatzaplenty/nixos.git"
|
||||
|
||||
echo "Fetching available NixOS hosts from flake..."
|
||||
# Two categories deliberately excluded from the menu:
|
||||
# lxc-* — these build a config.system.build.tarball meant for
|
||||
# `pct restore` on Proxmox directly, not an install.
|
||||
# Running nixos-install against one here would
|
||||
# bind-mount / onto /mnt and then refuse to touch the
|
||||
# filesystem it's currently running on — see
|
||||
# docs/auto-installer.md.
|
||||
# installer — this *is* the installer image's own flake target,
|
||||
# not a deployable host; "installing" it means
|
||||
# nixos-install-ing a copy of the installer into
|
||||
# itself.
|
||||
mapfile -t options < <(
|
||||
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
||||
"''${FLAKE_BASE_URL}#nixosConfigurations" \
|
||||
--apply builtins.attrNames \
|
||||
| jq -r '.[]
|
||||
| select(startswith("lxc-") | not)
|
||||
| select(. != "installer")'
|
||||
)
|
||||
|
||||
if [[ ''${#options[@]} -eq 0 ]]; then
|
||||
echo "ERROR: No NixOS hosts found in ''${FLAKE_BASE_URL}#nixosConfigurations" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Note: lxc-* targets aren't installed this way — build them with"
|
||||
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
|
||||
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
|
||||
|
||||
echo "Choose the flake profile to install:"
|
||||
select choice in "''${options[@]}"; do
|
||||
if [[ -n "$choice" ]]; then
|
||||
echo "You selected: $choice"
|
||||
break
|
||||
else
|
||||
echo "Invalid selection. Try again."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Starting install with flake: ''${FLAKE_BASE_URL}#''${choice}"
|
||||
|
||||
# Optional: confirm before proceeding
|
||||
read -rp "Proceed with installation? (y/N): " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A nix-cache host is *the* substituter/remote-builder for every other
|
||||
# host once installed (its own config explicitly excludes itself from
|
||||
# using either — see buildType != "nix-cache" in the nixos flake.nix).
|
||||
# Installing one shouldn't depend on a nix-cache substituter either,
|
||||
# for the same reason — plus in practice "nix-cache" only resolves over
|
||||
# Tailscale, which a fresh installer environment was never connected to
|
||||
# anyway, so it's dead weight even for non-nix-cache installs until
|
||||
# that's sorted out. Override it away here specifically for nix-cache
|
||||
# targets to keep install-time behaviour consistent with run-time.
|
||||
nix_extra_opts=()
|
||||
if [[ "''${choice}" == *-nix-cache ]]; then
|
||||
echo "Installing a nix-cache host — skipping the nix-cache substituter."
|
||||
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
||||
fi
|
||||
|
||||
# Every host reachable through this menu has a Disko config (lxc-*
|
||||
# is filtered out above, and is the only category that doesn't —
|
||||
# see docs/auto-installer.md), so this can run unconditionally: no
|
||||
# need to probe the flake first and branch on whether Disko applies.
|
||||
disko --mode destroy,format,mount \
|
||||
--flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes-wipe-all-disks
|
||||
|
||||
# sops-nix derives this host's decryption key from its own SSH host key
|
||||
# at *activation* time, which runs before systemd would otherwise
|
||||
# generate one on first boot. Without pre-seeding it here, secrets
|
||||
# (including the login password) fail to decrypt on first boot.
|
||||
# Generate the key with scripts/secrets/prepare-host-key.sh first.
|
||||
#
|
||||
# Two places a key can come from, checked in order:
|
||||
# /etc/host-keys — baked into this image at build time (see
|
||||
# modules/installer/host-keys.nix; only present
|
||||
# if built with NIXOS_HOST_KEYS_DIR set)
|
||||
# /root/host-keys — scp'd in manually after boot (older fallback,
|
||||
# still supported for images built without keys)
|
||||
mkdir -p /root/host-keys
|
||||
if [[ -f "/etc/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
||||
echo "Found baked-in SSH host key for ''${choice}, installing to target..."
|
||||
install -D -m 0600 "/etc/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||
install -D -m 0644 "/etc/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||
elif [[ -f "/root/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
||||
echo "Found pre-seeded SSH host key for ''${choice}, installing to target..."
|
||||
install -D -m 0600 "/root/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||
install -D -m 0644 "/root/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||
else
|
||||
echo "WARNING: no SSH host key found for ''${choice} (checked /etc/host-keys and /root/host-keys)"
|
||||
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
|
||||
echo "Run scripts/secrets/prepare-host-key.sh for host ''${choice} on your admin workstation first,"
|
||||
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, or scp the result to"
|
||||
echo "/root/host-keys/ on this machine."
|
||||
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
|
||||
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p /mnt/install-tmp
|
||||
export TMPDIR=/mnt/install-tmp
|
||||
|
||||
nixos-install \
|
||||
--flake "''${FLAKE_BASE_URL}#''${choice}" \
|
||||
"''${nix_extra_opts[@]}" \
|
||||
--no-root-password
|
||||
|
||||
|
||||
rm -rf /mnt/install-tmp
|
||||
# Redundant copy of the host's private key — the real one is now at
|
||||
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
|
||||
# up on its own since it was written imperatively, not declaratively.
|
||||
rm -rf /root/host-keys
|
||||
sleep 10
|
||||
reboot
|
||||
'';
|
||||
# Auto-install script, kept as a real, version-controlled shell file at
|
||||
# scripts/installer/auto-install.sh rather than an inline Nix string.
|
||||
# It sources scripts/env.sh itself (for LAN_DOMAIN, same as every other
|
||||
# script in this repo) rather than relying on Nix-level templating, so
|
||||
# it behaves identically whether it's run straight from a git checkout
|
||||
# or from here -- baking scripts/env.sh in alongside it at a matching
|
||||
# relative path (installer/auto-install.sh -> ../env.sh) is what makes
|
||||
# that resolve correctly in both places.
|
||||
etc = {
|
||||
"nixos-installer/env.sh".source = ../../scripts/env.sh;
|
||||
|
||||
"nixos-installer/installer/auto-install.sh" = {
|
||||
source = ../../scripts/installer/auto-install.sh;
|
||||
mode = "0755";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
programs.git.enable = true;
|
||||
|
||||
@@ -192,7 +73,7 @@
|
||||
# file-copying/chown.
|
||||
programs.bash.loginShellInit = ''
|
||||
if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then
|
||||
sudo /etc/auto-install.sh
|
||||
sudo /etc/nixos-installer/installer/auto-install.sh
|
||||
touch "$HOME/.auto_install_ran"
|
||||
fi
|
||||
'';
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{ config, lib, vars, ... }:
|
||||
|
||||
{
|
||||
# Prestages a NetworkManager connection profile for vars.wifiSsid so the
|
||||
# host associates on first boot with no manual nmtui/nmcli step. Guarded
|
||||
# on a non-empty SSID so leaving the placeholder blank in variables.nix
|
||||
# is a no-op rather than an empty, broken profile — fill it in once the
|
||||
# network is known.
|
||||
#
|
||||
# The password itself lives in secrets/gui.yaml, not variables.nix --
|
||||
# NetworkManager's ensureProfiles renders `psk = "$WIFI_PASSWORD"`
|
||||
# literally into the store (see nixpkgs' own ensureProfiles example,
|
||||
# which does the same for exactly this reason) and its systemd service
|
||||
# envsubst-expands it from environmentFiles at activation time, so the
|
||||
# real value only ever touches /run (root-only, UMask 0177), never the
|
||||
# Nix store.
|
||||
sops.secrets."wifi-password" = lib.mkIf (vars.wifiSsid != "") {
|
||||
sopsFile = ../../secrets/gui.yaml;
|
||||
};
|
||||
|
||||
sops.templates."wifi-password.env" = lib.mkIf (vars.wifiSsid != "") {
|
||||
content = "WIFI_PASSWORD=${config.sops.placeholder."wifi-password"}";
|
||||
};
|
||||
|
||||
networking.networkmanager.ensureProfiles = lib.mkIf (vars.wifiSsid != "") {
|
||||
environmentFiles = [ config.sops.templates."wifi-password.env".path ];
|
||||
|
||||
profiles.${vars.wifiSsid} = {
|
||||
connection = {
|
||||
id = vars.wifiSsid;
|
||||
type = "wifi";
|
||||
};
|
||||
wifi = {
|
||||
mode = "infrastructure";
|
||||
ssid = vars.wifiSsid;
|
||||
};
|
||||
wifi-security = {
|
||||
key-mgmt = "wpa-psk";
|
||||
psk = "$WIFI_PASSWORD";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
{
|
||||
nix.settings = {
|
||||
substituters = [
|
||||
"http://${vars.nixCacheHost}"
|
||||
"http://${vars.nixCacheHost}.${vars.homeDomain}"
|
||||
"https://cache.nixos.org/"
|
||||
];
|
||||
trusted-public-keys = [
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
{ pkgs, vars, ... }:
|
||||
|
||||
{
|
||||
# Install the remote builder key on each client host (do not commit private keys):
|
||||
# sudo install -d -m 0700 /root/.ssh
|
||||
# sudo install -m 0600 ./nixremote /root/.ssh/nixremote
|
||||
# sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
||||
# Authenticate as nixremote using the client host's own default root SSH
|
||||
# identity (/root/.ssh/id_ed25519) rather than a separately-named key --
|
||||
# matches vars.remoteBuilderAuthorizedKeys, which already authorizes
|
||||
# each host's own default key (one entry per host, not a shared
|
||||
# dedicated keypair). If this host doesn't have one yet:
|
||||
# sudo -u root ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
|
||||
# # then add its .pub to vars.remoteBuilderAuthorizedKeys and rebuild nix-cache
|
||||
# sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache.sweet.home nix-store --version
|
||||
# Trust nix-cache's SSH host key declaratively so the nix-daemon (root)
|
||||
# can connect the first time without a manual ssh-keyscan/known_hosts
|
||||
# step on every new client.
|
||||
programs.ssh.knownHosts.${vars.nixCacheHost} = {
|
||||
hostNames = [ vars.nixCacheHost ];
|
||||
programs.ssh.knownHosts."${vars.nixCacheHost}.${vars.homeDomain}" = {
|
||||
hostNames = [ "${vars.nixCacheHost}.${vars.homeDomain}" ];
|
||||
publicKey = vars.nixCacheHostKey;
|
||||
};
|
||||
|
||||
@@ -18,9 +22,9 @@
|
||||
|
||||
buildMachines = [
|
||||
{
|
||||
hostName = vars.nixCacheHost;
|
||||
hostName = "${vars.nixCacheHost}.${vars.homeDomain}";
|
||||
sshUser = vars.remoteBuilderUser;
|
||||
sshKey = "/root/.ssh/${vars.remoteBuilderUser}";
|
||||
sshKey = "/root/.ssh/id_ed25519";
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
maxJobs = 4;
|
||||
speedFactor = 2;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
virtualHosts.${vars.nixCacheHost} = {
|
||||
virtualHosts."${vars.nixCacheHost}.${vars.homeDomain}" = {
|
||||
locations."/" = {
|
||||
proxyPass = "http://${config.services.nix-serve.bindAddress}:${toString config.services.nix-serve.port}";
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../hardware-configuration/baremetal.nix
|
||||
../boot/efi.nix
|
||||
../disko/baremetal.nix
|
||||
../services/zfs/enable-service.nix
|
||||
];
|
||||
|
||||
# Needed for real wifi/bluetooth/GPU firmware blobs and CPU microcode
|
||||
# updates (hardware-configuration/baremetal.nix's amd.updateMicrocode
|
||||
# keys off this) -- irrelevant on the linode/proxmox/lxc platforms,
|
||||
# which are all VMs with no real hardware to load firmware for.
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
|
||||
# AMD GPU: the amdgpu kernel driver autoloads from the PCI ID with no
|
||||
# extra boot.kernelModules entry needed; this is the userspace half --
|
||||
# the dedicated Xorg driver (not just the generic modesetting fallback)
|
||||
# plus Mesa OpenGL/Vulkan (amdgpu/RADV), same firmware blobs as above.
|
||||
# 32-bit support is for compatibility with 32-bit apps/games.
|
||||
services.xserver.videoDrivers = [ "amdgpu" ];
|
||||
|
||||
hardware.graphics = {
|
||||
enable = true;
|
||||
enable32Bit = true;
|
||||
};
|
||||
|
||||
# The systemd-based initrd (default here since this host has a ZFS root --
|
||||
# see modules/disko/baremetal.nix) locks the root account by default, so
|
||||
# sulogin refuses to hand over a shell if something in the initrd (e.g.
|
||||
# the ZFS pool import) fails and it drops to emergency mode -- confirmed
|
||||
# live: it just loops re-entering the target instead of prompting. This
|
||||
# only affects the pre-switch-root initrd shell, not the installed
|
||||
# system's own login, and is worth the tradeoff on a box already reachable
|
||||
# at the physical console.
|
||||
boot.initrd.systemd.emergencyAccess = true;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{ lib, modulesPath, flakeTarget, ... }:
|
||||
{ config, lib, modulesPath, flakeTarget, ... }:
|
||||
|
||||
let
|
||||
# Bakes this exact flake target's pre-generated SSH host key straight
|
||||
@@ -58,8 +58,28 @@ in
|
||||
# host.nix declares each host's real hostname (networking.hostName);
|
||||
# keep that instead of letting Proxmox's ambient container config win.
|
||||
manageHostName = true;
|
||||
# Unprivileged matches how these containers are actually created.
|
||||
privileged = false;
|
||||
# Unprivileged by default -- matches how these containers are actually
|
||||
# created (scripts/proxmox/create-proxmox-resource.sh reads this value
|
||||
# back to decide `pct create`'s --unprivileged flag, so the two stay
|
||||
# in sync).
|
||||
#
|
||||
# lxc-docker is the one exception: the kernel's NFS client doesn't set
|
||||
# FS_USERNS_MOUNT, so mounting NFS from inside *any* non-init user
|
||||
# namespace -- which is exactly what an unprivileged container's
|
||||
# UID-mapped root runs in -- is rejected at the VFS layer with EPERM,
|
||||
# no matter what Proxmox's own `mount=nfs;nfs4` container feature
|
||||
# allows at the AppArmor layer (confirmed live: TCP to the NFS server
|
||||
# succeeds, the server's export table matches the container's IP, and
|
||||
# `mount.nfs: Operation not permitted` still fires immediately with no
|
||||
# corresponding denial anywhere in the server's logs -- a kernel-level
|
||||
# rejection, not a network or export-permission one). Keying off
|
||||
# hostName rather than something docker-build-type-specific because
|
||||
# modules/build-types/docker.nix is also composed for linode-docker/
|
||||
# proxmox-docker, which don't import proxmox-lxc.nix at all --setting
|
||||
# this option there would break their eval with "option does not
|
||||
# exist" regardless of any mkIf guard, since mkIf only makes a value
|
||||
# conditional, not whether the option needs to exist somewhere.
|
||||
privileged = config.networking.hostName == "docker";
|
||||
};
|
||||
|
||||
boot.loader = {
|
||||
@@ -86,6 +106,40 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# NixOS's etc activation removes any /etc file that was in the previous
|
||||
# generation's environment.etc but is absent from the current one — even
|
||||
# real (non-symlink) copies. On every routine nixos-rebuild switch/test that
|
||||
# lacks NIXOS_HOST_KEYS_DIR the key is absent from environment.etc, so it
|
||||
# gets removed as "obsolete". sops-nix derives its age decryption key from
|
||||
# /etc/ssh/ssh_host_ed25519_key; deletion cascades into every sops secret
|
||||
# failing with "Error getting data key: 0 successful groups required, got 0".
|
||||
#
|
||||
# Fix: two activation scripts that bracket the etc step.
|
||||
# preserveSshHostKey — no deps, runs before etc — saves the live key to
|
||||
# /run (tmpfs) before etc can delete it.
|
||||
# restoreSshHostKey — deps=[etc], runs after etc — reinstalls the key via
|
||||
# `install` (atomic, sets mode) if etc removed it.
|
||||
# The resulting file is not registered in environment.etc
|
||||
# for either the previous or current generation, so
|
||||
# subsequent rebuilds leave it alone permanently.
|
||||
system.activationScripts.preserveSshHostKey = ''
|
||||
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
|
||||
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
|
||||
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
|
||||
fi
|
||||
'';
|
||||
|
||||
system.activationScripts.restoreSshHostKey = {
|
||||
deps = [ "etc" ];
|
||||
text = ''
|
||||
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
|
||||
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
|
||||
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
|
||||
fi
|
||||
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
|
||||
'';
|
||||
};
|
||||
|
||||
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
|
||||
# store DB via a systemd service (register-nix-paths) -- it never runs
|
||||
# an activation script at all. Confirmed live this means neither
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
{ netbootSystem, ... }:
|
||||
{ netbootSystem, netbootMinimalSystem, ... }:
|
||||
|
||||
let
|
||||
# config.system.build.kernel and .netbootRamdisk are directories, not the
|
||||
# files themselves — nixpkgs' own system.build.kexecTree does the same
|
||||
# ${...}/<file> dereference for the same reason.
|
||||
inherit (netbootSystem.config.system.boot.loader) kernelFile;
|
||||
mkStageRules = { dirName, system }:
|
||||
let
|
||||
inherit (system.config.system.boot.loader) kernelFile;
|
||||
dir = "/srv/pxe/http/${dirName}";
|
||||
in
|
||||
{
|
||||
# Builds this flake's own installer netboot image (the same one
|
||||
# `nix build .#pxe` produces) and stages it where menu.ipxe's :nixos
|
||||
# entry expects it, so the pxe-boot host is self-contained — no manual
|
||||
# operator step to populate /srv/pxe/http/nixos after deploy.
|
||||
systemd.tmpfiles.rules = [
|
||||
[
|
||||
# Declared here too (not just in build-types/pxe-boot.nix) so this
|
||||
# module's C+ rules don't depend on cross-module list-merge ordering —
|
||||
# tmpfiles' C type needs the target directory to already exist.
|
||||
"d /srv/pxe/http/nixos 0755 root root -"
|
||||
"C+ /srv/pxe/http/nixos/${kernelFile} 0644 root root - ${netbootSystem.config.system.build.kernel}/${kernelFile}"
|
||||
"C+ /srv/pxe/http/nixos/initrd 0644 root root - ${netbootSystem.config.system.build.netbootRamdisk}/initrd"
|
||||
"C+ /srv/pxe/http/nixos/netboot.ipxe 0644 root root - ${netbootSystem.config.system.build.netbootIpxeScript}/netboot.ipxe"
|
||||
"d ${dir} 0755 root root -"
|
||||
"C+ ${dir}/${kernelFile} 0644 root root - ${system.config.system.build.kernel}/${kernelFile}"
|
||||
"C+ ${dir}/initrd 0644 root root - ${system.config.system.build.netbootRamdisk}/initrd"
|
||||
"C+ ${dir}/netboot.ipxe 0644 root root - ${system.config.system.build.netbootIpxeScript}/netboot.ipxe"
|
||||
];
|
||||
in
|
||||
{
|
||||
# Builds this flake's own installer netboot image (the same one
|
||||
# `nix build .#pxe` produces) plus the vanilla NixOS minimal netboot image
|
||||
# (`nix build .#pxe-minimal`), and stages both where menu.ipxe's
|
||||
# :auto-installer / :nixos-minimal entries expect them, so the pxe-boot
|
||||
# host is self-contained — no manual operator step to populate
|
||||
# /srv/pxe/http after deploy.
|
||||
systemd.tmpfiles.rules =
|
||||
mkStageRules { dirName = "auto-installer"; system = netbootSystem; }
|
||||
++ mkStageRules { dirName = "nixos-minimal"; system = netbootMinimalSystem; };
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
_:
|
||||
|
||||
{
|
||||
imports = [ ./enable-service.nix ];
|
||||
|
||||
services.tailscale = {
|
||||
# Enables the sysctl forwarding settings exit nodes/subnet routers need;
|
||||
# without this, --advertise-exit-node has no effect.
|
||||
useRoutingFeatures = "server";
|
||||
|
||||
# Lets peers reach this node directly over the tailscale UDP port
|
||||
# instead of relaying through DERP.
|
||||
openFirewall = true;
|
||||
|
||||
# extraSetFlags (tailscale set, via the always-on tailscaled-set
|
||||
# service), not extraUpFlags -- extraUpFlags is only ever applied by
|
||||
# tailscaled-autoconnect, which itself only runs when
|
||||
# services.tailscale.authKeyFile is set (nothing in this repo sets one,
|
||||
# so tailscale up is a manual, one-time operator step on every host that
|
||||
# uses this service). extraSetFlags has no such gate, so
|
||||
# --advertise-exit-node self-reapplies on every boot once the operator
|
||||
# has authenticated the node once.
|
||||
extraSetFlags = [
|
||||
"--advertise-exit-node"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
_:
|
||||
|
||||
{
|
||||
imports = [ ./enable-service.nix ];
|
||||
|
||||
services.tailscale = {
|
||||
# Enables the sysctl forwarding settings subnet routers need;
|
||||
# without this, --advertise-routes has no effect.
|
||||
useRoutingFeatures = "server";
|
||||
|
||||
# Lets peers reach this node directly over the tailscale UDP port
|
||||
# instead of relaying through DERP.
|
||||
openFirewall = true;
|
||||
};
|
||||
}
|
||||
Binary file not shown.
@@ -129,5 +129,6 @@ echo "flake.lock still points at the old input revisions until refreshed. Either
|
||||
echo " nix flake update nixpkgs home-manager # just these two inputs"
|
||||
echo " nix flake update # everything — see docs/flake-lock-automation.md"
|
||||
echo
|
||||
echo "Then run 'bash scripts/codex-maintenance.sh dry-run' before committing —"
|
||||
echo "a channel bump can shift option defaults across every host."
|
||||
echo "Then run 'bash scripts/codex-maintenance.sh --full-check --dry-run' before"
|
||||
echo "committing — a channel bump can shift option defaults across every host,"
|
||||
echo "and only --dry-run actually builds anything to catch that."
|
||||
|
||||
+252
-34
@@ -1,4 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Validation entry point for CI and local/agent review.
|
||||
#
|
||||
# Default mode (what CI runs on every push/PR): fmt-check, statix, and eval
|
||||
# are scoped to files that actually changed against a base ref, plus
|
||||
# whichever hosts/packages those changes can affect. This exists because
|
||||
# the unscoped sweep below is slow enough to time out CI runners -- see
|
||||
# --full-check.
|
||||
#
|
||||
# --full-check: the historical full sweep (every host, every package,
|
||||
# fmt --check ./statix check . over the whole tree). Slow -- minutes, not
|
||||
# seconds. CI never passes this; run it locally before a release or after
|
||||
# touching modules/common/*, flake.nix, or variables.nix if you want extra
|
||||
# confidence beyond what the changed-files scope already covers for those
|
||||
# paths (see below).
|
||||
#
|
||||
# --dry-run: adds `nix build --dry-run --no-link` for whatever scope is
|
||||
# active (changed-files scope by default, full scope under --full-check).
|
||||
#
|
||||
# Per-host/per-package eval and dry-run build calls run concurrently (see
|
||||
# scripts/lib/nix-parallel.sh) since they're independent of each other.
|
||||
# Concurrency defaults to core count capped by available memory (~1GB/job)
|
||||
# rather than plain core count, since each concurrent `nix eval` evaluates a
|
||||
# whole NixOS system closure and can OOM a small/memory-constrained CI
|
||||
# runner otherwise; override via NIX_PARALLEL_JOBS if a runner has more (or
|
||||
# less) room than that estimate assumes.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -6,8 +31,42 @@ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${script_dir}/lib/nix-bootstrap.sh"
|
||||
# shellcheck source=lib/nix-eval.sh
|
||||
source "${script_dir}/lib/nix-eval.sh"
|
||||
# shellcheck source=lib/nix-parallel.sh
|
||||
source "${script_dir}/lib/nix-parallel.sh"
|
||||
|
||||
MODE="${1:-validate}"
|
||||
repo_root="$(cd "${script_dir}/.." && pwd)"
|
||||
cd "$repo_root"
|
||||
|
||||
full_check=false
|
||||
dry_run=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/codex-maintenance.sh [--full-check] [--dry-run]
|
||||
|
||||
--full-check Run the full sweep: fmt-check and statix over the whole
|
||||
repo, eval every host and package. Slow. Never run by CI.
|
||||
--dry-run Additionally run `nix build --dry-run --no-link` for
|
||||
whatever scope is active.
|
||||
|
||||
With neither flag (the CI default), fmt-check/statix/eval are scoped to
|
||||
files changed against a base ref (env MAINT_BASE_SHA, else the PR base,
|
||||
else HEAD^), plus the hosts/packages those changes can affect.
|
||||
EOF
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--full-check) full_check=true ;;
|
||||
--dry-run) dry_run=true ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*)
|
||||
echo "Unknown argument: $arg" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_nix_profile
|
||||
|
||||
@@ -16,12 +75,6 @@ if ! command -v nix >/dev/null 2>&1; then
|
||||
exit 127
|
||||
fi
|
||||
|
||||
hosts="$(list_flake_targets .)"
|
||||
|
||||
echo "Hosts:"
|
||||
echo "$hosts"
|
||||
|
||||
echo
|
||||
echo "Checking for obvious committed secrets..."
|
||||
if grep -RInE 'github_pat_|ghp_|access-tokens|hashedPassword[[:space:]]*=' \
|
||||
--exclude-dir=.git \
|
||||
@@ -33,70 +86,235 @@ else
|
||||
echo "No obvious token patterns found."
|
||||
fi
|
||||
|
||||
mapfile -t all_hosts < <(list_flake_targets .)
|
||||
mapfile -t all_packages < <(nix eval --json "${NIX_EVAL_FLAGS[@]}" .#packages.x86_64-linux --apply builtins.attrNames | jq -r '.[]')
|
||||
|
||||
# host_targets_for_dir <hosts-subdir-name>
|
||||
# Prints the nixosConfigurations target names whose hostPath is
|
||||
# ./hosts/<dir>/host.nix, derived straight from flake.nix's generatedTargets
|
||||
# (one mkTarget { ... } call per line) rather than a hand-maintained table,
|
||||
# so it can't drift the way a copied mapping would.
|
||||
host_targets_for_dir() {
|
||||
local dir="$1"
|
||||
grep -oE '^[[:space:]]*[A-Za-z0-9_-]+ = mkTarget \{[^}]*hostPath = \./hosts/'"${dir}"'/host\.nix;[^}]*\};' flake.nix \
|
||||
| sed -E 's/^[[:space:]]*([A-Za-z0-9_-]+) = mkTarget.*/\1/' \
|
||||
|| true
|
||||
}
|
||||
|
||||
declare -a changed_files=()
|
||||
scope_desc="full repo"
|
||||
|
||||
if ! $full_check; then
|
||||
resolve_base_ref() {
|
||||
if [[ -n "${MAINT_BASE_SHA:-}" ]] && git cat-file -e "${MAINT_BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
echo "$MAINT_BASE_SHA"
|
||||
return
|
||||
fi
|
||||
if git rev-parse --verify -q HEAD^ >/dev/null 2>&1; then
|
||||
echo "HEAD^"
|
||||
return
|
||||
fi
|
||||
git hash-object -t tree /dev/null
|
||||
}
|
||||
|
||||
base_ref="$(resolve_base_ref)"
|
||||
echo
|
||||
echo "Changed-files scope: diffing against ${base_ref}"
|
||||
mapfile -t changed_files < <(git diff --name-only --diff-filter=ACMR "$base_ref" -- . | sort -u)
|
||||
|
||||
if [[ ${#changed_files[@]} -eq 0 ]]; then
|
||||
echo "No changed files detected."
|
||||
else
|
||||
printf ' %s\n' "${changed_files[@]}"
|
||||
fi
|
||||
scope_desc="changed files only (base: ${base_ref})"
|
||||
fi
|
||||
|
||||
# Whole-tree fmt/lint always run under --full-check; otherwise scoped below.
|
||||
declare -a changed_nix_files=()
|
||||
for f in "${changed_files[@]:-}"; do
|
||||
[[ "$f" == *.nix && -f "$f" ]] && changed_nix_files+=("$f")
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Checking Nix formatting with nixpkgs-fmt..."
|
||||
if $full_check; then
|
||||
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
|
||||
elif [[ ${#changed_nix_files[@]} -gt 0 ]]; then
|
||||
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check "${changed_nix_files[@]}"
|
||||
else
|
||||
echo "No changed .nix files; skipping."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Running statix lint..."
|
||||
if $full_check; then
|
||||
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check .
|
||||
elif [[ ${#changed_nix_files[@]} -gt 0 ]]; then
|
||||
for f in "${changed_nix_files[@]}"; do
|
||||
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check "$f"
|
||||
done
|
||||
else
|
||||
echo "No changed .nix files; skipping."
|
||||
fi
|
||||
|
||||
# Figure out which hosts/packages this run needs to eval (and, under
|
||||
# --dry-run, build). full_check always means "everything"; otherwise a
|
||||
# change to flake.nix/flake.lock/variables.nix/modules/common/* (repo-wide
|
||||
# inputs) or to any other modules/*.nix outside platforms//build-types
|
||||
# (whose blast radius isn't safely inferable from the path alone -- see
|
||||
# CLAUDE.md's "Grep modules/build-types/*.nix for each build type's imports
|
||||
# list") also falls back to everything, on the same reasoning CLAUDE.md
|
||||
# already gives interactive sessions for when to run the full sweep.
|
||||
# Anything more targeted -- a host.nix, a platform module, a build-type
|
||||
# module -- narrows to just the hosts it can affect.
|
||||
declare -A affected_hosts=()
|
||||
eval_packages=false
|
||||
|
||||
if $full_check; then
|
||||
for h in "${all_hosts[@]}"; do affected_hosts[$h]=1; done
|
||||
eval_packages=true
|
||||
else
|
||||
full_fallback=false
|
||||
for f in "${changed_files[@]:-}"; do
|
||||
case "$f" in
|
||||
flake.nix|flake.lock|variables.nix|modules/common/*)
|
||||
full_fallback=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! $full_fallback; then
|
||||
for f in "${changed_files[@]:-}"; do
|
||||
case "$f" in
|
||||
hosts/*/*)
|
||||
hostdir="${f#hosts/}"
|
||||
hostdir="${hostdir%%/*}"
|
||||
while IFS= read -r t; do
|
||||
[[ -n "$t" ]] && affected_hosts[$t]=1
|
||||
done < <(host_targets_for_dir "$hostdir")
|
||||
;;
|
||||
modules/platforms/*.nix)
|
||||
platform="$(basename "$f" .nix)"
|
||||
for h in "${all_hosts[@]}"; do
|
||||
[[ "$h" == "${platform}-"* ]] && affected_hosts[$h]=1
|
||||
done
|
||||
;;
|
||||
modules/build-types/*.nix)
|
||||
buildtype="$(basename "$f" .nix)"
|
||||
for h in "${all_hosts[@]}"; do
|
||||
[[ "$h" == *"-${buildtype}" ]] && affected_hosts[$h]=1
|
||||
done
|
||||
;;
|
||||
modules/installer/*)
|
||||
# iso.nix (imported by both the "installer" nixosConfigurations
|
||||
# target and netbootSystem, which backs packages.pxe) pulls in
|
||||
# common.nix, so a common.nix change reaches all three.
|
||||
affected_hosts[installer]=1
|
||||
eval_packages=true
|
||||
;;
|
||||
modules/pxe-boot/*)
|
||||
# stage-installer-artifacts.nix is imported by
|
||||
# modules/build-types/pxe-boot.nix only -- same blast radius as a
|
||||
# build-types/*.nix change, not a packages one.
|
||||
for h in "${all_hosts[@]}"; do
|
||||
[[ "$h" == *"-pxe-boot" ]] && affected_hosts[$h]=1
|
||||
done
|
||||
;;
|
||||
modules/*)
|
||||
full_fallback=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
if $full_fallback; then
|
||||
echo
|
||||
echo "Changed files affect shared config; falling back to evaluating every host/package."
|
||||
for h in "${all_hosts[@]}"; do affected_hosts[$h]=1; done
|
||||
eval_packages=true
|
||||
fi
|
||||
fi
|
||||
|
||||
mapfile -t hosts < <(for h in "${!affected_hosts[@]}"; do echo "$h"; done | sort)
|
||||
|
||||
echo
|
||||
echo "Evaluating host toplevel derivations..."
|
||||
for host in $hosts; do
|
||||
echo "==> $host"
|
||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
||||
echo "Checking nix-cache host key for drift..."
|
||||
if bash "${script_dir}/secrets/sync-nix-cache-host-key.sh" --check; then
|
||||
:
|
||||
else
|
||||
drift_status=$?
|
||||
if [[ "$drift_status" -eq 2 ]]; then
|
||||
echo "nix-cache unreachable from here -- skipping host-key drift check."
|
||||
else
|
||||
echo "WARNING: nix-cache's host key has drifted from variables.nix (see above)." >&2
|
||||
echo " Run 'bash scripts/secrets/sync-nix-cache-host-key.sh' to fix." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if [[ ${#hosts[@]} -eq 0 ]]; then
|
||||
echo "No hosts affected by changed files; skipping host eval."
|
||||
else
|
||||
echo "Evaluating host toplevel derivations (${scope_desc}, up to ${NIX_PARALLEL_JOBS} at a time)..."
|
||||
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of
|
||||
# nixos-install (see docs/auto-installer.md); proxmox-* hosts can
|
||||
# alternatively be built as a standalone disk image (see
|
||||
# docs/proxmox-images.md). Both are otherwise-unvalidated buildable
|
||||
# surface, easy to silently break without this.
|
||||
declare -a host_eval_jobs=()
|
||||
for host in "${hosts[@]}"; do
|
||||
host_eval_jobs+=("${host}${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.toplevel.drvPath")
|
||||
case "$host" in
|
||||
lxc-*)
|
||||
echo "==> $host (tarball)"
|
||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
|
||||
host_eval_jobs+=("${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball.drvPath")
|
||||
;;
|
||||
proxmox-*)
|
||||
echo "==> $host (diskoImagesScript)"
|
||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
|
||||
host_eval_jobs+=("${host} (diskoImagesScript)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
run_nix_parallel host_eval_jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Evaluating buildable packages..."
|
||||
packages="$(nix eval --json "${NIX_EVAL_FLAGS[@]}" .#packages.x86_64-linux --apply builtins.attrNames | jq -r '.[]')"
|
||||
for pkg in $packages; do
|
||||
echo "==> packages.x86_64-linux.${pkg}"
|
||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
|
||||
if ! $eval_packages; then
|
||||
echo "No packages affected by changed files; skipping package eval."
|
||||
else
|
||||
echo "Evaluating buildable packages (up to ${NIX_PARALLEL_JOBS} at a time)..."
|
||||
declare -a package_eval_jobs=()
|
||||
for pkg in "${all_packages[@]}"; do
|
||||
package_eval_jobs+=("packages.x86_64-linux.${pkg}${NIX_PARALLEL_SEP}.#packages.x86_64-linux.${pkg}")
|
||||
done
|
||||
run_nix_parallel package_eval_jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "dry-run" ]]; then
|
||||
if $dry_run; then
|
||||
echo
|
||||
echo "Running dry-run builds for all hosts. This will not create result symlinks."
|
||||
for host in $hosts; do
|
||||
echo "==> Dry-run build: $host"
|
||||
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel"
|
||||
|
||||
echo "Running dry-run builds for the active scope (up to ${NIX_PARALLEL_JOBS} at a time). This will not create result symlinks."
|
||||
declare -a host_build_jobs=()
|
||||
for host in "${hosts[@]:-}"; do
|
||||
host_build_jobs+=("Dry-run build: ${host}${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.toplevel")
|
||||
case "$host" in
|
||||
lxc-*)
|
||||
echo "==> Dry-run build: $host (tarball)"
|
||||
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball"
|
||||
host_build_jobs+=("Dry-run build: ${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball")
|
||||
;;
|
||||
proxmox-*)
|
||||
echo "==> Dry-run build: $host (diskoImagesScript)"
|
||||
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
|
||||
host_build_jobs+=("Dry-run build: ${host} (diskoImagesScript)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.diskoImagesScript")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
run_nix_parallel host_build_jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
|
||||
|
||||
if $eval_packages; then
|
||||
echo
|
||||
echo "Running dry-run builds for all packages."
|
||||
for pkg in $packages; do
|
||||
echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
|
||||
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
|
||||
echo "Running dry-run builds for packages."
|
||||
declare -a package_build_jobs=()
|
||||
for pkg in "${all_packages[@]}"; do
|
||||
package_build_jobs+=("Dry-run build: packages.x86_64-linux.${pkg}${NIX_PARALLEL_SEP}.#packages.x86_64-linux.${pkg}")
|
||||
done
|
||||
run_nix_parallel package_build_jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
@@ -68,6 +68,7 @@ cat > "$HOME/.config/nix/nix.conf" <<'EOF'
|
||||
experimental-features = nix-command flakes
|
||||
accept-flake-config = false
|
||||
warn-dirty = false
|
||||
build-users-group =
|
||||
EOF
|
||||
|
||||
echo "Nix version:"
|
||||
@@ -82,13 +83,6 @@ if ! command -v jq >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
echo "Available NixOS hosts:"
|
||||
hosts="$(list_flake_targets .)"
|
||||
echo "$hosts"
|
||||
list_flake_targets .
|
||||
|
||||
echo "Evaluating all host toplevel derivations..."
|
||||
for host in $hosts; do
|
||||
echo "==> Evaluating $host"
|
||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
||||
done
|
||||
|
||||
echo "Codex setup complete."
|
||||
echo "Codex setup complete. Run bash scripts/codex-maintenance.sh to validate changes."
|
||||
|
||||
+29
-13
@@ -6,13 +6,23 @@
|
||||
# environment (e.g. PROXMOX_STORAGE=tank-nvme ./scripts/proxmox/create-proxmox-resource.sh ...)
|
||||
# since each one only sets a default if unset.
|
||||
|
||||
# SSH-reachable Proxmox node that scripts/proxmox/create-proxmox-resource.sh runs
|
||||
# pct/qm on. Matches the Proxmox web UI hostname already used in
|
||||
# hosts/nixos/home.nix's desktop shortcuts (pve.<homeDomain> from
|
||||
# variables.nix) -- change this if that's not actually reachable over SSH,
|
||||
# or if you're targeting a different node in a multi-node cluster.
|
||||
: "${PROXMOX_HOST:=pve.sweet.home}"
|
||||
: "${PROXMOX_SSH_USER:=root}"
|
||||
# Two SSH-reachable Proxmox nodes exist on the LAN:
|
||||
# - pve1.sweet.home -- production. Real, live VMs/containers.
|
||||
# - pve-test.sweet.home -- sandbox/test node, for scratch VMs/containers
|
||||
# that don't belong on production.
|
||||
#
|
||||
# PROXMOX_HOST is what scripts/proxmox/create-proxmox-resource.sh actually
|
||||
# targets by default -- overridable per-invocation with --node <hostname>,
|
||||
# or per-variable as usual (e.g. PROXMOX_HOST=$PVE_TEST_HOST). It defaults
|
||||
# to production, matching this repo's behavior before pve-test existed --
|
||||
# see CLAUDE.md's "Two Proxmox nodes" section for the policy on which
|
||||
# situations should target which node (in particular: Claude defaults to
|
||||
# pve-test, not this variable's own default, unless explicitly told
|
||||
# otherwise).
|
||||
: "${PVE1_HOST:=pve1.sweet.home}"
|
||||
: "${PVE_TEST_HOST:=pve-test.sweet.home}"
|
||||
: "${PROXMOX_HOST:=$PVE1_HOST}"
|
||||
: "${PROXMOX_SSH_USER:=wayne}"
|
||||
|
||||
# Where this flake repo lives on the Proxmox node itself.
|
||||
# scripts/proxmox/create-proxmox-resource.sh builds images directly on the node
|
||||
@@ -20,12 +30,12 @@
|
||||
# (from this checkout's own `origin` remote) the first time it doesn't
|
||||
# find it, installing build tooling via scripts/codex-setup.sh, then
|
||||
# `git pull`s it before every subsequent build.
|
||||
: "${PROXMOX_REMOTE_REPO_DIR:=/root/nixos}"
|
||||
: "${PROXMOX_REMOTE_REPO_DIR:=/home/${PROXMOX_SSH_USER}/nixos}"
|
||||
|
||||
# Storage pool names -- Proxmox's own stock-install defaults, but this
|
||||
# varies a lot by setup (ZFS pool name, custom LVM-thin volume, etc.).
|
||||
# Verify with `pvesm status` on the node and correct these if wrong.
|
||||
: "${PROXMOX_STORAGE:=local-lvm}" # VM disks / CT rootfs
|
||||
: "${PROXMOX_STORAGE:=local-zfs}" # VM disks / CT rootfs
|
||||
: "${PROXMOX_ISO_STORAGE:=local}" # uploaded images/ISOs/CT templates
|
||||
|
||||
: "${PROXMOX_BRIDGE:=vmbr0}"
|
||||
@@ -63,15 +73,21 @@
|
||||
# nothing here forces a mount to happen.
|
||||
: "${PROXMOX_DEFAULT_LXC_FEATURES:=nesting=1,keyctl=1,mount=nfs;nfs4}"
|
||||
|
||||
export PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE PROXMOX_ISO_STORAGE \
|
||||
PROXMOX_BRIDGE PROXMOX_DEFAULT_CORES PROXMOX_DEFAULT_MEMORY_MB \
|
||||
PROXMOX_DEFAULT_LXC_DISK_GB PROXMOX_DEFAULT_LXC_FEATURES \
|
||||
PROXMOX_REMOTE_REPO_DIR
|
||||
export PVE1_HOST PVE_TEST_HOST PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE \
|
||||
PROXMOX_ISO_STORAGE PROXMOX_BRIDGE PROXMOX_DEFAULT_CORES \
|
||||
PROXMOX_DEFAULT_MEMORY_MB PROXMOX_DEFAULT_LXC_DISK_GB \
|
||||
PROXMOX_DEFAULT_LXC_FEATURES PROXMOX_REMOTE_REPO_DIR
|
||||
|
||||
# Matches variables.nix's nixCacheHost -- update both if it ever changes.
|
||||
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||
export NIX_CACHE_HOST
|
||||
|
||||
# Matches variables.nix's lanDomain (the Gitea host this flake's own repo
|
||||
# is served from -- see scripts/installer/auto-install.sh's FLAKE_BASE_URL)
|
||||
# -- update both if it ever changes.
|
||||
: "${LAN_DOMAIN:=gitea.lan.ddnsgeek.com}"
|
||||
export LAN_DOMAIN
|
||||
|
||||
# nix_extra_opts: call as a plain statement (NOT inside $(...)/<(...) --
|
||||
# that forks a subshell, and the whole point is exporting a decision back
|
||||
# into *this* shell) to populate the global NIX_OPTS array with whatever
|
||||
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i bash -p jq disko nixos-install-tools zfs
|
||||
# shellcheck shell=bash
|
||||
# The only genuinely external tools this script calls directly: `jq`
|
||||
# (parsing the `nix eval` host list), `disko`/`nixos-install` (the
|
||||
# install itself), and `zpool` (exporting a ZFS root pool before reboot,
|
||||
# see the comment above that call below). Everything disko shells out to
|
||||
# internally (parted/sgdisk/mkfs.*/zfs/...) is self-contained -- disko's
|
||||
# own generated scripts hardcode absolute Nix store paths for those, they
|
||||
# don't rely on this script's PATH at all (confirmed by inspecting a
|
||||
# generated system.build.formatScript). The built installer image
|
||||
# (modules/installer/common.nix, plus the upstream
|
||||
# installation-cd-minimal.nix it imports via iso.nix) already has all
|
||||
# four in environment.systemPackages, so this nix-shell wrapper is a
|
||||
# fast no-op there; it's what makes the script also work standalone
|
||||
# (e.g. run directly from a checkout on a stock ISO), where they aren't
|
||||
# guaranteed.
|
||||
set -eux
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=../env.sh
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/env.sh"
|
||||
|
||||
export FLAKE_BASE_URL="git+https://${LAN_DOMAIN}/beatzaplenty/nixos.git"
|
||||
|
||||
echo "Fetching available NixOS hosts from flake..."
|
||||
# Two categories deliberately excluded from the menu:
|
||||
# lxc-* — these build a config.system.build.tarball meant for
|
||||
# `pct restore` on Proxmox directly, not an install.
|
||||
# Running nixos-install against one here would
|
||||
# bind-mount / onto /mnt and then refuse to touch the
|
||||
# filesystem it's currently running on — see
|
||||
# docs/auto-installer.md.
|
||||
# installer — this *is* the installer image's own flake target,
|
||||
# not a deployable host; "installing" it means
|
||||
# nixos-install-ing a copy of the installer into
|
||||
# itself.
|
||||
mapfile -t options < <(
|
||||
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
||||
"${FLAKE_BASE_URL}#nixosConfigurations" \
|
||||
--apply builtins.attrNames \
|
||||
| jq -r '.[]
|
||||
| select(startswith("lxc-") | not)
|
||||
| select(. != "installer")'
|
||||
)
|
||||
|
||||
if [[ ${#options[@]} -eq 0 ]]; then
|
||||
echo "ERROR: No NixOS hosts found in ${FLAKE_BASE_URL}#nixosConfigurations" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Note: lxc-* targets aren't installed this way — build them with"
|
||||
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
|
||||
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
|
||||
|
||||
echo "Choose the flake profile to install:"
|
||||
select choice in "${options[@]}"; do
|
||||
if [[ -n "$choice" ]]; then
|
||||
echo "You selected: $choice"
|
||||
break
|
||||
else
|
||||
echo "Invalid selection. Try again."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Starting install with flake: ${FLAKE_BASE_URL}#${choice}"
|
||||
|
||||
# Optional: confirm before proceeding
|
||||
read -rp "Proceed with installation? (y/N): " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A nix-cache host is *the* substituter/remote-builder for every other
|
||||
# host once installed (its own config explicitly excludes itself from
|
||||
# using either — see buildType != "nix-cache" in the nixos flake.nix).
|
||||
# Installing one shouldn't depend on a nix-cache substituter either,
|
||||
# for the same reason — plus in practice "nix-cache" only resolves over
|
||||
# Tailscale, which a fresh installer environment was never connected to
|
||||
# anyway, so it's dead weight even for non-nix-cache installs until
|
||||
# that's sorted out. Override it away here specifically for nix-cache
|
||||
# targets to keep install-time behaviour consistent with run-time.
|
||||
nix_extra_opts=()
|
||||
if [[ "${choice}" == *-nix-cache ]]; then
|
||||
echo "Installing a nix-cache host — skipping the nix-cache substituter."
|
||||
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
||||
fi
|
||||
|
||||
# Every host reachable through this menu has a Disko config (lxc-*
|
||||
# is filtered out above, and is the only category that doesn't —
|
||||
# see docs/auto-installer.md), so this can run unconditionally: no
|
||||
# need to probe the flake first and branch on whether Disko applies.
|
||||
disko --mode destroy,format,mount \
|
||||
--flake "${FLAKE_BASE_URL}#${choice}" "${nix_extra_opts[@]}" --yes-wipe-all-disks
|
||||
|
||||
# sops-nix derives this host's decryption key from its own SSH host key
|
||||
# at *activation* time, which runs before systemd would otherwise
|
||||
# generate one on first boot. Without pre-seeding it here, secrets
|
||||
# (including the login password) fail to decrypt on first boot.
|
||||
# Generate the key with scripts/secrets/prepare-host-key.sh first.
|
||||
#
|
||||
# Two places a key can come from, checked in order:
|
||||
# /etc/host-keys — baked into this image at build time (see
|
||||
# modules/installer/host-keys.nix; only present
|
||||
# if built with NIXOS_HOST_KEYS_DIR set)
|
||||
# /root/host-keys — scp'd in manually after boot (older fallback,
|
||||
# still supported for images built without keys)
|
||||
mkdir -p /root/host-keys
|
||||
if [[ -f "/etc/host-keys/${choice}_ssh_host_ed25519_key" ]]; then
|
||||
echo "Found baked-in SSH host key for ${choice}, installing to target..."
|
||||
install -D -m 0600 "/etc/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||
install -D -m 0644 "/etc/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||
elif [[ -f "/root/host-keys/${choice}_ssh_host_ed25519_key" ]]; then
|
||||
echo "Found pre-seeded SSH host key for ${choice}, installing to target..."
|
||||
install -D -m 0600 "/root/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||
install -D -m 0644 "/root/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||
else
|
||||
# Third place a key can come from: an arbitrary path the operator
|
||||
# points at interactively (e.g. a USB stick, a mount from another
|
||||
# machine) -- only offered when there's an actual human at the other
|
||||
# end of stdin to ask, never in a non-interactive run.
|
||||
key_copied=0
|
||||
if [[ -t 0 ]]; then
|
||||
echo "No SSH host key found for ${choice} (checked /etc/host-keys and /root/host-keys)."
|
||||
read -rp "Path to a directory containing ${choice}_ssh_host_ed25519_key(.pub) (blank to skip): " key_src_dir
|
||||
if [[ -n "$key_src_dir" && -f "${key_src_dir}/${choice}_ssh_host_ed25519_key" && -f "${key_src_dir}/${choice}_ssh_host_ed25519_key.pub" ]]; then
|
||||
cp "${key_src_dir}/${choice}_ssh_host_ed25519_key" "${key_src_dir}/${choice}_ssh_host_ed25519_key.pub" /root/host-keys/
|
||||
key_copied=1
|
||||
elif [[ -n "$key_src_dir" ]]; then
|
||||
echo "WARNING: ${choice}_ssh_host_ed25519_key(.pub) not found in ${key_src_dir}."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$key_copied" -eq 1 ]]; then
|
||||
echo "Copied SSH host key for ${choice} from ${key_src_dir}, installing to target..."
|
||||
install -D -m 0600 "/root/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||
install -D -m 0644 "/root/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||
else
|
||||
echo "WARNING: no SSH host key found for ${choice} (checked /etc/host-keys and /root/host-keys)"
|
||||
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
|
||||
echo "Run scripts/secrets/prepare-host-key.sh for host ${choice} on your admin workstation first,"
|
||||
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, scp the result to"
|
||||
echo "/root/host-keys/ on this machine, or point at it when prompted above."
|
||||
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
|
||||
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p /mnt/install-tmp
|
||||
export TMPDIR=/mnt/install-tmp
|
||||
|
||||
nixos-install \
|
||||
--flake "${FLAKE_BASE_URL}#${choice}" \
|
||||
"${nix_extra_opts[@]}" \
|
||||
--no-root-password
|
||||
|
||||
|
||||
rm -rf /mnt/install-tmp
|
||||
# Redundant copy of the host's private key — the real one is now at
|
||||
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
|
||||
# up on its own since it was written imperatively, not declaratively.
|
||||
rm -rf /root/host-keys
|
||||
|
||||
# disko's --mode ...,mount left any ZFS root pool imported (that's what
|
||||
# let nixos-install write into /mnt). If we reboot with it still
|
||||
# imported, it isn't just "not exported" -- it's stamped with *this*
|
||||
# live installer environment's hostid, which almost never matches the
|
||||
# target's own networking.hostId (see hosts/*/host.nix; the installer
|
||||
# itself sets none). modules/services/zfs/enable-service.nix and
|
||||
# modules/common/configuration.nix both set boot.zfs.forceImportRoot =
|
||||
# false deliberately (the safe option per that setting's own docs), so
|
||||
# the freshly-installed system's first real boot sees a pool "in use by
|
||||
# another system" and refuses to import it without -f -- which is what
|
||||
# makes boot stall waiting on the ZFS import. Exporting here (a no-op
|
||||
# if the chosen host has no ZFS root, e.g. proxmox-*/linode-*) clears
|
||||
# that in-use state so the next import, from any hostid, succeeds.
|
||||
#
|
||||
# Anything still mounted under /mnt -- nixos-install's own leftover
|
||||
# chroot bind mounts for running the target's activation script
|
||||
# (/mnt/dev, /mnt/proc, /mnt/sys, /mnt/run), and disko's own /mnt/boot
|
||||
# ESP mount (modules/disko/baremetal.nix) -- blocks ZFS from unmounting
|
||||
# its root dataset at /mnt, the same way any nested mount blocks
|
||||
# unmounting its parent. Confirmed live: zpool export failed with
|
||||
# "cannot unmount '/mnt': pool or dataset busy" even after handling the
|
||||
# chroot mounts alone, because /mnt/boot was still mounted too. Because
|
||||
# of this script's `set -e`, that killed the script before it ever
|
||||
# reached reboot, silently defeating the whole point of exporting first.
|
||||
# Unmounting everything under /mnt up front (recursively, so nested
|
||||
# mounts like /mnt/dev/pts come along for free) sidesteps needing to
|
||||
# enumerate every mount disko/nixos-install might leave behind.
|
||||
if mountpoint -q /mnt; then
|
||||
umount -R /mnt
|
||||
fi
|
||||
|
||||
if [[ -n "$(zpool list -H -o name 2>/dev/null)" ]]; then
|
||||
echo "Exporting ZFS pool(s) before reboot..."
|
||||
zpool export -a
|
||||
fi
|
||||
|
||||
sleep 10
|
||||
reboot
|
||||
@@ -34,3 +34,21 @@ flake_target_hostname() {
|
||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" \
|
||||
"${flake_ref}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null
|
||||
}
|
||||
|
||||
# flake_target_lxc_privileged <flake_ref> <target>
|
||||
# Prints "true" or "false" for one lxc-* target's config.proxmoxLXC.privileged
|
||||
# (modules/platforms/lxc.nix is the single source of truth -- e.g.
|
||||
# lxc-docker sets this true so it can NFS-mount; every other lxc-* host
|
||||
# stays unprivileged). Only meaningful for lxc-* targets -- the option
|
||||
# doesn't exist for linode-*/proxmox-* (nixpkgs' proxmox-lxc.nix, which
|
||||
# declares it, is only ever imported by modules/platforms/lxc.nix). Empty
|
||||
# (not an error under set -e) if the eval fails.
|
||||
flake_target_lxc_privileged() {
|
||||
local flake_ref="$1" target="$2"
|
||||
# Not --raw: the option is a Nix boolean, and --raw can only coerce
|
||||
# strings ("cannot coerce a Boolean to a string"). Plain `nix eval`
|
||||
# prints a bare `true`/`false` for a boolean, which is exactly the
|
||||
# string this needs.
|
||||
nix eval "${NIX_EVAL_FLAGS[@]}" \
|
||||
"${flake_ref}#nixosConfigurations.${target}.config.proxmoxLXC.privileged" 2>/dev/null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared parallel-nix-invocation helper for scripts/codex-maintenance.sh.
|
||||
# Source alongside nix-eval.sh:
|
||||
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-parallel.sh"
|
||||
#
|
||||
# The per-host/per-package `nix eval`/`nix build --dry-run` calls in
|
||||
# codex-maintenance.sh are independent of each other, so running them one at
|
||||
# a time leaves most cores idle for most of the sweep -- run_nix_parallel
|
||||
# fans a batch of them out across up to NIX_PARALLEL_JOBS processes instead.
|
||||
|
||||
# NIX_PARALLEL_JOBS: how many `nix` invocations run_nix_parallel runs at
|
||||
# once. Defaults to core count capped by available memory (~1GB/job,
|
||||
# floor 1) rather than plain `nproc` -- each concurrent `nix eval` here
|
||||
# evaluates a whole NixOS system closure from scratch, and on a small/
|
||||
# memory-constrained CI runner, `nproc` concurrent evals can OOM-kill each
|
||||
# other (confirmed empirically: on a 4GB/6-core box, 5-6 concurrent evals
|
||||
# started getting killed while 3-4 ran clean and were still ~2x faster than
|
||||
# serial). Override via env if a given machine/CI runner has room to spare
|
||||
# or needs a tighter cap.
|
||||
default_nix_parallel_jobs() {
|
||||
local cores mem_avail_kb mem_cap
|
||||
cores="$(nproc 2>/dev/null || echo 4)"
|
||||
mem_avail_kb="$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo 2>/dev/null)"
|
||||
if [[ -z "$mem_avail_kb" ]]; then
|
||||
echo "$cores"
|
||||
return
|
||||
fi
|
||||
mem_cap=$((mem_avail_kb / 1024 / 1024))
|
||||
((mem_cap < 1)) && mem_cap=1
|
||||
((mem_cap < cores)) && echo "$mem_cap" || echo "$cores"
|
||||
}
|
||||
NIX_PARALLEL_JOBS="${NIX_PARALLEL_JOBS:-$(default_nix_parallel_jobs)}"
|
||||
|
||||
# Separator between a job's label and its flake attr in the arrays
|
||||
# run_nix_parallel takes -- a control character so it can't collide with
|
||||
# anything a label or attr path would plausibly contain.
|
||||
NIX_PARALLEL_SEP=$'\x1f'
|
||||
|
||||
# run_nix_parallel <jobs_array_name> <nix subcommand + flags...>
|
||||
#
|
||||
# jobs_array_name: name of an already-populated bash array whose entries are
|
||||
# "<label>${NIX_PARALLEL_SEP}<attr>" pairs, e.g.
|
||||
# jobs=("proxmox-docker${NIX_PARALLEL_SEP}.#nixosConfigurations.proxmox-docker...drvPath")
|
||||
# Remaining args are passed to `nix` before the attr, e.g.:
|
||||
# run_nix_parallel jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
|
||||
# run_nix_parallel jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
|
||||
#
|
||||
# Prints "==> <label>" followed by that job's stdout+stderr for every job,
|
||||
# in submission order (not completion order) so a run stays readable and
|
||||
# diffable across invocations even though the work itself doesn't finish in
|
||||
# that order. Returns non-zero if any job failed, only after every job has
|
||||
# finished and been printed -- same "surface everything, then fail" contract
|
||||
# a `set -e` caller gets, just parallelized instead of stopping at the first
|
||||
# failure.
|
||||
run_nix_parallel() {
|
||||
local -n jobs_ref="$1"
|
||||
shift
|
||||
local -a nix_args=("$@")
|
||||
|
||||
local n=${#jobs_ref[@]}
|
||||
[[ $n -eq 0 ]] && return 0
|
||||
|
||||
local tmp_dir
|
||||
tmp_dir="$(mktemp -d)"
|
||||
|
||||
local i=0 running=0
|
||||
for job in "${jobs_ref[@]}"; do
|
||||
local attr="${job#*"${NIX_PARALLEL_SEP}"}"
|
||||
printf '%s\n' "${job%%"${NIX_PARALLEL_SEP}"*}" >"${tmp_dir}/${i}.label"
|
||||
(
|
||||
if nix "${nix_args[@]}" "$attr" >"${tmp_dir}/${i}.out" 2>&1; then
|
||||
echo 0 >"${tmp_dir}/${i}.status"
|
||||
else
|
||||
echo 1 >"${tmp_dir}/${i}.status"
|
||||
fi
|
||||
) &
|
||||
i=$((i + 1))
|
||||
running=$((running + 1))
|
||||
if ((running >= NIX_PARALLEL_JOBS)); then
|
||||
wait -n
|
||||
running=$((running - 1))
|
||||
fi
|
||||
done
|
||||
wait
|
||||
|
||||
local failed=0 j
|
||||
for ((j = 0; j < n; j++)); do
|
||||
echo "==> $(cat "${tmp_dir}/${j}.label")"
|
||||
cat "${tmp_dir}/${j}.out"
|
||||
[[ "$(cat "${tmp_dir}/${j}.status")" -ne 0 ]] && failed=1
|
||||
done
|
||||
|
||||
rm -rf "$tmp_dir"
|
||||
return $failed
|
||||
}
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
# Ad hoc clone of a single VM/CT from pve1 (production) to pve-test
|
||||
# (sandbox), via vzdump + qmrestore/pct restore -- not a general-purpose
|
||||
# backup tool, just a quick "give me a disposable copy of this thing on
|
||||
# pve-test" for testing against real-ish data without touching prod.
|
||||
#
|
||||
# Flow:
|
||||
# 1. vzdump the resource on pve1 into its "local" storage (--mode
|
||||
# snapshot by default, so the source keeps running throughout --
|
||||
# see --mode below for when that's not possible).
|
||||
# 2. Stream the resulting archive straight from pve1 to pve-test
|
||||
# (ssh pve1 cat ... | ssh pve-test cat > ...) -- this machine is
|
||||
# just the relay, no separate on-disk staging copy here.
|
||||
# 3. qmrestore / pct restore it on pve-test under --new-vmid (default:
|
||||
# same VMID as the source -- pve-test is a separate node/cluster, so
|
||||
# no collision unless that VMID is already in use there too).
|
||||
# Always restored with --unique 1 (fresh MAC addresses) since the
|
||||
# source is typically still running on the same LAN -- restoring
|
||||
# with the *same* MAC would put two live guests on the wire with
|
||||
# identical hardware addresses.
|
||||
# 4. Delete the vzdump archive from pve1's local storage and the
|
||||
# relayed copy on pve-test, so neither node accumulates ad hoc
|
||||
# backup files from this script. Only the pve1 original is
|
||||
# preserved on any failure after step 1, so a failed
|
||||
# transfer/restore can be retried without re-running the backup.
|
||||
#
|
||||
# This script's own defaults are pve1 -> pve-test, unlike
|
||||
# create-proxmox-resource.sh's --node (which defaults to production) --
|
||||
# see CLAUDE.md's "Two Proxmox nodes" section. pve1 is only ever touched
|
||||
# here after typing the source VMID back to confirm; pve-test is treated
|
||||
# as disposable, matching this repo's usual policy for that node.
|
||||
#
|
||||
# See --help for the full option list.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
# shellcheck source=../env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
# shellcheck source=../lib/confirm.sh
|
||||
source "${repo_root}/scripts/lib/confirm.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --vmid <n> [options]
|
||||
|
||||
--vmid <n> Required: VMID on the source node to clone.
|
||||
Kind (qemu VM vs LXC CT) is auto-detected.
|
||||
--new-vmid <n> VMID to restore as on the target node
|
||||
(default: same as --vmid).
|
||||
--mode snapshot|suspend|stop
|
||||
vzdump backup mode (default: snapshot -- the
|
||||
source resource keeps running throughout;
|
||||
requires snapshot-capable storage, e.g.
|
||||
ZFS/LVM-thin/Ceph/qcow2). Fall back to
|
||||
"suspend" (brief pause) or "stop" (source
|
||||
goes down for the duration) if the source's
|
||||
storage doesn't support live snapshots --
|
||||
vzdump's own error will say so.
|
||||
--source-node <host> (default: \$PVE1_HOST, ${PVE1_HOST})
|
||||
--target-node <host> (default: \$PVE_TEST_HOST, ${PVE_TEST_HOST})
|
||||
--source-storage <pool> Where vzdump writes the backup on the
|
||||
source node (default: local).
|
||||
--target-storage <pool> Where the restored disk/rootfs lands on
|
||||
the target node (default: \$PROXMOX_STORAGE, ${PROXMOX_STORAGE}).
|
||||
--keep-backup Don't delete the vzdump archive from
|
||||
either node afterward (debugging aid).
|
||||
--yes Skip the typed VMID confirmation
|
||||
before touching the source node.
|
||||
--dry-run Print the full plan and skip every
|
||||
mutating step (vzdump, transfer,
|
||||
restore, delete) and the confirm
|
||||
prompt. Still makes read-only SSH
|
||||
calls to look up the source kind
|
||||
and check the target VMID is free
|
||||
-- harmless on either node.
|
||||
-h, --help
|
||||
EOF
|
||||
}
|
||||
|
||||
vmid=""
|
||||
new_vmid=""
|
||||
mode="snapshot"
|
||||
source_node="$PVE1_HOST"
|
||||
target_node="$PVE_TEST_HOST"
|
||||
source_storage="local"
|
||||
target_storage="$PROXMOX_STORAGE"
|
||||
keep_backup=0
|
||||
skip_confirm=0
|
||||
dry_run=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--vmid) vmid="$2"; shift 2 ;;
|
||||
--new-vmid) new_vmid="$2"; shift 2 ;;
|
||||
--mode) mode="$2"; shift 2 ;;
|
||||
--source-node) source_node="$2"; shift 2 ;;
|
||||
--target-node) target_node="$2"; shift 2 ;;
|
||||
--source-storage) source_storage="$2"; shift 2 ;;
|
||||
--target-storage) target_storage="$2"; shift 2 ;;
|
||||
--keep-backup) keep_backup=1; shift ;;
|
||||
--yes) skip_confirm=1; shift ;;
|
||||
--dry-run) dry_run=1; shift ;;
|
||||
-h | --help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$vmid" ]]; then
|
||||
echo "ERROR: --vmid is required." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$mode" != "snapshot" && "$mode" != "suspend" && "$mode" != "stop" ]]; then
|
||||
echo "ERROR: --mode must be snapshot, suspend, or stop." >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ -z "$new_vmid" ]] && new_vmid="$vmid"
|
||||
|
||||
source_target="${PROXMOX_SSH_USER}@${source_node}"
|
||||
target_target="${PROXMOX_SSH_USER}@${target_node}"
|
||||
|
||||
# No dry-run wrapper needed for the calls below: every mutating step
|
||||
# (vzdump, transfer, restore, delete) is reached only after the --dry-run
|
||||
# early-exit further down, so a plain `ssh` call is never in the dry-run
|
||||
# path.
|
||||
|
||||
# --- identify the resource kind on the source node -----------------------
|
||||
echo "==> Looking up VMID ${vmid} on ${source_node}..."
|
||||
kind=""
|
||||
if ssh "$source_target" "qm status ${vmid}" >/dev/null 2>&1; then
|
||||
kind="vm"
|
||||
elif ssh "$source_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
||||
kind="lxc"
|
||||
else
|
||||
echo "ERROR: VMID ${vmid} doesn't exist on ${source_node} as either a VM or CT." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "VMID ${vmid} on ${source_node} is a ${kind}."
|
||||
|
||||
# --- refuse to clobber an existing resource on the target node -----------
|
||||
if ssh "$target_target" "qm status ${new_vmid}" >/dev/null 2>&1 \
|
||||
|| ssh "$target_target" "pct status ${new_vmid}" >/dev/null 2>&1; then
|
||||
echo "ERROR: VMID ${new_vmid} already exists on ${target_node}. Pass --new-vmid" >&2
|
||||
echo "with a free ID, or remove the existing resource there first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Plan:"
|
||||
echo " source: ${kind} VMID ${vmid} on ${source_node} (storage: ${source_storage}, mode: ${mode})"
|
||||
echo " target: VMID ${new_vmid} on ${target_node} (storage: ${target_storage}, fresh MAC via --unique)"
|
||||
[[ "$keep_backup" -eq 1 ]] && echo " backup archives are kept on both nodes afterward (--keep-backup)"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo
|
||||
echo "[dry-run] No backup, transfer, restore, or delete was performed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$skip_confirm" -ne 1 ]]; then
|
||||
echo
|
||||
if ! confirm_typed "$vmid" "Type the source VMID (${vmid}) to confirm backing it up from ${source_node}: "; then
|
||||
echo "Cancelled -- input didn't match ${vmid}." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- vzdump on the source node --------------------------------------------
|
||||
echo
|
||||
echo "==> Backing up VMID ${vmid} on ${source_node} (mode=${mode}, storage=${source_storage})..."
|
||||
vzdump_log="$(ssh "$source_target" \
|
||||
"vzdump ${vmid} --mode ${mode} --storage ${source_storage} --compress zstd" 2>&1)" \
|
||||
|| {
|
||||
echo "$vzdump_log" >&2
|
||||
echo "ERROR: vzdump failed on ${source_node}." >&2
|
||||
exit 1
|
||||
}
|
||||
echo "$vzdump_log"
|
||||
|
||||
archive="$(echo "$vzdump_log" | grep -oP "creating vzdump archive '\K[^']+" | tail -n1)"
|
||||
if [[ -z "$archive" ]]; then
|
||||
echo "ERROR: couldn't find the archive path in vzdump's output above." >&2
|
||||
exit 1
|
||||
fi
|
||||
archive_basename="$(basename "$archive")"
|
||||
target_tmp_archive="/var/tmp/${archive_basename}"
|
||||
echo "Archive: ${archive}"
|
||||
|
||||
# Always clean up the relayed copy on the target node, success or failure
|
||||
# -- it's only ever a working copy, restored or not.
|
||||
cleanup_target_tmp() {
|
||||
if [[ "$keep_backup" -ne 1 ]]; then
|
||||
ssh "$target_target" "rm -f '${target_tmp_archive}'" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup_target_tmp EXIT
|
||||
|
||||
# --- relay the archive from source to target ------------------------------
|
||||
echo
|
||||
echo "==> Transferring archive to ${target_node}..."
|
||||
ssh "$source_target" "cat '${archive}'" | ssh "$target_target" "cat > '${target_tmp_archive}'"
|
||||
|
||||
# --- restore on the target node --------------------------------------------
|
||||
echo
|
||||
echo "==> Restoring as VMID ${new_vmid} on ${target_node} (storage=${target_storage})..."
|
||||
if [[ "$kind" == "vm" ]]; then
|
||||
ssh "$target_target" "qmrestore '${target_tmp_archive}' ${new_vmid} --storage ${target_storage} --unique 1"
|
||||
else
|
||||
ssh "$target_target" "pct restore ${new_vmid} '${target_tmp_archive}' --storage ${target_storage} --unique 1"
|
||||
fi
|
||||
|
||||
# --- clean up the source backup now that the restore succeeded -----------
|
||||
if [[ "$keep_backup" -ne 1 ]]; then
|
||||
echo
|
||||
echo "==> Deleting backup archive from ${source_node}'s ${source_storage} storage..."
|
||||
ssh "$source_target" "rm -f '${archive}' '${archive}.notes' '${archive}.log'" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Done. VMID ${new_vmid} (${kind}) is now on ${target_node}, cloned from" \
|
||||
"VMID ${vmid} on ${source_node}."
|
||||
@@ -6,17 +6,31 @@
|
||||
#
|
||||
# This is the non-NixOS equivalent of modules/nix-cache/client.nix +
|
||||
# modules/nix-cache/remote-builder-client.nix -- those two only apply to
|
||||
# hosts built from this flake. A plain Debian box with Nix installed
|
||||
# (single- or multi-user install, nix-daemon running) has no NixOS module
|
||||
# system to pick that config up, so this edits /etc/nix/nix.conf by hand
|
||||
# instead. Run this ON the target Debian machine, as root.
|
||||
# hosts built from this flake. A plain Debian box with Nix installed has no
|
||||
# NixOS module system to pick that config up, so this edits nix.conf by hand.
|
||||
#
|
||||
# Two modes depending on who runs it:
|
||||
#
|
||||
# root (multi-user / daemon install):
|
||||
# Writes /etc/nix/nix.conf, /etc/ssh/ssh_known_hosts, restarts nix-daemon.
|
||||
# Requires /etc/nix/nix.conf to already exist (i.e. nix-daemon is set up).
|
||||
# Run as: sudo ./configure-nix-cache-client.sh [options]
|
||||
#
|
||||
# non-root (single-user install):
|
||||
# Writes ~/.config/nix/nix.conf, ~/.ssh/known_hosts. No daemon to restart.
|
||||
# Run as: ./configure-nix-cache-client.sh [options]
|
||||
#
|
||||
# The values below mirror variables.nix / modules/nix-cache/client.nix in
|
||||
# this repo -- update both if nix-cache is ever rebuilt with a new host
|
||||
# key or the cache signing key is rotated (see docs/nix-cache.md).
|
||||
#
|
||||
# REMOTE_BUILDER_KEY defaults to the running user's default SSH identity
|
||||
# (root: /root/.ssh/id_ed25519, other user: ~/.ssh/id_ed25519). That key
|
||||
# must be listed in vars.remoteBuilderAuthorizedKeys in this repo and
|
||||
# nix-cache rebuilt before remote building works.
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./configure-nix-cache-client.sh [--dry-run] [--no-remote-builder] [--no-restart]
|
||||
# ./configure-nix-cache-client.sh [--dry-run] [--no-remote-builder] [--no-restart]
|
||||
#
|
||||
# Env overrides (defaults match variables.nix):
|
||||
# NIX_CACHE_HOST, NIX_CACHE_HOST_KEY, REMOTE_BUILDER_USER, REMOTE_BUILDER_KEY
|
||||
@@ -24,19 +38,30 @@
|
||||
set -euo pipefail
|
||||
|
||||
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHrMKZlIGUd3pH9G3AqbsruqUGjxIXMAZw52u9MwiBCn lxc-nix-cache}"
|
||||
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuHUxGNH6ei3BZD+EfZs3l4X8uJNcjQiOsM/G4yo4O/ lxc-nix-cache}"
|
||||
: "${REMOTE_BUILDER_USER:=nixremote}"
|
||||
: "${REMOTE_BUILDER_KEY:=/root/.ssh/nixremote}"
|
||||
|
||||
CACHE_PUB_KEY="cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
|
||||
FALLBACK_URL="https://cache.nixos.org/"
|
||||
FALLBACK_PUB_KEY="cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||
|
||||
NIX_CONF="/etc/nix/nix.conf"
|
||||
KNOWN_HOSTS="/etc/ssh/ssh_known_hosts"
|
||||
MARKER_BEGIN="# BEGIN nix-cache client config (configure-nix-cache-client.sh)"
|
||||
MARKER_END="# END nix-cache client config"
|
||||
|
||||
# Mode: root uses system-wide paths and restarts the daemon; non-root uses
|
||||
# user-level paths and has no daemon to restart.
|
||||
if [[ "$EUID" -eq 0 ]]; then
|
||||
install_mode="multi"
|
||||
NIX_CONF="/etc/nix/nix.conf"
|
||||
KNOWN_HOSTS="/etc/ssh/ssh_known_hosts"
|
||||
: "${REMOTE_BUILDER_KEY:=/root/.ssh/id_ed25519}"
|
||||
else
|
||||
install_mode="single"
|
||||
NIX_CONF="${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf"
|
||||
KNOWN_HOSTS="$HOME/.ssh/known_hosts"
|
||||
: "${REMOTE_BUILDER_KEY:=$HOME/.ssh/id_ed25519}"
|
||||
fi
|
||||
|
||||
dry_run=0
|
||||
with_remote_builder=1
|
||||
restart_daemon=1
|
||||
@@ -47,7 +72,7 @@ for arg in "$@"; do
|
||||
--no-remote-builder) with_remote_builder=0 ;;
|
||||
--no-restart) restart_daemon=0 ;;
|
||||
-h|--help)
|
||||
sed -n '2,20p' "$0"
|
||||
sed -n '2,37p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
@@ -57,21 +82,22 @@ for arg in "$@"; do
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$dry_run" -eq 0 && "$EUID" -ne 0 ]]; then
|
||||
echo "ERROR: must run as root (writes $NIX_CONF and, unless --no-remote-builder, $KNOWN_HOSTS)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v nix >/dev/null 2>&1; then
|
||||
echo "ERROR: no 'nix' binary on PATH -- install the Nix package manager first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$NIX_CONF" ]]; then
|
||||
if [[ "$install_mode" == "multi" && ! -f "$NIX_CONF" ]]; then
|
||||
echo "ERROR: $NIX_CONF not found -- expected an existing multi-user Nix install." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Single-user: create the config file if it doesn't exist yet.
|
||||
if [[ "$install_mode" == "single" && "$dry_run" -eq 0 ]]; then
|
||||
mkdir -p "$(dirname "$NIX_CONF")"
|
||||
[[ -f "$NIX_CONF" ]] || touch "$NIX_CONF"
|
||||
fi
|
||||
|
||||
builder_line=""
|
||||
if [[ "$with_remote_builder" -eq 1 ]]; then
|
||||
if [[ -f "$REMOTE_BUILDER_KEY" ]]; then
|
||||
@@ -107,7 +133,7 @@ fi
|
||||
block="${block}
|
||||
$MARKER_END"
|
||||
|
||||
echo "== nix.conf block to install =="
|
||||
echo "== nix.conf block to install ($NIX_CONF) =="
|
||||
echo "$block"
|
||||
echo "================================"
|
||||
|
||||
@@ -149,7 +175,8 @@ if [[ "$with_remote_builder" -eq 1 ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" -eq 0 && "$restart_daemon" -eq 1 ]]; then
|
||||
# Only restart the daemon for multi-user installs -- single-user has no daemon.
|
||||
if [[ "$dry_run" -eq 0 && "$restart_daemon" -eq 1 && "$install_mode" == "multi" ]]; then
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nix-daemon 2>/dev/null; then
|
||||
systemctl restart nix-daemon
|
||||
echo "Restarted nix-daemon to pick up the new config."
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
# copies over the locally-managed host-keys/ (gitignored, so a git pull
|
||||
# alone wouldn't carry it) before building.
|
||||
#
|
||||
# --node (default: $PROXMOX_HOST, see scripts/env.sh) picks which of the two
|
||||
# LAN Proxmox nodes this runs against: production, pve1.sweet.home
|
||||
# ($PVE1_HOST, PROXMOX_HOST's own default), or the sandbox node,
|
||||
# pve-test.sweet.home ($PVE_TEST_HOST) -- pass --node "$PVE_TEST_HOST" (or
|
||||
# set PROXMOX_HOST=$PVE_TEST_HOST) to target the sandbox instead. See
|
||||
# CLAUDE.md's "Two Proxmox nodes" section: an agent session should default
|
||||
# to pve-test and only touch pve1 when the operator has explicitly said so
|
||||
# for the current task -- this script itself doesn't enforce that (its own
|
||||
# default is production, matching this repo's behavior before pve-test
|
||||
# existed), it's a policy for whoever/whatever is driving it.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/proxmox/create-proxmox-resource.sh --type lxc|vm --host <name> [options]
|
||||
# scripts/proxmox/create-proxmox-resource.sh --type lxc|vm --list
|
||||
@@ -120,7 +131,9 @@ Shared:
|
||||
--iso-storage <pool> (default: \$PROXMOX_ISO_STORAGE, ${PROXMOX_ISO_STORAGE})
|
||||
--bridge <bridge> (default: \$PROXMOX_BRIDGE, ${PROXMOX_BRIDGE})
|
||||
--node <host> Proxmox node to SSH into (default:
|
||||
\$PROXMOX_HOST, ${PROXMOX_HOST})
|
||||
\$PROXMOX_HOST, ${PROXMOX_HOST} --
|
||||
production; the sandbox node is
|
||||
\$PVE_TEST_HOST, ${PVE_TEST_HOST}).
|
||||
--dry-run Print the full plan; touch nothing
|
||||
local or remote, no prompts.
|
||||
-h, --help
|
||||
@@ -180,6 +193,16 @@ done
|
||||
|
||||
ssh_target="${PROXMOX_SSH_USER}@${node}"
|
||||
|
||||
# Proxmox tools (pvesh, qm, pct) require root access to the cluster IPC
|
||||
# socket. When SSH-ing as a non-root user with sudo, prefix every remote
|
||||
# Proxmox command with sudo.
|
||||
sudo_prefix=""
|
||||
sudo_display=""
|
||||
if [[ "$PROXMOX_SSH_USER" != "root" ]]; then
|
||||
sudo_prefix="sudo"
|
||||
sudo_display="sudo "
|
||||
fi
|
||||
|
||||
remote() {
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] ssh ${ssh_target} -- $*"
|
||||
@@ -201,10 +224,10 @@ cmd_modify() {
|
||||
|
||||
echo "Looking up VMID ${vmid} on ${node}..."
|
||||
local kind current_cores current_memory disk_key
|
||||
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1; then
|
||||
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1; then
|
||||
kind="vm"
|
||||
disk_key="scsi0"
|
||||
elif ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
||||
elif ssh "$ssh_target" "${sudo_prefix} pct status ${vmid}" >/dev/null 2>&1; then
|
||||
kind="lxc"
|
||||
disk_key="rootfs"
|
||||
else
|
||||
@@ -212,8 +235,8 @@ cmd_modify() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local config_cmd="qm config ${vmid}"
|
||||
[[ "$kind" == "lxc" ]] && config_cmd="pct config ${vmid}"
|
||||
local config_cmd="${sudo_prefix} qm config ${vmid}"
|
||||
[[ "$kind" == "lxc" ]] && config_cmd="${sudo_prefix} pct config ${vmid}"
|
||||
local current_config
|
||||
current_config="$(ssh "$ssh_target" "$config_cmd")"
|
||||
current_cores="$(echo "$current_config" | grep -oP '^cores:\s*\K\S+' || echo '?')"
|
||||
@@ -237,9 +260,9 @@ cmd_modify() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local set_cmd="qm set"
|
||||
local resize_cmd="qm resize"
|
||||
[[ "$kind" == "lxc" ]] && set_cmd="pct set" && resize_cmd="pct resize"
|
||||
local set_cmd="${sudo_prefix} qm set"
|
||||
local resize_cmd="${sudo_prefix} qm resize"
|
||||
[[ "$kind" == "lxc" ]] && set_cmd="${sudo_prefix} pct set" && resize_cmd="${sudo_prefix} pct resize"
|
||||
|
||||
if [[ -n "$cores" || -n "$memory" ]]; then
|
||||
local args=""
|
||||
@@ -346,14 +369,15 @@ else
|
||||
echo
|
||||
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
|
||||
ssh_check_status=0
|
||||
existing="$(ssh "$ssh_target" bash -s -- "$host" <<'REMOTE_SCRIPT'
|
||||
existing="$(ssh "$ssh_target" bash -s -- "$host" "$sudo_prefix" <<'REMOTE_SCRIPT'
|
||||
target="$1"
|
||||
for id in $(qm list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||
n="$(qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
|
||||
sudo_pfx="$2"
|
||||
for id in $($sudo_pfx qm list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||
n="$($sudo_pfx qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
|
||||
[[ "$n" == "$target" ]] && echo "vm ${id} ${n}"
|
||||
done
|
||||
for id in $(pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||
n="$(pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
||||
for id in $($sudo_pfx pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||
n="$($sudo_pfx pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
||||
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
|
||||
done
|
||||
exit 0
|
||||
@@ -440,12 +464,12 @@ REMOTE_SCRIPT
|
||||
if [[ "$kind" == "vm" ]]; then
|
||||
# qm destroy has no --force to stop-then-destroy in one call (pct's
|
||||
# does) -- stop explicitly first if it's running.
|
||||
if ssh "$ssh_target" "qm status ${id}" 2>/dev/null | grep -q running; then
|
||||
ssh "$ssh_target" "qm stop ${id}"
|
||||
if ssh "$ssh_target" "${sudo_prefix} qm status ${id}" 2>/dev/null | grep -q running; then
|
||||
ssh "$ssh_target" "${sudo_prefix} qm stop ${id}"
|
||||
fi
|
||||
ssh "$ssh_target" "qm destroy ${id} --purge 1"
|
||||
ssh "$ssh_target" "${sudo_prefix} qm destroy ${id} --purge 1"
|
||||
else
|
||||
ssh "$ssh_target" "pct destroy ${id} --force 1 --purge 1"
|
||||
ssh "$ssh_target" "${sudo_prefix} pct destroy ${id} --force 1 --purge 1"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
@@ -474,7 +498,7 @@ if [[ -z "$vmid" ]]; then
|
||||
vmid="<next-free-vmid>"
|
||||
echo "[dry-run] would ask ${node} for the next free VMID (pvesh get /cluster/nextid)"
|
||||
else
|
||||
vmid="$(ssh "$ssh_target" "pvesh get /cluster/nextid" | tr -d '[:space:]')"
|
||||
vmid="$(ssh "$ssh_target" "${sudo_prefix} pvesh get /cluster/nextid" | tr -d '[:space:]')"
|
||||
echo "Auto-assigned VMID: ${vmid}"
|
||||
fi
|
||||
else
|
||||
@@ -488,8 +512,8 @@ if [[ "$dry_run" -eq 0 ]]; then
|
||||
# both. Any success here means something is already using this ID --
|
||||
# refuse to go anywhere near it. (Reconfiguring an existing resource is
|
||||
# --modify's job, not this one's.)
|
||||
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1 \
|
||||
|| ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
||||
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1 \
|
||||
|| ssh "$ssh_target" "${sudo_prefix} pct status ${vmid}" >/dev/null 2>&1; then
|
||||
echo "ERROR: VMID ${vmid} already exists on ${node}. Refusing to touch an" >&2
|
||||
echo "existing resource here -- use --modify to reconfigure it, pick a" >&2
|
||||
echo "different --vmid, or omit it to auto-assign." >&2
|
||||
@@ -659,11 +683,11 @@ if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
|
||||
# hands the result to the remote shell to re-split, which would
|
||||
# otherwise scatter NIX_EXTRA_OPTS (itself several space-separated,
|
||||
# %q-quoted tokens) across the wrong positional parameters below.
|
||||
printf -v remote_cmd 'bash -s -- %q %q %q %q %q' \
|
||||
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS"
|
||||
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q' \
|
||||
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
|
||||
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
||||
set -euo pipefail
|
||||
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"
|
||||
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_pfx="$6"
|
||||
declare -a NIX_OPTS=()
|
||||
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||
cd "$repo_dir"
|
||||
@@ -681,23 +705,14 @@ if [[ -z "$built" ]]; then
|
||||
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$dest_dir"
|
||||
cp "$built" "${dest_dir}/${dest_name}"
|
||||
$sudo_pfx mkdir -p "$dest_dir"
|
||||
$sudo_pfx cp "$built" "${dest_dir}/${dest_name}"
|
||||
echo "Built and staged: ${dest_dir}/${dest_name}"
|
||||
REMOTE_SCRIPT
|
||||
local_image="$remote_path"
|
||||
echo "Built on ${node}: ${remote_path}"
|
||||
fi
|
||||
else
|
||||
# PROXMOX_SSH_USER defaults to root (env.sh), which needs no sudo and
|
||||
# can't assume it's even installed on a minimal node -- only shell out
|
||||
# through sudo when actually running as a non-root SSH user.
|
||||
sudo_prefix="sudo"
|
||||
sudo_display="sudo "
|
||||
if [[ "$PROXMOX_SSH_USER" == "root" ]]; then
|
||||
sudo_prefix=""
|
||||
sudo_display=""
|
||||
fi
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would build on ${node}: nix build --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
|
||||
@@ -715,7 +730,7 @@ REMOTE_SCRIPT
|
||||
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
|
||||
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
||||
set -euo pipefail
|
||||
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_prefix="$6"
|
||||
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_pfx="$6"
|
||||
declare -a NIX_OPTS=()
|
||||
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||
cd "$repo_dir"
|
||||
@@ -724,7 +739,7 @@ ensure_nix_profile
|
||||
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||
".#nixosConfigurations.${target}.config.system.build.diskoImagesScript" \
|
||||
--out-link "result-${target}"
|
||||
$sudo_prefix "./result-${target}" \
|
||||
$sudo_pfx "./result-${target}" \
|
||||
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
||||
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
||||
--build-memory 2048
|
||||
@@ -733,8 +748,8 @@ if [[ -z "$built" ]]; then
|
||||
echo "ERROR: no .raw image found in ${repo_dir} after build." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$dest_dir"
|
||||
mv "$built" "${dest_dir}/${dest_name}"
|
||||
$sudo_pfx mkdir -p "$dest_dir"
|
||||
$sudo_pfx mv "$built" "${dest_dir}/${dest_name}"
|
||||
echo "Built and staged: ${dest_dir}/${dest_name}"
|
||||
REMOTE_SCRIPT
|
||||
local_image="$remote_path"
|
||||
@@ -766,13 +781,22 @@ if [[ "$type" == "lxc" ]]; then
|
||||
# 512M default otherwise (confirmed live: --memory 2048 left swap at
|
||||
# 512). Default to matching whatever --memory resolved to above.
|
||||
local_swap="${swap:-$memory}"
|
||||
# --unprivileged 1: modules/platforms/lxc.nix sets proxmoxLXC.privileged
|
||||
# = false, so the NixOS config inside the image assumes it's running as
|
||||
# an unprivileged container (cgroup/capability/mount expectations baked
|
||||
# in at boot). `pct create`'s own CLI default for this flag is
|
||||
# privileged (unlike the web UI, which defaults its checkbox the other
|
||||
# way) -- leaving it unset creates a privileged container running a
|
||||
# NixOS config that assumes unprivileged, a real mismatch.
|
||||
# --unprivileged: read back from modules/platforms/lxc.nix's own
|
||||
# proxmoxLXC.privileged (via flake_target_lxc_privileged) rather than
|
||||
# hardcoded, since that's no longer the same for every lxc-* target --
|
||||
# lxc-docker sets it true so the container's NFS mounts work at all (the
|
||||
# kernel's NFS client can't mount from inside any unprivileged
|
||||
# container's user namespace, no matter what AppArmor allows -- see that
|
||||
# option's own comment). The NixOS config inside the image bakes in
|
||||
# cgroup/capability/mount expectations matching whichever value it was
|
||||
# built with, so this must stay in sync with it -- `pct create`'s own
|
||||
# CLI default for this flag is privileged (unlike the web UI, which
|
||||
# defaults its checkbox the other way), so leaving it unset would create
|
||||
# a privileged container running a NixOS config that assumes
|
||||
# unprivileged for every target except lxc-docker, a real mismatch.
|
||||
privileged_eval="$(flake_target_lxc_privileged "$repo_root" "$flake_target")"
|
||||
unprivileged_flag=1
|
||||
[[ "$privileged_eval" == "true" ]] && unprivileged_flag=0
|
||||
#
|
||||
# --features nesting=1,keyctl=1: required for a modern (v247+) systemd
|
||||
# guest to actually boot unprivileged -- confirmed live: without this,
|
||||
@@ -790,36 +814,42 @@ if [[ "$type" == "lxc" ]]; then
|
||||
# hands the whole string to `ssh` as a single command for the *remote*
|
||||
# shell to parse -- unquoted, that `;` would be read as a remote
|
||||
# command separator and silently truncate this into two commands.
|
||||
create_cmd="pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged 1 --features '${PROXMOX_DEFAULT_LXC_FEATURES}' --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
|
||||
create_cmd="${sudo_prefix} pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged ${unprivileged_flag} --features '${PROXMOX_DEFAULT_LXC_FEATURES}' --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
|
||||
remote "$create_cmd"
|
||||
remote "pct start ${vmid}"
|
||||
remote "${sudo_prefix} pct start ${vmid}"
|
||||
else
|
||||
echo "==> Creating VM ${vmid} (${name})..."
|
||||
# pre-enrolled-keys=0 disables OVMF's Secure Boot key pre-enrollment --
|
||||
# required, or systemd-boot (unsigned) can't be trusted by the firmware.
|
||||
remote "qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
|
||||
# --agent 1: wires up the virtio-serial channel QEMU exposes to the guest.
|
||||
# modules/common/configuration.nix sets services.qemuGuest.enable = true
|
||||
# on every host, so the guest-side qemu-ga daemon is already running --
|
||||
# without this flag Proxmox never creates the channel it listens on, so
|
||||
# `qm guest exec`/`qm agent` and the UI's IP-address display silently
|
||||
# never work for any VM this script creates.
|
||||
remote "${sudo_prefix} qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
|
||||
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
|
||||
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0"
|
||||
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0 --agent enabled=1"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] ssh ${ssh_target} -- qm importdisk ${vmid} ${remote_path} ${storage}"
|
||||
echo "[dry-run] ssh ${ssh_target} -- ${sudo_display}qm importdisk ${vmid} ${remote_path} ${storage}"
|
||||
echo "[dry-run] (would parse the resulting disk identifier from that output)"
|
||||
echo "[dry-run] ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
|
||||
echo "[dry-run] ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
|
||||
else
|
||||
importdisk_output="$(ssh "$ssh_target" "qm importdisk ${vmid} ${remote_path} ${storage}")"
|
||||
importdisk_output="$(ssh "$ssh_target" "${sudo_prefix} qm importdisk ${vmid} ${remote_path} ${storage}")"
|
||||
echo "$importdisk_output"
|
||||
disk_id="$(echo "$importdisk_output" | grep -oP "(?<=Successfully imported disk as ')[^']+" | sed 's/^unused[0-9]*://')"
|
||||
if [[ -z "$disk_id" ]]; then
|
||||
echo "ERROR: couldn't parse the imported disk identifier from qm importdisk's output above." >&2
|
||||
echo "The VM shell (${vmid}) and imported disk both exist -- finish attaching it by hand:" >&2
|
||||
echo " ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<disk-id-from-output-above>" >&2
|
||||
echo " ssh ${ssh_target} -- qm set ${vmid} --boot order=scsi0" >&2
|
||||
echo " ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<disk-id-from-output-above>" >&2
|
||||
echo " ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --boot order=scsi0" >&2
|
||||
exit 1
|
||||
fi
|
||||
remote "qm set ${vmid} --scsi0 ${disk_id}"
|
||||
remote "${sudo_prefix} qm set ${vmid} --scsi0 ${disk_id}"
|
||||
fi
|
||||
remote "qm set ${vmid} --boot order=scsi0"
|
||||
remote "qm start ${vmid}"
|
||||
remote "${sudo_prefix} qm set ${vmid} --boot order=scsi0"
|
||||
remote "${sudo_prefix} qm start ${vmid}"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
Executable
+253
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env bash
|
||||
# recover-hosts.sh — Fix sops/SSH-key/GitHub-token issues on deployed NixOS hosts
|
||||
# and trigger a Switch-nix rebuild on each.
|
||||
#
|
||||
# Run from the repo root on the workstation (nixos@nixos):
|
||||
# bash scripts/recover-hosts.sh [<hostname> ...]
|
||||
#
|
||||
# With no args it discovers and checks every known hostname.
|
||||
# With args it checks only those hostnames:
|
||||
# bash scripts/recover-hosts.sh tor-relay
|
||||
#
|
||||
# Fixes applied automatically (then prompts before rebuilding):
|
||||
# 1. SSH host key drift — live key no longer matches host-keys/<target>_ssh_host_ed25519_key
|
||||
# Fix: scp the registered key back and restore it (needs sudo once per host).
|
||||
# To push new keys proactively (before drift, e.g. right after
|
||||
# sync-host-keys.sh --regenerate-all-keys), use instead:
|
||||
# scripts/secrets/push-host-keys.sh --all
|
||||
# 2. Stale/invalid GitHub access token — the rendered nix-github-token.conf has
|
||||
# a token GitHub rejects (401), blocking any rebuild that fetches disko or
|
||||
# other public GitHub flake inputs.
|
||||
# Fix: empty the rendered file so nix makes unauthenticated requests instead.
|
||||
# Public repos (disko, nixpkgs, etc.) work fine without auth. sops-nix
|
||||
# re-renders the correct new token automatically after the first successful
|
||||
# rebuild.
|
||||
#
|
||||
# Both fixes need one interactive sudo session per host. The script opens a
|
||||
# single ssh -t per broken host so you enter the password once and all steps
|
||||
# run in sequence.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh 2>/dev/null || true
|
||||
|
||||
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5)
|
||||
SSH_USER=nixos
|
||||
|
||||
# Known flake-target → ssh hostname map for all currently-defined hosts.
|
||||
# Add new hosts here as they are deployed.
|
||||
declare -A TARGET_HOST=(
|
||||
[lxc-docker]=docker
|
||||
[lxc-nix-cache]=nix-cache
|
||||
[lxc-pxe-boot]=pxe-boot
|
||||
[lxc-tor-relay]=tor-relay
|
||||
[lxc-minimal]=nix-minimal
|
||||
[proxmox-server]=server
|
||||
[baremetal-gui]=nixos
|
||||
)
|
||||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
info() { echo " [✓] $*"; }
|
||||
warn() { echo " [!] $*"; }
|
||||
step() { echo "==> $*"; }
|
||||
|
||||
ssh_host_age() {
|
||||
ssh-keyscan -t ed25519 "$1" 2>/dev/null \
|
||||
| nix shell nixpkgs#ssh-to-age --command ssh-to-age 2>/dev/null \
|
||||
| head -1 || true
|
||||
}
|
||||
|
||||
registered_age() {
|
||||
local keyfile="host-keys/${1}_ssh_host_ed25519_key.pub"
|
||||
[ -f "$keyfile" ] || return 0
|
||||
nix shell nixpkgs#ssh-to-age --command ssh-to-age < "$keyfile" 2>/dev/null \
|
||||
| head -1 || true
|
||||
}
|
||||
|
||||
github_token_valid() {
|
||||
local host=$1
|
||||
local raw token code
|
||||
raw=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||
"cat /run/secrets/rendered/nix-github-token.conf 2>/dev/null || true")
|
||||
token=$(echo "$raw" | grep -oP '(?<=github\.com=)\S+' || true)
|
||||
if [ -z "$token" ]; then
|
||||
return 0 # no token = unauthenticated, works for public repos
|
||||
fi
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token $token" \
|
||||
"https://api.github.com/repos/nix-community/disko" 2>/dev/null || echo 000)
|
||||
[ "$code" = "200" ]
|
||||
}
|
||||
|
||||
# ── discover hosts ────────────────────────────────────────────────────────────
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
HOSTNAMES=("$@")
|
||||
else
|
||||
HOSTNAMES=()
|
||||
seen=()
|
||||
for target in "${!TARGET_HOST[@]}"; do
|
||||
h="${TARGET_HOST[$target]}"
|
||||
# deduplicate (e.g. proxmox-server and lxc-server both map to "server")
|
||||
if [[ ! " ${seen[*]:-} " =~ " $h " ]]; then
|
||||
seen+=("$h")
|
||||
if ssh "${SSH_OPTS[@]}" "$SSH_USER@$h" "true" 2>/dev/null; then
|
||||
HOSTNAMES+=("$h")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ${#HOSTNAMES[@]} -eq 0 ]; then
|
||||
echo "No reachable hosts found. Pass hostnames explicitly or check SSH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Hosts to check: ${HOSTNAMES[*]}"
|
||||
echo ""
|
||||
|
||||
# ── check phase ───────────────────────────────────────────────────────────────
|
||||
|
||||
NEEDS_FIX=()
|
||||
|
||||
for host in "${HOSTNAMES[@]}"; do
|
||||
step "$host"
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" "true" 2>/dev/null; then
|
||||
warn "SSH unreachable — clearing stale known_hosts entry"
|
||||
ssh-keygen -R "$host" 2>/dev/null || true
|
||||
continue
|
||||
fi
|
||||
|
||||
flake_target=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||
"cat /etc/flake-target 2>/dev/null || true")
|
||||
echo " flake-target: ${flake_target:-unknown}"
|
||||
|
||||
host_broken=false
|
||||
|
||||
# SSH host key
|
||||
if [ -n "$flake_target" ] && [ -f "host-keys/${flake_target}_ssh_host_ed25519_key.pub" ]; then
|
||||
live=$(ssh_host_age "$host")
|
||||
want=$(registered_age "$flake_target")
|
||||
if [ "$live" = "$want" ]; then
|
||||
info "SSH host key OK"
|
||||
else
|
||||
warn "SSH host key MISMATCH (live ≠ host-keys/) -- use push-host-keys.sh proactively next time"
|
||||
echo " live: $live"
|
||||
echo " registered: $want"
|
||||
host_broken=true
|
||||
fi
|
||||
else
|
||||
echo " [~] No host-keys/ entry for ${flake_target:-unknown} — skipping key check"
|
||||
fi
|
||||
|
||||
# GitHub token
|
||||
if github_token_valid "$host"; then
|
||||
info "GitHub token OK"
|
||||
else
|
||||
warn "GitHub token invalid (rebuild will fail with 401)"
|
||||
host_broken=true
|
||||
fi
|
||||
|
||||
# sops-nix result
|
||||
sops_result=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||
"systemctl show sops-nix --property=Result --value 2>/dev/null || echo unknown")
|
||||
if [ "$sops_result" = "success" ]; then
|
||||
info "sops-nix: success"
|
||||
else
|
||||
warn "sops-nix: $sops_result"
|
||||
fi
|
||||
|
||||
$host_broken && NEEDS_FIX+=("$host")
|
||||
echo ""
|
||||
done
|
||||
|
||||
# ── fix phase ─────────────────────────────────────────────────────────────────
|
||||
|
||||
if [ ${#NEEDS_FIX[@]} -eq 0 ]; then
|
||||
echo "All hosts healthy — nothing to fix."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Hosts needing fixes: ${NEEDS_FIX[*]}"
|
||||
echo ""
|
||||
echo "Each fix requires one sudo session per host. You will be prompted for"
|
||||
echo "the nixos sudo password once per host; all steps run in that session."
|
||||
echo ""
|
||||
read -r -p "Proceed with fixes + Switch-nix on each broken host? [y/N] " confirm
|
||||
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
||||
echo ""
|
||||
|
||||
for host in "${NEEDS_FIX[@]}"; do
|
||||
step "Fixing $host"
|
||||
flake_target=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||
"cat /etc/flake-target 2>/dev/null || true")
|
||||
|
||||
fix_script=""
|
||||
|
||||
# Fix 1: restore SSH host key
|
||||
live=$(ssh_host_age "$host")
|
||||
want=$(registered_age "${flake_target:-}")
|
||||
if [ -n "$want" ] && [ "$live" != "$want" ]; then
|
||||
echo " Uploading registered SSH host key (private + public)..."
|
||||
scp -o StrictHostKeyChecking=no \
|
||||
"host-keys/${flake_target}_ssh_host_ed25519_key" \
|
||||
"$SSH_USER@$host:/tmp/recover_ed25519_key"
|
||||
scp -o StrictHostKeyChecking=no \
|
||||
"host-keys/${flake_target}_ssh_host_ed25519_key.pub" \
|
||||
"$SSH_USER@$host:/tmp/recover_ed25519_key.pub"
|
||||
fix_script+='
|
||||
echo "[fix] Restoring SSH host key..."
|
||||
install -m 0600 /tmp/recover_ed25519_key /etc/ssh/ssh_host_ed25519_key
|
||||
install -m 0644 /tmp/recover_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
|
||||
rm -f /tmp/recover_ed25519_key /tmp/recover_ed25519_key.pub
|
||||
echo " Done."
|
||||
'
|
||||
ssh-keygen -R "$host" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Fix 2: clear invalid GitHub token
|
||||
if ! github_token_valid "$host"; then
|
||||
fix_script+='
|
||||
echo "[fix] Clearing stale GitHub token (nix will use unauthenticated access)..."
|
||||
echo "" > /run/secrets/rendered/nix-github-token.conf
|
||||
systemctl restart nix-daemon 2>/dev/null || true
|
||||
echo " Done."
|
||||
'
|
||||
fi
|
||||
|
||||
# Fix 3: rebuild
|
||||
fix_script+='
|
||||
echo "[fix] Running nixos-rebuild switch..."
|
||||
nixos-rebuild switch \
|
||||
--no-write-lock-file \
|
||||
--refresh \
|
||||
--flake "git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(cat /etc/flake-target)"
|
||||
echo "[fix] Rebuild complete."
|
||||
'
|
||||
|
||||
echo " Opening SSH session (enter sudo password when prompted)..."
|
||||
if ssh -t -o StrictHostKeyChecking=no "$SSH_USER@$host" \
|
||||
"sudo bash -s" <<< "$fix_script"; then
|
||||
echo ""
|
||||
info "$host fixed and rebuilt"
|
||||
else
|
||||
rc=$?
|
||||
echo ""
|
||||
warn "$host: rebuild exited with code $rc (may still have succeeded — check sops-nix below)"
|
||||
fi
|
||||
|
||||
# Verify: re-check sops-nix result post-rebuild
|
||||
sops_result_after=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||
"systemctl show sops-nix --property=Result --value 2>/dev/null || echo unknown" 2>/dev/null || echo "ssh-failed")
|
||||
if [ "$sops_result_after" = "success" ]; then
|
||||
info "$host sops-nix: success post-rebuild"
|
||||
else
|
||||
warn "$host sops-nix: $sops_result_after post-rebuild (may need another pass)"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "Recovery complete."
|
||||
Executable
+318
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pushes newly-generated SSH host keys from host-keys/ to already-running
|
||||
# NixOS hosts, so they can decrypt sops secrets after a nixos-rebuild
|
||||
# following scripts/secrets/sync-host-keys.sh --regenerate-all-keys.
|
||||
#
|
||||
# Before pushing any key, verifies that .sops.yaml and secrets/*.yaml are
|
||||
# committed and pushed to the remote -- hosts rebuild from the remote Gitea
|
||||
# flake, so recipient changes must land there before any rebuild, not just
|
||||
# before the key push.
|
||||
#
|
||||
# push-host-keys.sh --all [--dry-run] [--skip-git-check]
|
||||
# push-host-keys.sh <target> [--dry-run] [--skip-git-check]
|
||||
#
|
||||
# --all Push to every reachable managed host. Default when no
|
||||
# target is given.
|
||||
# <target> Push to one flake target only (e.g. lxc-server).
|
||||
# --dry-run Print what would be done; write nothing.
|
||||
# --skip-git-check Skip the commit/push check. Use only when the remote
|
||||
# already has the current .sops.yaml/secrets/*.yaml.
|
||||
#
|
||||
# SSH: connects as SSH_USER@<hostname> (default: nixos, the user with the
|
||||
# admin authorized key), then installs files via sudo -S (reads the sudo
|
||||
# password from stdin). The password is prompted once at startup and reused
|
||||
# for every host -- no PTY or terminal required on the remote side.
|
||||
# Hosts are reached at their bare hostname (relies on LAN DNS/mDNS).
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
keydir="${repo_root}/host-keys"
|
||||
|
||||
# shellcheck source=../env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
# shellcheck source=../lib/nix-eval.sh
|
||||
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||
|
||||
: "${SSH_USER:=nixos}"
|
||||
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5)
|
||||
|
||||
dry_run=0
|
||||
skip_git_check=0
|
||||
sudo_password=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [--all | <target>] [--dry-run] [--skip-git-check]
|
||||
|
||||
--all Push to every reachable managed host. Default when no
|
||||
target is given.
|
||||
<target> Push to one flake target only (e.g. lxc-server).
|
||||
--dry-run Print what would be done; write nothing.
|
||||
--skip-git-check Skip the check that .sops.yaml/secrets/*.yaml are
|
||||
committed and pushed to the remote repo.
|
||||
|
||||
Environment:
|
||||
SSH_USER SSH username (default: nixos).
|
||||
SUDO_PASS Sudo password (skips the interactive prompt; useful
|
||||
when calling from another script).
|
||||
EOF
|
||||
}
|
||||
|
||||
# Prompt for the sudo password once; store it for all _do_push calls.
|
||||
# Accepts SUDO_PASS from the environment to allow non-interactive callers.
|
||||
prompt_sudo_password() {
|
||||
[[ "$dry_run" -eq 1 ]] && return
|
||||
if [[ -n "${SUDO_PASS:-}" ]]; then
|
||||
sudo_password="$SUDO_PASS"
|
||||
return
|
||||
fi
|
||||
# read exits non-zero when stdin is not a terminal (e.g. CI, background
|
||||
# agents). Catch that and give a clear message rather than a silent exit.
|
||||
if ! read -r -s -p "sudo password for ${SSH_USER} on remote hosts: " sudo_password; then
|
||||
echo >&2
|
||||
echo "ERROR: stdin is not a terminal -- cannot prompt for sudo password." >&2
|
||||
echo " Set SUDO_PASS=<password> in the environment and re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo >&2
|
||||
}
|
||||
|
||||
locally_managed_hosts() {
|
||||
for f in "${keydir}"/*_ssh_host_ed25519_key.pub; do
|
||||
[[ -e "$f" ]] || continue
|
||||
basename "$f" _ssh_host_ed25519_key.pub
|
||||
done
|
||||
}
|
||||
|
||||
# --- git state check/fix --------------------------------------------------
|
||||
# Hosts rebuild from the remote Gitea flake:
|
||||
# nixos-rebuild switch --flake "git+https://<gitea>/nixos.git#<target>"
|
||||
# so .sops.yaml (updated recipients) and secrets/*.yaml (re-encrypted DEKs)
|
||||
# must be committed and pushed before any rebuild can succeed. This check
|
||||
# catches the common case where --regenerate-all-keys was just run but the
|
||||
# resulting diff hasn't been committed/pushed yet.
|
||||
ensure_remote_current() {
|
||||
[[ "$skip_git_check" -eq 1 ]] && return
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
local dirty_unstaged dirty_staged
|
||||
dirty_unstaged="$(git diff --name-only -- .sops.yaml secrets/ 2>/dev/null || true)"
|
||||
dirty_staged="$(git diff --cached --name-only -- .sops.yaml secrets/ 2>/dev/null || true)"
|
||||
|
||||
if [[ -n "$dirty_unstaged" || -n "$dirty_staged" ]]; then
|
||||
echo "Uncommitted changes in sops-managed files:"
|
||||
[[ -n "$dirty_unstaged" ]] && sed 's/^/ (unstaged) /' <<<"$dirty_unstaged"
|
||||
[[ -n "$dirty_staged" ]] && sed 's/^/ (staged) /' <<<"$dirty_staged"
|
||||
echo
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would prompt to commit .sops.yaml/secrets/ before continuing."
|
||||
else
|
||||
read -rp "Commit .sops.yaml + secrets/ now? [y/N]: " ans
|
||||
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
||||
git add -- .sops.yaml secrets/
|
||||
git commit -m "secrets: update recipients and re-encrypt for host key changes"
|
||||
echo "Committed."
|
||||
else
|
||||
echo "Continuing with uncommitted changes -- the remote won't have the"
|
||||
echo "updated recipients until you commit and push."
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
fi
|
||||
|
||||
# Check if we're ahead of the remote tracking branch
|
||||
local ahead
|
||||
ahead="$(git rev-list --count '@{upstream}..HEAD' 2>/dev/null || echo "")"
|
||||
if [[ -z "$ahead" ]]; then
|
||||
echo "NOTE: no remote tracking branch found -- skipping push check."
|
||||
echo " Ensure the remote has the current .sops.yaml/secrets/ before"
|
||||
echo " triggering nixos-rebuild on any host."
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$ahead" -gt 0 ]]; then
|
||||
echo "Local branch is ${ahead} commit(s) ahead of remote."
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would prompt to push before continuing."
|
||||
else
|
||||
read -rp "Push to remote now? [y/N]: " ans
|
||||
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
||||
git push
|
||||
echo "Pushed."
|
||||
else
|
||||
echo "Continuing without pushing -- remember to push before running"
|
||||
echo "nixos-rebuild on any of these hosts."
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
# --- key installation (shared) -------------------------------------------
|
||||
_do_push() {
|
||||
local hostname="$1" target="$2"
|
||||
local keyfile="${keydir}/${target}_ssh_host_ed25519_key"
|
||||
local pubfile="${keyfile}.pub"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo " [dry-run] would scp host-keys/${target}_ssh_host_ed25519_key{,.pub} to /tmp/"
|
||||
echo " [dry-run] would: sudo -S install -m 0600/0644 to /etc/ssh/ and rm /tmp copies"
|
||||
return
|
||||
fi
|
||||
|
||||
# Upload to /tmp (writable as nixos, no privilege needed)
|
||||
scp -o StrictHostKeyChecking=no \
|
||||
"$keyfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key"
|
||||
scp -o StrictHostKeyChecking=no \
|
||||
"$pubfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key.pub"
|
||||
|
||||
# Install via sudo -S: the password is piped via herestring so no PTY is
|
||||
# needed on either side. -p '' suppresses sudo's own prompt string.
|
||||
ssh -o StrictHostKeyChecking=no "${SSH_USER}@${hostname}" \
|
||||
"sudo -S -p '' bash -c '
|
||||
install -m 0600 /tmp/push_ed25519_key /etc/ssh/ssh_host_ed25519_key
|
||||
install -m 0644 /tmp/push_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
|
||||
rm -f /tmp/push_ed25519_key /tmp/push_ed25519_key.pub
|
||||
echo \" [ok] host key installed\"
|
||||
'" <<< "$sudo_password"
|
||||
|
||||
# Drop the stale known_hosts entry for this host (public key just changed)
|
||||
ssh-keygen -R "$hostname" 2>/dev/null || true
|
||||
|
||||
echo " Done. Run nixos-rebuild switch on ${hostname} to activate."
|
||||
}
|
||||
|
||||
# --- single named target --------------------------------------------------
|
||||
push_target() {
|
||||
local target="$1"
|
||||
local keyfile="${keydir}/${target}_ssh_host_ed25519_key"
|
||||
|
||||
if [[ ! -f "$keyfile" ]]; then
|
||||
echo "ERROR: host-keys/${target}_ssh_host_ed25519_key not found." >&2
|
||||
echo " This target may not be locally managed (e.g. &${target} was" >&2
|
||||
echo " registered from the host's real SSH key, not generated here)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local hostname
|
||||
hostname="$(flake_target_hostname "$repo_root" "$target")"
|
||||
if [[ -z "$hostname" ]]; then
|
||||
echo "ERROR: cannot resolve hostname for '${target}' from the flake." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> ${target} (→ ${hostname})"
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" true 2>/dev/null; then
|
||||
echo " SKIP: ${SSH_USER}@${hostname} unreachable."
|
||||
return
|
||||
fi
|
||||
|
||||
# Sanity-check that /etc/flake-target on the host agrees
|
||||
local live_target
|
||||
live_target="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" \
|
||||
"cat /etc/flake-target 2>/dev/null || true")"
|
||||
if [[ -n "$live_target" && "$live_target" != "$target" ]]; then
|
||||
echo " WARN: host reports /etc/flake-target='${live_target}', not '${target}'."
|
||||
echo " Pushing the key you specified (${target}) anyway."
|
||||
fi
|
||||
|
||||
_do_push "$hostname" "$target"
|
||||
}
|
||||
|
||||
# --- all managed hosts ----------------------------------------------------
|
||||
# For each unique hostname derived from managed targets, SSHes in and reads
|
||||
# /etc/flake-target to determine which key to push -- handles the case where
|
||||
# multiple targets share a hostname (e.g. lxc-server and proxmox-server both
|
||||
# resolve to "server"; only one is actually running).
|
||||
push_all() {
|
||||
mapfile -t managed < <(locally_managed_hosts)
|
||||
if [[ "${#managed[@]}" -eq 0 ]]; then
|
||||
echo "No managed keys in host-keys/ -- nothing to push."
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Pushing to all reachable managed hosts..."
|
||||
echo
|
||||
|
||||
declare -A seen_hostnames=()
|
||||
local t hostname
|
||||
for t in "${managed[@]}"; do
|
||||
hostname="$(flake_target_hostname "$repo_root" "$t" 2>/dev/null || true)"
|
||||
[[ -z "$hostname" ]] && continue
|
||||
[[ -n "${seen_hostnames[$hostname]+x}" ]] && continue
|
||||
seen_hostnames["$hostname"]=1
|
||||
|
||||
echo "==> checking ${hostname}"
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" true 2>/dev/null; then
|
||||
echo " SKIP: ${SSH_USER}@${hostname} unreachable."
|
||||
continue
|
||||
fi
|
||||
|
||||
# Ask the host which flake target it actually is
|
||||
local live_target
|
||||
live_target="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" \
|
||||
"cat /etc/flake-target 2>/dev/null || true")"
|
||||
|
||||
if [[ -z "$live_target" ]]; then
|
||||
echo " SKIP: no /etc/flake-target on host -- can't determine which key to push."
|
||||
continue
|
||||
fi
|
||||
|
||||
local live_keyfile="${keydir}/${live_target}_ssh_host_ed25519_key"
|
||||
if [[ ! -f "$live_keyfile" ]]; then
|
||||
echo " SKIP: host is '${live_target}' but no host-keys/${live_target}_... (hand-registered key, not managed here)."
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " target: ${live_target}"
|
||||
_do_push "$hostname" "$live_target"
|
||||
done
|
||||
}
|
||||
|
||||
# --- main -----------------------------------------------------------------
|
||||
mode="all"
|
||||
target_arg=""
|
||||
extra_args=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) dry_run=1 ;;
|
||||
--skip-git-check) skip_git_check=1 ;;
|
||||
--all) mode="all" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--*) echo "Unknown option: $arg" >&2; usage >&2; exit 1 ;;
|
||||
*) extra_args+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${#extra_args[@]}" -gt 1 ]]; then
|
||||
echo "ERROR: specify at most one target (or --all)." >&2
|
||||
usage >&2; exit 1
|
||||
elif [[ "${#extra_args[@]}" -eq 1 ]]; then
|
||||
mode="single"
|
||||
target_arg="${extra_args[0]}"
|
||||
fi
|
||||
|
||||
[[ "$dry_run" -eq 1 ]] && { echo "[dry-run] no changes will be made"; echo; }
|
||||
|
||||
nix_extra_opts
|
||||
ensure_remote_current
|
||||
prompt_sudo_password
|
||||
|
||||
if [[ "$mode" == "single" ]]; then
|
||||
push_target "$target_arg"
|
||||
else
|
||||
push_all
|
||||
fi
|
||||
|
||||
echo
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply."
|
||||
else
|
||||
echo "Key push complete. For each updated host, run nixos-rebuild switch to"
|
||||
echo "apply the config and let sops-nix decrypt secrets with the new key."
|
||||
fi
|
||||
@@ -359,8 +359,11 @@ cmd_regenerate_all() {
|
||||
echo "This will remove and freshly regenerate ALL locally-managed keys:"
|
||||
printf ' %s\n' "${hosts[@]}"
|
||||
echo
|
||||
echo "Every host above will need its new key baked into a rebuilt install"
|
||||
echo "image/tarball before it can decrypt secrets again."
|
||||
echo "After regenerating, each host needs its new key before it can decrypt secrets:"
|
||||
echo " • Already running: push the key before rebuilding:"
|
||||
echo " scripts/secrets/push-host-keys.sh --all"
|
||||
echo " • Not yet deployed: rebuild the install image with the new keys baked in"
|
||||
echo " (see docs/auto-installer.md)."
|
||||
|
||||
if [[ "$dry_run" -ne 1 ]]; then
|
||||
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
|
||||
@@ -397,6 +400,16 @@ cmd_regenerate_all() {
|
||||
echo
|
||||
echo "Regenerating fresh keys for every current flake target..."
|
||||
cmd_all
|
||||
|
||||
echo
|
||||
echo "Next steps:"
|
||||
echo " 1. Commit and push .sops.yaml + secrets/ so the remote flake is current."
|
||||
echo " 2. Push the new host key to each already-running managed host:"
|
||||
echo " scripts/secrets/push-host-keys.sh --all"
|
||||
echo " (this also prompts to commit/push if step 1 wasn't done yet)"
|
||||
echo " 3. Run nixos-rebuild switch on each updated host."
|
||||
echo " 4. For hosts not yet deployed, rebuild the install image (see"
|
||||
echo " docs/auto-installer.md)."
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# Detects and fixes drift between the ed25519 SSH host key nix-cache is
|
||||
# actually serving right now and vars.nixCacheHostKey (variables.nix) --
|
||||
# the value modules/nix-cache/remote-builder-client.nix bakes into every
|
||||
# client's declarative programs.ssh.knownHosts, and
|
||||
# scripts/proxmox/configure-nix-cache-client.sh hardcodes as its own
|
||||
# default for non-NixOS clients.
|
||||
#
|
||||
# This value has no automatic source of truth: nix-cache's host key is
|
||||
# generated once (first boot / container recreate) and never touches this
|
||||
# repo again unless someone remembers to update it by hand afterwards. It
|
||||
# drifted silently once already -- confirmed live: variables.nix recorded
|
||||
# a key that no longer matched what nix-cache actually presented, which
|
||||
# would fail every real client's SSH host-key verification for
|
||||
# distributed builds without ever producing an obvious error pointing
|
||||
# back here (a client just sees "Host key verification failed" against
|
||||
# *some* key, with no hint that the trusted value itself was stale).
|
||||
#
|
||||
# codex-maintenance.sh runs this in --check mode on every invocation so
|
||||
# that drift surfaces as a warning instead of a future debugging session.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/secrets/sync-nix-cache-host-key.sh [--check] [--dry-run] [--host <name>]
|
||||
#
|
||||
# --check Only report drift (exit 1 if found, 2 if nix-cache is
|
||||
# unreachable); never writes. For CI/maintenance use.
|
||||
# --dry-run Show what would change; never writes.
|
||||
# --host Override the hostname to scan (default: variables.nix's
|
||||
# nixCacheHost / env.sh's NIX_CACHE_HOST).
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
variables_nix="${repo_root}/variables.nix"
|
||||
client_script="${repo_root}/scripts/proxmox/configure-nix-cache-client.sh"
|
||||
|
||||
# shellcheck source=../env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
|
||||
check_only=0
|
||||
dry_run=0
|
||||
host="${NIX_CACHE_HOST}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--check) check_only=1; shift ;;
|
||||
--dry-run) dry_run=1; shift ;;
|
||||
--host)
|
||||
host="${2:?--host requires a hostname}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
sed -n '2,23p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
current_value="$(grep -oE 'nixCacheHostKey = "[^"]+"' "$variables_nix" | sed -E 's/nixCacheHostKey = "(.*)"/\1/')"
|
||||
if [[ -z "$current_value" ]]; then
|
||||
echo "ERROR: couldn't find nixCacheHostKey in $variables_nix" >&2
|
||||
exit 1
|
||||
fi
|
||||
current_type_blob="$(awk '{print $1, $2}' <<<"$current_value")"
|
||||
current_label="$(awk '{print $3}' <<<"$current_value")"
|
||||
|
||||
echo "Scanning ${host} for its current ed25519 SSH host key..."
|
||||
nix_extra_opts
|
||||
scanned="$(nix-shell "${NIX_OPTS[@]}" -p openssh --run "ssh-keyscan -t ed25519 -T 5 '${host}'" 2>/dev/null | grep -v '^#' | head -1 || true)"
|
||||
if [[ -z "$scanned" ]]; then
|
||||
echo "ERROR: couldn't reach ${host} (or got no ed25519 host key back) via ssh-keyscan." >&2
|
||||
exit 2
|
||||
fi
|
||||
scanned_type_blob="$(awk '{print $2, $3}' <<<"$scanned")"
|
||||
|
||||
if [[ "$current_type_blob" == "$scanned_type_blob" ]]; then
|
||||
echo "Up to date: ${host}'s host key matches variables.nix's nixCacheHostKey."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "DRIFT DETECTED:"
|
||||
echo " variables.nix has: $current_type_blob"
|
||||
echo " ${host} is now: $scanned_type_blob"
|
||||
|
||||
if [[ "$check_only" -eq 1 ]]; then
|
||||
echo
|
||||
echo "Run 'scripts/secrets/sync-nix-cache-host-key.sh' (no flags) to fix." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
new_value="${scanned_type_blob} ${current_label}"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "(--dry-run: would update variables.nix and ${client_script##*/} to:)"
|
||||
echo " $new_value"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
sed -i "s|nixCacheHostKey = \"[^\"]*\"|nixCacheHostKey = \"${new_value}\"|" "$variables_nix"
|
||||
sed -i "s|NIX_CACHE_HOST_KEY:=[^}]*}|NIX_CACHE_HOST_KEY:=${new_value}}|" "$client_script"
|
||||
|
||||
echo "Updated variables.nix and ${client_script##*/} to:"
|
||||
echo " $new_value"
|
||||
echo
|
||||
echo "This only takes effect on already-deployed NixOS clients after their"
|
||||
echo "next rebuild (programs.ssh.knownHosts is declarative). Review with"
|
||||
echo "'git diff', then run 'bash scripts/codex-maintenance.sh' before committing."
|
||||
+220
-58
@@ -1,90 +1,252 @@
|
||||
root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str]
|
||||
nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,type:str]
|
||||
nix-github-token: ENC[AES256_GCM,data:OfNRGJg16Ede6EilWUetCs9za+xk5/Lsa3SpVajsqz8PMdA1xQNeCWdX7ZAMdijHClpBhU6ETFGsXvt41O9aORS951uijeGSW7/NH35/bnPISrKdYeBx/+xEiqwH,iv:QGU3v7xOy89uzRTCb1U9ICyJ8XYIpXrUsDt12aL3g2Y=,tag:Bde2wcWNv8H4WLxSEUAodg==,type:str]
|
||||
nix-github-token: ENC[AES256_GCM,data:k1vYz7SqVhzpWa6jTL6NUD8lKOCpHCgTm+HT4IcnbzbSTUZP/bJUYw==,iv:UqAULZnr/4+VcioUDfTwvOSuwM8K9JgGhiApvYQPyoc=,tag:1LKHXhWAO/AHPDIZFBb04A==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaMUpxYmUzaVY4d1pRY3g3
|
||||
empaVUJSN0JaSlNQQlJtZENwc0VMYVhJSkFvCmJQMkpSd3RoYzlKQjBSZWErQzR2
|
||||
aG5SVndOQWpSRTBDSWJVQkg0c0hiNFUKLS0tIHhiaEdpY3gwZkpCcHl2TW5CNThn
|
||||
aVVUQy9Qd0trb0RNdUpVTXdrdGlrTmcK1uphQAyDV+Gk5+K1YOqw1Z8ynGP5sAPF
|
||||
q5icujja/SGexX18hPYXbkyUtOrBYjW62gCuGJinSBPROoFUJbiP7g==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBibGF2a2FRcTRFTk9rQ0NN
|
||||
TTFoRlBWbGdSMzVVRHYvVFFvVm05ZDZBb1VrCkFoM2dTMEJDMHRKd240OEExWnUv
|
||||
RHV2S0taWnl0dS80YVIvTjBpVEk5a0kKLS0tIGF2TkNQdlVJTTNOSnI4cEZCRll3
|
||||
NE9aSWxVVTlyV2hPQ284cU94Y2hVNEkKhUFgr345o91orzqCzkKdh6dAXlPfwqTJ
|
||||
su5kATsokLzcyvT/gcOKsDBzO219CJ0cSVmpgVJtImSCJDwUS3A6Ug==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKOWZxcFlhRHhKelhDS29M
|
||||
NGRlTEhJUGUzcFV6QVFCdTNkdEN3MlljWnl3CnI3LzVxSHBwSi9TYlRUcFdyN2py
|
||||
cVZBV0Q3Z3FhRXA0T1NFQzd2R00yeEUKLS0tIHkxL2dHV3RkVFYxZTJNTVZvVFIr
|
||||
TXpJZzdnYlpJaXBmcjdWWUtxNkc4dWMKVsImJiavzUzSFn78pciNJPHaS3KWqJer
|
||||
VkxF6kF3tl2HmW46eVXtsQowdu+zVR9HS35i/HvQ1r1TyP6qT5ofcQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGc3cwNzk1d2xDajFqQTNH
|
||||
QlhoS0ZqWnJ6b1NoU3EvMlFsTG1oYkVrYTI0CjNEekFrV285aVc2Vk5pc0VHNDBO
|
||||
MndVZHgrbE10TXVOL0I2Y1hKWEJCcm8KLS0tIHRoa2JtNHVLU1YySmlXMnpsY3lh
|
||||
R0NTR2szNVhvVy81bHRjLzJSTWNWMEkK26wIFqhVk1fYV79odbslVaFxoDLykRNN
|
||||
ZiOkWewH/JOuIXR260UWAGq4bKXfnUs78mAchh9R2I0F4p/CjGMdQA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyWjdSanRRaEI4VHFZVkdw
|
||||
NkxMODRuakFJTFNoTHZIRHptZFNxaGZ3K3lnCjJGR1FJalRsYmtLcHFxZW8yNU5W
|
||||
MzBjWmltbzI3MTByUjB6djlEdllHNXMKLS0tIEhEOVhLVjZkRi9vUEtDWUhxT0NX
|
||||
eUZ3bGdEaHdGbXZIYkowYUZuTWJ3d2MKMwgxOqlMH7GfLlseD4J277Dcg0KCD3d5
|
||||
jwJDb82kRWoBMicTJZXoq/5oe5blJNa7dWYoqgkYxMA2O+0igT+I6g==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVRTE5TUNYWUpxbWdIenB0
|
||||
WVdlMXFWK01vMndHTDhRdFppRC9MY09xeFZVCkhEL21Pd1pzcjlKQld5a0JvMjlu
|
||||
QnhXNDlETmV0QXJ6ZWhzdGROZ25MQjAKLS0tIDQyclo1ZWtkM3VNbjJ1dHhTZXk4
|
||||
OWRVdXZEZ2pEWXJGQmNaMm1aT2l0aWsK43Oc54CGekbbBkcnctXnUMyqo8ygmBL0
|
||||
BMC6FtUVYMkP++Y7KFR6sudMemf9f+FbsCVRqBd9x+c2YzzgyH8ogw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsZzdRMm9zdlJHckFoREVs
|
||||
UkVXL3dCeWRuN2NqcnVsOWVNT1FLUHY3andrCmNFY0ZCby9SdnUyYlV6R05YNDB0
|
||||
em43SjRCNVhYbmpRZUVWT0cxQlhGdmsKLS0tIDZHazNyck5VNHBuNVM5bmRZUlpR
|
||||
QzBSNy84VDdLVkZZbnNlUFYydXlreEEKYZaR2b7tyRAhPdP+ytpP0veUTi7pY9Nw
|
||||
pK0h4hcegLNYJL1AfOYwFQoW7vb256GdmwdcuuBl1YBGXWGraaBnZg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEaEtNL243bFF0am9HSkZp
|
||||
anh1Ukl0bTJVUGJXUzM2NFo3Ry8yeTBVUUIwCktPVkpKNXBOYjRTMWNlay9Ia3Nj
|
||||
bURMbFk4WUw3NVhUb25mZmtSU1ZRbHcKLS0tIHdkVEovUVpxMk5aVGRPMWdpaUdj
|
||||
NzdISWtmYTFBb1FkT3hTeWFnaFJWOGcKdcLL3/fAgDME0Xsx7lS4q1boG8t7no3T
|
||||
BZ6kdHnWe7fdzoTrCsaxWZPRom1hs10FZ4H+VI+CiEKki0N2XDMB4Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFSnZiOGpsL1BESWVEdUxx
|
||||
dXc5LzBHTlNodGJ1S3hOeHd5Wmgxb3BPSHl3CnBEWDNTc3NDdzN3RHQxUlNLOEs5
|
||||
RU1SS2tVT29XbDJCWVliWnVkekN4L1kKLS0tIGFqY2pNK2h2S2ZWMndrRVN6eUdN
|
||||
M0NLY2EvTTVIYlZvdk9XY0NwNE14ZlEKcOwgCK3g56kId/4tEt+2iCcylisn+Fca
|
||||
5VWamsXdbwxbxmCEEUbgN3aHrdVz3mV4+8FZqA34yXz8pc45/PUcFg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJU1cydFFuRnpCUU5mbjFu
|
||||
TTIxREkxZ1hMalVtNjNjcHVaUXpPNWdib2tzCkRScWRTWHhxN3lhckI4UFpMSFRa
|
||||
RGxWcldlWnd5SVJFNWxQWjB3R0pMb3MKLS0tIGdjLzlhTHgyT2hjTmRTK1RiZmxS
|
||||
OWRWYklBWkoyUG8raFNzd3JlRUtEUEUKw+NdAp1Mz0dEOUGPbqCV8y7029I3Kye0
|
||||
keU2T29JGCN1D6x//1NcMUYSaFZKgv0ZVSjVUCl1EnmlJ0nBdbaDXA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQTkRYelcvY1VEcThELzVo
|
||||
a2E5QVNnME5VMU56SDduVlZKQUdhaTlKY25rCllGRk5GZlJMb1c4M0o0dkREZFFt
|
||||
MW9BcDU2MkxWeHVESUtqcHpWMDRzLzQKLS0tIFZSU2J0RXoyQUxlYVNUa2t5ZVhp
|
||||
bEVlNURjeGRvdXkrS0N1eDllV3lJTGsKc/4cEEhEJwmLSxUDFzAMxD+7TWcOKNjD
|
||||
+Kcz2+sX8sTXLBLCGABR1XQdy1aejVku4atujnDOE6BzYjjfkRfc9A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnNFlzYVBqaGFDVE9xbmt5
|
||||
OVd4ZlZRdEVJd0JFdkRIYVA5ZUEwYjJjWVJjCnZzTExNbDlYbGN6c1dYU2ZlNEVs
|
||||
ZUo4MXdpc0tzbDI0MHl5eXc3d09VRDQKLS0tIGs4SGMya1RZQ0hpa2NtQk9Wa29C
|
||||
bmxIYWk1VThsaFhxRDlOOGhGQ2tYamsKs+PcOiaeNFujCWwZBr+nq1MzrGohl+ch
|
||||
TASI7eNsiHmSVRzSMHv4mX+8yXDHIRgHbFuUty2gdgGFLRjYXe4gDA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkcmVQYXV1NzRrQ2IxTElt
|
||||
QUJRMkhiUUJqRDdwQW03K0lCTEtkSTU5UDJrCjh6SldpQkF1NzIybTJTdnBlOHdQ
|
||||
VlREMy9hM0ZaL21Pa0VPUzcvb0pQQjQKLS0tIFcrK0I3Z3Nhem45eHpZYmdWeit6
|
||||
eXFtRHVxODhaNTVDdVR1a2tOb1N1U1EKZBdLHq8PRMKX2ndFr3AxFVAZRyvhFa1u
|
||||
72R5tordo4IR3HSxG3Z06rokOITd+KKhaQ8NEWEC8qioAAMxEC9QWw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGa3RDdTdFK2VxTGYvYm1R
|
||||
RlRkRFZqTWVXSWRMd2ZjckV4bXZSWnVXYmc4CnFTNnRQSFJORWdlTGRUajNjZTNL
|
||||
SkhjdVhFSGJHZ2ZvRnhvaHFhVXQzek0KLS0tIDRXUTZTU0Rlb2crTWd6VDV5VVcx
|
||||
Q2x1TnAxem45RXordHlqYy9CRzBzTlEKvKncXgXO1rJcHRXJE//7YbnwqCx+472D
|
||||
XXby5thGhzTSTgcNcakyK20fzNlOYr3D/iRA4Ytcd0zHcGlj4qVaQQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0WjJPbXhXNTNyQUZzMmxS
|
||||
Rkd3Yk5Ga3pPRXFtMFpXcmpBZ3RJYnJlaHhjClRDbFBHRVZ0SDdrNFdVWmUwalpT
|
||||
WUt5a2RUTnQ4VFBGMlowR3JycDM3aXMKLS0tIERSeFd4cGdSbzRpSmpSQnB1Yng5
|
||||
OHBma3lYeWEwQ2RvelpzZHRkN1JPWFEKLXkJfEkH3lo5Z4mj8PZXTfAfZn6B46To
|
||||
U0G54tUh0U1EeMnI5ZPRGwDxi5K0eD68HjeE9MBvFBysOChP/ANDPQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnMVZiZUNob2pqdTA0NlJ1
|
||||
UTdGdlNEVW5YOHZaYlZFZy9YaGVoV3FoR2hzCjdwc2F3bkVaUmg3dTZ6b1F2Ymwy
|
||||
QkNvb1NEYnhLUkgxUnFYb0R4U0puZ2cKLS0tIFdrbktMWTRSQS9DT3doY2RaODJL
|
||||
Wi9YazhEZzZFVXZpS1k3SE5wRWlLdzgKnlng++3X0NvOvl3qES0J166pXGM46dy8
|
||||
38/RE0rjc+92+JnOXsgBUfvmS0td+oR5T4lxGpdngksv6gyPaU71Pg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
||||
lastmodified: "2026-07-19T02:30:40Z"
|
||||
mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str]
|
||||
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLRmpiOTdKYmVJOEJoRFRl
|
||||
bm1UcjhXeWQxdWhWMm0vQXgxWEg1VTREekJJCjFJYnFkZFFlRUM1MHo4djBjWC9h
|
||||
YlBlUVl4R3BOelNqY1lPNVBINHFtRTAKLS0tIFk5QU9qWW1jRW90SmdXVU1naUJk
|
||||
R1V2eGhrci9CYzdkQnRYTnAybVY4L2cKw3LFTr8m8OMmlVysBZbiMKKxaA7Nt8F4
|
||||
CJEgkcPyQGOFnNen7UAX1ksJrm+3gV7ZL91VxzVvCdd6zScHlG0PFg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5Y1ROSGlLcFczdjJNazFE
|
||||
TUZJVmg2RldsT0pQdlZrRmNNL2ZibXNoZVNFCmZ0TXN6ZTJ6SVluZDlIVjJGSm8v
|
||||
N1d4T2VHZEdiSVZ4ckpCY1pRQlFDcjgKLS0tIDAySFJGVC9kM1hVbEZqUUdqSHhv
|
||||
YUFUeGlCcU9GMHAyZVBnWlRDSm4zUU0Kw6c8zMdxGwpNlukkw9ECT2aXJW98qNh3
|
||||
wjUXDzmXNk9f8LNvj67na/JvsqXR4yxXFsEQkphAm6DvLkZ+5BfJiA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzQ0trMEVBcm9RQzFXUUlF
|
||||
ejBOYkt4ekFWcmxub2FETElidzI1SXZCZ0JJCldEczV6TGhzazdiOWFiWjVObU9N
|
||||
NWdDOWpHWFRydHo0UjdzdHVYZ2pIeUkKLS0tIGN0RHBTbEUzMDVnK1Z3UGVhZStW
|
||||
ZXJiQmhKYUtSdzRDd0JGK1h3VExoc0kKM+tX68bgbAgdi1Gk3MeW2YXwmmszGEeR
|
||||
WCrKI5hkBU6ZaAMF6LU9idvdUqFCdJ4l8up5WSQuO+10a0pvYGj/Kg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEYVRwdE5vVjBKczFBY3pj
|
||||
Q0R0ZDZkUU43N3Rpb3NCTWg1czFrMWtrWEhBCjlONWhjWWZISy9oUS9pRFFlNXlZ
|
||||
Ri8rbEMzKzV3R1NvN2orT2o4ejNPYlUKLS0tIFVDSXZaaGxMSWRpTmphYmRuMXpH
|
||||
US9qYS9MZThjbkppcXNQUFdzZ1hDUWMKa9UH9GgZt8UD+TWcR3jnkNsVsfOlWAFw
|
||||
cgh/gddnKu8lTa+9g6P3gi85/JhNqt3Fl4mmzk1aU0RjOIadzPWs4w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1SXpOLytMRGtzSUR6aE9S
|
||||
NWpIZ3RhRElIN3dTKzNrZWoxeDVTK1Y5eUdJCjF1cVJTWnFKT2RGYkNOQjRCUm9P
|
||||
V1pPSlhQaDFhWXBJV3FMSlp3N3FXVzgKLS0tIC9PQkRrZ1g4YWMyaW9zbGJ4V0lw
|
||||
VDJneWs3NTJoem5oWEZFVkd5YlplQ0UKFXMBfbrVMyydpCHRaqmkYOS/+CufsDGN
|
||||
69Whneu9oXKGnbrFN+ikh+2O48VLWr9DSiWb0QcIagkNlxXcV3Vd+Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEL0d5NytCUzNyV0k5c3VH
|
||||
Tk9FK3FrcmdGVDJmN0lMTTVoaThJbUUybVhjCmVjNXVrdGp5SXJ2czVPa095cU5j
|
||||
YUo0RXNXQ0dBQmlVOFlGZ3dsQTJ3MUUKLS0tIE4welZDbDZ5OVQwNk9reGozSG9P
|
||||
Rkdiazg1ZjBmZVlORWtGVEREM0F3em8KFqIg8N82g3E8irYf8Zw0OtLWVH7PzLmh
|
||||
bQkotIzeemREJjA6Q9mIxojG1mD4DcexIBSkBAhIH6vM8Nksa5H/xg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkdjNHQlpCd1RJWFg5WkVY
|
||||
NDU2TFA3YnlKekRKNkUzcmYrem95SG1uWXlrCkFpSThsdjNDSzh0eFpESVNEalJY
|
||||
QjVlVkhQeTB1SXVpT0JvNWV3VXJFVFUKLS0tIG5SbWp1bDhDdFlvWXEvMUhxcy9R
|
||||
KzBuczlWZDQ0cTFFb1dyVEpidjR5OXMKUXPeEpeoxb5egqBBI8auM4MJzZ3RbMrG
|
||||
0L9Y24zY+1i+QIM7WIlanYsNyhrRMHy1mXBjRHs/ka7ibyO9HUSaBw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuMHVoK2VEYUZnNkc2VnFN
|
||||
ZVRxTzh5Rk0zQjRzT3ZvcG1FWnBGL1k3Zm5BCm16amZ5Yk1PT3dBRmJEMzhBdlRK
|
||||
bG5hMFdDS1owZ3lsMkJhSmZSc0F4S28KLS0tIFVhaGFCd3dUaThFYUZYT3hGaEJD
|
||||
OE92TnowVjN1OTl0UVE5SjdtNFVaMmMKGcM0fE5+iL6Oz9pHCCMlpOwwQruAG2Ur
|
||||
SXu4gqz4vQNZlmY5o3hQi9zmZV4OOlKhwmInc+5vE2ypeL1/zNBX8A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBobTM5QWgyem5uUlBybWVv
|
||||
RE1jUGtlYXdZOHhmMFlIekpvVWNxV0J1L0RJCjhsTEVBY1V4WnhGUWNmSVJNNFor
|
||||
OVI0bjJwd09NMkJkU214bnRFQVcvdzgKLS0tIFpIaWhZVWh2TGFNaG1aMWYzcDFF
|
||||
NFdMeGdBTkxoWTgyeUdxL1RqTkNSdEUKb2tuVoaqRY+B+KgsbfCnWwi299aVSH/N
|
||||
4GLnmC97DoIIcMF3NJbHXPyUTCCrJ142j3rXGrpATkO3IaeMTfeN1g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpS2ZqR09PSEpaN1FYSXpm
|
||||
akl0QzAwbnUrNXBFL2RaaythUU1Td3FicWtNCkFtMG9Ka05PSUpCR0hiNnpBVyth
|
||||
TXNLa3Z2L041SEl0bjRzR2dRWGZNVDAKLS0tIDcrV255cVFRTXJ0bVcrcVRFWVdZ
|
||||
WGdVNHNIL09tVS8zTSs4VHp4dXdoUFEKkXqeKKwQ8fp4AvIy5Mzf/hD9IO3NgAw3
|
||||
70TH4YjCEiBpd3Q7/+iyB4yBYCayJ8U+KjC1Q8aTLQEqQKyiEcheoA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6b2hSbm02NjhPb2s3WDQ3
|
||||
VkFuMVg5eVdoTmtnbjl0L05EN0hqVThXY0RZCk1STXZveTZSSURkNnpDTVU0L3c3
|
||||
UkJNclhLcEdaMXFDaEdLZEVjL2w5c1UKLS0tIGNnME5mVWk3aU1LelJGb1UrV0xS
|
||||
UHl0Z0hWYlp3b3MvN0FETndicCtpd1kKOMn31Y0XKogmJpm3CrIoqEIrwKqjZxE1
|
||||
2N8LX8N7FJNQW3LaoyTok2hbe7KjhASlXVapDM8aozNouKnGv5h6Kw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvTGpYNDdYbU5qWkRJRVRu
|
||||
cmt2MEs5bTJUV0k2QjA3VW91eDQ2ODVUWVRrClZORzJSVmh0eFNyQmtDNjMxMGZs
|
||||
L1grblE1V2hPcXBFMHRIOERJL2pBZ0kKLS0tIFNLbkZRK2Z2Y3N2Y2hTSUgzcW5m
|
||||
WXpCZEdoelo0SXNKcnNuMmg4cDAzeDgKtGn1ZDRCbrTdmzCn9st/61rKOvEZqCtG
|
||||
0brTsCk21iWLzKrMsg/B+kNiPB9/bAhessFJ/TDqZPn9/DWZ0x97Dg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyQ3Z6SkNyVW1kNWdHemZi
|
||||
R3owRUViRVBFMFhXZjYrcWhzUnFjZzVkb1c0ClhZNWp5NTVpcitkSmlkMWoyL0kw
|
||||
ZkExZ21Nb1E3TUtvT3BNdXEycTYwL2sKLS0tIE5MaEFyMEJnV1FiWUZxYWFsNkQw
|
||||
czNCdjRZZEZIOWxXeFR5Q1kxQkNYK3cKLq/eGWojEiqYwcwvnKGxHLHFhvCGLqlP
|
||||
rVu2LqLqmEkrblRas5JJ4cQg9wd9XQuxdTOT5GSFUtZrX+P7XTbhNA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuUS9MK0hsbytBWTk3bVdJ
|
||||
L0xiT2xhUG5UU1dtOXBlMVg4VFVZTDJkZVJrCk9leDBtUkN2bnBUeE9BNnY1NnlF
|
||||
TVVHdUowY2pBZXBIcnNUTG1aYllwUDgKLS0tIFdpRnVZRGxmK2JYeVJMUEtMQWxT
|
||||
cnBIanpxYmQ1U0U1UXdhUS8zdXRkTVUKrEoNKzY3DXyu547GmbSsXVmIDvIoNCi9
|
||||
hqVSCXG1r4t/tKfVuNyUsBDzfboiRgH+5gNk+h4hxmbI2w472nvPXg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZR1F4Q1JBa2pscXVKTUli
|
||||
eFdDZWdsYlY4cmQ1ZFBreXJDOW9LaW45ZWx3CnA1MWJYSC9ucW5OT3VWQ0w1ajR6
|
||||
eUN5eE4xUVFxdGNEakpQYVNuaWR5eEUKLS0tIG53RDIwZDVDWFd3ZjIwd2laK3JQ
|
||||
a0hRZERlalBWTXZoUm5lYmlYR2dGdFkKIbB5cuiUOGGmZx5QxuZbYeUjNORT4pKa
|
||||
ASvNnGv9aM5uJebWLhhO0Kak3dM+OxP6+tt1/MNNP1LxUqFPJuJdeA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxb254bVQ0OXQ3RDNZMWU3
|
||||
aUhLQWZYa1VobDdCbHZyb1JFbGRBNHBQUkVRCmR4ei9oTEhQSkhUK29PemdRbVNn
|
||||
elQvN0UvUldnRmRNZ04xbDlCL202WlEKLS0tIFdiVS9maFYrODMrSU9aMFlXRU9U
|
||||
a1BXUWozUEF3YlU4RHBhTytEVktTNGMK8KiRVfHfgWojDSS5k5g+NuIIFZSucBOc
|
||||
PoZNWFmDyMmL0IDvTYWS8lgzgppgjSXkecIGtTGmiiXpTxot05jLAA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMU1ZFRVpld1ptM2lnYm9u
|
||||
QmhSTE95ZUM1cVJUV09sdVhTMGVtRnRpb0JJCnFucml1RGtxdVJCOE0xUXNXeVdt
|
||||
VnVBYXFhVUpGamhCbWtySnhGeEtKNU0KLS0tIEp4OGRGYXR3WHhNZHgzU3hNS3E2
|
||||
RVpSMForUGZRS1dUR0hacnBQQ0dLRW8KK77iAvB2e75eGPC587lCBic6HaKypnnn
|
||||
HAbjBkl7ZwyKWpyR0pOTNauHWh2D8WDGjc2ciGaI7j0cbNeTdzCyAg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTTUd4VWF0bXZQbGFKSFg1
|
||||
K1ZsYUNlMHpSNG1WTlRxMUJmZ2I1aVNHYVY0Cm9FWGZDYnhhdXNCZUxJZmlZdGh2
|
||||
ZG5FWkJJUitVMDc5L0Qza0JqQzBsN3MKLS0tIEYxanNSakVud3c4NHRyZlNnNTVp
|
||||
ZVo5TUVXcFYyeUk1eXZ6VldWa2l3b1UKCuXfXW12E0ybEmADPBsuno9lUH+yZC30
|
||||
buTHSyG2WUpV2J8Om0uxUrnbGXKLUhdjUut2OijAi/GQP63CWMNlMA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhbjAzd1lOM3FaVGxIUE5Y
|
||||
ZENGZlZLNVZsaTg4U3MxQU9PMTNhTjRVY1NjClVVWUNUaWY1c05udUJVOG8vOGJv
|
||||
dXFlZUxxQzlCaGgwYWp5TmtBbFpiNzgKLS0tIHJSMDNMRG5ONzd0d3hoQ0FLOU93
|
||||
TW5HWnBRTXJwN2VjTllqTVRBcHBiTUkKcI7du8jo9LWLtwnsDFyec+fn4w6zWyaw
|
||||
r7m05cm4sKv6xDPMZrGNlwRziA327qn8/7/xB+Tl62+eECaeIqLUUQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArSG5nejgrdDBBMnhzYVpo
|
||||
eENwVzA2dnlPZS9DaXhBa25EdzArS0VIbURjCmhiYUNMOXRHLy9xaEJZZExRanpl
|
||||
dGhiZzlyN2d5NStOdmZMWnBhbURQNU0KLS0tIDhDNFNleWNsK2VITHRvRVZKb2xs
|
||||
NjFkYzJKdUFFS2dUeG4reW9NWlkvZXMKkUob0p2OZXPgjLJ3MUto1tiJTUL6iyFw
|
||||
uZZKNG4C2phTb9Ry4+wvKb5PakAS8cmoBFRcVz0p6EBsl/yxbwcEMQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||
lastmodified: "2026-07-23T21:15:41Z"
|
||||
mac: ENC[AES256_GCM,data:qFhnPra6IE3wyKQ4WKweON0S0YtD5I0adGZVfA0m6BVilN6bX5oC/1j5NK2oHrsz920hSl0SOF8LrpqOrUyGjSRkPsN4kq8qr9bJcrX4URiktP0oRden5LLt6hf+ZRP7WmRXFqixPkPHJnZIoAvkNnTFce7cDq5NEAHkKUEKG7k=,iv:nyblUDGeu3TUfFivYylOn3C/HITj99qiPI2+mh8AGh4=,tag:FrtRzSCylC4wlIoqZdfx7w==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.1
|
||||
version: 3.13.2
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
wifi-password: ENC[AES256_GCM,data:SZQPtU6PYHbf9o83wq3KTupx,iv:FxO68Pn/+N58r/OPLfkAMYPFpP8TYxszMniFd/01E38=,tag:jwxaY6zDEcO5r9OWSfvUyw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoVjNIOHJseTl6WDBzMDds
|
||||
RGJqdTlyaWV1UUY2V1cvUkN6dUxTam1yUWtrCjBwQ25kZ2RVTTRwenlzQmZaM2hR
|
||||
Ums4Z1NueFEzcXk1YmFjNHhVZTNHZUEKLS0tIDl2aVJlNk5jeG1wT3FMTnNvNU51
|
||||
bVhFd3NTbWJLdDZzUDB6bkJnYkpUUWcKnz6lOojAEcROmGNLXGc+DWfaRLWFMS6g
|
||||
V2Kx+Ouhojx5anoE0ZkFs+7iVEvCX+oe5LOeqzxoXUFEMBldu3AKhQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhR3lVVldmZnJTWEFpZDVn
|
||||
c29IVGttK1pvdXlaWVRIU2kwaGNIejJLc0hFCmsybnRia0oyRUZTcHg2V1BCeHg2
|
||||
RUszSVN5dDlqVEhybFpBME1ramNKWWcKLS0tIHkyMDV1Z0V6ZzFyMlN4dDhjNVBp
|
||||
Ylo3OXBIM2VOUGc0eTdJNGtYUFhLRmcKy55/uTFdjQflrDfGU8o0B6i+eF9gmaoN
|
||||
TR47KGtDk3PNE7M+H8YLTEYGEypoP5Rtiwy778HaOQkLlturEJ3qhg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzZFZjZVI3QkVjQ0FjWlAx
|
||||
VDhCOG5lcVUvQkhkdFRPZDY3aUpDRWppWUN3CmEzQVBBVFhzNVN0VVRTdXFVS2wz
|
||||
cXJiTE1MN0krY0FvZ3crL0kwWWtRREUKLS0tIGtkWXdMVC9MRVpJM3ZVK3M4SElZ
|
||||
OWwzbDMyd2c3WllxZUtGTHRBQ3FsbDAKkFlJVvJxaqc41fG2NXEkf2ZWmwxPqvrt
|
||||
zqr1xc5kn846G+VF97U4Ei3Kpe6TgSYc3evmZDsV6ajINM0Xi+3JKA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOZ0NJN1MyekRaNk56V2t0
|
||||
enNBUVpVYnZjVm1nSnBhTithcXdKVE9VclRvCkZkbDVFNnVnSzhWWmdPQURqdFpC
|
||||
MTJHcVJmSFBpRDVmbExGNVJuQWQzZTgKLS0tIEhjYVRkb0FWWnd0QWFCQUZLSnp4
|
||||
RVpVWU9kNlZjajVxNk13V1k1UUNzV2MKVglPgF8juRhUfN4/MI6E9Z/yONCjUkzO
|
||||
dCH6rY3eZh3EQm2F0XJpfsXSRw8bT4fEm5LCqAtTjdMLrgEWoPh4EQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUQVJhUjBFUURMUEtWNzV1
|
||||
YVJrYnFFY3JyUnJtV2ZadGxBOWkzY28zK2tRCnJRck9UV0RjYmxUcEUreGVyaEd3
|
||||
aU5td2ltRUZ0UjZqRm43YXlYSDV3SGsKLS0tIDArcTZZTXI4ZXJWZzdBL0dsN3Vk
|
||||
U3lXWnRTaVJIVzl3NWw0dE1VMWEwbXMKUEgFevlDaV2GwzHfoFDjts6DXIQd76tF
|
||||
RekC7ZRIzd3b0/6+oDv02Now7Fj+r5NuBrmwu7H4PYvF1gYtZ4eIPw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||
lastmodified: "2026-07-22T01:15:21Z"
|
||||
mac: ENC[AES256_GCM,data:dC/oIqMUHkOh3AocOwP7Gc6XGH3L+nTqJfhFNts1DNbRXsopNIxVBtIz2pEhwnWSQrqPisDLmPHFBwRpGVn01u8w8IU1FKbAKC0J2nJXF8ozpInbjzDOmehqPWZG7yaKoq8cwAnp5XOk+IVO4l6tPxLxkExU5fT2ALuMq+sgOko=,iv:jaVyArpf6zMCFa6J9X1aQMGrmFq+W2CPZdWO6vVW68c=,tag:S+qF8/FkgHc4uW0e4ICmSQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
+34
-16
@@ -4,31 +4,49 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmOHlPcGZvN3o5aEZCcFdz
|
||||
WlYydFdoQ0ZHbElMdTRkZWljMmoxTHZqNjJZCnE3cmgzKzZya3FROEczbHVveDND
|
||||
VnEzRW12cnRKdzhkZm5uTXpkTEtrNUEKLS0tIHFDbkc4Mk4wVlM2R29zZXYwR2Ri
|
||||
ODBML1p4eUZiZldYUERQTUhTU1llV0UKxjvH6zbW6wKghzR1o34CyKPEa2FqZmo0
|
||||
PxgqyuXkIwas9soXVAkScx7ElaV09Fjaj+mDrKwi4a+DwdoSP7czyA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMWjdNblFJdUpyMGFrSWJm
|
||||
a21ESTZTK05YNk5CWFlKNlhIdXhIUFhQNVR3CnZZK1N2N3VKVWs5ZGxZaUhCb1VP
|
||||
dlJaNXM2alNiUG42NlFqeVV3WDdtbnMKLS0tIFpvTlBZU2RpNUtoNVoxeUdKU2Zt
|
||||
QjFydW1xeDd6N2xBU2lhRmtuREhkNDQK49/mcHVCj9tBtGe1ZnK14YWHCtgzBBvV
|
||||
giDA8BzThifXjWtL8zF60kibKFVRGrsRZT38tRuv5QKzu6Z5Cbc+2g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0VDBXYUZNbnpEZHlqV05C
|
||||
WWxQZjVYcWlZeXlGc0RiaUpERzR0T21CWWtBCkM0dW5kYlFNK1RGSzRudFZ2Z0t3
|
||||
aVBreElGM1BIT0RzQkxTVTA3S2NhQncKLS0tIERyTU8zWWRTRm95SmRZQ1BhalVV
|
||||
SVdBajN1V1BuM2s4K216S3c0VHczNlkKM5jvsSEfCBA5uZRjBJNbM91lLRQkj+jK
|
||||
rM5uSfGLTvSjPgXIMIq03OXxH1CE7GoKxAPwFrJdAFMMQcutIethhw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkZmtLL3pzbmNBNkkzRDhF
|
||||
MFZrS3FlZ3I0ZlNJbVN4aUpxYkQ5VkRSYlRjCnUxM0tqRFJDMis0TDd6cUthN0ZE
|
||||
eTBrSDd4a1BzNHZqZVJIc3V4NzBoeGsKLS0tIE9ENFBWUUtadC9vQ1UyRmFLWG9B
|
||||
UUZLeThnUzBNRSttemtuaTN1NWlkUVkKbDeId9JVSvjszcA0TT4L9C4IB5xLCuJK
|
||||
ESqfwJe7QoTBpYA3RMmtIcYK3oYX9KKGrdUU0J+TV0Flr6wqHBHbOQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvbk9tRE9jYkpxNThQeElH
|
||||
dWpKYThDb1ZkUWtMWlRxc2tYbjhBbEFEcWtVCkNiK0NBRko2TkJQdUVtdGtHNGcv
|
||||
andSaUlKSFA4THRyZXNTYmI0Yk5WbmsKLS0tIHlQTUtpWDdPeTVZL0M1RElRdFNk
|
||||
QWlGdFo5NkZWSmY0YXdpNzNUQnlsK3MKtzC0bM7Ek+K73nMranOA1Mc98RUnYnq1
|
||||
hAt0QEFKWK4QVKubaN/rG3AzE0U7qPKWHTzoxgnAiL3WyV9teLW+iA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4OVJvSTVkcDdMQW9HdXcz
|
||||
bmVOVjQvYU81MExSZ3NtUzZTNHNRb1VjNmljCkZ6bDN1OGx4NkhGVENOTjQxWFVy
|
||||
QWNNbENkMVpOTS90dzFWalVtczZ1YU0KLS0tIFZSQVNkUGE0TERyQ1BPTjVQUEdN
|
||||
Y2tFOXlkRHhGTmNWVDZWRnQ5dTNTM1EKNEZZqO6mmj3GnJJ0/8UrisHODEKPjyah
|
||||
aAi5rMnEDAniqyouTP1Mq3GOn67KoESqZX4qY3nHvJC7QBIDii3sBg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5aW8rZG5MenF5RmFWdUgx
|
||||
dk1oV3BpYmNGUnJHTkhOVjVVM0k1T2hlQkdjCm9xYXNkVWkrTWloWEtRcmRuUUkr
|
||||
NUEyZ0ZFeFZ0M2dHUTNYdzFVQ2pDS2MKLS0tIGNnaXcxUUo0cGE4T28vdGJPT1d5
|
||||
SnliakxSZjBNQlpjSlNJelVWVUJORDgKWVuK6VYN5BySdRLHLlU0Sk6PPAsz5lVS
|
||||
Vw96AbKrmczhkiYUWGacde0KjBJb/7tyfXeckV1iqj1/6vT03dDNlA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCckVtTWtCSWpxSGFEbWI5
|
||||
MkVkeTRYN1MzMEk2L1RQemRvM1RSaEVpakVNCjBsbm5qcDFlRXRCQXNkQTd2Snlk
|
||||
WW51Sm1xYjJldmJ0R09WR0hHbWk3NWMKLS0tIGlWYk4zNElVQk9qcGJUUUptMVZV
|
||||
aStXT2lVNXlwRG54SmhxV21tb2M3NzAK2nlgUrlarGERUClDvytMXwPO5e+zaIlW
|
||||
T68ePI4e1vh2qj5xpjXoK5Pzv6oUcjhB0P7Ij2lA8bRdFLWfHPAqvA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||
lastmodified: "2026-07-19T23:30:21Z"
|
||||
mac: ENC[AES256_GCM,data:kLGE2xawQT7mx+sfw68hmGk5nCEGiEjZrqTEl9B1dtQmTrMwmoVr/1RISi4LfJrwxy31mDgff4lcIL4wIJuM373uk3X8j4RNyYQNTfKEkORT6r8NHeepNs267O77pKGd7OmcM4MT/BqOnB8ELS7Wlf2ect7CAlvUUVyc8icxgZE=,iv:EYLDsHYHZ1XOQXafOTqHHWpk/OBNq/R6IJnOBYV33E4=,tag:thxrCPC5oGvDjhK7Dz87YA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
|
||||
+37
-10
@@ -3,22 +3,49 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURmMzN3hrSlNrUkkvVWNl
|
||||
L3M1dEhWeW14N0RFNVRPci9QK1YyTFdqRVVJCk5WaWswT2NicldkYzZjbVhYU2xu
|
||||
MGFsNmUzeTN2TS9wOEdvRURpVUVYZXMKLS0tIDZ6MEdPTVhCaTQ2UXFWTUFtc0pm
|
||||
MFlJb0c2WXJtMGRLZEZYY0pZWWpFWm8K/mlYZIe8UC0QU+1mq3NtrtTF5b2m5hCK
|
||||
+K0QiZLTKmmDcr4bRhZ32VE7R7GRwtMNnOP/mElZvPAyWyHHhRiOHg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxT3dteUY4T0VxaUljbFQ3
|
||||
UjBSSjZCdWpmTGpDS1BabjV4R0lyOE9haEZrCjlmSXllWTB0L0E5N1MrTk1IODR2
|
||||
bXZuamN3Yjk0THdka1dBZE5uS05DRkEKLS0tIDBkTmtpU1JoNmN0cHd0WGVseERt
|
||||
eEcrOXdFaUNBQ3g2aytYM3RwM1dBd00KiSRrzv6eRMdIzZaUlC6JmsZV6rYJgrre
|
||||
vbI1KHtNZKetx81+MmElIKog6kMtTPtI7MVpMwCCPO8zAU5Uh7jSoQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURHlUL0RNMUtNallEcG5p
|
||||
eG4xby8yVzdQUTBaNkl4ano0YjBMcDd0Wm5NCkZvaXNPZm9wemJkMmNSdGdOaTI4
|
||||
Z1RwUnhiRUpCMWZaeWtlSVBmNW5KOXMKLS0tIDk0R0k3ZHczTFNCWUZxSWF0M0FJ
|
||||
MGlZMmtuSFYrcG1meDNMWDNqSjFxcE0KDu2dAc0gqmmPkpbpBe4YohM7rYmUwEkI
|
||||
V2FUQwjlvh50svtjCVdYbx2xuq4sQLnKelk/q1onLw60FwsVfzD8sQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmWUMyVGpua25ZZ1FRZ21p
|
||||
a1lxdS85d2JzOXBLVC92MjlNbTZOOVgvSlhNCm12WmtIb3dQanNkanBCU2t4VTd2
|
||||
TGFidEpSR254Qm5jTVF0WnprcjJWVncKLS0tIGwzRWZkY25rZXE4Q2VETU93Mno3
|
||||
aGN6MmcvVkRGZkJQN3V1ak90T3BMVFEKLcEdqKQ3SStSdH5vjQ6RH+oA10OLGXad
|
||||
TZE0h435sg/tq/Rff+KWngczFRuq7Yg0KYph8bNAKy6uf+RceZ8Ngg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSMG12OTVwdFhnTkFrNmZl
|
||||
UDFLcmJtL2FNejJnWlZYd093MlZmWG04dmdJCkhGLzNGcVQyVVp6SGdISTF0ejhv
|
||||
OVRoQXNlSWZCWDdDMTM0YTFJYlRQd2cKLS0tIHJxVDVscUxEdTV1MGhNVlpWVS8v
|
||||
aWhSNDh1NEZHYzRHeXZNOFRuRzBjazAK2RMK5ylm4uM55fXgrRrY+GeN86ym9wC9
|
||||
JNDcNyxE6H/sZGBZJbFLWKDj+VM9X86j5TFFpnjSrKi2CPOzmzgmKw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLRHdnSkFZc0NYYnQvd0Mz
|
||||
a3dmTHdLN3Z6UGh3SkpBMGhtSHRJTC9SblJnCmFuOTJGSE5nOWIwQWRNV010RkMw
|
||||
VnBJVXdtVm1tL2d2bXFWWnBtamVRVk0KLS0tIDNHUndvTnd2Z2VSb3dxVUFUcTcz
|
||||
ODVDRzBBclhPZjJDYVkwWktMUENSek0KnTFdNMa8L5uPLvl2aJLvaZEUTbGwL+va
|
||||
wLnWhf/bm8edMeyfWcpS2uHMSDpozWfqObrrP23LCrDBf/B7EG+TWw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyeTJMS1A0eXJnQVcvaVEy
|
||||
VEttdGdMcVIzNlIvclZESkJoTHBZeW1GUVFBCmhmRlBLN3lLdW9TS1JKOW83Yis4
|
||||
dVBGNjdPZXdGYllzN0VyRGFVRFFCMU0KLS0tIEZqN1ljQ0s3TDJYWEEwWk1YVmFs
|
||||
R1l1LzdHUDVvUHQwY1NtZUUxajZkZUkKZv1ejN+LKZqFqDRak5ktFaR3kMPCQeFl
|
||||
q8/DHLFx+1IFChxMGkgstGGmkZdxG4khpYJJXAVNyQsyIW9bjJiqZQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||
lastmodified: "2026-07-19T02:30:40Z"
|
||||
mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
beszel-token: ENC[AES256_GCM,data:gjbT3uROiVKQOJaUeafTxjVknQO1Tvbyx/Pl2bTad7DezByX,iv:3ikf7OaT2omO8yd6G6UwYbaRBSzyvbn+NghxAe5bcgI=,tag:Zuc2EP8rUtdDhr5CzSW2Pw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJL25EUUZack1FMzlnMmdk
|
||||
Z1hnejZRNnMwVHpmWkFNdGcyeHVsNnRsSGtZCk9OdDhhcnR1WW9ZMEZ1OUVYbm1n
|
||||
RmZRVy8wb1J3emJBK3Rrd1d4U1dYUDAKLS0tIGJaaElvSk1sOTBOM0lKck16OUtu
|
||||
NnRZb3U0ZndmaHBZTm8zczhWdE1oaEUKkf6fLomAHoKPhuM4e9q96YmmH+h4VrEj
|
||||
2x0rnwBwOoRzYWutB2MVtlsphAZmZ/PK0tEecT2MM0XXayVG/33qdg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpeEJvaHl4akc1TFdEVGV2
|
||||
czg3OVprU3p4ejNpTktXZEpneDgrOEhrZGlZCmJwT1dhSkZneHE5UmR0WTd5UENq
|
||||
VHJEWG1EekJLY2pRZldtVGtxTHlGaGMKLS0tIEYrWHE0WTgyUlIwdktmNzNIS3FW
|
||||
ZVRvT1dHa1Vzc2RSakVISzdMTlpnVGsKeT+edn4+LUkVtpRUNd/gKX3H1HG2bvNo
|
||||
c8iI6qr/l6oxfP85OrKYFDU9IGvDMxSSdbixHtojPEb5OKVurV0WPQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||
lastmodified: "2026-07-23T23:32:57Z"
|
||||
mac: ENC[AES256_GCM,data:l9a/yNRoxY1hvSkLuR4N7deeKue/1JPlSvZvJfCSNbQ21p1qR433BbSDYvfW+kXQXS8GVcfgXSd9ywNzgVvkA5lR1++uYsZBLbYxJ+s3TKWs6/yECAZ0eM1KBA0BEm7cLSsHTOwd+2WspvmCYir++FDO9XRuS3guiMnQBglDf/E=,iv:eE6GVqexQNSiLYfmTTUdUx5AO//wyjSIsr96xAX1pcI=,tag:CNsoyDZYLUt5Seu7W5wJrw==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
+16
-1
@@ -26,7 +26,7 @@
|
||||
# fresh client that has never manually ssh'd to nix-cache before. Update
|
||||
# this if nix-cache's host key is ever rotated or the host is rebuilt
|
||||
# from scratch.
|
||||
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHrMKZlIGUd3pH9G3AqbsruqUGjxIXMAZw52u9MwiBCn lxc-nix-cache";
|
||||
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuHUxGNH6ei3BZD+EfZs3l4X8uJNcjQiOsM/G4yo4O/ lxc-nix-cache";
|
||||
|
||||
# Public keys authorized to SSH in as remoteBuilderUser on the nix-cache
|
||||
# host (modules/nix-cache/server.nix) — one per client host that's allowed
|
||||
@@ -38,12 +38,27 @@
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIGtOWOCS+ImHc7NehguoyD7PbonGosKMZqc9+QR3v/h root@nixos"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxXTQxFnArK5HXG7czeoybZebCGfxpUdusJkPn+BCSp root@server"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJEj26SL/emsVjW2YhRucJVp2kTz8WgcEQgjBEBLRikk root@claude"
|
||||
];
|
||||
|
||||
# Admin SSH public key, authorized on the primary user of every host and
|
||||
# the installer image's nixos/root users.
|
||||
adminSshKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream";
|
||||
|
||||
# Prestaged wifi SSID for the gui host's NetworkManager profile
|
||||
# (modules/networking/wifi.nix). The password is not here -- it's
|
||||
# sops-encrypted in secrets/gui.yaml (wifi-password) instead, since this
|
||||
# file isn't a secret store.
|
||||
wifiSsid = "nbn-fttp-net-5G";
|
||||
|
||||
# Bare-metal gui host's two disks for a ZFS RAID0 (striped) root pool
|
||||
# (modules/disko/baremetal.nix). Only used transiently at disko-format
|
||||
# time (partitioning); the resulting fileSystems/zpool import reference
|
||||
# by-partlabel/by-id paths afterward regardless, same as
|
||||
# modules/disko/proxmox.nix's own plain "/dev/sda".
|
||||
guiRootDisk1 = "/dev/sda";
|
||||
guiRootDisk2 = "/dev/sdb";
|
||||
|
||||
# System
|
||||
timeZone = "Australia/Brisbane";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user