Archived
Compare commits
135
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4b335be23 | ||
|
|
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 | ||
|
|
d8687d979c | ||
|
|
a2b557c034 | ||
|
|
1d44523181 | ||
|
|
222a3ced69 | ||
|
|
03137eef9a | ||
|
|
af0fe5bdfd | ||
|
|
23b910a011 | ||
|
|
19f076bba1 | ||
|
|
9a1d6842d7 | ||
|
|
f5ef3194d4 | ||
|
|
90e3397b42 | ||
|
|
be5812d5bb | ||
|
|
84f7e038cb | ||
|
|
91d8f8fab1 | ||
|
|
723212a81f | ||
|
|
a5990ccf7d | ||
|
|
75d09d57e3 | ||
|
|
6847a7a6f4 | ||
|
|
17dd00bee1 | ||
|
|
b3c81453e4 | ||
|
|
656dd975f0 | ||
|
|
2661f6d271 | ||
|
|
0532c3a282 | ||
|
|
ac8c9a20e3 | ||
|
|
0e66cdabc9 | ||
|
|
9c892ce1c2 | ||
|
|
bfeea90597 | ||
|
|
cafeb8853b | ||
|
|
2c2d464503 | ||
|
|
5ec7033439 | ||
|
|
9133afd444 | ||
|
|
eeec9ce302 | ||
|
|
a62c4fc023 | ||
|
|
7e51168d1b | ||
|
|
97ede62f6d | ||
|
|
ab5206b1c7 | ||
|
|
2041557ab3 | ||
|
|
2fd483697b | ||
|
|
89186b0dee | ||
|
|
8e3606cbd3 | ||
|
|
a18dfb0127 | ||
|
|
75f1342339 | ||
|
|
36ba99c9a1 | ||
|
|
0cd8f15b48 | ||
|
|
8613b93fa8 | ||
|
|
c939454983 |
Submodule
+1
Submodule .claude/worktrees/scripts-dedup added at e578443914
@@ -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
|
||||
|
||||
+64
-3
@@ -1,12 +1,32 @@
|
||||
keys:
|
||||
- &admin age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
- &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- &lxc-minimal age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||
- &lxc-nix-cache age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||
- &lxc-nix-cache age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
||||
- &lxc-docker age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
||||
- &lxc-minimal age1jy444f9d9stygj4p3w9kh54cqcfr654tvr75tdvee5cxsgtdtc9q3v60ep
|
||||
- &lxc-pxe-boot age1fxxzpnfse8nd9wz78ht3m0plrmraacf4cpga0pe8fm2tdnqcgy8q7qsyvp
|
||||
- &lxc-gui age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
||||
- &proxmox-server age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
||||
- &vm-server age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
||||
- &baremetal-gui age1ehkswwz2pqaz4svzh7ela5tdnssl8kn6d4vwwxd6zwg8exfpd43syyrrjp
|
||||
- &lxc-tor-relay age1vvev5m3shgckl62awa64rtug3lyact7jgxehkuu3vn3wpzulhans75w65s
|
||||
- &linode-docker age1eu65wsmez68gegnufl0gqrs0e6w2409mypjlajlq383d7l2e3pjqy5kn6v
|
||||
- &linode-gui age17pwyghxr6lq06fw46gwqzhc9ut4paz28rpwx5pmv3cxwak6rgyjsw7lk3w
|
||||
- &linode-minimal age1jukmg69cqxnjd0lp5f534jhqe65rxew2hufcmyjxa3rkw6ayef6s2ylcmy
|
||||
- &linode-nix-cache age1tzsrtwd3p3lrr9g7nv3z5nvmzsz54t2uc6tfqwutp6usmav83s2sck25cc
|
||||
- &linode-server age1rf4kj99wuq59k7w8ar326djmgmpl9hcwlnuag07f8gauq8c3y5mqne87s4
|
||||
- &linode-tailscale-exit-node age1d0zhx7u3mfs3nktl67npey87cze4dwsfvfvgaje4rh3gwv500yssckcf6u
|
||||
- &lxc-server age1jc6wx33hdhgwhk6nzy5rr8fkgmqxk9um639tk5h632nqfyaw8czskdptj9
|
||||
- &lxc-tailscale-exit-node age1pgykvq4pmxhhjrqupcp99fyad2uht40pt79dkzg66cfvsjy5apjqls8u68
|
||||
- &proxmox-docker age19ht95nv8uhz2shjmakeut8mc3l5spvrcxs3thhe85an7u02r6sysyv457n
|
||||
- &proxmox-gui age1mhaze5tgvc9lwjpml6dnp3xc292337wgm6376yh6tq4fn492ndjq9h23dq
|
||||
- &proxmox-nix-cache age15me6sx0f8r58xh9v7aqrj6e99n7eu555jkpw422txpkqt4r02v0qpahlyq
|
||||
- &proxmox-pxe-boot age1q3hu6eh3mt4saey7dc3yu04s7knnk2ygpm9xt6mg6rqtem6l0uyq4wzul5
|
||||
- &proxmox-tailscale-exit-node age1tczst3x7thwtcz4vce4rg6kmlsszzm6j45kn2nv860z9wa93a92s05ppmc
|
||||
|
||||
creation_rules:
|
||||
# Shared across every currently-deployed host: root/nixos password hash,
|
||||
@@ -24,6 +44,26 @@ creation_rules:
|
||||
- *nix-minimal
|
||||
- *lxc-nix-cache
|
||||
- *proxmox-minimal
|
||||
- *lxc-docker
|
||||
- *lxc-pxe-boot
|
||||
- *lxc-gui
|
||||
- *proxmox-server
|
||||
- *vm-server
|
||||
- *baremetal-gui
|
||||
- *lxc-tor-relay
|
||||
- *linode-docker
|
||||
- *linode-gui
|
||||
- *linode-minimal
|
||||
- *linode-nix-cache
|
||||
- *linode-server
|
||||
- *linode-tailscale-exit-node
|
||||
- *lxc-server
|
||||
- *lxc-tailscale-exit-node
|
||||
- *proxmox-docker
|
||||
- *proxmox-gui
|
||||
- *proxmox-nix-cache
|
||||
- *proxmox-pxe-boot
|
||||
- *proxmox-tailscale-exit-node
|
||||
|
||||
- path_regex: secrets/nix-cache\.yaml$
|
||||
key_groups:
|
||||
@@ -31,15 +71,36 @@ creation_rules:
|
||||
- *admin
|
||||
- *nix-cache
|
||||
- *lxc-nix-cache
|
||||
- *linode-nix-cache
|
||||
- *proxmox-nix-cache
|
||||
|
||||
- path_regex: secrets/server\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *server
|
||||
- *proxmox-server
|
||||
- *vm-server
|
||||
- *linode-server
|
||||
- *lxc-server
|
||||
|
||||
- path_regex: secrets/docker\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *docker
|
||||
|
||||
# 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
|
||||
|
||||
@@ -7,7 +7,7 @@ servers and workstation.
|
||||
|
||||
The flake exposes NixOS configurations named `<platform>-<buildtype>`
|
||||
(platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`,
|
||||
`server`, `docker`, `gui`, `pxe-boot`), generated from `modules/platforms/*`
|
||||
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`), generated from `modules/platforms/*`
|
||||
and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
|
||||
every combination is built — `pxe-boot` has no `linode` variant. See
|
||||
`README.md` for the full current target list; treat `flake.nix` as the
|
||||
@@ -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,59 +137,196 @@ 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 18 hosts plus every package/tarball/
|
||||
image variant now 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/` also has:
|
||||
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/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/sync-host-keys.sh` — generates/registers SSH host keys and their
|
||||
`.sops.yaml`/`secrets/*.yaml` recipients for flake targets, idempotently
|
||||
(`--all`, `<target>`, `--remove`, `--regenerate-all-keys`, all with
|
||||
`--dry-run`). The primary tool for provisioning a new host's secrets
|
||||
access — see "Creating a new machine" in `docs/auto-installer.md`.
|
||||
- `scripts/prepare-host-key.sh` — narrower predecessor: generates a key by
|
||||
an arbitrary name without touching `.sops.yaml`. Still useful to
|
||||
### `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/`
|
||||
|
||||
- `scripts/secrets/sync-host-keys.sh` — generates/registers SSH host keys
|
||||
and their `.sops.yaml`/`secrets/*.yaml` recipients for flake targets,
|
||||
idempotently (`--all`, `<target>`, `--remove`, `--regenerate-all-keys`,
|
||||
all with `--dry-run`). The primary tool for provisioning a new host's
|
||||
secrets access — see "Creating a new machine" in `docs/auto-installer.md`.
|
||||
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a
|
||||
key by an arbitrary name without touching `.sops.yaml`. Still useful to
|
||||
pre-generate a key before its flake target exists yet, since
|
||||
`sync-host-keys.sh` can only act on targets `nixosConfigurations` already
|
||||
has.
|
||||
- `scripts/create-proxmox-resource.sh` — builds a `lxc-*`/`proxmox-*`
|
||||
target's tarball/disk image and creates it on a real Proxmox node
|
||||
(`pct create` against the tarball as a CT template / `qm create`+
|
||||
- `scripts/secrets/rotate-admin-key.sh <backup-admin-key> [--new-key-file
|
||||
<path>] [--dry-run]` — rotates `.sops.yaml`'s `&admin` age key: decrypts
|
||||
with a backed-up copy of the key currently trusted as `&admin` (verified
|
||||
by deriving its public key and comparing, not taken on faith), replaces
|
||||
the `&admin` line with a new key already present in the environment
|
||||
(defaults to wherever sops/age itself would look), and runs
|
||||
`sops updatekeys` on every `secrets/*.yaml`. One-way: the old key can no
|
||||
longer decrypt anything re-encrypted this way. This is the automation
|
||||
for the manual steps `sync-host-keys.sh`/`create-proxmox-resource.sh`
|
||||
print when they bootstrap a brand-new, not-yet-trusted key on a machine
|
||||
with no prior admin access.
|
||||
- `scripts/secrets/backup-admin-key.sh <dest-path> [--key-file <path>]
|
||||
[--force] [--dry-run]` — copies the local sops age key (source
|
||||
resolution matches sops/age itself: `$SOPS_AGE_KEY` inline, then
|
||||
`--key-file`, then `$SOPS_AGE_KEY_FILE`, then the XDG default) to an
|
||||
arbitrary destination path with `0600` permissions, validating it's a
|
||||
real age identity and round-tripping the public key before and after the
|
||||
write. Refuses to overwrite an existing `<dest-path>` without `--force`.
|
||||
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/`
|
||||
|
||||
- `scripts/proxmox/create-proxmox-resource.sh` — builds a `lxc-*`/
|
||||
`proxmox-*` target's tarball/disk image and creates it on a real Proxmox
|
||||
node (`pct create` against the tarball as a CT template / `qm create`+
|
||||
`importdisk`), or reconfigures an existing resource's cores/memory/disk
|
||||
size (`--modify`, always requires typing the VMID back to confirm).
|
||||
Checks for an already-uploaded image on the node before building
|
||||
(`--force-rebuild` to skip that and always rebuild), and probes
|
||||
nix-cache's substituter/remote-builder reachability once up front rather
|
||||
than letting every `nix build` call retry against it individually.
|
||||
Refuses to create a target whose host identity already has a real
|
||||
deployment elsewhere (`variables.nix`'s `deployedTargets`) unless
|
||||
`--allow-duplicate-host` is passed. `--dry-run` throughout both modes.
|
||||
Refuses to create a target whose host identity already exists live on
|
||||
the node (checked directly via `qm`/`pct`, not any file in this repo)
|
||||
unless `--allow-duplicate-host` is passed. `--dry-run` throughout both
|
||||
modes. The first time it has to bootstrap build tooling on a node (i.e.
|
||||
`nix` wasn't already on its `PATH`), it also runs
|
||||
`scripts/proxmox/configure-nix-cache-client.sh` there (non-fatally — a
|
||||
failure just falls back to building from source / `cache.nixos.org`) so
|
||||
the node substitutes from and can offload builds to nix-cache on every
|
||||
subsequent run, not just this one.
|
||||
- `scripts/proxmox/configure-nix-cache-client.sh [--dry-run]
|
||||
[--no-remote-builder] [--no-restart]` — the non-NixOS equivalent of
|
||||
`modules/nix-cache/client.nix`/`remote-builder-client.nix`, for a plain
|
||||
Debian machine with the Nix package manager (not NixOS) already
|
||||
installed: run as root *on that machine* to add nix-cache as a
|
||||
substituter in `/etc/nix/nix.conf` (`https://cache.nixos.org/` kept as
|
||||
fallback) via `extra-substituters`/`extra-trusted-public-keys` so it
|
||||
layers on top of whatever's already there instead of clobbering it, and,
|
||||
if `/root/.ssh/nixremote` is already present (see docs/nix-cache.md
|
||||
"Remote builder SSH keys"), configures it as a distributed-build
|
||||
machine too and trusts nix-cache's SSH host key in
|
||||
`/etc/ssh/ssh_known_hosts`. Idempotent (re-running replaces its own
|
||||
marked block rather than duplicating it); restarts `nix-daemon` by
|
||||
default so the change takes effect immediately.
|
||||
|
||||
### `scripts/lib/`
|
||||
|
||||
Sourced by the scripts above, never run directly:
|
||||
|
||||
- `nix-bootstrap.sh` — `NIX_CONFIG`/`ensure_nix_profile`, shared by
|
||||
`codex-setup.sh`/`codex-maintenance.sh` and the remote build commands
|
||||
`create-proxmox-resource.sh` runs over SSH.
|
||||
- `nix-eval.sh` — `NIX_EVAL_FLAGS` plus `list_flake_targets`/
|
||||
`flake_target_hostname` flake-introspection helpers.
|
||||
- `ssh-host-keys.sh` — `generate_host_ed25519_key`/`ssh_pubkey_to_age`,
|
||||
shared by `sync-host-keys.sh` and `prepare-host-key.sh`.
|
||||
- `sops-age.sh` — `age_pubkey_from_identity_file`/`sops_yaml_admin_pubkey`/
|
||||
`sops_updatekeys` plus the shared sops/age default key-file resolution,
|
||||
shared by `backup-admin-key.sh`, `rotate-admin-key.sh`, and
|
||||
`sync-host-keys.sh`.
|
||||
- `confirm.sh` — `confirm_typed`, the "type X back to confirm" destructive-
|
||||
action prompt shared by `create-proxmox-resource.sh` and
|
||||
`sync-host-keys.sh`.
|
||||
- `sync-host-keys-edit-sops.py` — the `.sops.yaml` anchor/key_groups editor
|
||||
`sync-host-keys.sh` shells out to (see that script for why: precise,
|
||||
idempotent YAML edits are impractical in bash).
|
||||
|
||||
### 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`
|
||||
errors on it), so this is the closest equivalent to a single source of
|
||||
truth for the tracked release.
|
||||
|
||||
`sync-host-keys.sh` and `create-proxmox-resource.sh` genuinely mutate real
|
||||
state when run for real (not `--dry-run`): real `secrets/*.yaml`
|
||||
recipients, real Proxmox VMs/containers. They require the operator's own
|
||||
SSH/sops access, which an agent session doesn't have — but don't suggest
|
||||
running either non-dry-run without the operator's explicit go-ahead even
|
||||
if it becomes technically reachable.
|
||||
`sync-host-keys.sh`, `create-proxmox-resource.sh`, and
|
||||
`rotate-admin-key.sh` genuinely mutate real state when run for real (not
|
||||
`--dry-run`): real `secrets/*.yaml` recipients, real Proxmox VMs/
|
||||
containers, real revocation of decrypt access. They require the
|
||||
operator's own SSH/sops access, which an agent session doesn't have — but
|
||||
don't suggest running any of them non-dry-run without the operator's
|
||||
explicit go-ahead even if it becomes technically reachable.
|
||||
`backup-admin-key.sh` only writes a key copy to a path the operator gives
|
||||
it — lower-stakes than the others, but it still handles a real private
|
||||
key, so treat its destination path choice as the operator's call too.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -132,10 +348,14 @@ nixosSystem {
|
||||
}
|
||||
```
|
||||
|
||||
Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
|
||||
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`. 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). 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
|
||||
@@ -151,19 +371,23 @@ 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
|
||||
`vzdump` backup-archive metadata this doesn't have), no install step —
|
||||
see `docs/auto-installer.md`.
|
||||
- `modules/build-types/*.nix` — what a system is for:
|
||||
minimal/server/docker/gui/pxe-boot/nix-cache.
|
||||
minimal/server/docker/gui/pxe-boot/nix-cache/tailscale-exit-node/tor-relay.
|
||||
- `modules/common/configuration.nix` — base NixOS config imported by every
|
||||
host: locale, users, nix settings, git.
|
||||
- `modules/common/home.nix` / `hosts/nixos/home.nix` — Home Manager config for
|
||||
@@ -180,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
|
||||
@@ -198,7 +432,8 @@ removing a host.
|
||||
and `environmentFile`; used by `hosts/server/host.nix` and
|
||||
`hosts/nix-cache/host.nix` to avoid duplicating that boilerplate.
|
||||
- `modules/tailscale/`, `modules/docker/`, `modules/networking/`,
|
||||
`modules/traefik/`, `modules/services/*` — single-purpose, single-host
|
||||
`modules/traefik/`, `modules/tor/`, `modules/services/*` — single-purpose,
|
||||
single-host
|
||||
feature modules (e.g. `docker/enable-service.nix`,
|
||||
`services/zfs/enable-service.nix`). Grep `modules/build-types/*.nix` for
|
||||
each build type's `imports` list to see which modules apply where.
|
||||
|
||||
@@ -8,30 +8,42 @@ 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`
|
||||
`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. 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 |
|
||||
| --- | --- |
|
||||
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS (real, deployed) |
|
||||
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox (real, deployed — previously the flat `nix-minimal` target) |
|
||||
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS |
|
||||
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox — previously the flat `nix-minimal` target |
|
||||
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
|
||||
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder (`proxmox-nix-cache` is the real, deployed one — previously the flat `nix-cache` target) |
|
||||
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host (`proxmox-server` is the real, deployed one — previously the flat `server` target) |
|
||||
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack (`proxmox-docker` is the real, deployed one — previously the flat `docker` target) |
|
||||
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation (`proxmox-gui` is the real, deployed one — previously the flat `nixos` target) |
|
||||
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host (`proxmox-pxe-boot` is the real, deployed one — previously the flat `pxe-boot` target) |
|
||||
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node (no deployed target yet; `lxc-tailscale-exit-node` is the one planned for actual use) |
|
||||
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder — previously the flat `nix-cache` target |
|
||||
| `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 |
|
||||
|
||||
The "(real, deployed)" targets above are also tracked machine-readably in
|
||||
`variables.nix`'s `deployedTargets` — keep both in sync when a deployment
|
||||
changes. `scripts/create-proxmox-resource.sh` reads that list to refuse
|
||||
creating a same-identity duplicate of an already-deployed host by accident.
|
||||
Which variant of a given buildtype is actually deployed isn't tracked
|
||||
anywhere in this repo — that's live infrastructure state, not something a
|
||||
committed file can keep accurate, and it changes independently of the code.
|
||||
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
|
||||
@@ -55,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 |
|
||||
@@ -70,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
|
||||
@@ -110,10 +131,11 @@ Three different paths depending on target, none of them involving a manual
|
||||
disk image and attached to a new VM with no install step — see
|
||||
`docs/proxmox-images.md`.
|
||||
|
||||
`scripts/create-proxmox-resource.sh --type lxc|vm --host <name>` automates
|
||||
either of the last two end to end (build, host-key registration, upload,
|
||||
`pct create`/`qm create`), with `--dry-run` and a guard against duplicating
|
||||
an already-deployed host's identity. See its `--help`.
|
||||
`scripts/proxmox/create-proxmox-resource.sh --type lxc|vm --host <name>` automates
|
||||
either of the last two end to end (host-key registration, building the
|
||||
image directly on the Proxmox node itself, `pct create`/`qm create`), with
|
||||
`--dry-run` and a guard against duplicating an already-deployed host's
|
||||
identity. See its `--help`.
|
||||
|
||||
## Security Notes
|
||||
|
||||
|
||||
+40
-15
@@ -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.**
|
||||
@@ -19,7 +22,7 @@ see "LXC hosts" immediately below for why those are different.**
|
||||
## LXC hosts
|
||||
|
||||
`lxc-*` targets (`lxc-minimal`, `lxc-nix-cache`, `lxc-server`, `lxc-docker`,
|
||||
`lxc-gui`, `lxc-pxe-boot`) are **not** installed via `auto-install.sh` — the
|
||||
`lxc-gui`, `lxc-pxe-boot`, `lxc-tailscale-exit-node`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the
|
||||
interactive menu deliberately excludes them. Don't try to select one there;
|
||||
`nixos-install` would bind-mount `/` onto `/mnt` (LXC containers have no raw
|
||||
disk to partition) and then refuse to touch the filesystem it's currently
|
||||
@@ -73,9 +76,9 @@ booting one:
|
||||
|
||||
First boot runs `boot.postBootCommands` (registers the Nix store DB and
|
||||
system profile) — there's no separate activation step to run yourself.
|
||||
`scripts/create-proxmox-resource.sh --type lxc --host <name>` automates all
|
||||
of this (build, host-key handling, upload, `pct create` with the flags
|
||||
above) — see its `--help`.
|
||||
`scripts/proxmox/create-proxmox-resource.sh --type lxc --host <name>` automates all
|
||||
of this (host-key handling, building the tarball directly on the Proxmox
|
||||
node itself, `pct create` with the flags above) — see its `--help`.
|
||||
|
||||
Host keys still need pre-seeding the same way as any other host — the
|
||||
sops-nix activation-vs-first-boot race is identical regardless of how the
|
||||
@@ -102,7 +105,7 @@ groups required, got 0`, and *every* secret (including this host's own
|
||||
login) permanently fails to decrypt, silently — no error in the boot log
|
||||
at all, since the activation step that would install secrets only runs on
|
||||
a from-scratch first activation and skips silently once `/run/current-system`
|
||||
already exists. `scripts/create-proxmox-resource.sh` always builds with
|
||||
already exists. `scripts/proxmox/create-proxmox-resource.sh` always builds with
|
||||
`NIXOS_HOST_KEYS_DIR` set for this reason.
|
||||
|
||||
## Layout
|
||||
@@ -116,10 +119,10 @@ already exists. `scripts/create-proxmox-resource.sh` always builds with
|
||||
`docs/pxe-boot.md`).
|
||||
- `modules/installer/host-keys.nix` — optionally bakes pre-generated SSH
|
||||
host keys into the image; see "Host keys" below.
|
||||
- `scripts/sync-host-keys.sh` — admin-workstation tool that generates,
|
||||
- `scripts/secrets/sync-host-keys.sh` — admin-workstation tool that generates,
|
||||
registers, and (via `--remove`/`--regenerate-all-keys`) retires host
|
||||
keys; see "Creating a New Machine" below.
|
||||
- `scripts/prepare-host-key.sh` — narrower predecessor: generates a single
|
||||
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a single
|
||||
key by an arbitrary name without touching `.sops.yaml`. Still useful for
|
||||
pre-generating a key *before* its flake target exists (`sync-host-keys.sh`
|
||||
can only act on targets `nixosConfigurations` already has); otherwise
|
||||
@@ -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.
|
||||
@@ -238,7 +263,7 @@ GitHub token behind sops-nix for all of them).
|
||||
2. **On your admin workstation, generate and register its host key:**
|
||||
|
||||
```sh
|
||||
./scripts/sync-host-keys.sh <flake-target>
|
||||
./scripts/secrets/sync-host-keys.sh <flake-target>
|
||||
```
|
||||
|
||||
This generates `host-keys/<flake-target>_ssh_host_ed25519_key(.pub)`,
|
||||
@@ -250,7 +275,7 @@ GitHub token behind sops-nix for all of them).
|
||||
|
||||
Doing this for every host that needs one at once — after adding several
|
||||
new targets, or just to catch up any that were missed — is
|
||||
`./scripts/sync-host-keys.sh --all`. See `scripts/sync-host-keys.sh --help`
|
||||
`./scripts/secrets/sync-host-keys.sh --all`. See `scripts/secrets/sync-host-keys.sh --help`
|
||||
for its other modes (`--remove`, `--regenerate-all-keys`).
|
||||
|
||||
3. **Commit and push.** The flake build the installer uses has to see the
|
||||
|
||||
@@ -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`
|
||||
|
||||
+26
-9
@@ -46,18 +46,35 @@ 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`,
|
||||
sourced from `vars.nixCacheHostKey` (`variables.nix`) — every client rebuild
|
||||
picks it up automatically, so distributed builds don't fail with "Host key
|
||||
verification failed" on a client that has never manually SSH'd to nix-cache
|
||||
before. If nix-cache's host key is ever rotated or the host rebuilt from
|
||||
scratch, update `vars.nixCacheHostKey` to match its new
|
||||
`/etc/ssh/ssh_host_ed25519_key.pub`.
|
||||
|
||||
## Manual verification
|
||||
|
||||
@@ -67,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"
|
||||
```
|
||||
|
||||
@@ -7,11 +7,13 @@ config (`modules/disko/proxmox.nix`) already used to format a real disk on
|
||||
install, so there's nothing host-specific to write; it's available for every
|
||||
`proxmox-*` target automatically.
|
||||
|
||||
`scripts/create-proxmox-resource.sh --type vm --host <name>` automates the
|
||||
`scripts/proxmox/create-proxmox-resource.sh --type vm --host <name>` automates the
|
||||
whole walkthrough below (and the equivalent LXC one) end to end, including
|
||||
host-key handling and upload — see its `--help`. The steps here are what it
|
||||
runs under the hood, useful for doing any of it by hand or understanding
|
||||
what it does before you trust it against real infrastructure.
|
||||
host-key handling and building the image directly on the Proxmox node
|
||||
itself (no local build, no image transfer) — see its `--help`. The steps
|
||||
here are what it runs under the hood, useful for doing any of it by hand
|
||||
or understanding what it does before you trust it against real
|
||||
infrastructure.
|
||||
|
||||
## Building
|
||||
|
||||
@@ -49,7 +51,7 @@ sudo ./result \
|
||||
--build-memory 2048
|
||||
```
|
||||
|
||||
Generate the key first with `scripts/sync-host-keys.sh <hostname>`, same
|
||||
Generate the key first with `scripts/secrets/sync-host-keys.sh <hostname>`, same
|
||||
as any other host — see `docs/auto-installer.md` for the full walkthrough
|
||||
(it registers the new key in `.sops.yaml` and re-encrypts the affected
|
||||
`secrets/*.yaml` files too, no manual editing needed).
|
||||
|
||||
+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:
|
||||
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
# Spec: Refactor Flake Targets into Platform × Build-Type Matrix
|
||||
|
||||
## Context
|
||||
|
||||
The flake at `~/nixos` currently defines these output targets (flat, ad-hoc naming):
|
||||
|
||||
- `docker`
|
||||
- `linode-minimal`
|
||||
- `nix-cache`
|
||||
- `nix-minimal`
|
||||
- `nixos`
|
||||
- `server`
|
||||
- `pxe-boot`
|
||||
|
||||
Some already follow a `platform-buildtype` convention (`linode-minimal`), most don't.
|
||||
`~/nix-auto-installer` is a related repo and should be checked for any coupling to
|
||||
these target names (scripts, docs, CI, or install automation that reference them by
|
||||
name) before renaming anything.
|
||||
|
||||
## Goal
|
||||
|
||||
Restructure the flake so targets are generated from two orthogonal concepts:
|
||||
|
||||
**Build types** (what the system is for):
|
||||
- `minimal`
|
||||
- `nix-cache`
|
||||
- `server`
|
||||
- `docker`
|
||||
- `pxe-boot`
|
||||
- `gui`
|
||||
|
||||
**Platforms** (what it's deployed on):
|
||||
- `linode` (Linode VM)
|
||||
- `proxmox` (Proxmox VM)
|
||||
- `lxc` (Proxmox LXC container)
|
||||
|
||||
Final targets should be named consistently as `<platform>-<buildtype>`, e.g.:
|
||||
|
||||
```
|
||||
linode-minimal proxmox-minimal lxc-minimal
|
||||
linode-nix-cache proxmox-nix-cache lxc-nix-cache
|
||||
linode-server proxmox-server lxc-server
|
||||
linode-docker proxmox-docker lxc-docker
|
||||
linode-pxe-boot proxmox-pxe-boot lxc-pxe-boot
|
||||
linode-gui proxmox-gui lxc-gui
|
||||
```
|
||||
|
||||
That's the full matrix (18 targets) if every build type applies to every platform.
|
||||
See **Open Questions** below — some combinations may not make sense and should be
|
||||
confirmed with me before being built out, not silently included or dropped.
|
||||
|
||||
## Migration mapping (old → new)
|
||||
|
||||
| Old target | New target | Notes |
|
||||
|--------------------|------------------------------------------------------|-------|
|
||||
| `linode-minimal` | `linode-minimal` | Already correct, keep as-is |
|
||||
| `nix-minimal` | likely `proxmox-minimal` or a platform-less base module | Ambiguous — see Open Questions |
|
||||
| `nix-cache` | base module consumed by `linode-nix-cache`, `proxmox-nix-cache`, `lxc-nix-cache` | Currently platform-less; needs to become a build-type module, not a standalone target |
|
||||
| `server` | base module consumed by `linode-server`, `proxmox-server`, `lxc-server` | Same as above |
|
||||
| `docker` | base module consumed by `linode-docker`, `proxmox-docker`, `lxc-docker` | Confirm docker actually makes sense as an LXC/VM guest build vs. a standalone container image — see Open Questions |
|
||||
| `pxe-boot` | TBD — may stay a single target rather than a per-platform one | See Open Questions |
|
||||
| `nixos` | TBD — unclear what this maps to in the new scheme | See Open Questions |
|
||||
|
||||
## Open Questions (Claude Code: raise these with me before implementing, don't guess)
|
||||
|
||||
1. **`nixos` target** — what is this currently used for (bare metal install, dev
|
||||
shell, template)? It doesn't obviously map to any of the six build types.
|
||||
2. **`nix-minimal` vs `linode-minimal`** — are these two different things, or is
|
||||
`nix-minimal` a leftover/duplicate?
|
||||
3. **`pxe-boot` and `gui` across all three platforms** — does PXE boot make sense
|
||||
for an LXC container or a cloud VM (Linode), or is it inherently bare-metal/
|
||||
network-boot only and should remain a single non-platform target? Does `gui`
|
||||
make sense inside an LXC container?
|
||||
4. **`docker` as a build type** — is this "a NixOS host configured to run Docker"
|
||||
(which would sensibly have linode/proxmox/lxc variants), or "a Docker container
|
||||
image built by the flake" (which wouldn't take a platform prefix at all, since
|
||||
it doesn't run on Linode/Proxmox/LXC as a guest OS)? These are structurally
|
||||
different and change how it should be wired in.
|
||||
5. Confirm whether all 18 combinations should actually exist, or whether this is
|
||||
meant to produce only the combinations that are genuinely useful (e.g. maybe no
|
||||
one needs `lxc-pxe-boot`).
|
||||
|
||||
## Implementation approach
|
||||
|
||||
1. **Inventory first.** Read the current `flake.nix` and any `nixosConfigurations`/
|
||||
`modules` structure. Map every existing target to what module(s) it actually
|
||||
pulls in. Don't assume — confirm against the real file contents.
|
||||
2. **Separate build-type and platform into their own module directories**, e.g.:
|
||||
```
|
||||
modules/build-types/minimal.nix
|
||||
modules/build-types/nix-cache.nix
|
||||
modules/build-types/server.nix
|
||||
modules/build-types/docker.nix
|
||||
modules/build-types/pxe-boot.nix
|
||||
modules/build-types/gui.nix
|
||||
|
||||
modules/platforms/linode.nix
|
||||
modules/platforms/proxmox.nix
|
||||
modules/platforms/lxc.nix
|
||||
```
|
||||
Build-type modules should contain only what makes a system "minimal" vs
|
||||
"server" vs "gui", etc. Platform modules should contain only what's specific
|
||||
to running as a Linode VM vs Proxmox VM vs LXC container (virtualisation
|
||||
guest tools, boot method, filesystem/image format, LXC-specific constraints
|
||||
like no kernel modules, etc).
|
||||
3. **Generate the target matrix programmatically** in `flake.nix` rather than
|
||||
hand-writing 18 near-identical `nixosConfigurations` entries — e.g. a small
|
||||
function that takes a platform name and build-type name, composes the two
|
||||
modules plus any shared base module, and produces the named output. This
|
||||
keeps future build types/platforms a one-line addition rather than a copy-paste
|
||||
job.
|
||||
4. **Only build combinations we've confirmed make sense** (see Open Questions) —
|
||||
don't emit all 18 by default if some are structurally invalid.
|
||||
5. **Preserve existing working configs during the transition.** Don't delete the
|
||||
old target names until their replacements build successfully — rename/alias
|
||||
at the end, not the start, so there's no window where the flake is broken.
|
||||
|
||||
## Verification
|
||||
|
||||
For every new target produced:
|
||||
```bash
|
||||
nix flake check
|
||||
nix build .#nixosConfigurations.<target>.config.system.build.toplevel
|
||||
```
|
||||
Confirm each builds without evaluation errors before considering it done. If a
|
||||
target fails to build, report which one and why rather than silently skipping it.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- Refactored `flake.nix` using the composed module + generated-matrix approach.
|
||||
- New `modules/build-types/*.nix` and `modules/platforms/*.nix` files.
|
||||
- Old flat target names removed only after their replacements are verified.
|
||||
- A short `README.md` (or section in existing docs) listing the final target
|
||||
names and what each one is for.
|
||||
- A summary at the end of what changed, what was removed, and any of the Open
|
||||
Questions above that got resolved differently than expected.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Don't touch `~/nix-auto-installer` contents beyond checking it for references
|
||||
to the old target names — if changes there are needed, flag them, don't make
|
||||
them without confirming.
|
||||
- Don't add new build types or platforms beyond the ones listed here.
|
||||
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,16 @@
|
||||
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-subnet-router = mkTarget { platform = "linode"; buildType = "tailscale-subnet-router"; hostPath = ./hosts/tailscale-subnet-router/host.nix; };
|
||||
proxmox-tailscale-subnet-router = mkTarget { platform = "proxmox"; buildType = "tailscale-subnet-router"; hostPath = ./hosts/tailscale-subnet-router/host.nix; };
|
||||
lxc-tailscale-subnet-router = mkTarget { platform = "lxc"; buildType = "tailscale-subnet-router"; hostPath = ./hosts/tailscale-subnet-router/host.nix; };
|
||||
|
||||
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
|
||||
};
|
||||
|
||||
# Auto-install environments (migrated from the former nix-auto-installer
|
||||
@@ -117,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
|
||||
{
|
||||
|
||||
@@ -151,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 = "/home/nixos/Nextcloud/Filing Cabinet/keys/nixos-sops-age-key-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";
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
_:
|
||||
|
||||
{
|
||||
networking.hostName = "exit-node";
|
||||
networking.hostName = "tor-relay";
|
||||
|
||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
||||
# for pool-import safety, and this host does neither.
|
||||
@@ -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" ];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../tor/enable-relay.nix
|
||||
../beszel/enable-agent.nix
|
||||
];
|
||||
}
|
||||
@@ -17,6 +17,26 @@ let
|
||||
--refresh \
|
||||
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
||||
'';
|
||||
|
||||
# lxc-* hosts pre-seed their SSH host key at build time (see
|
||||
# modules/platforms/lxc.nix) so sops-nix's .sops.yaml recipient matches on
|
||||
# first boot -- without it, secrets permanently fail to decrypt (see that
|
||||
# file's comment for the confirmed failure). That requires --impure plus
|
||||
# NIXOS_HOST_KEYS_DIR pointing at the repo's host-keys/ dir, same pattern
|
||||
# docs/auto-installer.md uses for the installer ISO. A function, not a
|
||||
# shellAlias, since the target name has to interpolate into the middle of
|
||||
# the flake attribute path, not just append after it. Must be run from the
|
||||
# repo root, same as every other host-keys/ command in this repo.
|
||||
buildImageFn = ''
|
||||
buildImage() {
|
||||
if [ -z "$1" ]; then
|
||||
echo "usage: buildImage <flake-target> (e.g. lxc-docker)" >&2
|
||||
return 1
|
||||
fi
|
||||
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||
".#nixosConfigurations.$1.config.system.build.tarball"
|
||||
}
|
||||
'';
|
||||
in
|
||||
{
|
||||
programs.bash = {
|
||||
@@ -25,5 +45,6 @@ in
|
||||
"Switch-nix" = mySwitchCmd;
|
||||
"Test-nix" = myTestCmd;
|
||||
};
|
||||
initExtra = buildImageFn;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,65 +1,84 @@
|
||||
{ config, lib, pkgs, vars, ... }:
|
||||
|
||||
let
|
||||
# `x-systemd.automount` never works inside a Linux container (LXC
|
||||
# included, regardless of privilege) -- confirmed live on lxc-docker:
|
||||
# systemd logs "Starting of <unit>.automount unsupported" for every
|
||||
# share and never mounts them. Mount eagerly there instead, with
|
||||
# `nofail` so a boot with the NFS server unreachable doesn't hang
|
||||
# (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 = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
] ++ automountOpts;
|
||||
};
|
||||
|
||||
${vars.nfsShares.dockerDatabases.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
] ++ automountOpts;
|
||||
};
|
||||
|
||||
${vars.nfsShares.dockerVolumes.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
] ++ automountOpts;
|
||||
};
|
||||
|
||||
${vars.nfsShares.nextcloudData.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
] ++ automountOpts;
|
||||
};
|
||||
|
||||
${vars.nfsShares.raspiVolumes.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
] ++ automountOpts;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+15
-134
@@ -45,140 +45,21 @@
|
||||
disko
|
||||
];
|
||||
|
||||
# Write auto-install script to /root
|
||||
etc."auto-install.sh" = {
|
||||
text = ''
|
||||
#!/run/current-system/sw/bin/bash
|
||||
set -eux
|
||||
# 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;
|
||||
|
||||
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/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/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
|
||||
'';
|
||||
|
||||
mode = "0755";
|
||||
"nixos-installer/installer/auto-install.sh" = {
|
||||
source = ../../scripts/installer/auto-install.sh;
|
||||
mode = "0755";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,22 @@
|
||||
{ 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 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 ];
|
||||
publicKey = vars.nixCacheHostKey;
|
||||
};
|
||||
|
||||
nix = {
|
||||
distributedBuilds = true;
|
||||
|
||||
@@ -12,7 +24,7 @@
|
||||
{
|
||||
hostName = vars.nixCacheHost;
|
||||
sshUser = vars.remoteBuilderUser;
|
||||
sshKey = "/root/.ssh/${vars.remoteBuilderUser}";
|
||||
sshKey = "/root/.ssh/id_ed25519";
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
maxJobs = 4;
|
||||
speedFactor = 2;
|
||||
|
||||
@@ -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
|
||||
@@ -14,7 +14,7 @@ let
|
||||
# Without this, config.system.build.tarball's built-in system just
|
||||
# generates a fresh host key at first boot like any other host would --
|
||||
# but sops-nix derives its decryption key from *this* file, and
|
||||
# .sops.yaml only trusts whatever key scripts/sync-host-keys.sh already
|
||||
# .sops.yaml only trusts whatever key scripts/secrets/sync-host-keys.sh already
|
||||
# registered for this exact target name. A freshly-generated key can
|
||||
# never match that, so every secret (including this host's own login)
|
||||
# permanently fails to decrypt. Confirmed live: sops-install-secrets
|
||||
@@ -26,7 +26,7 @@ let
|
||||
hostKeysDir = /. + hostKeysDirStr;
|
||||
|
||||
# flakeTarget ("${platform}-${buildType}") comes in via specialArgs from
|
||||
# flake.nix's mkTarget -- exactly the name scripts/sync-host-keys.sh
|
||||
# flake.nix's mkTarget -- exactly the name scripts/secrets/sync-host-keys.sh
|
||||
# registers keys under. Deliberately not read back from
|
||||
# config.environment.etc."flake-target" (which is set to the same value)
|
||||
# -- this module also *contributes* to environment.etc below, and a
|
||||
@@ -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
|
||||
[
|
||||
# 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 ${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) 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"
|
||||
];
|
||||
# `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,4 +1,4 @@
|
||||
{ vars, ... }:
|
||||
{ config, lib, vars, ... }:
|
||||
|
||||
{
|
||||
fileSystems.${vars.raspiMountpoint} = {
|
||||
@@ -9,6 +9,15 @@
|
||||
"_netdev"
|
||||
"noatime"
|
||||
|
||||
# Explicitly use NFSv4.2 if supported
|
||||
"nfsvers=4.2"
|
||||
] ++ lib.optionals (!config.boot.isContainer) [
|
||||
# `x-systemd.automount` never works inside a Linux container (LXC
|
||||
# included) -- confirmed live on lxc-docker: systemd logs "Starting
|
||||
# of <unit>.automount unsupported" and never mounts it. `nofail`
|
||||
# above already keeps boot non-blocking there, so plain eager
|
||||
# mounting is fine.
|
||||
|
||||
# Don't mount until first access
|
||||
"x-systemd.automount"
|
||||
|
||||
@@ -17,9 +26,6 @@
|
||||
|
||||
# Give the Pi/Tailscale a little time to appear
|
||||
"x-systemd.device-timeout=10s"
|
||||
|
||||
# Explicitly use NFSv4.2 if supported
|
||||
"nfsvers=4.2"
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
_:
|
||||
|
||||
{
|
||||
services.tailscale = {
|
||||
enable = 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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{ pkgs, vars, ... }:
|
||||
|
||||
{
|
||||
services.tor = {
|
||||
enable = true;
|
||||
|
||||
# Opens settings.ORPort (and DirPort, unset here) in the firewall —
|
||||
# see the nixpkgs tor module's own networking.firewall.mkIf block.
|
||||
openFirewall = true;
|
||||
|
||||
relay = {
|
||||
enable = true;
|
||||
# Plain middle/guard relay, not "exit" — relays onion traffic between
|
||||
# other Tor nodes without ever making requests to the public internet
|
||||
# on a user's behalf, avoiding the abuse complaints and legal exposure
|
||||
# an exit node invites.
|
||||
role = "relay";
|
||||
};
|
||||
|
||||
settings.ORPort = vars.ports.torRelayOrPort;
|
||||
|
||||
# Unix control socket at /run/tor/control (GroupWritable, group "tor")
|
||||
# -- what nyx below actually monitors the relay through. Nyx's own
|
||||
# default control-socket path (/var/run/tor/control) resolves to the
|
||||
# same place, so no extra nyx config is needed.
|
||||
controlSocket.enable = true;
|
||||
};
|
||||
|
||||
# Lets the primary user's shell session read/write the control socket
|
||||
# above without being root -- otherwise nyx fails to authenticate against
|
||||
# it at all.
|
||||
users.users.${vars.primaryUser}.extraGroups = [ "tor" ];
|
||||
|
||||
environment.systemPackages = [ pkgs.nyx ];
|
||||
}
|
||||
Binary file not shown.
@@ -1,134 +0,0 @@
|
||||
# Spec: Remove Sensitive Information from NixOS Flake
|
||||
|
||||
## Goal
|
||||
|
||||
Every secret currently readable in plaintext anywhere in this repo (working tree *and* git history) gets removed, replaced with `sops-nix`-managed encrypted references, and rotated. When this is done, the repo should be safe to make public without exposing anything about the systems it configures.
|
||||
|
||||
Treat this as three sequential milestones. Do not start git history rewriting (Milestone 3) until Milestones 1 and 2 are fully verified and the flake still builds. This should be its own branch (`refactor/secrets`) until fully verified, then merged.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 1 — Audit
|
||||
|
||||
Before touching anything, produce a complete inventory. Do not guess at scope — grep the whole tree and the whole history.
|
||||
|
||||
1. Run a secret scanner across the working tree and full history. Use both, since they catch different things:
|
||||
- `gitleaks detect --source . -v --log-opts="--all"` (scans history too)
|
||||
- `trufflehog git file://. --since-commit=$(git rev-list --max-parents=0 HEAD) --only-verified=false`
|
||||
If neither is installed, add them via a temporary `nix-shell -p gitleaks trufflehog` — don't install anything globally on the host.
|
||||
|
||||
2. Manually grep for the categories below, since scanners miss config-specific patterns:
|
||||
- `hashedPassword`, `password`, `initialPassword`, `initialHashedPassword` in any `users.users.*` block
|
||||
- `age.secrets`, `sops.secrets` (if any partial secrets work already exists — check for it)
|
||||
- PSK / `preSharedKey`, `privateKeyFile` inline values (vs. file references) for WireGuard
|
||||
- `authKey`, `apiToken`, `api_key`, `token =`, `secret =` in service modules (Tailscale, Cloudflare, backup tools, etc.)
|
||||
- SSH private key material: search for `BEGIN OPENSSH PRIVATE KEY` / `BEGIN RSA PRIVATE KEY` literals
|
||||
- TLS cert/key pairs committed under e.g. `secrets/`, `certs/`, `pki/`
|
||||
- Real name, personal email, home address, or anything in comments/hostnames that maps a machine to your physical identity or network layout (e.g. hostnames like `wayne-desktop`, static LAN IPs, ISP-identifying info)
|
||||
- `.env` files, `secrets.nix`, `secrets.yaml`, or any file that looks like it was meant to be gitignored but wasn't
|
||||
|
||||
3. Produce `secrets-inventory.md` (temporary, delete before finishing) listing: file path, line, secret type, and which host/service it belongs to. This becomes the checklist for Milestone 2 — every row must be either migrated to sops or deleted, with nothing left unaccounted for.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 2 — Migrate to sops-nix
|
||||
|
||||
### 2.1 Set up sops-nix
|
||||
|
||||
1. Add the flake input:
|
||||
```nix
|
||||
sops-nix.url = "github:Mic92/sops-nix";
|
||||
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
```
|
||||
2. Import `sops-nix.nixosModules.sops` into each host's module list (or into a shared `common.nix` if all hosts use it).
|
||||
3. Generate an age keypair **per host** (not one shared key for everything — a compromised host shouldn't decrypt every other host's secrets):
|
||||
```
|
||||
nix-shell -p age --run "age-keygen -o /var/lib/sops-nix/key.txt"
|
||||
```
|
||||
Print the public key (`age-keygen -y`) for each host — you'll need it for `.sops.yaml`.
|
||||
4. Also generate one age key for yourself (your admin workstation) so you can edit secrets without needing to SSH into a host: store it at `~/.config/sops/age/keys.txt`, back it up somewhere outside this repo (password manager, offline). **If this key is lost, every secret encrypted with it is unrecoverable — losing the age key is equivalent to losing the secrets.**
|
||||
5. Create `.sops.yaml` at the repo root defining creation rules: which age public keys can decrypt which secrets files, keyed by path regex, so e.g. `secrets/hostA.yaml` is decryptable by your admin key + hostA's key, `secrets/hostB.yaml` by your admin key + hostB's key.
|
||||
|
||||
### 2.2 Migrate each secret category from the inventory
|
||||
|
||||
For each row in `secrets-inventory.md`:
|
||||
|
||||
- **Password hashes**: generate hash with `mkpasswd -m sha-512` (or `bcrypt` if your setup wants that), store under `sops.secrets."<name>/hashedPassword"`, reference via `users.users.<name>.hashedPasswordFile = config.sops.secrets."<name>/hashedPassword".path;`. Do not put the *plaintext* password anywhere, only the hash, and only the hash goes into the encrypted sops file.
|
||||
- **API tokens / auth keys**: move the raw value into the per-host sops YAML, reference in the module via `config.sops.secrets."<service>/token".path` — most NixOS service modules that take a token also accept a `*File` variant (e.g. `environmentFile`, `tokenFile`); use that instead of passing the value directly.
|
||||
- **Private keys / certs**: move the PEM/key content wholesale into a sops secret, output as a file with appropriate `sops.secrets.<name>.path`, `owner`, `mode`, `restartUnits` so the depending service (sshd, wireguard, nginx) reloads when the secret changes.
|
||||
- **Personal/identifying info**: this doesn't belong in sops (it's not "secret," it's just information you don't want public). Replace real names/emails with placeholders or move to a small untracked `local.nix` that's `.gitignore`'d and imported conditionally, with a documented template (`local.nix.example`) committed instead.
|
||||
|
||||
### 2.3 Verify before moving on
|
||||
|
||||
- `nixos-rebuild dry-build --flake .#<host>` succeeds for every host.
|
||||
- `sudo nixos-rebuild switch --flake .#<host>` on at least one real machine (or a VM) confirms secrets decrypt and services start.
|
||||
- Confirm decrypted secrets land under `/run/secrets/` (not the Nix store — anything placed in `/nix/store` is world-readable by design, so sops-nix's runtime-only placement is the whole point; double check no module accidentally pulls a secret path into a store-built config file).
|
||||
- Re-run the grep/scanner sweep from Milestone 1 against the *working tree only* (not history yet) — it should now come back clean.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 3 — Scrub git history
|
||||
|
||||
Do this only after Milestone 2 is merged to your main branch and confirmed working, since it rewrites every commit SHA from the point of the earliest offending commit onward.
|
||||
|
||||
**This is destructive and irreversible on your local clone. Back up first:**
|
||||
```
|
||||
cp -r /path/to/nixos-repo /path/to/nixos-repo-backup-$(date +%F)
|
||||
```
|
||||
|
||||
1. Install `git-filter-repo` (not the older `git filter-branch` / BFG — filter-repo is the currently maintained, faster, safer tool):
|
||||
```
|
||||
nix-shell -p git-filter-repo
|
||||
```
|
||||
2. Use the `secrets-inventory.md` list to build a list of literal strings/paths to strip. Two approaches, use both:
|
||||
- Path-based: if whole files were secret (e.g. `secrets.nix`, a `.env`, a private key file), remove them entirely from history:
|
||||
```
|
||||
git filter-repo --path secrets.nix --path .env --invert-paths
|
||||
```
|
||||
- Value-based: for secrets embedded inline in files you're keeping (not deleting the whole file), use `--replace-text` with a file listing each literal secret string to replace with `***REMOVED***`:
|
||||
```
|
||||
git filter-repo --replace-text expressions.txt
|
||||
```
|
||||
3. After filtering, verify: run the Milestone 1 scanners again against full history (`--log-opts="--all"`). They must come back clean.
|
||||
4. Force-push the rewritten history:
|
||||
```
|
||||
git push origin --force --all
|
||||
git push origin --force --tags
|
||||
```
|
||||
5. **Every other clone of this repo (other machines, WSL instances, CI) must be deleted and re-cloned fresh** — a `git pull` against rewritten history will not work cleanly and risks resurrecting the old commits. Don't try to reconcile old clones; throw them away and re-clone.
|
||||
6. If this repo has ever been pushed to a public host (GitHub, etc.) or a fork/mirror exists, treat every secret that was ever in history as **permanently compromised regardless of the rewrite** — caches, forks, and Wayback-style archives can retain old commits indefinitely. History scrubbing prevents *future* exposure via `git clone`; it does not undo past exposure.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 4 — Rotate everything
|
||||
|
||||
Because the secrets were exposed in history (even briefly, even in a private repo), the migration is not complete until every credential in the inventory has been **rotated**, not just re-encrypted. Re-encrypting an already-leaked value protects it going forward but doesn't undo the leak.
|
||||
|
||||
For each row in the original inventory:
|
||||
- Password hashes → change the actual account password, regenerate the hash, update the sops file.
|
||||
- API tokens/auth keys → revoke the old token in the issuing service's dashboard (Cloudflare, Tailscale, backup provider, etc.) and generate a new one.
|
||||
- SSH/WireGuard private keys → generate new keypairs, update the corresponding public key wherever it's trusted (authorized_keys, peer configs, etc.), retire the old ones.
|
||||
- TLS certs → reissue if the private key was exposed.
|
||||
|
||||
Keep `secrets-inventory.md` open during this step and check off each row as rotated. Delete the file only once every row is checked off — it should not be committed.
|
||||
|
||||
---
|
||||
|
||||
## Ongoing prevention
|
||||
|
||||
Add a pre-commit hook (or a `nix flake check` step) running `gitleaks protect --staged` so a secret can't be committed again by accident. Document in the repo README (briefly) that new secrets go through `sops <file>` to edit, never as plaintext in a tracked file.
|
||||
|
||||
---
|
||||
|
||||
## Definition of done
|
||||
|
||||
- [ ] Milestone 1 inventory complete and reviewed
|
||||
- [ ] All hosts have per-host age keys; admin key backed up outside the repo
|
||||
- [ ] Every inventoried secret migrated to sops-nix, referenced via `*File`/`sops.secrets.*.path`, nothing plaintext in the working tree
|
||||
- [ ] `nixos-rebuild dry-build` and at least one real `switch` verified per host
|
||||
- [ ] Working-tree scanner sweep clean
|
||||
- [ ] History rewritten with `git-filter-repo`, force-pushed, full-history scanner sweep clean
|
||||
- [ ] All other clones deleted and re-cloned from the rewritten history
|
||||
- [ ] Every credential in the original inventory rotated (not just re-encrypted)
|
||||
- [ ] Pre-commit secret scanning hook added
|
||||
- [ ] `secrets-inventory.md` deleted from the working directory (never committed)
|
||||
@@ -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."
|
||||
|
||||
+270
-62
@@ -1,22 +1,73 @@
|
||||
#!/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
|
||||
|
||||
export NIX_CONFIG="${NIX_CONFIG:-}
|
||||
experimental-features = nix-command flakes
|
||||
accept-flake-config = false
|
||||
warn-dirty = false
|
||||
"
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/nix-bootstrap.sh
|
||||
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"
|
||||
|
||||
ensure_nix_profile() {
|
||||
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
||||
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||
fi
|
||||
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
|
||||
|
||||
if ! command -v nix >/dev/null 2>&1; then
|
||||
@@ -24,13 +75,6 @@ if ! command -v nix >/dev/null 2>&1; then
|
||||
exit 127
|
||||
fi
|
||||
|
||||
hosts_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames)"
|
||||
hosts="$(echo "$hosts_json" | jq -r '.[]')"
|
||||
|
||||
echo "Hosts:"
|
||||
echo "$hosts"
|
||||
|
||||
echo
|
||||
echo "Checking for obvious committed secrets..."
|
||||
if grep -RInE 'github_pat_|ghp_|access-tokens|hashedPassword[[:space:]]*=' \
|
||||
--exclude-dir=.git \
|
||||
@@ -42,71 +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..."
|
||||
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
|
||||
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..."
|
||||
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#statix -- check .
|
||||
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 --no-use-registries --no-accept-flake-config ".#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.
|
||||
case "$host" in
|
||||
lxc-*)
|
||||
echo "==> $host (tarball)"
|
||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
|
||||
;;
|
||||
proxmox-*)
|
||||
echo "==> $host (diskoImagesScript)"
|
||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Evaluating buildable packages..."
|
||||
packages_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#packages.x86_64-linux --apply builtins.attrNames)"
|
||||
packages="$(echo "$packages_json" | jq -r '.[]')"
|
||||
for pkg in $packages; do
|
||||
echo "==> packages.x86_64-linux.${pkg}"
|
||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}"
|
||||
done
|
||||
|
||||
if [[ "$MODE" == "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 --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel"
|
||||
|
||||
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 "==> Dry-run build: $host (tarball)"
|
||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball"
|
||||
host_eval_jobs+=("${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball.drvPath")
|
||||
;;
|
||||
proxmox-*)
|
||||
echo "==> Dry-run build: $host (diskoImagesScript)"
|
||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
|
||||
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 "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 --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}"
|
||||
echo
|
||||
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 $dry_run; then
|
||||
echo
|
||||
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-*)
|
||||
host_build_jobs+=("Dry-run build: ${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball")
|
||||
;;
|
||||
proxmox-*)
|
||||
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 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
|
||||
|
||||
+19
-22
@@ -1,19 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export NIX_CONFIG="${NIX_CONFIG:-}
|
||||
experimental-features = nix-command flakes
|
||||
accept-flake-config = false
|
||||
warn-dirty = false
|
||||
"
|
||||
|
||||
ensure_nix_profile() {
|
||||
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
||||
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||
fi
|
||||
}
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/nix-bootstrap.sh
|
||||
source "${script_dir}/lib/nix-bootstrap.sh"
|
||||
# shellcheck source=lib/nix-eval.sh
|
||||
source "${script_dir}/lib/nix-eval.sh"
|
||||
|
||||
install_nix_if_missing() {
|
||||
if command -v nix >/dev/null 2>&1; then
|
||||
@@ -49,6 +41,17 @@ warn-dirty = false
|
||||
build-users-group = nixbld
|
||||
EOF
|
||||
|
||||
# The official installer's single-user root path still shells out to
|
||||
# `sudo` to create /nix even though it already knows it's running as
|
||||
# root -- confirmed live against a sudo-less minimal Debian/Proxmox
|
||||
# node, where it fails with "sudo: not found" and prints this exact
|
||||
# mkdir/chown as the manual fix. Pre-create it so that branch of the
|
||||
# installer is skipped entirely.
|
||||
if [ ! -d /nix ]; then
|
||||
mkdir -m 0755 /nix
|
||||
chown root /nix
|
||||
fi
|
||||
|
||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||
else
|
||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||
@@ -65,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:"
|
||||
@@ -79,13 +83,6 @@ if ! command -v jq >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
echo "Available NixOS hosts:"
|
||||
hosts="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')"
|
||||
echo "$hosts"
|
||||
list_flake_targets .
|
||||
|
||||
echo "Evaluating all host toplevel derivations..."
|
||||
for host in $hosts; do
|
||||
echo "==> Evaluating $host"
|
||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#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."
|
||||
|
||||
@@ -1,521 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Creates new Proxmox VMs/LXC containers from this flake, and reconfigures
|
||||
# existing ones -- the manual workflows in docs/proxmox-images.md (VM) and
|
||||
# docs/auto-installer.md's "LXC hosts" section (container), automated.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/create-proxmox-resource.sh --type lxc|vm --host <name> [options]
|
||||
# scripts/create-proxmox-resource.sh --type lxc|vm --list
|
||||
# scripts/create-proxmox-resource.sh --modify --vmid <n> [--cores N] [--memory MB] [--grow-disk GB]
|
||||
#
|
||||
# SAFETY:
|
||||
# - The default (create) mode only ever creates a NEW resource -- it
|
||||
# refuses to run if the target VMID already exists on the node.
|
||||
# - --modify only ever touches a resource you name explicitly via
|
||||
# --vmid, shows exactly what will change first, and (outside
|
||||
# --dry-run) always requires typing that VMID back to confirm before
|
||||
# anything is sent to the node. There is no bulk/implicit modify.
|
||||
# - Neither mode can start/stop/delete a resource. Not implemented on
|
||||
# purpose -- ask before adding it.
|
||||
#
|
||||
# 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"
|
||||
|
||||
sync_keys="${repo_root}/scripts/sync-host-keys.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --type lxc|vm --host <name> [options] (create)
|
||||
$0 --type lxc|vm --list (list --host values)
|
||||
$0 --modify --vmid <n> [options] (reconfigure)
|
||||
|
||||
Create mode (default):
|
||||
--type lxc|vm lxc = container, built as a CT template tarball.
|
||||
vm = VM, built as a Disko .raw disk image (UEFI/OVMF).
|
||||
--host <name> Which host identity to deploy -- matches
|
||||
config.networking.hostName (server, docker,
|
||||
nix-cache, nixos, pxe-boot, nix-minimal). Use
|
||||
--list to see what's available for --type.
|
||||
--name <name> Proxmox display name/hostname (default: --host's
|
||||
value, e.g. nix-cache -- for lxc this becomes the
|
||||
guest's real networking.hostName too, since
|
||||
proxmoxLXC.manageHostName pulls it from Proxmox's
|
||||
own container config, so it must match host.nix
|
||||
regardless of build type)
|
||||
--vmid <n> Numeric VMID (default: next free, via
|
||||
\`pvesh get /cluster/nextid\` on the node).
|
||||
Refuses to run if this ID already exists.
|
||||
--disk-size <GB> lxc only: rootfs size for \`pct create\`
|
||||
(default: \$PROXMOX_DEFAULT_LXC_DISK_GB, ${PROXMOX_DEFAULT_LXC_DISK_GB}).
|
||||
--image <path> Use this local image/tarball instead of
|
||||
checking the node / building one from the flake.
|
||||
--force-rebuild Skip the "does the node already have this
|
||||
image" check -- always build fresh and
|
||||
overwrite what's there.
|
||||
--allow-duplicate-host Required if --host already has a real
|
||||
deployment elsewhere (variables.nix's
|
||||
deployedTargets) -- otherwise refused, since
|
||||
it'd share that host's hostName/hostId.
|
||||
|
||||
Modify mode (reconfigure an EXISTING resource -- requires --modify):
|
||||
--modify Switch to modify mode.
|
||||
--vmid <n> Required: which existing resource to change.
|
||||
Type/VM-vs-CT is auto-detected on the node.
|
||||
--grow-disk <GB> Grow the primary disk by this many GB
|
||||
(qm/pct resize; Proxmox only supports
|
||||
growing, never shrinking, an existing disk).
|
||||
At least one of --cores / --memory / --grow-disk is required. Always
|
||||
prints the current -> new values and requires typing the VMID back to
|
||||
confirm, even outside --dry-run.
|
||||
|
||||
Shared:
|
||||
--cores <n> create: default \$PROXMOX_DEFAULT_CORES (${PROXMOX_DEFAULT_CORES}).
|
||||
modify: omit to leave unchanged.
|
||||
--memory <MB> create: default \$PROXMOX_DEFAULT_MEMORY_MB (${PROXMOX_DEFAULT_MEMORY_MB}).
|
||||
modify: omit to leave unchanged.
|
||||
--swap <MB> lxc only, create time: \`--memory\` doesn't
|
||||
touch swap -- it silently stays at Proxmox's
|
||||
own 512M default otherwise. (default: matches
|
||||
whatever --memory resolves to)
|
||||
--storage <pool> (default: \$PROXMOX_STORAGE, ${PROXMOX_STORAGE})
|
||||
--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})
|
||||
--dry-run Print the full plan; touch nothing
|
||||
local or remote, no prompts.
|
||||
-h, --help
|
||||
|
||||
Config for --storage/--bridge/--node/etc. lives in scripts/env.sh -- edit
|
||||
that instead of passing the same flag every time.
|
||||
EOF
|
||||
}
|
||||
|
||||
dry_run=0
|
||||
modify=0
|
||||
type=""
|
||||
host=""
|
||||
name=""
|
||||
vmid=""
|
||||
cores=""
|
||||
memory=""
|
||||
swap=""
|
||||
disk_size=""
|
||||
grow_disk=""
|
||||
image=""
|
||||
storage="$PROXMOX_STORAGE"
|
||||
iso_storage="$PROXMOX_ISO_STORAGE"
|
||||
bridge="$PROXMOX_BRIDGE"
|
||||
node="$PROXMOX_HOST"
|
||||
do_list=0
|
||||
allow_duplicate_host=0
|
||||
force_rebuild=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--type) type="$2"; shift 2 ;;
|
||||
--host) host="$2"; shift 2 ;;
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--vmid) vmid="$2"; shift 2 ;;
|
||||
--cores) cores="$2"; shift 2 ;;
|
||||
--memory) memory="$2"; shift 2 ;;
|
||||
--swap) swap="$2"; shift 2 ;;
|
||||
--disk-size) disk_size="$2"; shift 2 ;;
|
||||
--grow-disk) grow_disk="$2"; shift 2 ;;
|
||||
--image) image="$2"; shift 2 ;;
|
||||
--storage) storage="$2"; shift 2 ;;
|
||||
--iso-storage) iso_storage="$2"; shift 2 ;;
|
||||
--bridge) bridge="$2"; shift 2 ;;
|
||||
--node) node="$2"; shift 2 ;;
|
||||
--list) do_list=1; shift ;;
|
||||
--allow-duplicate-host) allow_duplicate_host=1; shift ;;
|
||||
--force-rebuild) force_rebuild=1; shift ;;
|
||||
--modify) modify=1; shift ;;
|
||||
--dry-run) dry_run=1; shift ;;
|
||||
-h | --help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ssh_target="${PROXMOX_SSH_USER}@${node}"
|
||||
|
||||
remote() {
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] ssh ${ssh_target} -- $*"
|
||||
else
|
||||
ssh "$ssh_target" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================ modify mode
|
||||
cmd_modify() {
|
||||
if [[ -z "$vmid" ]]; then
|
||||
echo "ERROR: --modify requires --vmid." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$cores" && -z "$memory" && -z "$grow_disk" ]]; then
|
||||
echo "ERROR: --modify needs at least one of --cores / --memory / --grow-disk." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
kind="vm"
|
||||
disk_key="scsi0"
|
||||
elif ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
||||
kind="lxc"
|
||||
disk_key="rootfs"
|
||||
else
|
||||
echo "ERROR: VMID ${vmid} doesn't exist on ${node} -- nothing to modify." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local config_cmd="qm config ${vmid}"
|
||||
[[ "$kind" == "lxc" ]] && config_cmd="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 '?')"
|
||||
current_memory="$(echo "$current_config" | grep -oP '^memory:\s*\K\S+' || echo '?')"
|
||||
|
||||
echo
|
||||
echo "VMID ${vmid} is a ${kind} on ${node}. Planned changes:"
|
||||
[[ -n "$cores" ]] && echo " cores: ${current_cores} -> ${cores}"
|
||||
[[ -n "$memory" ]] && echo " memory: ${current_memory} MB -> ${memory} MB"
|
||||
[[ -n "$grow_disk" ]] && echo " ${disk_key}: grow by +${grow_disk}G (Proxmox can only grow, not shrink, an existing disk)"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo
|
||||
echo "[dry-run] Nothing was changed."
|
||||
return
|
||||
fi
|
||||
|
||||
echo
|
||||
read -rp "Type the VMID (${vmid}) to confirm these changes: " confirm
|
||||
if [[ "$confirm" != "$vmid" ]]; then
|
||||
echo "Cancelled -- input didn't match ${vmid}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local set_cmd="qm set"
|
||||
local resize_cmd="qm resize"
|
||||
[[ "$kind" == "lxc" ]] && set_cmd="pct set" && resize_cmd="pct resize"
|
||||
|
||||
if [[ -n "$cores" || -n "$memory" ]]; then
|
||||
local args=""
|
||||
[[ -n "$cores" ]] && args="${args} --cores ${cores}"
|
||||
[[ -n "$memory" ]] && args="${args} --memory ${memory}"
|
||||
remote "${set_cmd} ${vmid}${args}"
|
||||
fi
|
||||
if [[ -n "$grow_disk" ]]; then
|
||||
remote "${resize_cmd} ${vmid} ${disk_key} +${grow_disk}G"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Done. VMID ${vmid} updated."
|
||||
}
|
||||
|
||||
if [[ "$modify" -eq 1 ]]; then
|
||||
cmd_modify
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================================================= create mode
|
||||
if [[ "$type" != "lxc" && "$type" != "vm" ]]; then
|
||||
echo "ERROR: --type must be 'lxc' or 'vm'." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
platform_prefix="lxc"
|
||||
[[ "$type" == "vm" ]] && platform_prefix="proxmox"
|
||||
[[ -z "$cores" ]] && cores="$PROXMOX_DEFAULT_CORES"
|
||||
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
|
||||
|
||||
# --- discover / resolve the flake target from --host --------------------
|
||||
list_hosts() {
|
||||
local target hostname
|
||||
for target in $(nix eval --json --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames 2>/dev/null \
|
||||
| jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do
|
||||
hostname="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null)"
|
||||
printf ' %-12s -> %s\n' "$hostname" "$target"
|
||||
done
|
||||
}
|
||||
|
||||
if [[ "$do_list" -eq 1 ]]; then
|
||||
echo "Available --host values for --type ${type}:"
|
||||
list_hosts
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$host" ]]; then
|
||||
echo "ERROR: --host is required (or use --list to see options)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
flake_target=""
|
||||
for target in $(nix eval --json --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames \
|
||||
| jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do
|
||||
hn="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations.${target}.config.networking.hostName")"
|
||||
if [[ "$hn" == "$host" ]]; then
|
||||
flake_target="$target"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$flake_target" ]]; then
|
||||
echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2
|
||||
echo "Available:" >&2
|
||||
list_hosts >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The container/VM's real identity is --host (e.g. "nix-cache"), validated
|
||||
# above against config.networking.hostName -- not the flake target name
|
||||
# (e.g. "lxc-nix-cache"), which is build-type-specific and only exists to
|
||||
# pick which platform variant to build. Defaulting --name to the flake
|
||||
# target would make lxc's --hostname (which proxmoxLXC.manageHostName
|
||||
# feeds straight into the guest's real hostname) disagree with host.nix.
|
||||
[[ -z "$name" ]] && name="$host"
|
||||
|
||||
# --- refuse to duplicate a host that's already really deployed ----------
|
||||
# Checked by hostName, not exact flake target: proxmox-server being
|
||||
# deployed also blocks --type lxc --host server, since both would carry
|
||||
# the same hosts/server/host.nix identity (hostName, hostId).
|
||||
if [[ "$allow_duplicate_host" -eq 0 ]]; then
|
||||
deployed_targets_json="$(nix eval --json --no-use-registries --no-accept-flake-config \
|
||||
--file "${repo_root}/variables.nix" deployedTargets)"
|
||||
for dt in $(echo "$deployed_targets_json" | jq -r '.[]'); do
|
||||
dt_hostname="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations.${dt}.config.networking.hostName" 2>/dev/null || true)"
|
||||
if [[ "$dt_hostname" == "$host" ]]; then
|
||||
echo "ERROR: '${host}' already has a real deployment (${dt}, per variables.nix's" >&2
|
||||
echo "deployedTargets). Creating ${flake_target} would share its hostName/hostId --" >&2
|
||||
echo "refusing by default. Pass --allow-duplicate-host if you really mean to spin" >&2
|
||||
echo "up a separate test instance of this host (it'll still get its own distinct" >&2
|
||||
echo "sops key and VMID, never touching ${dt})." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'"
|
||||
|
||||
# Decide on nix-cache once, here -- this is the earliest point that needs
|
||||
# it (sync-host-keys.sh below needs nix-shell packages regardless of
|
||||
# whether an image ends up getting built later), and the decision is
|
||||
# exported so that subprocess -- and this script's own later build step,
|
||||
# if it gets there -- both reuse it instead of probing again.
|
||||
nix_extra_opts
|
||||
|
||||
# --- make sure this target has a registered host key --------------------
|
||||
echo
|
||||
echo "==> Ensuring host key exists and is registered..."
|
||||
sync_args=("$flake_target")
|
||||
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
|
||||
bash "$sync_keys" "${sync_args[@]}"
|
||||
|
||||
# --- VMID: pick one, and refuse to touch anything that already exists ---
|
||||
echo
|
||||
if [[ -z "$vmid" ]]; then
|
||||
if [[ "$dry_run" -eq 1 ]]; 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:]')"
|
||||
echo "Auto-assigned VMID: ${vmid}"
|
||||
fi
|
||||
else
|
||||
echo "Requested VMID: ${vmid}"
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" -eq 0 ]]; then
|
||||
# qm/pct status exits non-zero (and prints "does not exist") for a free
|
||||
# ID on that resource type -- but a VMID could exist as the OTHER
|
||||
# resource type (e.g. requested a CT id that's actually a VM), so check
|
||||
# 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
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- resolve the remote path -- fixed naming (not the nix store's own
|
||||
# derivation-hash-based filename), so a later run can check for it by name.
|
||||
# lxc uploads as a CT *template* (Proxmox's "vztmpl" content type, under
|
||||
# iso_storage) -- config.system.build.tarball is a plain rootfs tarball,
|
||||
# not a vzdump backup archive, so it's created with `pct create ... vztmpl`,
|
||||
# not restored with `pct restore` (that expects backup-archive metadata
|
||||
# this tarball doesn't have, and fails with "archive contains no
|
||||
# configuration file").
|
||||
remote_dir="/var/lib/vz/import"
|
||||
remote_filename="${flake_target}.raw"
|
||||
if [[ "$type" == "lxc" ]]; then
|
||||
remote_dir="/var/lib/vz/template/cache"
|
||||
remote_filename="${flake_target}.tar.xz"
|
||||
fi
|
||||
remote_path="${remote_dir}/${remote_filename}"
|
||||
|
||||
# --- build (or reuse an image already on the node) ------------------------
|
||||
echo
|
||||
local_image=""
|
||||
image_already_remote=0
|
||||
|
||||
if [[ -n "$image" ]]; then
|
||||
[[ -f "$image" ]] || { echo "ERROR: --image '${image}' not found." >&2; exit 1; }
|
||||
local_image="$image"
|
||||
echo "Using provided image: ${local_image}"
|
||||
elif [[ "$force_rebuild" -eq 1 ]]; then
|
||||
echo "--force-rebuild: skipping the existing-image check on ${node}."
|
||||
else
|
||||
echo "==> Checking whether ${node} already has ${remote_path}..."
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would check: ssh ${ssh_target} -- test -f ${remote_path}"
|
||||
elif ssh "$ssh_target" "test -f '${remote_path}'" 2>/dev/null; then
|
||||
echo "Found it -- reusing, skipping build and upload (use --force-rebuild to override)."
|
||||
image_already_remote=1
|
||||
else
|
||||
echo "Not found -- will build."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
|
||||
# Mirrors the real build commands' "${NIX_OPTS[@]}" below -- nix_extra_opts
|
||||
# (called earlier, once) has already decided whether nix-cache is in play,
|
||||
# and the dry-run preview needs to reflect that decision instead of always
|
||||
# printing the same command regardless of outcome.
|
||||
nix_opts_display=""
|
||||
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
|
||||
printf -v nix_opts_display '%q ' "${NIX_OPTS[@]}"
|
||||
nix_opts_display=" ${nix_opts_display% }"
|
||||
fi
|
||||
if [[ "$type" == "lxc" ]]; then
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would build: NIXOS_HOST_KEYS_DIR=${repo_root}/host-keys nix build --impure \\"
|
||||
echo "[dry-run] --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.tarball"
|
||||
local_image="<built-tarball>"
|
||||
else
|
||||
echo "==> Building LXC tarball for ${flake_target}..."
|
||||
NIXOS_HOST_KEYS_DIR="${repo_root}/host-keys" nix build --impure \
|
||||
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||
".#nixosConfigurations.${flake_target}.config.system.build.tarball" \
|
||||
--out-link "${repo_root}/result-${flake_target}"
|
||||
local_image="$(find "${repo_root}/result-${flake_target}/tarball" -maxdepth 1 -type f | head -1)"
|
||||
echo "Built: ${local_image}"
|
||||
fi
|
||||
else
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would build: nix build --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
|
||||
echo "[dry-run] would run: sudo ./result-${flake_target} \\"
|
||||
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \\"
|
||||
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \\"
|
||||
echo "[dry-run] --build-memory 2048"
|
||||
local_image="<built-image>.raw"
|
||||
else
|
||||
echo "==> Building Disko image script for ${flake_target}..."
|
||||
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||
".#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript" \
|
||||
--out-link "${repo_root}/result-${flake_target}"
|
||||
echo "==> Running it (builds the .raw image in a temporary QEMU VM, needs sudo)..."
|
||||
( cd "$repo_root" && sudo "./result-${flake_target}" \
|
||||
--pre-format-files "host-keys/${flake_target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
||||
--pre-format-files "host-keys/${flake_target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
||||
--build-memory 2048 )
|
||||
local_image="$(find "$repo_root" -maxdepth 1 -name "*.raw" -newer "${repo_root}/result-${flake_target}" | head -1)"
|
||||
if [[ -z "$local_image" ]]; then
|
||||
echo "ERROR: expected a .raw image after the build but didn't find one in ${repo_root}." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Built: ${local_image}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- upload (skip entirely if reusing an image already on the node) ------
|
||||
echo
|
||||
if [[ "$image_already_remote" -eq 1 ]]; then
|
||||
: # nothing to upload
|
||||
elif [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would upload: scp ${local_image} ${ssh_target}:${remote_path}"
|
||||
else
|
||||
echo "==> Uploading to ${node}:${remote_path}..."
|
||||
ssh "$ssh_target" "mkdir -p ${remote_dir}"
|
||||
scp "$local_image" "${ssh_target}:${remote_path}"
|
||||
fi
|
||||
|
||||
# --- create -----------------------------------------------------------------
|
||||
echo
|
||||
if [[ "$type" == "lxc" ]]; then
|
||||
echo "==> Creating LXC container ${vmid} (${name})..."
|
||||
local_disk_size="${disk_size:-$PROXMOX_DEFAULT_LXC_DISK_GB}"
|
||||
# --memory doesn't touch swap -- it silently stays at Proxmox's own
|
||||
# 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.
|
||||
#
|
||||
# --features nesting=1,keyctl=1: required for a modern (v247+) systemd
|
||||
# guest to actually boot unprivileged -- confirmed live: without this,
|
||||
# AppArmor denies the nested user namespaces and credential mounts
|
||||
# systemd routinely uses (even plain getty units), and every getty
|
||||
# crash-loops on a denied mount every ~3s (visible as garbage on the
|
||||
# console) while core services like nsncd fail the same way.
|
||||
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"
|
||||
remote "$create_cmd"
|
||||
remote "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} \
|
||||
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
|
||||
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] ssh ${ssh_target} -- 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>"
|
||||
else
|
||||
importdisk_output="$(ssh "$ssh_target" "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
|
||||
exit 1
|
||||
fi
|
||||
remote "qm set ${vmid} --scsi0 ${disk_id}"
|
||||
fi
|
||||
remote "qm set ${vmid} --boot order=scsi0"
|
||||
remote "qm start ${vmid}"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] Nothing was built, uploaded, or created."
|
||||
else
|
||||
echo "Done. ${name} (VMID ${vmid}) should be booting on ${node}."
|
||||
fi
|
||||
+46
-13
@@ -3,21 +3,39 @@
|
||||
# second copy of these values in every script:
|
||||
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
|
||||
# Every variable can still be overridden per-invocation via the
|
||||
# environment (e.g. PROXMOX_STORAGE=tank-nvme ./scripts/create-proxmox-resource.sh ...)
|
||||
# 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/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
|
||||
# instead of transferring them over the network -- it clones the repo here
|
||||
# (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:=/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}"
|
||||
@@ -45,16 +63,31 @@
|
||||
# crash-loops on a denied `/run/credentials/*` mount every ~3s (visible
|
||||
# as garbage on the console) and core services like nsncd fail the same
|
||||
# way on userns_create; system.build.tarball never finishes activating.
|
||||
: "${PROXMOX_DEFAULT_LXC_FEATURES:=nesting=1,keyctl=1}"
|
||||
#
|
||||
# mount=nfs;nfs4: without it, AppArmor blanket-denies the `nfs`/
|
||||
# `rpc_pipefs` mount syscalls any NFS client share needs -- confirmed
|
||||
# live on lxc-docker (which mounts several, see modules/docker/mount-data.nix
|
||||
# and modules/raspi/mount-data.nix): `mount: /var/lib/nfs/rpc_pipefs:
|
||||
# permission denied`. Harmless to grant on lxc targets that don't mount
|
||||
# NFS at all -- it only widens what the container is *allowed* to mount,
|
||||
# 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
|
||||
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
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared "type X to confirm" prompt for scripts/proxmox/create-proxmox-resource.sh
|
||||
# (--modify, and replacing an existing --allow-duplicate-host resource) and
|
||||
# scripts/secrets/sync-host-keys.sh (--regenerate-all-keys) -- three destructive
|
||||
# confirmations that all work the same way (echo the expected value back
|
||||
# exactly), kept in one place so the prompt/comparison logic can't drift.
|
||||
# Source alongside env.sh:
|
||||
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/confirm.sh"
|
||||
#
|
||||
# Deliberately does NOT print anything on mismatch or decide exit-vs-return
|
||||
# -- callers vary on both (a top-level script exits, a subcommand function
|
||||
# returns; wording differs too), so that stays at the call site.
|
||||
|
||||
# confirm_typed <expected> <prompt>
|
||||
# Prints <prompt> via `read -rp`, then reports (via exit status) whether the
|
||||
# typed input matched <expected> exactly.
|
||||
confirm_typed() {
|
||||
local expected="$1" prompt="$2" input
|
||||
read -rp "$prompt" input
|
||||
[[ "$input" == "$expected" ]]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared Nix bootstrap for scripts/codex-setup.sh and
|
||||
# scripts/codex-maintenance.sh: the nix.conf settings both need in effect
|
||||
# before a single `nix` command runs (flakes enabled, never honor a flake
|
||||
# input's own nixConfig, no "dirty tree" warning spam), plus a helper to
|
||||
# pull an already-installed Nix's daemon/profile script onto PATH if it
|
||||
# isn't there yet. Source this instead of copying it -- see CLAUDE.md.
|
||||
export NIX_CONFIG="${NIX_CONFIG:-}
|
||||
experimental-features = nix-command flakes
|
||||
accept-flake-config = false
|
||||
warn-dirty = false
|
||||
"
|
||||
|
||||
ensure_nix_profile() {
|
||||
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
||||
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared flake-introspection helpers for scripts/*.sh. Source alongside
|
||||
# env.sh:
|
||||
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-eval.sh"
|
||||
#
|
||||
# NIX_EVAL_FLAGS: --no-use-registries so a call here never resolves through
|
||||
# the user's global flake registry (every call targets this repo's own
|
||||
# flake, or an explicit github: ref, not a registry alias); --no-accept-flake-config
|
||||
# so a flake input's own nixConfig (e.g. a dependency's substituters) is
|
||||
# never honored -- matches accept-flake-config = false already set repo-wide
|
||||
# (see lib/nix-bootstrap.sh / CLAUDE.md). Reuse this array rather than
|
||||
# retyping the two flags at each call site.
|
||||
declare -a NIX_EVAL_FLAGS=(--no-use-registries --no-accept-flake-config)
|
||||
|
||||
# list_flake_targets <flake_ref>
|
||||
# Prints the attribute names under <flake_ref>#nixosConfigurations, one per
|
||||
# line, e.g.:
|
||||
# list_flake_targets . # from inside the repo
|
||||
# list_flake_targets "$repo_root" # from anywhere
|
||||
list_flake_targets() {
|
||||
local flake_ref="$1"
|
||||
nix eval --json "${NIX_EVAL_FLAGS[@]}" \
|
||||
"${flake_ref}#nixosConfigurations" --apply builtins.attrNames \
|
||||
| jq -r '.[]'
|
||||
}
|
||||
|
||||
# flake_target_hostname <flake_ref> <target>
|
||||
# Prints one nixosConfigurations target's config.networking.hostName.
|
||||
# Empty (not an error under set -e) if the target doesn't exist or the
|
||||
# eval otherwise fails -- callers that need to distinguish "empty" from
|
||||
# "eval failed" should check $? themselves instead of relying on this.
|
||||
flake_target_hostname() {
|
||||
local flake_ref="$1" target="$2"
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared sops/age helpers for scripts/secrets/backup-admin-key.sh,
|
||||
# scripts/secrets/rotate-admin-key.sh, and scripts/secrets/sync-host-keys.sh -- all three
|
||||
# derive an age public key from a private identity file the same way, two
|
||||
# of them resolve the same sops/age default key-file path, and two of them
|
||||
# run `sops updatekeys` the same way. Kept in one place so they can't drift
|
||||
# apart. Source alongside env.sh:
|
||||
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/sops-age.sh"
|
||||
#
|
||||
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
|
||||
# nix_extra_opts) if the caller has already set it, same convention as
|
||||
# lib/ssh-host-keys.sh. Falls back to no extra options if the caller never
|
||||
# sourced env.sh.
|
||||
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
|
||||
declare -a NIX_OPTS=()
|
||||
fi
|
||||
|
||||
# sops/age's own default identity-file resolution order, minus $SOPS_AGE_KEY
|
||||
# itself (an inline identity, not a path -- callers that accept it check it
|
||||
# separately, before falling back to this).
|
||||
: "${DEFAULT_SOPS_AGE_KEY_FILE:=${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}}"
|
||||
|
||||
# age_pubkey_from_identity_file <identity-file>
|
||||
# Prints the age public key for a private identity file (age-keygen -y).
|
||||
age_pubkey_from_identity_file() {
|
||||
local identity_file="$1"
|
||||
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '${identity_file}'"
|
||||
}
|
||||
|
||||
# sops_yaml_admin_pubkey <sops-yaml-path>
|
||||
# Prints .sops.yaml's current &admin age public key, or empty (not an error
|
||||
# under set -e) if no such anchor line exists -- callers that need to treat
|
||||
# "missing" as fatal check for an empty result themselves.
|
||||
sops_yaml_admin_pubkey() {
|
||||
local sops_yaml="$1"
|
||||
grep -E '^ - &admin age1' "$sops_yaml" 2>/dev/null | awk '{print $NF}' || true
|
||||
}
|
||||
|
||||
# sops_updatekeys <secrets-file> [key-file]
|
||||
# Re-encrypts <secrets-file> for .sops.yaml's current recipient set. If
|
||||
# <key-file> is given, decrypts with that identity (SOPS_AGE_KEY_FILE)
|
||||
# instead of whatever's ambient -- needed when the ambient default key
|
||||
# doesn't match yet (e.g. mid-rotation, decrypting with the outgoing key).
|
||||
sops_updatekeys() {
|
||||
local secrets_file="$1" key_file="${2:-}"
|
||||
if [[ -n "$key_file" ]]; then
|
||||
SOPS_AGE_KEY_FILE="$key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||
"sops updatekeys --yes '${secrets_file}'"
|
||||
else
|
||||
nix-shell "${NIX_OPTS[@]}" -p sops --run "sops updatekeys --yes '${secrets_file}'"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared SSH-host-key / age-conversion helpers for scripts/secrets/sync-host-keys.sh
|
||||
# and scripts/secrets/prepare-host-key.sh -- both generate the same kind of key
|
||||
# (ed25519, no passphrase, the sops-nix age-derivation input) and convert it
|
||||
# to an age recipient the same way; kept in one place so the two can't
|
||||
# drift apart.
|
||||
#
|
||||
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
|
||||
# nix_extra_opts) if the caller has already set it, so a decision to avoid
|
||||
# an unreachable nix-cache is reused here instead of probed again. Falls
|
||||
# back to no extra options if the caller never sourced env.sh.
|
||||
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
|
||||
declare -a NIX_OPTS=()
|
||||
fi
|
||||
|
||||
# generate_host_ed25519_key <hostname> <keyfile>
|
||||
# Writes <keyfile> and <keyfile>.pub. Caller is responsible for refusing to
|
||||
# overwrite an existing keyfile -- this always runs ssh-keygen fresh.
|
||||
generate_host_ed25519_key() {
|
||||
local hostname="$1" keyfile="$2"
|
||||
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
|
||||
"ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
|
||||
}
|
||||
|
||||
# ssh_pubkey_to_age <pubkeyfile>
|
||||
# Prints the age public key derived from an ed25519 SSH public key file.
|
||||
ssh_pubkey_to_age() {
|
||||
local pubkeyfile="$1"
|
||||
nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${pubkeyfile}'"
|
||||
}
|
||||
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}."
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
# Points a non-NixOS Debian machine's Nix install at nix-cache: adds it as
|
||||
# a substituter (with cache.nixos.org kept as fallback) and, once the
|
||||
# remote-builder private key is installed, as a distributed-build machine
|
||||
# too.
|
||||
#
|
||||
# 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 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:
|
||||
# ./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
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPeWgMsdaiz4axT/deFc1+0B5bN+GX/NOeW9bbQ0c/IT lxc-nix-cache}"
|
||||
: "${REMOTE_BUILDER_USER:=nixremote}"
|
||||
|
||||
CACHE_PUB_KEY="cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
|
||||
FALLBACK_URL="https://cache.nixos.org/"
|
||||
FALLBACK_PUB_KEY="cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||
|
||||
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
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) dry_run=1 ;;
|
||||
--no-remote-builder) with_remote_builder=0 ;;
|
||||
--no-restart) restart_daemon=0 ;;
|
||||
-h|--help)
|
||||
sed -n '2,37p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown argument: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
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 [[ "$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
|
||||
case "$(uname -m)" in
|
||||
x86_64) nix_system="x86_64-linux" ;;
|
||||
aarch64) nix_system="aarch64-linux" ;;
|
||||
*)
|
||||
echo "WARNING: unrecognized architecture '$(uname -m)' -- skipping remote builder, keeping substituter config." >&2
|
||||
with_remote_builder=0
|
||||
;;
|
||||
esac
|
||||
if [[ "$with_remote_builder" -eq 1 ]]; then
|
||||
builder_line="builders = ssh://${REMOTE_BUILDER_USER}@${NIX_CACHE_HOST} ${nix_system} ${REMOTE_BUILDER_KEY} 4 2 big-parallel,kvm,nixos-test,benchmark"
|
||||
fi
|
||||
else
|
||||
echo "WARNING: $REMOTE_BUILDER_KEY not found -- skipping remote builder config (substituter still configured)." >&2
|
||||
echo " See docs/nix-cache.md 'Remote builder SSH keys' for how to install it, then re-run this script." >&2
|
||||
with_remote_builder=0
|
||||
fi
|
||||
fi
|
||||
|
||||
block="$(cat <<EOF
|
||||
$MARKER_BEGIN
|
||||
extra-substituters = http://${NIX_CACHE_HOST} ${FALLBACK_URL}
|
||||
extra-trusted-public-keys = ${CACHE_PUB_KEY} ${FALLBACK_PUB_KEY}
|
||||
EOF
|
||||
)"
|
||||
if [[ "$with_remote_builder" -eq 1 ]]; then
|
||||
block="${block}
|
||||
builders-use-substitutes = true
|
||||
${builder_line}"
|
||||
fi
|
||||
block="${block}
|
||||
$MARKER_END"
|
||||
|
||||
echo "== nix.conf block to install ($NIX_CONF) =="
|
||||
echo "$block"
|
||||
echo "================================"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "(--dry-run: not writing $NIX_CONF)"
|
||||
else
|
||||
tmp_conf="$(mktemp)"
|
||||
trap 'rm -f "$tmp_conf"' EXIT
|
||||
|
||||
if grep -qF "$MARKER_BEGIN" "$NIX_CONF"; then
|
||||
awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" -v block="$block" '
|
||||
$0 == begin { print block; skip = 1; next }
|
||||
$0 == end { skip = 0; next }
|
||||
skip { next }
|
||||
{ print }
|
||||
' "$NIX_CONF" > "$tmp_conf"
|
||||
else
|
||||
cp "$NIX_CONF" "$tmp_conf"
|
||||
printf '\n%s\n' "$block" >> "$tmp_conf"
|
||||
fi
|
||||
|
||||
cp "$NIX_CONF" "${NIX_CONF}.bak.$(date +%Y%m%d%H%M%S)"
|
||||
install -m 0644 "$tmp_conf" "$NIX_CONF"
|
||||
echo "Updated $NIX_CONF (backup saved alongside it)."
|
||||
fi
|
||||
|
||||
if [[ "$with_remote_builder" -eq 1 ]]; then
|
||||
known_hosts_line="${NIX_CACHE_HOST} ${NIX_CACHE_HOST_KEY}"
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "(--dry-run: would ensure this line is present in $KNOWN_HOSTS)"
|
||||
echo " $known_hosts_line"
|
||||
else
|
||||
mkdir -p "$(dirname "$KNOWN_HOSTS")"
|
||||
touch "$KNOWN_HOSTS"
|
||||
if ! grep -qF "$known_hosts_line" "$KNOWN_HOSTS" 2>/dev/null; then
|
||||
echo "$known_hosts_line" >> "$KNOWN_HOSTS"
|
||||
echo "Added nix-cache's SSH host key to $KNOWN_HOSTS."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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."
|
||||
else
|
||||
echo "nix-daemon not managed by systemd (or not running) -- restart it manually to pick up the new config."
|
||||
fi
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done. Verify with:
|
||||
curl http://${NIX_CACHE_HOST}/nix-cache-info
|
||||
nix show-config | grep -E 'substituters|trusted-public-keys|builders'
|
||||
EOF
|
||||
if [[ "$with_remote_builder" -eq 1 ]]; then
|
||||
cat <<EOF
|
||||
ssh -i ${REMOTE_BUILDER_KEY} ${REMOTE_BUILDER_USER}@${NIX_CACHE_HOST} nix-store --version
|
||||
nix build nixpkgs#hello -L
|
||||
EOF
|
||||
fi
|
||||
Executable
+860
@@ -0,0 +1,860 @@
|
||||
#!/usr/bin/env bash
|
||||
# Creates new Proxmox VMs/LXC containers from this flake, and reconfigures
|
||||
# existing ones -- the manual workflows in docs/proxmox-images.md (VM) and
|
||||
# docs/auto-installer.md's "LXC hosts" section (container), automated.
|
||||
#
|
||||
# Images are built directly on the Proxmox node (PROXMOX_REMOTE_REPO_DIR /
|
||||
# --remote-repo-dir in scripts/env.sh), not on whatever machine runs this
|
||||
# script -- there's no multi-gigabyte image to transfer afterward. The first
|
||||
# time a node doesn't have that repo path yet, it's bootstrapped: cloned from
|
||||
# this checkout's own `origin` remote, then scripts/codex-setup.sh installs
|
||||
# the build tooling (Nix, etc.). Every run after that just `git pull`s it and
|
||||
# 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
|
||||
# scripts/proxmox/create-proxmox-resource.sh --modify --vmid <n> [--cores N] [--memory MB] [--grow-disk GB]
|
||||
#
|
||||
# SAFETY:
|
||||
# - The default (create) mode only ever creates a NEW resource -- it
|
||||
# refuses to run if the target VMID already exists on the node, or if
|
||||
# a VM/CT identified as --host already exists under any other VMID
|
||||
# (checked live against the node; --allow-duplicate-host overrides).
|
||||
# - --allow-duplicate-host distinguishes an exact match (same --type
|
||||
# *and* --host, e.g. re-running --type lxc --host docker while an
|
||||
# lxc-docker container already exists -- almost always a redeploy of
|
||||
# the same target to pick up a rebuilt image) from a cross-type match
|
||||
# (a different platform sharing the same host identity, e.g. a
|
||||
# proxmox-docker VM coexisting with lxc-docker). Only the exact match
|
||||
# is destroyed and replaced, after typing the hostname back to
|
||||
# confirm (outside --dry-run) -- a cross-type match is always left
|
||||
# untouched, matching-or-not.
|
||||
# - --modify only ever touches a resource you name explicitly via
|
||||
# --vmid, shows exactly what will change first, and (outside
|
||||
# --dry-run) always requires typing that VMID back to confirm before
|
||||
# anything is sent to the node. There is no bulk/implicit modify.
|
||||
# - Outside of --allow-duplicate-host's exact-match replace above,
|
||||
# neither mode can start/stop/delete a resource.
|
||||
#
|
||||
# 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/nix-eval.sh
|
||||
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||
# shellcheck source=../lib/confirm.sh
|
||||
source "${repo_root}/scripts/lib/confirm.sh"
|
||||
|
||||
sync_keys="${repo_root}/scripts/secrets/sync-host-keys.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --type lxc|vm --host <name> [options] (create)
|
||||
$0 --type lxc|vm --list (list --host values)
|
||||
$0 --modify --vmid <n> [options] (reconfigure)
|
||||
|
||||
Create mode (default):
|
||||
--type lxc|vm lxc = container, built as a CT template tarball.
|
||||
vm = VM, built as a Disko .raw disk image (UEFI/OVMF).
|
||||
--host <name> Which host identity to deploy -- matches
|
||||
config.networking.hostName (server, docker,
|
||||
nix-cache, nixos, pxe-boot, nix-minimal). Use
|
||||
--list to see what's available for --type.
|
||||
--name <name> Proxmox display name/hostname (default: --host's
|
||||
value, e.g. nix-cache -- for lxc this becomes the
|
||||
guest's real networking.hostName too, since
|
||||
proxmoxLXC.manageHostName pulls it from Proxmox's
|
||||
own container config, so it must match host.nix
|
||||
regardless of build type)
|
||||
--vmid <n> Numeric VMID (default: next free, via
|
||||
\`pvesh get /cluster/nextid\` on the node).
|
||||
Refuses to run if this ID already exists.
|
||||
--disk-size <GB> lxc only: rootfs size for \`pct create\`
|
||||
(default: \$PROXMOX_DEFAULT_LXC_DISK_GB, ${PROXMOX_DEFAULT_LXC_DISK_GB}).
|
||||
--image <path> Use this local image/tarball (uploaded to the
|
||||
node via scp) instead of checking the node /
|
||||
building one there from the flake.
|
||||
--force-rebuild Skip the "does the node already have this
|
||||
image" check -- always build fresh and
|
||||
overwrite what's there.
|
||||
--remote-repo-dir <path> Where this flake repo lives (or gets
|
||||
cloned) on the node, and is built from
|
||||
(default: \$PROXMOX_REMOTE_REPO_DIR, ${PROXMOX_REMOTE_REPO_DIR}).
|
||||
--allow-duplicate-host Required if a VM/CT identified as --host
|
||||
already exists on the node (checked live via
|
||||
qm/pct, not any file in this repo) --
|
||||
otherwise refused, since it'd share that
|
||||
host's hostName/hostId. An existing resource
|
||||
of this *same* --type (e.g. re-running --type
|
||||
lxc --host docker over an existing lxc-docker)
|
||||
is destroyed and replaced, after confirming --
|
||||
a different --type sharing the same --host
|
||||
(e.g. a proxmox-docker VM) is always left
|
||||
untouched.
|
||||
|
||||
Modify mode (reconfigure an EXISTING resource -- requires --modify):
|
||||
--modify Switch to modify mode.
|
||||
--vmid <n> Required: which existing resource to change.
|
||||
Type/VM-vs-CT is auto-detected on the node.
|
||||
--grow-disk <GB> Grow the primary disk by this many GB
|
||||
(qm/pct resize; Proxmox only supports
|
||||
growing, never shrinking, an existing disk).
|
||||
At least one of --cores / --memory / --grow-disk is required. Always
|
||||
prints the current -> new values and requires typing the VMID back to
|
||||
confirm, even outside --dry-run.
|
||||
|
||||
Shared:
|
||||
--cores <n> create: default \$PROXMOX_DEFAULT_CORES (${PROXMOX_DEFAULT_CORES}).
|
||||
modify: omit to leave unchanged.
|
||||
--memory <MB> create: default \$PROXMOX_DEFAULT_MEMORY_MB (${PROXMOX_DEFAULT_MEMORY_MB}).
|
||||
modify: omit to leave unchanged.
|
||||
--swap <MB> lxc only, create time: \`--memory\` doesn't
|
||||
touch swap -- it silently stays at Proxmox's
|
||||
own 512M default otherwise. (default: matches
|
||||
whatever --memory resolves to)
|
||||
--storage <pool> (default: \$PROXMOX_STORAGE, ${PROXMOX_STORAGE})
|
||||
--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} --
|
||||
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
|
||||
|
||||
Config for --storage/--bridge/--node/etc. lives in scripts/env.sh -- edit
|
||||
that instead of passing the same flag every time.
|
||||
EOF
|
||||
}
|
||||
|
||||
dry_run=0
|
||||
modify=0
|
||||
type=""
|
||||
host=""
|
||||
name=""
|
||||
vmid=""
|
||||
cores=""
|
||||
memory=""
|
||||
swap=""
|
||||
disk_size=""
|
||||
grow_disk=""
|
||||
image=""
|
||||
storage="$PROXMOX_STORAGE"
|
||||
iso_storage="$PROXMOX_ISO_STORAGE"
|
||||
bridge="$PROXMOX_BRIDGE"
|
||||
node="$PROXMOX_HOST"
|
||||
remote_repo_dir="$PROXMOX_REMOTE_REPO_DIR"
|
||||
do_list=0
|
||||
allow_duplicate_host=0
|
||||
force_rebuild=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--type) type="$2"; shift 2 ;;
|
||||
--host) host="$2"; shift 2 ;;
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--vmid) vmid="$2"; shift 2 ;;
|
||||
--cores) cores="$2"; shift 2 ;;
|
||||
--memory) memory="$2"; shift 2 ;;
|
||||
--swap) swap="$2"; shift 2 ;;
|
||||
--disk-size) disk_size="$2"; shift 2 ;;
|
||||
--grow-disk) grow_disk="$2"; shift 2 ;;
|
||||
--image) image="$2"; shift 2 ;;
|
||||
--storage) storage="$2"; shift 2 ;;
|
||||
--iso-storage) iso_storage="$2"; shift 2 ;;
|
||||
--bridge) bridge="$2"; shift 2 ;;
|
||||
--node) node="$2"; shift 2 ;;
|
||||
--remote-repo-dir) remote_repo_dir="$2"; shift 2 ;;
|
||||
--list) do_list=1; shift ;;
|
||||
--allow-duplicate-host) allow_duplicate_host=1; shift ;;
|
||||
--force-rebuild) force_rebuild=1; shift ;;
|
||||
--modify) modify=1; shift ;;
|
||||
--dry-run) dry_run=1; shift ;;
|
||||
-h | --help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
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} -- $*"
|
||||
else
|
||||
ssh "$ssh_target" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================ modify mode
|
||||
cmd_modify() {
|
||||
if [[ -z "$vmid" ]]; then
|
||||
echo "ERROR: --modify requires --vmid." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$cores" && -z "$memory" && -z "$grow_disk" ]]; then
|
||||
echo "ERROR: --modify needs at least one of --cores / --memory / --grow-disk." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Looking up VMID ${vmid} on ${node}..."
|
||||
local kind current_cores current_memory disk_key
|
||||
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1; then
|
||||
kind="vm"
|
||||
disk_key="scsi0"
|
||||
elif ssh "$ssh_target" "${sudo_prefix} pct status ${vmid}" >/dev/null 2>&1; then
|
||||
kind="lxc"
|
||||
disk_key="rootfs"
|
||||
else
|
||||
echo "ERROR: VMID ${vmid} doesn't exist on ${node} -- nothing to modify." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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 '?')"
|
||||
current_memory="$(echo "$current_config" | grep -oP '^memory:\s*\K\S+' || echo '?')"
|
||||
|
||||
echo
|
||||
echo "VMID ${vmid} is a ${kind} on ${node}. Planned changes:"
|
||||
[[ -n "$cores" ]] && echo " cores: ${current_cores} -> ${cores}"
|
||||
[[ -n "$memory" ]] && echo " memory: ${current_memory} MB -> ${memory} MB"
|
||||
[[ -n "$grow_disk" ]] && echo " ${disk_key}: grow by +${grow_disk}G (Proxmox can only grow, not shrink, an existing disk)"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo
|
||||
echo "[dry-run] Nothing was changed."
|
||||
return
|
||||
fi
|
||||
|
||||
echo
|
||||
if ! confirm_typed "$vmid" "Type the VMID (${vmid}) to confirm these changes: "; then
|
||||
echo "Cancelled -- input didn't match ${vmid}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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=""
|
||||
[[ -n "$cores" ]] && args="${args} --cores ${cores}"
|
||||
[[ -n "$memory" ]] && args="${args} --memory ${memory}"
|
||||
remote "${set_cmd} ${vmid}${args}"
|
||||
fi
|
||||
if [[ -n "$grow_disk" ]]; then
|
||||
remote "${resize_cmd} ${vmid} ${disk_key} +${grow_disk}G"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Done. VMID ${vmid} updated."
|
||||
}
|
||||
|
||||
if [[ "$modify" -eq 1 ]]; then
|
||||
cmd_modify
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================================================= create mode
|
||||
if [[ "$type" != "lxc" && "$type" != "vm" ]]; then
|
||||
echo "ERROR: --type must be 'lxc' or 'vm'." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
platform_prefix="lxc"
|
||||
[[ "$type" == "vm" ]] && platform_prefix="proxmox"
|
||||
[[ -z "$cores" ]] && cores="$PROXMOX_DEFAULT_CORES"
|
||||
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
|
||||
|
||||
# --- discover / resolve the flake target from --host --------------------
|
||||
# Emits "<target>\t<hostName>" pairs for every ${platform_prefix}-* flake
|
||||
# target -- the one source both --list and the --host lookup below read
|
||||
# from, so they can never see a different set of targets from each other.
|
||||
targets_for_platform() {
|
||||
local target
|
||||
for target in $(list_flake_targets "$repo_root" 2>/dev/null | grep -- "^${platform_prefix}-"); do
|
||||
printf '%s\t%s\n' "$target" "$(flake_target_hostname "$repo_root" "$target")"
|
||||
done
|
||||
}
|
||||
|
||||
list_hosts() {
|
||||
local target hostname
|
||||
while IFS=$'\t' read -r target hostname; do
|
||||
printf ' %-12s -> %s\n' "$hostname" "$target"
|
||||
done < <(targets_for_platform)
|
||||
}
|
||||
|
||||
if [[ "$do_list" -eq 1 ]]; then
|
||||
echo "Available --host values for --type ${type}:"
|
||||
list_hosts
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$host" ]]; then
|
||||
echo "ERROR: --host is required (or use --list to see options)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
flake_target=""
|
||||
while IFS=$'\t' read -r target hostname; do
|
||||
if [[ "$hostname" == "$host" ]]; then
|
||||
flake_target="$target"
|
||||
break
|
||||
fi
|
||||
done < <(targets_for_platform)
|
||||
|
||||
if [[ -z "$flake_target" ]]; then
|
||||
echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2
|
||||
echo "Available:" >&2
|
||||
list_hosts >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The container/VM's real identity is --host (e.g. "nix-cache"), validated
|
||||
# above against config.networking.hostName -- not the flake target name
|
||||
# (e.g. "lxc-nix-cache"), which is build-type-specific and only exists to
|
||||
# pick which platform variant to build. Defaulting --name to the flake
|
||||
# target would make lxc's --hostname (which proxmoxLXC.manageHostName
|
||||
# feeds straight into the guest's real hostname) disagree with host.nix.
|
||||
[[ -z "$name" ]] && name="$host"
|
||||
|
||||
# --- refuse to duplicate a host that's already live on the node ---------
|
||||
# Queries the node itself (qm/pct's own name/hostname config), not any
|
||||
# static list in this repo -- a file can't track whether a resource still
|
||||
# actually exists, and this used to be checked against variables.nix's
|
||||
# deployedTargets, which drifted stale (it kept naming a VM as "the real
|
||||
# deployment" well after that VM had been destroyed, blocking its own
|
||||
# redeploy) until that list was dropped in favour of this live check. This
|
||||
# only catches guests identified with the default --name (== --host, what
|
||||
# this script itself always uses unless --name is overridden) -- a guest
|
||||
# manually renamed on the node afterwards wouldn't match, but nothing here
|
||||
# creates guests that way.
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo
|
||||
echo "[dry-run] would check ${node} for an existing VM/CT identified as '${host}'"
|
||||
if [[ "$allow_duplicate_host" -eq 1 ]]; then
|
||||
echo "[dry-run] --allow-duplicate-host: an existing ${type} named '${host}' would be" \
|
||||
"destroyed and replaced; a different-type match would be left untouched"
|
||||
fi
|
||||
else
|
||||
echo
|
||||
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
|
||||
ssh_check_status=0
|
||||
existing="$(ssh "$ssh_target" bash -s -- "$host" "$sudo_prefix" <<'REMOTE_SCRIPT'
|
||||
target="$1"
|
||||
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 $($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
|
||||
REMOTE_SCRIPT
|
||||
)" || ssh_check_status=$?
|
||||
if [[ "$ssh_check_status" -ne 0 ]]; then
|
||||
echo "ERROR: couldn't reach ${node} (ssh exited ${ssh_check_status}) to check for an" >&2
|
||||
echo "existing '${host}' resource -- refusing to guess. Fix connectivity and retry," >&2
|
||||
echo "or pass --allow-duplicate-host if you're sure none exists (this skips the" >&2
|
||||
echo "check entirely)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Split into "exact" (same resource kind as --type -- i.e. literally this
|
||||
# same host+platform combo already exists, almost always a redeploy of
|
||||
# the same target to test a rebuilt image) vs "cross-type" (a different
|
||||
# platform sharing this host identity, e.g. a stopped proxmox-docker VM
|
||||
# coexisting with an lxc-docker container -- a deliberate, valid setup
|
||||
# this script has never managed and still won't). Read via a herestring
|
||||
# (not a pipe) so the appends below survive outside the loop.
|
||||
this_kind="$type"
|
||||
exact_matches=""
|
||||
cross_matches=""
|
||||
if [[ -n "$existing" ]]; then
|
||||
while read -r kind id n; do
|
||||
[[ -z "$kind" ]] && continue
|
||||
if [[ "$kind" == "$this_kind" ]]; then
|
||||
exact_matches+="${kind} ${id} ${n}"$'\n'
|
||||
else
|
||||
cross_matches+="${kind} ${id} ${n}"$'\n'
|
||||
fi
|
||||
done <<<"$existing"
|
||||
fi
|
||||
|
||||
if [[ -n "$exact_matches" && "$allow_duplicate_host" -ne 1 ]]; then
|
||||
echo "ERROR: '${host}' already exists on ${node} as this same resource type:" >&2
|
||||
echo "$exact_matches" | while read -r kind id n; do
|
||||
[[ -z "$kind" ]] && continue
|
||||
echo " - ${kind} VMID ${id} (${n})" >&2
|
||||
done
|
||||
echo "Refusing to create a second ${this_kind} sharing this identity. Pass" >&2
|
||||
echo "--allow-duplicate-host to destroy it and create a fresh one in its place" >&2
|
||||
echo "(after confirming), or use --modify to reconfigure the existing one instead." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$cross_matches" && "$allow_duplicate_host" -ne 1 ]]; then
|
||||
echo "ERROR: '${host}' already exists on ${node} as a different resource type:" >&2
|
||||
echo "$cross_matches" | while read -r kind id n; do
|
||||
[[ -z "$kind" ]] && continue
|
||||
echo " - ${kind} VMID ${id} (${n})" >&2
|
||||
done
|
||||
echo "Refusing to create a second resource sharing this identity. Pass" >&2
|
||||
echo "--allow-duplicate-host to create one anyway (it gets its own distinct" >&2
|
||||
echo "sops key and VMID -- the existing resource above is left untouched)," >&2
|
||||
echo "or use --modify to reconfigure the existing one instead." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$cross_matches" ]]; then
|
||||
echo "--allow-duplicate-host: '${host}' also exists on ${node} as a different resource" \
|
||||
"type -- leaving it untouched:"
|
||||
echo "$cross_matches" | while read -r kind id n; do
|
||||
[[ -z "$kind" ]] && continue
|
||||
echo " - ${kind} VMID ${id} (${n})"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n "$exact_matches" ]]; then
|
||||
echo "--allow-duplicate-host: '${host}' already exists on ${node} as this same resource" \
|
||||
"type -- it will be destroyed and replaced:"
|
||||
echo "$exact_matches" | while read -r kind id n; do
|
||||
[[ -z "$kind" ]] && continue
|
||||
echo " - ${kind} VMID ${id} (${n})"
|
||||
done
|
||||
echo
|
||||
if ! confirm_typed "$host" "Type the hostname (${host}) to confirm destroying the above and replacing it: "; then
|
||||
echo "Cancelled -- input didn't match ${host}." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$exact_matches" | while read -r kind id n; do
|
||||
[[ -z "$kind" ]] && continue
|
||||
echo "==> Destroying ${kind} VMID ${id} (${n})..."
|
||||
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" "${sudo_prefix} qm status ${id}" 2>/dev/null | grep -q running; then
|
||||
ssh "$ssh_target" "${sudo_prefix} qm stop ${id}"
|
||||
fi
|
||||
ssh "$ssh_target" "${sudo_prefix} qm destroy ${id} --purge 1"
|
||||
else
|
||||
ssh "$ssh_target" "${sudo_prefix} pct destroy ${id} --force 1 --purge 1"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'"
|
||||
|
||||
# Decide on nix-cache once, here -- this is the earliest point that needs
|
||||
# it (sync-host-keys.sh below needs nix-shell packages regardless of
|
||||
# whether an image ends up getting built later), and the decision is
|
||||
# exported so that subprocess -- and this script's own later build step,
|
||||
# if it gets there -- both reuse it instead of probing again.
|
||||
nix_extra_opts
|
||||
|
||||
# --- make sure this target has a registered host key --------------------
|
||||
echo
|
||||
echo "==> Ensuring host key exists and is registered..."
|
||||
sync_args=("$flake_target")
|
||||
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
|
||||
bash "$sync_keys" "${sync_args[@]}"
|
||||
|
||||
# --- VMID: pick one, and refuse to touch anything that already exists ---
|
||||
echo
|
||||
if [[ -z "$vmid" ]]; then
|
||||
if [[ "$dry_run" -eq 1 ]]; 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" "${sudo_prefix} pvesh get /cluster/nextid" | tr -d '[:space:]')"
|
||||
echo "Auto-assigned VMID: ${vmid}"
|
||||
fi
|
||||
else
|
||||
echo "Requested VMID: ${vmid}"
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" -eq 0 ]]; then
|
||||
# qm/pct status exits non-zero (and prints "does not exist") for a free
|
||||
# ID on that resource type -- but a VMID could exist as the OTHER
|
||||
# resource type (e.g. requested a CT id that's actually a VM), so check
|
||||
# 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" "${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
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- resolve the remote path -- fixed naming (not the nix store's own
|
||||
# derivation-hash-based filename), so a later run can check for it by name.
|
||||
# lxc uploads as a CT *template* (Proxmox's "vztmpl" content type, under
|
||||
# iso_storage) -- config.system.build.tarball is a plain rootfs tarball,
|
||||
# not a vzdump backup archive, so it's created with `pct create ... vztmpl`,
|
||||
# not restored with `pct restore` (that expects backup-archive metadata
|
||||
# this tarball doesn't have, and fails with "archive contains no
|
||||
# configuration file").
|
||||
remote_dir="/var/lib/vz/import"
|
||||
remote_filename="${flake_target}.raw"
|
||||
if [[ "$type" == "lxc" ]]; then
|
||||
remote_dir="/var/lib/vz/template/cache"
|
||||
remote_filename="${flake_target}.tar.xz"
|
||||
fi
|
||||
remote_path="${remote_dir}/${remote_filename}"
|
||||
|
||||
# --- ensure the flake repo (+ tooling) exists on the node, and is current --
|
||||
# Bootstraps once (git clone from this checkout's own `origin`, then
|
||||
# scripts/codex-setup.sh installs Nix + friends) if ${remote_repo_dir}
|
||||
# doesn't exist yet on the node; otherwise just `git pull`s it, so the image
|
||||
# built there reflects what's actually committed and pushed. Only called
|
||||
# right before an actual remote build below -- reusing an image already on
|
||||
# the node, or an explicit --image, never touch the node's checkout at all.
|
||||
ensure_remote_repo() {
|
||||
echo
|
||||
echo "==> Ensuring ${remote_repo_dir} exists and is current on ${node}..."
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would ensure ${remote_repo_dir} exists on ${node} (clone if missing, git pull if present), and would verify/bootstrap build tooling there (scripts/codex-setup.sh) if \`nix\` isn't already on PATH -- and if that bootstrap actually ran, would also configure ${node} as a nix-cache client (scripts/proxmox/configure-nix-cache-client.sh)"
|
||||
return
|
||||
fi
|
||||
|
||||
if ssh "$ssh_target" "test -d '${remote_repo_dir}/.git'"; then
|
||||
echo "Repo present -- pulling latest..."
|
||||
ssh "$ssh_target" "cd '${remote_repo_dir}' && git pull --ff-only"
|
||||
else
|
||||
local origin_url
|
||||
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
|
||||
if [[ -z "$origin_url" ]]; then
|
||||
echo "ERROR: ${remote_repo_dir} doesn't exist on ${node}, and this checkout has no" >&2
|
||||
echo "'origin' remote to clone from. Set one (git remote add origin <url>) or create" >&2
|
||||
echo "${remote_repo_dir} on ${node} yourself (e.g. git clone), then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Not present -- cloning from ${origin_url}..."
|
||||
ssh "$ssh_target" "git clone '${origin_url}' '${remote_repo_dir}'"
|
||||
fi
|
||||
|
||||
# Trivial check, run every time (not just right after a fresh clone) --
|
||||
# confirmed live: a first bootstrap can clone the repo successfully and
|
||||
# still leave the node without a working `nix` (e.g. the node had no
|
||||
# `sudo`, which the Nix installer's root path depends on -- see the fix
|
||||
# in scripts/codex-setup.sh), and a later run with the repo already
|
||||
# present would otherwise never retry it. Sources
|
||||
# scripts/lib/nix-bootstrap.sh's ensure_nix_profile first -- a
|
||||
# single-user Nix install typically only gets sourced into login shells,
|
||||
# and ssh's non-interactive command execution is neither, so a
|
||||
# freshly-installed `nix` still wouldn't be on PATH here without it.
|
||||
#
|
||||
# Just `nix` today -- the only thing the remote build commands below
|
||||
# actually invoke -- but a list (not a single hardcoded check) so a
|
||||
# future remote step needing another tool can add itself here instead of
|
||||
# growing a parallel check.
|
||||
local remote_required_cmds=(nix)
|
||||
local tooling_check_cmd="cd '${remote_repo_dir}' && . scripts/lib/nix-bootstrap.sh && ensure_nix_profile"
|
||||
local cmd
|
||||
for cmd in "${remote_required_cmds[@]}"; do
|
||||
tooling_check_cmd="${tooling_check_cmd} && command -v ${cmd}"
|
||||
done
|
||||
|
||||
if ssh "$ssh_target" "$tooling_check_cmd" >/dev/null 2>&1; then
|
||||
echo "Build tooling already present on ${node}."
|
||||
else
|
||||
echo "==> Bootstrapping build tooling on ${node} (scripts/codex-setup.sh)..."
|
||||
ssh "$ssh_target" "cd '${remote_repo_dir}' && bash scripts/codex-setup.sh"
|
||||
|
||||
# Only on this first-time bootstrap, not every run -- a node that
|
||||
# already has tooling either already went through this once, or had
|
||||
# it configured some other way, and re-running is harmless but
|
||||
# pointless. Non-fatal: this only makes the node's own builds faster
|
||||
# (substitute from nix-cache instead of building from source) and
|
||||
# offloadable to it as a remote builder -- worth trying, not worth
|
||||
# aborting the image build over if nix-cache happens to be down right
|
||||
# now. Needs ensure_nix_profile first, same as the tooling_check_cmd
|
||||
# above -- ssh's non-interactive command execution won't have picked
|
||||
# up a freshly single-user-installed `nix` otherwise.
|
||||
echo "==> Configuring ${node} as a nix-cache substituter/remote-builder client..."
|
||||
if ! ssh "$ssh_target" "cd '${remote_repo_dir}' && . scripts/lib/nix-bootstrap.sh && ensure_nix_profile && bash scripts/proxmox/configure-nix-cache-client.sh"; then
|
||||
echo "WARNING: configure-nix-cache-client.sh failed on ${node} -- continuing without it (${node} will build from source / against cache.nixos.org only)." >&2
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# --- sync locally-managed host-keys/ to the node ---------------------------
|
||||
# Gitignored (see .gitignore), so `git pull` above never carries it -- both
|
||||
# build paths need it present as NIXOS_HOST_KEYS_DIR / --pre-format-files
|
||||
# input on the node itself now that the build runs there. scp (not rsync,
|
||||
# not already a dependency anywhere else in this repo) mirrors how this
|
||||
# script already transfers the --image case below.
|
||||
sync_remote_host_keys() {
|
||||
echo
|
||||
echo "==> Syncing host-keys/ to ${node}..."
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would copy ${repo_root}/host-keys/ to ${ssh_target}:${remote_repo_dir}/host-keys/"
|
||||
return
|
||||
fi
|
||||
ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'"
|
||||
scp -pr "${repo_root}/host-keys/." "${ssh_target}:${remote_repo_dir}/host-keys/"
|
||||
}
|
||||
|
||||
# --- build (or reuse an image already on the node) ------------------------
|
||||
echo
|
||||
local_image=""
|
||||
image_already_remote=0
|
||||
|
||||
if [[ -n "$image" ]]; then
|
||||
[[ -f "$image" ]] || { echo "ERROR: --image '${image}' not found." >&2; exit 1; }
|
||||
local_image="$image"
|
||||
echo "Using provided image: ${local_image}"
|
||||
elif [[ "$force_rebuild" -eq 1 ]]; then
|
||||
echo "--force-rebuild: skipping the existing-image check on ${node}."
|
||||
else
|
||||
echo "==> Checking whether ${node} already has ${remote_path}..."
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would check: ssh ${ssh_target} -- test -f ${remote_path}"
|
||||
elif ssh "$ssh_target" "test -f '${remote_path}'" 2>/dev/null; then
|
||||
echo "Found it -- reusing, skipping build (use --force-rebuild to override)."
|
||||
image_already_remote=1
|
||||
else
|
||||
echo "Not found -- will build."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
|
||||
ensure_remote_repo
|
||||
sync_remote_host_keys
|
||||
|
||||
# Relayed into the remote build below exactly as decided by the local
|
||||
# nix_extra_opts call earlier in this script -- that decision (whether
|
||||
# nix-cache is reachable) is made once, locally, same as it always has
|
||||
# been; only *where* the resulting "${NIX_OPTS[@]}" gets used as a `nix
|
||||
# build` flag moves to the node. NIX_EXTRA_OPTS is already a %q-quoted
|
||||
# string built for exactly this eval-based reconstruction (see env.sh).
|
||||
nix_opts_display=""
|
||||
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
|
||||
printf -v nix_opts_display '%q ' "${NIX_OPTS[@]}"
|
||||
nix_opts_display=" ${nix_opts_display% }"
|
||||
fi
|
||||
|
||||
if [[ "$type" == "lxc" ]]; then
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would build on ${node}: NIXOS_HOST_KEYS_DIR=\$(pwd)/host-keys nix build --impure \\"
|
||||
echo "[dry-run] --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.tarball"
|
||||
echo "[dry-run] would stage the result at ${remote_path}"
|
||||
local_image="<built-tarball>"
|
||||
else
|
||||
echo "==> Building LXC tarball for ${flake_target} on ${node}..."
|
||||
# Built as a single already-%q-quoted command string, not separate ssh
|
||||
# argv elements -- ssh joins remote command args with plain spaces and
|
||||
# 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 %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"; sudo_pfx="$6"
|
||||
declare -a NIX_OPTS=()
|
||||
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||
cd "$repo_dir"
|
||||
# A single-user Nix install only gets sourced into login shells; this ssh
|
||||
# session is neither, so `nix` wouldn't otherwise be on PATH here even
|
||||
# right after a successful install.
|
||||
. scripts/lib/nix-bootstrap.sh
|
||||
ensure_nix_profile
|
||||
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||
".#nixosConfigurations.${target}.config.system.build.tarball" \
|
||||
--out-link "result-${target}"
|
||||
built="$(find "result-${target}/tarball" -maxdepth 1 -type f | head -1)"
|
||||
if [[ -z "$built" ]]; then
|
||||
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
|
||||
exit 1
|
||||
fi
|
||||
$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
|
||||
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"
|
||||
echo "[dry-run] would run: ${sudo_display}./result-${flake_target} \\"
|
||||
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \\"
|
||||
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \\"
|
||||
echo "[dry-run] --build-memory 2048"
|
||||
echo "[dry-run] would stage the result at ${remote_path}"
|
||||
local_image="<built-image>.raw"
|
||||
else
|
||||
echo "==> Building Disko image for ${flake_target} on ${node}..."
|
||||
# See the LXC branch above for why this is one %q-quoted command
|
||||
# string rather than separate ssh argv elements.
|
||||
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"; sudo_pfx="$6"
|
||||
declare -a NIX_OPTS=()
|
||||
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||
cd "$repo_dir"
|
||||
. scripts/lib/nix-bootstrap.sh
|
||||
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_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
|
||||
built="$(find . -maxdepth 1 -name '*.raw' -newer "result-${target}" | head -1)"
|
||||
if [[ -z "$built" ]]; then
|
||||
echo "ERROR: no .raw image found in ${repo_dir} after build." >&2
|
||||
exit 1
|
||||
fi
|
||||
$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"
|
||||
echo "Built on ${node}: ${remote_path}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- upload -- only for an explicit --image; a build above stages its
|
||||
# result directly at ${remote_path} on the node already, and reusing an
|
||||
# image already on the node needs nothing transferred either. ------------
|
||||
echo
|
||||
if [[ -n "$image" ]]; then
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would upload: scp ${local_image} ${ssh_target}:${remote_path}"
|
||||
else
|
||||
echo "==> Uploading to ${node}:${remote_path}..."
|
||||
ssh "$ssh_target" "mkdir -p ${remote_dir}"
|
||||
scp "$local_image" "${ssh_target}:${remote_path}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- create -----------------------------------------------------------------
|
||||
echo
|
||||
if [[ "$type" == "lxc" ]]; then
|
||||
echo "==> Creating LXC container ${vmid} (${name})..."
|
||||
local_disk_size="${disk_size:-$PROXMOX_DEFAULT_LXC_DISK_GB}"
|
||||
# --memory doesn't touch swap -- it silently stays at Proxmox's own
|
||||
# 512M default otherwise (confirmed live: --memory 2048 left swap at
|
||||
# 512). Default to matching whatever --memory resolved to above.
|
||||
local_swap="${swap:-$memory}"
|
||||
# --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,
|
||||
# AppArmor denies the nested user namespaces and credential mounts
|
||||
# systemd routinely uses (even plain getty units), and every getty
|
||||
# crash-loops on a denied mount every ~3s (visible as garbage on the
|
||||
# console) while core services like nsncd fail the same way.
|
||||
#
|
||||
# ...,mount=nfs;nfs4: without it AppArmor blanket-denies the `nfs`/
|
||||
# `rpc_pipefs` mount syscalls any NFS client share needs -- confirmed
|
||||
# live on lxc-docker: `mount: /var/lib/nfs/rpc_pipefs: permission
|
||||
# denied`. The value's `;` (Proxmox's own multi-fstype separator for
|
||||
# this one feature, per PVE::LXC's use of PVE::ParseUtils::split_list)
|
||||
# must stay single-quoted here: create_cmd is sent to `remote()`, which
|
||||
# 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="${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 "${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.
|
||||
# --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 --agent enabled=1"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
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} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
|
||||
else
|
||||
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} -- ${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 "${sudo_prefix} qm set ${vmid} --scsi0 ${disk_id}"
|
||||
fi
|
||||
remote "${sudo_prefix} qm set ${vmid} --boot order=scsi0"
|
||||
remote "${sudo_prefix} qm start ${vmid}"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] Nothing was built, uploaded, or created."
|
||||
else
|
||||
echo "Done. ${name} (VMID ${vmid}) should be booting on ${node}."
|
||||
fi
|
||||
Executable
+250
@@ -0,0 +1,250 @@
|
||||
#!/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).
|
||||
# 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/)"
|
||||
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
+150
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# Backs up the local sops age key (the private key that decrypts
|
||||
# secrets/*.yaml -- normally the one trusted as &admin) to an arbitrary
|
||||
# destination path, e.g. a USB drive or other offline storage, so it can
|
||||
# later be restored and handed to rotate-admin-key.sh if this machine's
|
||||
# copy is ever lost, or to run either script from a different machine.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/secrets/backup-admin-key.sh <dest-path> [--key-file <path>] [--force] [--dry-run]
|
||||
#
|
||||
# Source key resolution matches sops/age's own default order:
|
||||
# $SOPS_AGE_KEY (inline identity text) if set, else
|
||||
# --key-file if given, else
|
||||
# $SOPS_AGE_KEY_FILE if set, else
|
||||
# ${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
sops_yaml="${repo_root}/.sops.yaml"
|
||||
|
||||
# shellcheck source=../env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
# shellcheck source=../lib/sops-age.sh
|
||||
source "${repo_root}/scripts/lib/sops-age.sh"
|
||||
|
||||
# Pin cwd for the same reason rotate-admin-key.sh does: age/sops calls
|
||||
# below should never depend on wherever the caller's shell happened to be.
|
||||
cd "$repo_root"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <dest-path> [--key-file <path>] [--force] [--dry-run]
|
||||
|
||||
<dest-path> Where to write the backup. Parent directories are
|
||||
created as needed. Written with 0600 permissions.
|
||||
--key-file <path> Read the key from here instead of the default
|
||||
sops/age resolution (\$SOPS_AGE_KEY_FILE, then
|
||||
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt).
|
||||
Ignored if \$SOPS_AGE_KEY is set (that always wins,
|
||||
same precedence sops/age itself uses).
|
||||
--force Overwrite <dest-path> if it already exists.
|
||||
--dry-run Print what would happen; write nothing.
|
||||
EOF
|
||||
}
|
||||
|
||||
dry_run=0
|
||||
force=0
|
||||
key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
|
||||
args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
force=1
|
||||
shift
|
||||
;;
|
||||
--key-file)
|
||||
key_file="${2:?--key-file requires a path}"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${#args[@]}" -ne 1 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
dest="${args[0]}"
|
||||
|
||||
nix_extra_opts
|
||||
|
||||
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
|
||||
echo "==> Source: \$SOPS_AGE_KEY (inline identity from the environment)."
|
||||
src_content="$SOPS_AGE_KEY"
|
||||
else
|
||||
[[ -s "$key_file" ]] || {
|
||||
echo "ERROR: no key found. \$SOPS_AGE_KEY is unset and ${key_file} doesn't exist or is empty." >&2
|
||||
exit 1
|
||||
}
|
||||
echo "==> Source: ${key_file}"
|
||||
src_content="$(cat "$key_file")"
|
||||
fi
|
||||
|
||||
# Round-trip through a private scratch file (rather than trusting the
|
||||
# source string as-is) so age-keygen -y validates it's a real identity
|
||||
# before anything is written to <dest-path>.
|
||||
scratch="$(mktemp)"
|
||||
trap 'rm -f "$scratch"' EXIT
|
||||
( umask 077; printf '%s\n' "$src_content" > "$scratch" )
|
||||
|
||||
src_pub="$(age_pubkey_from_identity_file "$scratch")" || {
|
||||
echo "ERROR: source doesn't look like a valid age identity (age-keygen -y failed)." >&2
|
||||
exit 1
|
||||
}
|
||||
echo " public key: ${src_pub}"
|
||||
|
||||
current_admin_pub="$(sops_yaml_admin_pubkey "$sops_yaml")"
|
||||
if [[ -n "$current_admin_pub" && "$current_admin_pub" != "$src_pub" ]]; then
|
||||
echo "NOTE: this key does not match .sops.yaml's current &admin entry (${current_admin_pub})."
|
||||
echo " Backing it up anyway -- this script doesn't require it to be the admin key."
|
||||
fi
|
||||
|
||||
if [[ -e "$dest" && "$force" -ne 1 ]]; then
|
||||
echo "ERROR: ${dest} already exists. Pass --force to overwrite." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo
|
||||
echo "[dry-run] would write $(wc -c <"$scratch" | tr -d ' ') bytes to ${dest} (mode 0600)"
|
||||
[[ -e "$dest" ]] && echo "[dry-run] would overwrite existing file (--force given)"
|
||||
echo "[dry-run] Nothing was written. Re-run without --dry-run to apply this."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
install -m 600 "$scratch" "$dest"
|
||||
|
||||
dest_pub="$(age_pubkey_from_identity_file "$dest")"
|
||||
if [[ "$dest_pub" != "$src_pub" ]]; then
|
||||
echo "ERROR: ${dest} was written but its public key doesn't match the source -- investigate before relying on this backup." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done. Backed up to: ${dest}
|
||||
public key: ${dest_pub}
|
||||
|
||||
This is a private key -- store it somewhere offline/secure, not in this
|
||||
repo or anywhere it'd get committed. Restore it with:
|
||||
scripts/secrets/rotate-admin-key.sh ${dest}
|
||||
EOF
|
||||
@@ -2,7 +2,7 @@
|
||||
# Generates a new machine's SSH host key by an arbitrary name, before it
|
||||
# necessarily has a flake target yet -- prints the .sops.yaml snippet to
|
||||
# add by hand. For any host that already has a flake target,
|
||||
# scripts/sync-host-keys.sh <target> does this same job plus the
|
||||
# scripts/secrets/sync-host-keys.sh <target> does this same job plus the
|
||||
# .sops.yaml/key_groups registration and re-encryption automatically; use
|
||||
# this script only to pre-generate a key ahead of adding the flake target
|
||||
# itself.
|
||||
@@ -22,9 +22,13 @@
|
||||
# new machine.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
# shellcheck source=../env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
# shellcheck source=../lib/ssh-host-keys.sh
|
||||
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
||||
|
||||
hostname="${1:?usage: scripts/prepare-host-key.sh <hostname>}"
|
||||
hostname="${1:?usage: scripts/secrets/prepare-host-key.sh <hostname>}"
|
||||
sops_yaml="${repo_root}/.sops.yaml"
|
||||
|
||||
if [[ ! -f "$sops_yaml" ]]; then
|
||||
@@ -41,9 +45,10 @@ if [[ -f "$keyfile" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
nix-shell -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
|
||||
nix_extra_opts
|
||||
generate_host_ed25519_key "$hostname" "$keyfile"
|
||||
|
||||
age_pub="$(nix-shell -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")"
|
||||
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Executable
+193
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rotates the &admin sops age key: decrypts with a backed-up copy of the
|
||||
# key CURRENTLY trusted as &admin, replaces .sops.yaml's &admin entry with
|
||||
# a new key already present in this environment, and re-encrypts every
|
||||
# secrets/*.yaml for the new recipient set. After this runs, the old key
|
||||
# can no longer decrypt anything -- this is a real, one-way handoff of
|
||||
# trust, not a preview.
|
||||
#
|
||||
# This is the automation for the manual steps create-proxmox-resource.sh /
|
||||
# sync-host-keys.sh print when they bootstrap a brand-new, not-yet-trusted
|
||||
# age key on a machine that's never had admin access before:
|
||||
#
|
||||
# scripts/secrets/rotate-admin-key.sh /path/to/backed-up/admin/keys.txt
|
||||
#
|
||||
# The backup key's *public* key must match .sops.yaml's current &admin
|
||||
# entry -- this script verifies that by deriving it, it doesn't just trust
|
||||
# the filename or take it on faith. The new key defaults to wherever sops
|
||||
# itself would already look ($SOPS_AGE_KEY_FILE, then the XDG default), so
|
||||
# the common case is just pointing this at the restored backup.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
sops_yaml="${repo_root}/.sops.yaml"
|
||||
|
||||
# shellcheck source=../env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
# shellcheck source=../lib/sops-age.sh
|
||||
source "${repo_root}/scripts/lib/sops-age.sh"
|
||||
|
||||
# sops resolves .sops.yaml by walking up from the process's cwd, not from
|
||||
# the target file's own path -- if this script were invoked from somewhere
|
||||
# other than the repo root (or from inside another checkout/worktree that
|
||||
# happens to have its own .sops.yaml), `sops updatekeys` would silently
|
||||
# re-encrypt against the WRONG config's recipient list instead of this
|
||||
# repo's. Pin cwd here so every sops/age call below is unambiguous
|
||||
# regardless of where the caller's shell started out.
|
||||
cd "$repo_root"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <path-to-backed-up-admin-key> [--new-key-file <path>] [--dry-run]
|
||||
|
||||
<path-to-backed-up-admin-key> age identity file for the key CURRENTLY
|
||||
trusted as &admin. Only ever read -- never
|
||||
copied or modified.
|
||||
--new-key-file <path> age identity file for the key to promote
|
||||
to &admin. Defaults to \$SOPS_AGE_KEY_FILE,
|
||||
then
|
||||
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt
|
||||
(sops/age's own default resolution order).
|
||||
--dry-run Print what would change; touches nothing
|
||||
(.sops.yaml untouched, no sops updatekeys
|
||||
calls).
|
||||
EOF
|
||||
}
|
||||
|
||||
dry_run=0
|
||||
new_key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
|
||||
args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
--new-key-file)
|
||||
new_key_file="${2:?--new-key-file requires a path}"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${#args[@]}" -ne 1 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
backup_key="${args[0]}"
|
||||
|
||||
[[ -s "$backup_key" ]] || { echo "ERROR: backup key file not found or empty: ${backup_key}" >&2; exit 1; }
|
||||
[[ -s "$new_key_file" ]] || { echo "ERROR: new key file not found or empty: ${new_key_file}" >&2; exit 1; }
|
||||
|
||||
nix_extra_opts
|
||||
|
||||
echo "==> Deriving public keys..."
|
||||
old_pub="$(age_pubkey_from_identity_file "$backup_key")"
|
||||
new_pub="$(age_pubkey_from_identity_file "$new_key_file")"
|
||||
echo " backup (old admin) key: ${old_pub}"
|
||||
echo " new admin key: ${new_pub}"
|
||||
|
||||
if [[ "$old_pub" == "$new_pub" ]]; then
|
||||
echo "ERROR: backup key and new key are identical -- nothing to rotate." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_admin_pub="$(sops_yaml_admin_pubkey "$sops_yaml")"
|
||||
if [[ -z "$current_admin_pub" ]]; then
|
||||
echo "ERROR: couldn't find a '&admin age1...' line in ${sops_yaml}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$current_admin_pub" != "$old_pub" ]]; then
|
||||
echo "ERROR: ${backup_key} doesn't match the current &admin key in .sops.yaml." >&2
|
||||
echo " .sops.yaml &admin: ${current_admin_pub}" >&2
|
||||
echo " backup key pubkey: ${old_pub}" >&2
|
||||
echo "Wrong backup file, or .sops.yaml has already moved on -- not touching anything." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t secrets_files < <(find "${repo_root}/secrets" -maxdepth 1 -name '*.yaml' | sort)
|
||||
if [[ "${#secrets_files[@]}" -eq 0 ]]; then
|
||||
echo "ERROR: no secrets/*.yaml files found under ${repo_root}/secrets." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# sops_can_decrypt <key-file> <secrets-file>: used both to confirm the
|
||||
# backup key still works before touching anything, and again after
|
||||
# rotation to confirm the new key does too.
|
||||
sops_can_decrypt() {
|
||||
local key_file="$1" secrets_file="$2"
|
||||
SOPS_AGE_KEY_FILE="$key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||
"sops -d '${secrets_file}'" >/dev/null
|
||||
}
|
||||
|
||||
echo "==> Confirming the backup key can actually decrypt..."
|
||||
if ! sops_can_decrypt "$backup_key" "${secrets_files[0]}"; then
|
||||
echo "ERROR: backup key failed to decrypt $(basename "${secrets_files[0]}") -- aborting." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " OK: decrypted $(basename "${secrets_files[0]}")"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo
|
||||
echo "[dry-run] would replace .sops.yaml's &admin line:"
|
||||
echo "[dry-run] - ${current_admin_pub}"
|
||||
echo "[dry-run] + ${new_pub}"
|
||||
echo "[dry-run] would then re-encrypt (sops updatekeys --yes) for the new recipient set:"
|
||||
for f in "${secrets_files[@]}"; do
|
||||
echo "[dry-run] secrets/$(basename "$f")"
|
||||
done
|
||||
echo
|
||||
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> Rotating .sops.yaml's &admin key..."
|
||||
sed -i "s|^ - &admin age1[a-z0-9]*| - \&admin ${new_pub}|" "$sops_yaml"
|
||||
grep -qF "$new_pub" "$sops_yaml" || {
|
||||
echo "ERROR: sed edit didn't take -- .sops.yaml left unchanged, check it by hand." >&2
|
||||
exit 1
|
||||
}
|
||||
echo " Updated."
|
||||
|
||||
echo "==> Re-encrypting secrets/*.yaml for the new recipient set..."
|
||||
for f in "${secrets_files[@]}"; do
|
||||
echo "==> $(basename "$f")"
|
||||
sops_updatekeys "$f" "$backup_key"
|
||||
done
|
||||
|
||||
echo "==> Verifying the new key can decrypt everything..."
|
||||
for f in "${secrets_files[@]}"; do
|
||||
if ! sops_can_decrypt "$new_key_file" "$f"; then
|
||||
echo "ERROR: new key failed to decrypt $(basename "$f") after rotation -- investigate before committing." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " OK: $(basename "$f")"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done. .sops.yaml's &admin key is now:
|
||||
${new_pub}
|
||||
|
||||
The old key (${old_pub}) can no longer decrypt any secrets/*.yaml
|
||||
re-encrypted above.
|
||||
|
||||
Review the diff, then commit:
|
||||
git add .sops.yaml secrets/*.yaml
|
||||
git commit -m "Rotate sops admin age key"
|
||||
EOF
|
||||
@@ -24,13 +24,21 @@
|
||||
# ever touches keys it itself manages.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
sops_yaml="${repo_root}/.sops.yaml"
|
||||
keydir="${repo_root}/host-keys"
|
||||
editor="${repo_root}/scripts/lib/sync-host-keys-edit-sops.py"
|
||||
|
||||
# shellcheck source=env.sh
|
||||
# shellcheck source=../env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
# shellcheck source=../lib/nix-eval.sh
|
||||
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||
# shellcheck source=../lib/ssh-host-keys.sh
|
||||
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
||||
# shellcheck source=../lib/sops-age.sh
|
||||
source "${repo_root}/scripts/lib/sops-age.sh"
|
||||
# shellcheck source=../lib/confirm.sh
|
||||
source "${repo_root}/scripts/lib/confirm.sh"
|
||||
|
||||
mkdir -p "$keydir"
|
||||
|
||||
@@ -74,7 +82,7 @@ ensure_admin_decrypt_key() {
|
||||
return
|
||||
fi
|
||||
|
||||
local key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
||||
local key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
|
||||
|
||||
if [[ -s "$key_file" ]]; then
|
||||
echo "Found existing sops age key at ${key_file}."
|
||||
@@ -93,7 +101,7 @@ ensure_admin_decrypt_key() {
|
||||
mkdir -p "$(dirname "$key_file")"
|
||||
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -o '${key_file}'" 2>&1 | grep -v "^Public key:" || true
|
||||
local new_pub
|
||||
new_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '${key_file}'")"
|
||||
new_pub="$(age_pubkey_from_identity_file "$key_file")"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
@@ -118,12 +126,10 @@ EOF
|
||||
}
|
||||
|
||||
discover_targets() {
|
||||
nix eval --json --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames \
|
||||
| jq -r '.[] | select(. != "installer")'
|
||||
# installer is the one nixosConfigurations target that doesn't import
|
||||
# sops-nix at all (see CLAUDE.md's "Security Notes" -- hardcoded login
|
||||
# password instead) -- config.sops.secrets doesn't exist for it.
|
||||
list_flake_targets "$repo_root" | grep -v '^installer$'
|
||||
}
|
||||
|
||||
locally_managed_hosts() {
|
||||
@@ -159,7 +165,7 @@ queue_host_sync() {
|
||||
echo "[dry-run] ${host}: would generate host key"
|
||||
else
|
||||
echo "==> ${host}: generating host key"
|
||||
nix-shell "${NIX_OPTS[@]}" -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${host}' -f '${keyfile}'" >/dev/null
|
||||
generate_host_ed25519_key "$host" "$keyfile"
|
||||
fi
|
||||
else
|
||||
echo "==> ${host}: host key already present"
|
||||
@@ -170,7 +176,7 @@ queue_host_sync() {
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
age_pub="dry-run-placeholder-not-a-real-key"
|
||||
else
|
||||
age_pub="$(nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")"
|
||||
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
|
||||
fi
|
||||
add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \
|
||||
'. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")"
|
||||
@@ -237,7 +243,7 @@ apply_edit_plan() {
|
||||
while IFS= read -r basename; do
|
||||
[[ -z "$basename" ]] && continue
|
||||
echo "==> secrets/${basename}"
|
||||
nix-shell "${NIX_OPTS[@]}" -p sops --run "sops updatekeys --yes '${repo_root}/secrets/${basename}'"
|
||||
sops_updatekeys "${repo_root}/secrets/${basename}"
|
||||
done <<<"$changed"
|
||||
fi
|
||||
fi
|
||||
@@ -357,8 +363,7 @@ cmd_regenerate_all() {
|
||||
echo "image/tarball before it can decrypt secrets again."
|
||||
|
||||
if [[ "$dry_run" -ne 1 ]]; then
|
||||
read -rp "Type REGENERATE to confirm: " confirm
|
||||
if [[ "$confirm" != "REGENERATE" ]]; then
|
||||
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
|
||||
echo "Cancelled."
|
||||
return
|
||||
fi
|
||||
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."
|
||||
+227
-47
@@ -1,81 +1,261 @@
|
||||
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+IFgyNTUxOSBpWjNSdEdZbUUzamswa08w
|
||||
aCtzSHB0bFVZMnYxTkpuM1psdVYzWW55SzMwCkliMWVOUlBqRG5wOGZjQVg4MkFz
|
||||
NEVkMXdkTjhWRlZmVGlzZElid2pUMXMKLS0tIGtHUmRCNXNhVmloUHYzQnE5YlBS
|
||||
YnVSQjJlT3JnQ1RNMm9xV2xKOGRZUDAKc4VTl9NEI9Rv8+4J3JTeHTt2h8Dr2IJv
|
||||
tfvoNJQM/w6RAJWNTkaDmzZa9OnUW+grDlBQKlDuAnr6fZmuNTH2hQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBndmw0dXdSd3paLzdlYXBv
|
||||
K25ZMy9sZ3Ftci8yckZzOFdITmFEaXpiamlzCnZFSDZqWnVKQzNZUXBITXZmOGF5
|
||||
ZUFwdTVuNWptS0lyei9XTWJiQkppenMKLS0tIFZRa255YVBKcTBCV3ZBSUN4NWdv
|
||||
UG5vV3FDaUNWM0Zwb1BrWnpDZXZCek0KhuUPg0MwDO9qGlCHhft3tmuvmjAVsWMu
|
||||
eO3S1tANZ/YesQylxXQl+pDYC4J1X4BZmtf9tXGLTQoC0nxSTbqQEA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOR25UN1c0aE5SYWphbU0y
|
||||
SHRTZ1B0WC9NU3Z5VHpzTXpLSUxHTDI1ZFE0CnFpYUN1eGZQejJMblZPd1ROeUth
|
||||
dklZYVVNa1ZNZ1d4dW9vMCsvQWp1RkEKLS0tIERxakx5L0JrQitib1EyNDRMbDQ5
|
||||
Z3hDWUFEazdxczVhaHJYK3VZeEJSSDgKkw9T4ZuT+VHIF4WopqRHt8vW30kOysJ3
|
||||
vOq6EZ3Fqkgmoxm69Zp2gFnuE9GZIBy3VPQVLU2k6dZGJ3IvmLYeBA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2U0VHSzh0SllHaFBoNzVU
|
||||
UlJtcE1YdG9tYWN6eitqSzFMR1IzbGtISFZzCmo2VEkrWlFlRW9lbmFpSUM1WkJl
|
||||
aHN1djk1eWMyaWdJUXZjQVo3QWNYUUUKLS0tIHVQaUI1Zzd6Ly9kYVoySjVYYVM2
|
||||
L3FxMmVJOUFkdXVQa1A2U2svZjRtUjQKMQPIeUzyKIaqZqONldMNoqACcQgXFV2D
|
||||
0B0Rn1bgDOKskMjUc5ffp4PztNGKr/Y3oVqzEChQc3V78WvzAGcjyQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0THJzMFBTTCtDMWRmZ25M
|
||||
dU55OVhBb0trWWRUNlArTnEzRjhiYngvRENNCjF4d0M5NlYyQW50TTdMRXpuUjRr
|
||||
M1NwV05JOHV6T2cxT2FheVpuZ0w2T0kKLS0tIFBxdlVpVEoxOUpSWjk1ejRsK1NM
|
||||
V1UwTU1scG91L2FIemtwSW5JbFlmeG8K/1WIlaIidy3x3ptoRpS/DG88064LQ6Mq
|
||||
GbfB0jfq5PILDQMMuZu5oIBY31SxwnhZ02Ns7gA67kgNIRSCmk9WyQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArbGVOaXlmb0hhUFpiSnBE
|
||||
OVJFdFpSeEpNLzhYNzFBVnExY0FPM1RzckVRCkVRQmgzTUFHRnl5MkljNFZVRFJY
|
||||
OVZ6REkyNkprWGV6Y1QrSHVCUlJlYXcKLS0tIGltTk9HekdHSGpHZUVtU2g1MHpJ
|
||||
TEkrYmlvNWMvcmtrMXh4YXZjU3lKREUKV/QdP8vXM8qjiwdLrZFpBe8EBm2j3LT9
|
||||
KHEiXxViVtLmRRsJzBPFbbQZmiyrI8IPTAb6wO9y0QteolHUst4PnQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmRURQVGw1a1E1MkIzV2pa
|
||||
RG5HSkM0c0huUGVWcnFZOGlacGlGOHFMTVFrCm5OS09HRFc3TGVUYmtzTStxL0Q5
|
||||
N2hRMEcxUE9MTmZXL0wvME5EZXF1Z00KLS0tIHdHMVVHcTZzMmdXU0s4QlVqSS9Y
|
||||
ZUVmcWhPaURIUFJGR0V4bUZwKzM1bm8KlvGMNEClbLlfvJqNQHhd0dI4ihShLChF
|
||||
GI/fydgrBruw3Otv6KLZu3CBC7iNcKlvZxz+YGD2qbicmyQ5hAhDSQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTV0lJR1NDZkVmNmNTekJo
|
||||
TjkyakRUbkZTaWpRY05HT1E5eHlIMTJLblhJClp4c3Y0Q3g2VDRWRlFSSHpwc2NJ
|
||||
eGJ1akRwb3dBUzMzdHNObDRMTTBXcm8KLS0tIEp6RnR0bVdzckNhWmlHMmhoY1FD
|
||||
d2NkaDRROFdHbUQ1V0hQM1E3eE5hZEkKFHuCbyc6tFL7H0UxxP8poPTomNRqHsrP
|
||||
b0Dfa26NrmK7tFNG1SHiW02wRZwrfFgHbfsOaJEJYnzaX9uGt0w4yQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5bWpFenBlQldna3RhSFpr
|
||||
bEJBaHJDMzM3OHlqTmozcWU0VDM1bTFUWFZzCnZtNHZjZ1U1RzNkUlBHZFozWXdt
|
||||
VVkxQjMvTDJtbFZnclpkUEd4TEVmNTAKLS0tIFlhV2ZSSzJLRVNoMmFyVktDOElR
|
||||
YUxqZUFoY1ZWeGlldGplMjVQa1A5aUUKWelY6yO7Mr6dRvj4MVMbq/Z9JgrAnahz
|
||||
BDhHqObzJrOCtfDCTWiYuP+0yvIFWItMWhGSMw9MwwivvwnrEa+ZuQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGUytEOTlCVW1zdGZxVHF1
|
||||
MmRlbVAxWXcwYWdQNU5JanFXTmVzcVBMY0c4CmhiYkZ6YklwOTE3c0pnU1ovblFv
|
||||
NDYrUzRwcDA1cCtpNEUwTDlKQkRnNUEKLS0tIDJFTmM5Y1VOalVueEV1ajdqeE83
|
||||
MW94VHdRSzlQWk1JbWFhcXcwQmtrdlEK1nx9rEHzQLkA5HTfqozP4503fdLfhpS1
|
||||
hXF1jyOUMTEpcCnjoVe9ApTuXuXJgpt9ld7XpQq9IzDmN/VPprnkOQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||
recipient: age1jy444f9d9stygj4p3w9kh54cqcfr654tvr75tdvee5cxsgtdtc9q3v60ep
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiK2hKb2VaVnoyaEVYUHZZ
|
||||
OWVUSGtONGs5dEljTjJQdlhEcjNjdjViT2pVCkpaOHVZMlpXOFRveVlMeXZqWmoz
|
||||
ZHJRQTR2dmJQSEozeTRGMEdUdFlmZ2MKLS0tIERWS1RVdW1jQytBZzlkb3puNjhH
|
||||
ZjdlZmtzNXVOQ25DeCthUzhRRm1MT2cKaxc7zGm57iJFSeYc2IPqF4Eaxa44nR37
|
||||
pWZw+erG4F9AAZ2F047q+oLKe0B8FLSF54IbcXdQhitgGNR7B2HVeA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEdzJ0N0RVWVZrenRhaGJH
|
||||
YnpmQnhjb3FhRGwvMWRUWVhQbUMvRS9oZ1UwCk9IU1FnLyt4bUFsSW5LUzRONFU5
|
||||
SndHQ3dBT3lRWFVxWWVDMFFFT2xGK0UKLS0tIDR6ZHg4WGl2RTA2SDRncnBLdHd0
|
||||
cFdWZE8zNkZGL2dGVkphUkxzNEMvd2cKsHB45C+Rj/glZAy0nCucuFeYpzDsJsRw
|
||||
Tua2XQMBF1CJQ1/Hweyg/H+d3v0q2tiT7Sf4d3Hjjn1pMUZHW1ovaw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1ekhUTG5VOFErL3pFeWZM
|
||||
RG9NVnN6NFl3bzlFeTQzdHFtZmhwem04alQ0ClpJRStObERMZ0w2V0NhR1FSeW96
|
||||
M1d2V2NjUkUrLzN2ZVNSbGY4bll5WmsKLS0tIC94dVFQcXJ6d3pLU0VHNEFGR0ls
|
||||
a1Q2UmNuSjVMNG5XZGZKV1VmNHFPQXcKQJrZGw/9fPnXeFZ4omrkEgrzwplhwvRW
|
||||
i0FXuepoU353sR7enyL34qPoOdm05ivowuPKNzkq8D4i5AF6vGv+YA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnSFI1cFVWdklWcCtUbkcw
|
||||
S3dFK2k2OFR4K2Z3V3FVVHVlQUV1V2ZtYVcwClBnRDJkeTFRL2JzSFN1eGZmdno3
|
||||
OWZwbFVLRWVEWVh2ektid1B5dk1hSWcKLS0tIG16SW1JbDdHMXVHcTBHOFU2S0J6
|
||||
aDlUOFRjcC8rcmVSQXZ6WFVxUHdibG8Kc4/bnpWJ0FmmmRSACJ6zXG0FTg4cGb8D
|
||||
OFLrJo0HVlSZnqs6jX6K6qr+K8KyGMTIJEPdUl6au7tSEFsJjvEylQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoZ2lHM2RBQ3lUK216dkcr
|
||||
Y3RrcXR3QTdFVjJRWlhpQWVFQ1cyZWNabkRFClAwRi9PSHF5ZWFSUzJuRXF3bU1R
|
||||
b0U5TFRZaFdmR1NMS3RRT3E3M2hUdE0KLS0tIEtrUy8ydkNyVHBiOVR6WEdjV2VN
|
||||
NVJHUVgwRkhxcmlwcFkrRlFwTEF6YVkKzXyJk0UnmUsvb+NzNVcf/gf7OEEt3P/K
|
||||
OIGxDrGfs/zNQgeKXNbQlQ4p4jOaybG8aCmX+A4qTk6/I8yY8LTJWg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6UHNRMUd1akx0ampjSE5I
|
||||
VFA5M2d1TytJZ01aV2x1eDdoNENMWUErL1ZvCmwxTFZhQUdIOGxSYzhYQ1R4K1g5
|
||||
RVJFenhVeWRBa0Y3M0FYdkRnMXJYNk0KLS0tIFFhOUt0MEdIVHB0b2FGM3Z6NEhK
|
||||
MGFremtoYVJHMzJtcnJTTm5CWk00ZWcK2KlNsdk6D+UC8C4koAjm6t5gIa+t/NnV
|
||||
Wnl1/zRxw+fVbtDWfmeoMnmJStukh3Qg+Wm5n6iw8n3ywBYxvQRr+Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||
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]
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzMTJxYWhTcVBNYlN1R1Rw
|
||||
UmY3ZWVmcjFEUmlEcm5zMHI2bEE2dTErM0V3Cm5PVTV0STc0dFgwa3ZHamxSY3N6
|
||||
aStvVExTdGM2dTVjazYwNzVZRGdDdTgKLS0tIGpodmd1ZnczbWlibmdIYjBVcXNN
|
||||
RWJtMVVnQ0pWNjFwblBUVm5wWUJvVFUKr13nSwN1+RrXYhDNDA1MnoVr6ynSKJB7
|
||||
7yz9O9LwukKwKj7C6dIt7yZSiUAHotm2nJPYZYYmyLCfUodMFZAIVg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0N3N4N3Y2VDk2MWVRbW9t
|
||||
eDQrVWMyb2NGRVVuay9Fam1HOW9keHE4MXdNCnIrNUFrQ2xMK2c4ZHYyNDVwblpo
|
||||
ZTJMak91NEJwc2RuN1FUV3VLOWdGSG8KLS0tIGI3alVhcmVDeW5LN25yL0dzYldj
|
||||
MSs3WEgySmRKQnBqNEN1eHVJY3FucmMKFstvTOgCy786oP5MuT9xuT1j3Y1hz+N4
|
||||
/R8tfGwjpN62ozYKB0Mj12WayhTSOWnl2FQ8c3LgtOiRhRbmfF/XDw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1fxxzpnfse8nd9wz78ht3m0plrmraacf4cpga0pe8fm2tdnqcgy8q7qsyvp
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0MG56am1GTHFCVmtCOG1B
|
||||
ZjVtbjE3L3BnTWhjdTc2bkpJRFJKZE1LNUJnClREQTh6WnlEVjVJemVBeHVmVkZ6
|
||||
SGZTeXp5cVUvdk8zcmhnMjhsRUo3SkEKLS0tIDc1dk04cWlmQ0xWUWFpRHFoUy9K
|
||||
RExCTWJxMFpoMGpCV3JuVmg4VDgzb00KQ9CDA5NrQJmY7dKyBXbdsN1jKQ0wttYR
|
||||
HEdf2txIPpczrk2hFwereL0yxvClOjLovvePcn7Z1G5pQiq5Up5/XA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFVGhuUFh3UjRWMDBrNHl2
|
||||
Z29qVnhkV2JIZ2J4NEpyVEVrbEpleVRaTEdJClpRaC9DSmltd2FRVVdNLzJXRkxz
|
||||
M29IMkh4aVh6RlgycTZFbERGQnNQVlkKLS0tIG8ycnVoOGR6cmplMkNYOXVrcGtj
|
||||
OHJNenBRZ1pFSDEzZDBieHFkRjZZaFkKBCrrAxHld4OYTBUfKMq1E2V3bEaZOgdu
|
||||
4I0fGBQ9VTKK6JM4vVzIM9/RHmPqJWkxNSesxxqkOAewex/RR6ndLA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAySkhiUEhlSko1SEc5NlNX
|
||||
dXN0OGJxZkZLZ0NPSmN2QTU3SXhVODFVakNzCkVhT25PTDBiaGEzTlV6ekRDZWN4
|
||||
Z25WYkd6ZzRYaEdCQzBIZU1WSU13L2cKLS0tIHJ5V0JkYkVSdkMwYVNONzZoT0RV
|
||||
TkdtZW1GR3ppYldJalgvYi82dllDaXMKx2kvTvWlSsVkvAd6rLQC+AClnGSi+nk1
|
||||
7toIeJqNsIloj92IWi2cZf57pw0BJX0Aa83FMr+AQAajhydh5gbizQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzM3RTZ1VOaXQwTCsrMEV3
|
||||
Zmg2VERzZy95dkM5eFZkbzBWRmtQSDVaT3lvClZzQWJFSkVEeFc2YU05aXVKczdx
|
||||
YzJGeWE4K2pmNytLNmNoeTZwVExydm8KLS0tIEVpYitFU2plblRQTk12MHcrZXNR
|
||||
dGdBUXhXTnRQTXBUMlVkdkFHcFpKZ3cKN2fMs6FsNX4M50GjGBO5ILa+ZW1Xm+hT
|
||||
LsGH6c6JssI4Yf7BDagZsZRi3pCcVZsW8r3PneVz2F0fII3I9ej9yQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ehkswwz2pqaz4svzh7ela5tdnssl8kn6d4vwwxd6zwg8exfpd43syyrrjp
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3L0hLSUxKRDY5WTRKQ0hY
|
||||
bEJ1WEhCYm8vYjI1ekFyZDU3ZjlySUV3c1RvCkN1ak1YNTEyY0VOeldWTVNpeFJJ
|
||||
UkhDcmZ2a1hHTXZYc3Yxa2JSSzROb2sKLS0tIEpXbTN2T3VuRTM2MGxpRFMzN3Vu
|
||||
SWN0Vzg1RXdxa05nd0lHSmFtVGhjRkkKburqnTF/LOrMqiOomMc8vg7Dr2ucTqFd
|
||||
UL+ZXizcKT+LVWoBZpOBmHZ3/3/6rWHKDTa3SGNVvcvDVMZQ/BJNyA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1vvev5m3shgckl62awa64rtug3lyact7jgxehkuu3vn3wpzulhans75w65s
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5Y2JQMC9tZnBSazhSQmsw
|
||||
Ym0vTXVCbUt4ZlUrZUQvR1BVc3E4Yi9KWXpZCmRTblBidHo2RHo1UGhqZG1PYjVm
|
||||
M2ZxRWxML25MK201YXhkdVlIMTBTeUEKLS0tIHRud1pZT09kN3h3WnNFRE9BaTZP
|
||||
YWg2Z0F5a0U3TzRiS0JJOVViRGpPelEKbkfSlclZIO7Owdqxg7ETwf7uNkZKdla1
|
||||
JRft9Nitth0HxEVbkIsvvHIzGjuPfhFgJRxs+pYmdBboLopadMjRFQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1eu65wsmez68gegnufl0gqrs0e6w2409mypjlajlq383d7l2e3pjqy5kn6v
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0SUJhTXE3b2RxRmg4WmdQ
|
||||
bW1lcldBd2dRalRZUG1PUVpxTTRoSjlMbWxZCkFqUFhsSzBjekhDWFAwczJlam1H
|
||||
RFJkZHBnOTlYT3JIOXFiS1Y1TTBhNEUKLS0tIGc2VVQwbHhGWnRTcTloZ2lZTEFM
|
||||
NzJGS1h2MUdSRHVoLzlQTHdVU2JMV00KWiwaygiH0aY1i9qF+2+373JXLcTKewkb
|
||||
F2b3/loCqC7T5OaJDDXpLteNLsZznIi7zLCBw/t3E4K++cJJvxS1ng==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age17pwyghxr6lq06fw46gwqzhc9ut4paz28rpwx5pmv3cxwak6rgyjsw7lk3w
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNRU9uRnBTbHNKVkQxOTZv
|
||||
V2l0dEdsaU5WK0JzWHJXSWhrbGFnVUMvWjFzCm5QVFZSaldNUXkwVTA1anprZTIx
|
||||
YzcyVnRRVU8zNjNyb2g4eEFuMGhTbVUKLS0tIDlyczdueTVOL3poZ2RqdGVNcEFy
|
||||
YldOaUNkbnJUU2lSRTZHMzNCdWFHNEEKDm7/U7J+rAwRBV4XSc52jWtpsjabG4ZR
|
||||
QEqqtRZKYPFehKV6jM4vdfqt+sJIcR8QdrZYllryjRsb6Cw/YrDtJw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jukmg69cqxnjd0lp5f534jhqe65rxew2hufcmyjxa3rkw6ayef6s2ylcmy
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3RWgrbVpEZXRJekVCQnVW
|
||||
d1FEaFVsZTBPOEFGdHpDZ0tHcXdoVG55czJZClpkemkwTFAxeFJWN3NwRzZOTGFz
|
||||
RzhYMjVoQ0ZRUjhGYnRFZ2taQVBqck0KLS0tIDU5bGExSEVsRm9OQXZTY1NsN2U4
|
||||
K2gra3dVZnBTcXZkOXAwbW43RnhBM0EKPZupNGKWC8JvY6zwmPWnvNXGMZQ4VBMK
|
||||
B/A0RoNEny481g4PEnR8RgFY6IWsotT6rveoYDcLgumJnlITUpanZg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1tzsrtwd3p3lrr9g7nv3z5nvmzsz54t2uc6tfqwutp6usmav83s2sck25cc
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRdUlycEJEMStRYzNCUVNo
|
||||
aFlZeTEycVVlMWhWNDFEZHhhbDBqWDMxK1dJCkZObzJWdW5nUGlNWTFVd0tMNmlm
|
||||
cHZSNlZZN0VRU0xZRk5TT0dFYUFHbkkKLS0tIDdaUlk2eHRCTktPK0lSWGl6MmxK
|
||||
c3B3QTVKSU85a0NDMUdtelRaR0tXdncK1yQTKQixLdMoYHD7EfiLrUbcVkOC9eck
|
||||
/Zm8r+LWS8+lAyvd7eUkrN3AuR/H/9kXXfa888T0z1L/LfeXPUhuJw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1rf4kj99wuq59k7w8ar326djmgmpl9hcwlnuag07f8gauq8c3y5mqne87s4
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmakp4M2hBczlrNkFHaTZW
|
||||
ZlYvbGN4alpGRFQ5Z3NucmxhdHpDWUtYS1Y0CkhUaXpKNVpEQ2RicDdhTTJoY0dI
|
||||
eHg4bHRDbHNUeURmeEppdnhjUVVOa00KLS0tIEJJQ25MVzVubGJOTHZXYldoTG04
|
||||
UThuekxFZlBLeEJFVnIrd203VVBPS0UKQ7BTKth3+AxJEYCAp2CtQdXHWCjdMVFD
|
||||
nbijayg+b2Cv5nACxX7adFcfN80GvmbiVx+PKDnK68213SAHugxi3w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1d0zhx7u3mfs3nktl67npey87cze4dwsfvfvgaje4rh3gwv500yssckcf6u
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSTDRxc1Y4UTkxc3dxQUN2
|
||||
Zi9icFhRdTVmOHRNZ0QrVU9qMTlQNmt6Y1NjCkZkekxiZWJzdC9XV3ZaWGdDLysw
|
||||
Z0IySzFLZmg2VVJWbmg4ZnJLWW9FeVkKLS0tIHp1QStLTENGcnJrZVVERW5jMytn
|
||||
ODJDdUhoUzJpc2FNTVBMUTZWUWZYMmMKefmM/fewwjRyv7pPmln0mu2XpLyoZDtv
|
||||
qDqS0q0NMdi+ASlKgYl8A2DMO9TBSJmc6SxNMS9LIp0dZYCREJFMRQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jc6wx33hdhgwhk6nzy5rr8fkgmqxk9um639tk5h632nqfyaw8czskdptj9
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUTjIwa05wTXR1R1FaYVVn
|
||||
R3kxYTlXa1lZR0FTWSsrSE1lN0g5MXAxL21ZCm1Xd0t1UVJXTmR1UVE5Y0plVmtu
|
||||
RCt5RTRBL252aTdxUi93YnNJZ2tWM00KLS0tIEJhK2d2YTJjVHh6dE8ramxtTGpC
|
||||
U0VmdVNRUFRYdjJYZm4vWTBLdjRYVDgK9eJTpg3aGyV+YHSIWPh+0FtmIitchaY6
|
||||
4B2FJZt71VsCwZf4n/GfokVN946MLAEJ1G2TavjLWqWCNhmkM64ngA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1pgykvq4pmxhhjrqupcp99fyad2uht40pt79dkzg66cfvsjy5apjqls8u68
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3dmlIK3VHZHlZanIyNkpx
|
||||
dUtkWkljSFU0SXE0K3oxZ08vSTBhL3B3cHhBCldGSzhjN2lMRThOOFZDUWtDZjdq
|
||||
aTVIeXN5djB3OEdmTUtFZmpKVU5XNjgKLS0tIGdBcnljeE5iZVJKQkJYdnpyUU1Q
|
||||
Z2FONUlveWhnWGs2VXM1ZlNaWENSS2MKTuPGfA505R4vJ7lAsrkodve2Q6jaVqzU
|
||||
dqixxBAdcFQ89rq0z3He15ucgu/Vm4345LSm8JpcAMqupX0Ikbnsqg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19ht95nv8uhz2shjmakeut8mc3l5spvrcxs3thhe85an7u02r6sysyv457n
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5S0o4SlR2aHIrRXlLeHdp
|
||||
MmtEK1BxS2hlVlBVNHYvZ1d0QldDWERiQVVVCk04ZWNjNnZGQkVJL0hscDlGa0cv
|
||||
SlpJNEcwZVhrNk9FcUo3VVQxQSt1Tm8KLS0tIGJuWXpZZFJUaUdtSG54UTFoSzYv
|
||||
MFRxaFhubkJhcTFoV1pzMGc1ZGhEam8KOv07PvhyoG+LIll5pRbyOcMPZLL6GKn/
|
||||
KGnwao8QgHy9FLesA9q9LEYy1IQ8U76l4TtLDKE5+mOqDSBJCFo8cw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1mhaze5tgvc9lwjpml6dnp3xc292337wgm6376yh6tq4fn492ndjq9h23dq
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0eFluenpiUGFDL0o3ZW5P
|
||||
SDdUVGFlSk5kaldzeTd3Y1h0ejAyTCtNMXhJCjdudHBzdnZBRkxuWVc4YkRFaThN
|
||||
dDQ0MThTcENaNEN3YlpqSDNBQkJhQUUKLS0tIFpoNCt5aktyVjFrUmJSQkM3UDBB
|
||||
MkFVQ1VFcjVCbkNWZmdRN085YUo4WkUKJaoop5n/3Rb4myk5cl1kcFwp3RCEHvaW
|
||||
283WmSGgy8G2MzhtAlUkH1vTshz2bJUpjkCtHJ0PxYrzAk/gxBnDaA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age15me6sx0f8r58xh9v7aqrj6e99n7eu555jkpw422txpkqt4r02v0qpahlyq
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnWnpoRGVUQmNGRWVhRzRq
|
||||
MEpsSjkvZlJsc3VIMkNnTFEvcWlwRG1BdlRzClUydU5rbHJ3TlFvRWhXSjBVUWx2
|
||||
Y0VvQmJSbDNObWY4TkY2QXJyNlgyN3MKLS0tIEdtcjhmSVZ1ZXMzNC95cXFWdjNM
|
||||
RUp0enNxcnpiNzlWQmh5TXRuOFFPSmMKv7hPMN3kww+zyrKLZ+OgF74S2IXfgeG6
|
||||
Z2M6dvEfPKTyCgHoeo58OJKKGFqC7AmnUBWHhReZfWBRwyK4ES1DAg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1q3hu6eh3mt4saey7dc3yu04s7knnk2ygpm9xt6mg6rqtem6l0uyq4wzul5
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpeEV2eU9BZUZkL2xLaFI0
|
||||
V2E5NlhJeGhoODVjc3RsbU95UnNscnhPbVVJCnUrdmc3UERsWmgwWG16Qm9PMzhU
|
||||
NGpFRVlZREtiWnFEZnpnL3pCT3k3NlkKLS0tIEVvRStnRWFjQnZFUCtYTzBxd01V
|
||||
N3Z5M1FMclduSStKb1EzbEp5b2V6VHcKtsXI/VWWARdH+FDHB+R1YKyJqN4g0ahp
|
||||
AGdj6KLPZZDjbGpGITPy+uA8929//X5ZxKOjpccgCJqXFLUjh577oQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1tczst3x7thwtcz4vce4rg6kmlsszzm6j45kn2nv860z9wa93a92s05ppmc
|
||||
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+IFgyNTUxOSAvMGJzZjVoandxc1FCaDR6
|
||||
Z0hKTVFYRU9JMzA5djk1V3VBZjBETDJDS2xBCnZaRTkzTGg5OVBwckw0cXA4bzFi
|
||||
OHJpdHFwRFBGbXZCbzUrKytGTGx2S2sKLS0tIGNSdW1rQmdOL2lpek91N051L3gz
|
||||
K0duYnF5RlE3cmNXUVN3V0hLM0lUWlUKRYmIfAAfGCJOFwlimYk6bAvIKLokKufR
|
||||
OYnDcUZuPlj9cx/FJNC+7/K7WWmfrpEEeNOVzv7JdTDLjWBslTCWGQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGNHJPODlpaHpIRFNVUlhF
|
||||
WTJsdjB6Rmd6RlEvdDVXckduUkwxUGhVOFF3CkVJMXYzUVRkVjV0VVkzbGc1UlUr
|
||||
VVVXU0wwYkpzU2pZdmlpaVB3enZ1djAKLS0tIGdkeFVrYkdzM1ZNdElJamhpajFp
|
||||
YVZuVCtqUW95R3VHTlBadVoyZUZvNU0K/AtAp1bZ3QXKcVChHfMZVI9AHzTktXIB
|
||||
JNAPKJAxQycMOYPNFklyslOj+oKmvKm5AdXx4XPYW5qOK4k2HMDIQw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0a0dPSmdFbFRjQk1OWWNo
|
||||
Q09VSHhmNlUrVjVXQzcrdi9hVklnRjliWG5vCjJ3bVRUNWdPV3d6STEzcjFxZVQr
|
||||
MXZvdEVOVTlkWEVVNFV5UkUzWThSc2MKLS0tIGIwR1dNMnI3dlIxQ1d6NjhxZ3Vk
|
||||
a1N5aHQvUU5PSEgwVlprcUNEeVorVGsKmmdhYnpOqk3x+5P0h4AfGJco1PGLmAtS
|
||||
67hvtBQo1KSTwFyaYQUByfiTwIeMSu50SxvLi56ogT24CNADW6FjUQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ehkswwz2pqaz4svzh7ela5tdnssl8kn6d4vwwxd6zwg8exfpd43syyrrjp
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTTXYwS0dXUEI2WjRhZHNp
|
||||
Qzd5STdlWUNFTG04UEdySUhSODF2NklvNVhrCkxFd1pVSjRmb0hPaFpyWCtoQi9K
|
||||
eHVBcUU0amt3K1BONnV5Vnc2Z0JoazQKLS0tIGFrL0I5SjJYYThUS0Y0VVlraW9x
|
||||
Z1VLT1hpNWswd3FsWWVGRTkrNkJOZG8KY74xa+6Pfc9lpupTodTBa1hqIrotE2Gu
|
||||
H5+0g5D9838zxf/p7pzxh7rE7eo2gd9JT9bW2I4Pwn257Omblsyv1A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age17pwyghxr6lq06fw46gwqzhc9ut4paz28rpwx5pmv3cxwak6rgyjsw7lk3w
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpUExNcmw2S3hSKytvMVQr
|
||||
ZGJkVStyVjgvTURPSTRyMXZkdjdpRnhQZFV3CkVtQzBsUjFlRjNsc2dwU0R4UnlL
|
||||
YkhtZWVNSW5zWkhJM0NwOUpkVlJGQVUKLS0tIEJiTWxQT0dPeUI0TTIvRys0Sk9s
|
||||
RDFmcjZ6UGkrbWlFb1h0bmdRUlQwT28KK5vH6Svg8lFUKqXSzweb9405B0Z91TaW
|
||||
65G51im23qSOeD+NLBHP+nqIwYrjDlODrgOeW9Up/9rE5rLgFfjoOw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1mhaze5tgvc9lwjpml6dnp3xc292337wgm6376yh6tq4fn492ndjq9h23dq
|
||||
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
|
||||
+35
-17
@@ -4,31 +4,49 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbHd1Y1dpa2ZiQ2E1bXRP
|
||||
SnkrWHI0V2U1eGFKY0RZamZQQjluS0hGeENZCmduUFQyd1Q5RkZGMGFoenJhNmZh
|
||||
VmZ2OStNaFpJejFxQ2JUZXFpNThaMncKLS0tIEhJdGdQeXEzb25Nbk5YZktCWVUz
|
||||
NUJDUWRoTkd5R2pFblZjS0NSbVR5dkkKDYeW+zRpha04/CasFM91K6v1PpkNGHRu
|
||||
qAoKs9KSg9VxS7ya8RuLmylKRdpPkupm/8SXIJvQuCXp5LWmNJ4zkA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYcFFSVFVxOWp2dCtEVzg4
|
||||
VFF3Mk94Q0xVSDZNL1lBSXBKNzVXaU05TnlzCnhueTd1ek1BZ1JpZUMwM3ZBUHFh
|
||||
UVFKZjduZUpUejJ0enBXK0pwMjZKSDQKLS0tIHJ1Mi9OQThrWndiWFE3WTRJREJN
|
||||
Z2E5WTI5dzB3WDFtaFRoUERZa3N6TDQKy2FK57wDNLSPl2/L/FUBUP8rJZuAOqAR
|
||||
I0hBe4epAKL28PlJ4M9P77qqg893beZK3UIJQ5O6d5fGPMNTJiIzAg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4UktiUUdmd092bnZMdFMz
|
||||
Q05xVlhnNzhPZUY4TjF5bjRCbklacTdJd1RBCnJ4bEVMaWRLZExIUFVEUDBXTnlV
|
||||
MDU4WUk5VlJjdlhVUVpzZXlnWHhqTjQKLS0tIE9iclhGVElPcU9OLzF0bEVjMjVp
|
||||
YzlQTGFYZEhLdTk3N3ZramVjVGRXREUKjOjqO/jX5iEKN43WiJ8qS/1mDfxdj8uO
|
||||
K7Yyc/Yj0qMvGwLNzphjmbT29P7dXk4Ht4TedqCd+8DfhpOgLVIaSA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoaEZ5clpxV0ZQWmhSckxY
|
||||
a3I5djhISUphMmV1RU9DREJsZFpDdktyaUJzClBQSUNvN054aXp4SzNkMmNqNVhP
|
||||
QUdnUEppMmdENzUraHAyRVJLWm95V00KLS0tIHZuVXh4SkdhVU05YzV6VlI2c2RS
|
||||
SWttRW9nS29MM0lJT2x0QS9rVU4yb2cKYZ1gts1qRFdod8PFWl6Tg0Ry0sXhKMrP
|
||||
VwttGQj0iDg6UudB+qa2ngFgAc0C++OJebuLMFnK1K+PvUpfw2nRQg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjaXpvQU9yRUc3ektPMmU5
|
||||
Q0xLWU9mYXVOeTVBNGNsZzJPa1lkVlRUZHlvClhoSVV6YjB1Q3pTTzVvNFlNYkU4
|
||||
Y2RrbFk5SUFKSDVjeFU0Wlc2aGFUMFUKLS0tIDlVeXM4WWRUakg4Zk5mcjdJL3VC
|
||||
S1k5eFBpVnREQlZwYllpeTNhWW5GS1EKi2sMwyJJ0D8acjCZmxlcwdU1sglBuxR7
|
||||
2SEMsctdGC+5E3ilPXvPpZ5RONZHbXxn6kQRBlBv6AJERpGDzsfgfA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKNXBOeS9od1lBd0VGSDRX
|
||||
RE5BM3g0Zk53ZzBhdW9XUFhscmpHdEV1aG1vCnU0OGZZdENBdHd0Tzd5b05BR3RR
|
||||
ZGNZRlVSVk41YzgyRnlHK1NUaXhqZVUKLS0tIGFKcWF3VGdibldnMndmSis5Skc5
|
||||
aEZJVGh1UDFVUldrRndVZURGbVJMUXMKia/z+zwpfcaILCt4fpy7wsR0QkFKf07d
|
||||
QwkV+gEfM3EydhcA7invDieIKbcOUoKKgOmf2sXMgW07CsjG1QXk6w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpcTJHRy9CZlVWcERMRzlL
|
||||
MVFyVHlRT1hnQTBsSlBYOWtoSzVyejJIM0RzCmFKM3J5cE1BejhHYVl0VzNTSXdj
|
||||
dENsTCtJT0JFT3I0eGJtOFMvdjFmTm8KLS0tIE1zSmhBMjNnelQrdkJ3eVR6SFo1
|
||||
QXJtMVRuS1daOU53dmxGdFIwN0ZOVW8KS0XIDkbp+1W+wUDHkaKXZ84QpcO0zK54
|
||||
FrMzkChowtm/a+RSLVadBGDQv2R4gV89NArP/YAEQa7GK9aHvDlH/g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1tzsrtwd3p3lrr9g7nv3z5nvmzsz54t2uc6tfqwutp6usmav83s2sck25cc
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2bnJZUFVVRTBrZExoSWJV
|
||||
NDI2WUluaWJHVVdiR1JBUjhJT0owaTNoL1JBCjh2Y3ZvZXYrSE4raGtrcXVZai9L
|
||||
cUFERVRvMEZPaC9oa01NSHR1ejBIdk0KLS0tIG5ZNmZqZWZxKzd4c2pYOEhwa0li
|
||||
NDRQV1hDZ3ZwZksyTUtjcEVGWVpFMTgKmpLn5Ov03MKITQ7QihjK4GsYlHQiAFAc
|
||||
pI7z78qQv34e7DLLJotxbeWYyX/IOe+DvUm9T8NGY4ujI2y3HjXPlQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age15me6sx0f8r58xh9v7aqrj6e99n7eu555jkpw422txpkqt4r02v0qpahlyq
|
||||
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
|
||||
|
||||
+47
-11
@@ -3,22 +3,58 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaFZURjAxMGRJZEJ5MW1x
|
||||
ZjVwWEQrQlkwNmRibVNiL2RpTTFLeUVDQ3hRCkVIamVnZkM1MnlueloxMHVFQnBF
|
||||
RjV2bnUrZUo4WGZJTmR4Y0xITkxRUkUKLS0tIGlJdVQ5MFBubVhxRUVMWW0wSGpP
|
||||
UGdKNUNPYW9nek1UZ0tWbXd3QVNUNDgKIHOiKelITQdH5R4Nc3WF7mzz15D1f9on
|
||||
VaTdr5qkf8LNNvPI0fxsXA9is5cqeg+KbDRHtUumEhNp6Zrf8zWBkw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwbWFOSlB0ckJFc2hiQTlh
|
||||
eEo1NzZSTWovRXNnTGJDWTVZRUsvbjQ2NGx3CithSDVSbFpCYW9KQjJndzR3aVlL
|
||||
MHBDRmQxSEpTZ1FsRXFWQlE2aFc1VGsKLS0tIDc5bmI4WWtvcStZVk5WNDhva1dk
|
||||
cEVKM2ZCTGdLM2NEbmRudk1SdDNzMTAKB6QCrxxNzazSbif28j6RNIfdfUTKxklM
|
||||
Xip5xoLjTRNOjq561OS34gaMA1GE27YQqsqtgiU15i7SZ7IGefWpZQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N0hvV01naytDSWVwK1B1
|
||||
MXM1ZkdpaVc2Q3FPV2VBcC83WmcvSjdGUGgwClJGSXZ6YW5JeFlValNJbjVhK29u
|
||||
bUFqN2dRQTI4ZkwyeXNWYk5JeWVJRXcKLS0tIDlMMkNBUnNUSTJwVVFmc2dlcEZS
|
||||
VGQ1VHR2dXB0M3RsalppVWxiUUROM0UKZM/4QDTam3LDTzjnDs41Ije50R7Q7GC4
|
||||
IZbUZjs72rBzY8IkJDbN9JidadEc4NAtMOJwXiJbpZGiCBNfc8+SXw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2MDVmZGp0bEpwZWRLZFFM
|
||||
ckY1eERsSHFuMnhPK3J0eFRLY3lHYnlEK0NnCmNhQjlnUTNFOVF0UVBLendHYmdx
|
||||
aXVIa2hmNENUNGdZL2FmbTlNekd5NUkKLS0tIDdwbmFGZVFlaG94UEgvOUNxdVlz
|
||||
VHJZenplYVVPa2NUSUFWeEN5bGQ1MkUKT5CqEiCN61aOpvMlBp0effS3bh345E7u
|
||||
wfxrMwMhrEzfnmpkvcafLCn9PMicqLuz48J9ipF5IHLf7MIvbT31ow==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPS05tWG41Z3BwREhxUVRY
|
||||
ZVBMQjhxRDNwUDBjVE1jNnlVOWZtcE81Smk0CnlHaEl2OTd2UnJhWmlHNmY2V3NQ
|
||||
SUJKakZOZkdXeFhnZTF6dmxzTGFzZjAKLS0tIFhwKy84Z0l3cEhSQ0NBbVJIKytM
|
||||
QzhxbTY0SkdkOWN5VDU2d0dTMzlGN00KtgobQ2hsilZnvoq/BesaM/RcYbVNHxSh
|
||||
vEA80nUhyEdRim8WP/yu5KrgHsEVQ9ryclU4B49F1pINdAVaGRo9LA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5NUxUMHdrbzl0OEl2RXoy
|
||||
S1ovL05lTXhSNXBXZEtWU2hsdXhBZHlqc21rCnY0a1Z4QXZnTnJyTDR0WVgyYk5m
|
||||
cHJJZkhoM2lNaG51TmY3K0hZeXZKM1kKLS0tIGU2MmV1TTFTY3QyQVNEb2ZxYmlU
|
||||
c1doTkVLZEhKeDJTWmtLWjIrclRUenMKRem1nKPK/ghuipzOLTf6V3Ke5LK5S6LW
|
||||
5ExYf7y++FC90IqkAqeIA67l2KihcPE0DqnBmwrDlNgL5tXyhYV39g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvZkxwdUhOU3diRVFjSkhH
|
||||
SmFDbjYzblFUZ2NLRmpiVGxuUGVrWC9BWERZCkl6MVJ0M3dvNGU0UmQ5Nm1VWmRm
|
||||
ckVjSENWb05qMWtLQzNJd0tUWmgydWsKLS0tICtydnQyUXluWmxwWVpQWVRqOW9L
|
||||
VjhoelJCZjM4aHh4UG0wcUJMSVFGbncK6B1Rou2axRoRQNXudD6BVq5buz9MJapB
|
||||
vYMEhQPVCU2SinltqIrgd00qjgE7+oPnGr5pTFSQisJLlN3X1LUbhQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1rf4kj99wuq59k7w8ar326djmgmpl9hcwlnuag07f8gauq8c3y5mqne87s4
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvV0E2MnpDZzNkbC9DNGZ4
|
||||
U1JLQ3J3ZHo2cEpraFlSdTA0V01wdlNaMkE0CjQ0SDBXT2tYU2VmZjNja0M0VG5K
|
||||
Z1FkVWhYODVDNkhBR0o1NW43SjZRZW8KLS0tIEtrMFI0YjZIQzd5bHVMSmRSWi83
|
||||
U20zMkg2UWpEclMxalNhMThlWXdhaDQKX6qsiRZqlFVhq5lP5B/XVqStq8Impvx9
|
||||
MTmVRuMzFQ5qTs4BjhIqO25Ez+CI2GAq2K10prdSEin69gSTxntkIg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jc6wx33hdhgwhk6nzy5rr8fkgmqxk9um639tk5h632nqfyaw8czskdptj9
|
||||
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
|
||||
|
||||
+32
-16
@@ -19,6 +19,15 @@
|
||||
|
||||
remoteBuilderUser = "nixremote"; # remote builder SSH user
|
||||
|
||||
# nix-cache's own SSH host public key (not a secret — the private half
|
||||
# never leaves the host). Wired into every client's
|
||||
# programs.ssh.knownHosts by modules/nix-cache/remote-builder-client.nix
|
||||
# so distributed builds don't hit "Host key verification failed" on a
|
||||
# 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 AAAAC3NzaC1lZDI1NTE5AAAAIPeWgMsdaiz4axT/deFc1+0B5bN+GX/NOeW9bbQ0c/IT 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
|
||||
# to use it as a distributed builder.
|
||||
@@ -29,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";
|
||||
|
||||
@@ -128,6 +152,14 @@
|
||||
# shortcuts on the gui build type (hosts/nixos/home.nix).
|
||||
pveWeb = 8006;
|
||||
pbsWeb = 8007;
|
||||
|
||||
# Tor relay's ORPort — the port other Tor relays connect to for onion
|
||||
# routing traffic (modules/tor/enable-relay.nix). Tor's own conventional
|
||||
# default; opened via services.tor.openFirewall rather than
|
||||
# networking.firewall.allowedTCPPorts directly, but kept here anyway so
|
||||
# it's not a bare literal duplicated between the relay's settings and
|
||||
# anything else that ever needs to reference it.
|
||||
torRelayOrPort = 9001;
|
||||
};
|
||||
|
||||
# .raw disk image size for every proxmox-* host's standalone Disko image
|
||||
@@ -147,20 +179,4 @@
|
||||
keep = 20; # number of rotated logs to retain before deleting the oldest
|
||||
};
|
||||
|
||||
# Flake targets with a real, currently-running deployment somewhere —
|
||||
# matches README.md's Hosts table "(real, deployed)" annotations; update
|
||||
# both together. Not consumed by any NixOS module (nothing in the actual
|
||||
# system config should behave differently because of this) — it's read
|
||||
# by scripts/create-proxmox-resource.sh to refuse creating a same-identity
|
||||
# duplicate of an already-deployed host (shared hostName/hostId) unless
|
||||
# you explicitly pass --allow-duplicate-host.
|
||||
deployedTargets = [
|
||||
"linode-minimal"
|
||||
"proxmox-minimal"
|
||||
"proxmox-nix-cache"
|
||||
"proxmox-server"
|
||||
"proxmox-docker"
|
||||
"proxmox-gui"
|
||||
"proxmox-pxe-boot"
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user