Archived
Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b65d78a515 | ||
|
|
fa53d37360 | ||
|
|
cbb86704a3 | ||
|
|
7f621dfcca | ||
|
|
c3eb2c9e9f | ||
|
|
d74efd9f66 | ||
|
|
63a8c627f5 | ||
|
|
e4b335be23 | ||
|
|
0bf99c56cc | ||
|
|
e368f68ad7 | ||
|
|
fa52c2849a | ||
|
|
dce3788499 | ||
|
|
e00be5d2da | ||
|
|
82eea7f088 | ||
|
|
955a443b36 | ||
|
|
4800aebf43 | ||
|
|
cb737642e5 | ||
|
|
6d5670c8d2 | ||
|
|
c911a605e9 | ||
|
|
6002c5c738 | ||
|
|
92c50df2f1 | ||
|
|
45e61844d5 | ||
|
|
e72df8fed5 | ||
|
|
5497a5b0ae | ||
|
|
e10e493ddd | ||
|
|
ae9acecbf3 | ||
|
|
a3be05538b | ||
|
|
289163c712 | ||
|
|
2123e4ad69 | ||
|
|
177950dd3d | ||
|
|
cbf1239be4 | ||
|
|
8a282ee32e | ||
|
|
25079a7f0a | ||
|
|
4952e5224d | ||
|
|
98409f4502 | ||
|
|
7779f3e137 | ||
|
|
1462829aa6 | ||
|
|
48ce2c4097 | ||
|
|
bcae177d8e | ||
|
|
d35aca3138 | ||
|
|
147cb3803a | ||
|
|
98445565d6 | ||
|
|
eef4b05254 | ||
|
|
619324589a | ||
|
|
400af07154 | ||
|
|
9bb626327f | ||
|
|
1f8bf8c852 | ||
|
|
5d7a6327b7 | ||
|
|
0b9f124713 | ||
|
|
9479d56e11 | ||
|
|
c53c1940d6 | ||
|
|
79e8f9f2ce | ||
|
|
5fe575d362 | ||
|
|
b46424343f | ||
|
|
33b1d5ec79 | ||
|
|
f565e9c2a1 | ||
|
|
a91634c460 | ||
|
|
42919ea15c | ||
|
|
60c155327d | ||
|
|
0f78e96b81 | ||
|
|
12a2354fad | ||
|
|
f237a6a3d2 | ||
|
|
a2ce01d6ce | ||
|
|
fb6ee27e10 | ||
|
|
85ff5e01e8 | ||
|
|
eb881d4cd8 | ||
|
|
96cc63671a | ||
|
|
104804dbf6 | ||
|
|
0a2298b0e2 | ||
|
|
e73ae6044e | ||
|
|
14621e7ad5 |
+55
@@ -12,6 +12,21 @@ keys:
|
|||||||
- &lxc-gui age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
- &lxc-gui age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
||||||
- &proxmox-server age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
- &proxmox-server age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
||||||
- &vm-server age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
- &vm-server age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
||||||
|
- &baremetal-gui age1ehkswwz2pqaz4svzh7ela5tdnssl8kn6d4vwwxd6zwg8exfpd43syyrrjp
|
||||||
|
- &lxc-tor-relay age1gl5ujmhd2pe37acd0eaar8tmnf2na28n8l28s8xct40vqpxvsdwqf49g4l
|
||||||
|
- &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:
|
creation_rules:
|
||||||
# Shared across every currently-deployed host: root/nixos password hash,
|
# Shared across every currently-deployed host: root/nixos password hash,
|
||||||
@@ -34,6 +49,21 @@ creation_rules:
|
|||||||
- *lxc-gui
|
- *lxc-gui
|
||||||
- *proxmox-server
|
- *proxmox-server
|
||||||
- *vm-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$
|
- path_regex: secrets/nix-cache\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
@@ -41,6 +71,8 @@ creation_rules:
|
|||||||
- *admin
|
- *admin
|
||||||
- *nix-cache
|
- *nix-cache
|
||||||
- *lxc-nix-cache
|
- *lxc-nix-cache
|
||||||
|
- *linode-nix-cache
|
||||||
|
- *proxmox-nix-cache
|
||||||
|
|
||||||
- path_regex: secrets/server\.yaml$
|
- path_regex: secrets/server\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
@@ -49,9 +81,32 @@ creation_rules:
|
|||||||
- *server
|
- *server
|
||||||
- *proxmox-server
|
- *proxmox-server
|
||||||
- *vm-server
|
- *vm-server
|
||||||
|
- *linode-server
|
||||||
|
- *lxc-server
|
||||||
|
|
||||||
- path_regex: secrets/docker\.yaml$
|
- path_regex: secrets/docker\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *admin
|
- *admin
|
||||||
- *docker
|
- *docker
|
||||||
|
|
||||||
|
- path_regex: secrets/tor-relay\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
- *lxc-tor-relay
|
||||||
|
|
||||||
|
# gui-host-specific secrets (currently: wifi-password, see
|
||||||
|
# modules/networking/wifi.nix). Only *lxc-gui has a registered key today
|
||||||
|
# -- proxmox-gui/linode-gui/baremetal-gui haven't been provisioned via
|
||||||
|
# scripts/secrets/sync-host-keys.sh yet, so whichever variant is actually
|
||||||
|
# deployed next needs its recipient added here (and `sops updatekeys` rerun)
|
||||||
|
# before it can decrypt this.
|
||||||
|
- path_regex: secrets/gui\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
- *lxc-gui
|
||||||
|
- *baremetal-gui
|
||||||
|
- *linode-gui
|
||||||
|
- *proxmox-gui
|
||||||
|
|||||||
@@ -164,13 +164,39 @@ before committing.
|
|||||||
|
|
||||||
Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` is
|
Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` is
|
||||||
organized by purpose: `scripts/secrets/` (sops/age + SSH host-key
|
organized by purpose: `scripts/secrets/` (sops/age + SSH host-key
|
||||||
management), `scripts/proxmox/` (Proxmox deployment), `scripts/lib/`
|
management), `scripts/proxmox/` (Proxmox deployment), `scripts/installer/`
|
||||||
(shared helpers, sourced by the scripts below — not run directly), and a
|
(the auto-installer's own shell script, templated into the image — see
|
||||||
handful of repo-wide scripts left at the top level (`env.sh`,
|
below), `scripts/lib/` (shared helpers, sourced by the scripts below — not
|
||||||
`bump-nixpkgs-release.sh`, plus `codex-setup.sh`/`codex-maintenance.sh`
|
run directly), and a handful of repo-wide scripts left at the top level
|
||||||
above). When adding a new script, put it in the matching subfolder rather
|
(`env.sh`, `bump-nixpkgs-release.sh`, plus `codex-setup.sh`/
|
||||||
than the top level, and if it duplicates logic another script already has,
|
`codex-maintenance.sh` above). When adding a new script, put it in the
|
||||||
lift the shared part into `scripts/lib/` instead of copying it.
|
matching subfolder rather than the top level, and if it duplicates logic
|
||||||
|
another script already has, lift the shared part into `scripts/lib/`
|
||||||
|
instead of copying it.
|
||||||
|
|
||||||
|
### `scripts/installer/`
|
||||||
|
|
||||||
|
- `scripts/installer/auto-install.sh` — the interactive install script
|
||||||
|
baked into the auto-installer image (see `docs/auto-installer.md`), kept
|
||||||
|
as a real, version-controlled shell file rather than inline in
|
||||||
|
`modules/installer/common.nix`'s Nix. It sources `scripts/env.sh` itself
|
||||||
|
for `LAN_DOMAIN` (`export LAN_DOMAIN`/`: "${LAN_DOMAIN:=...}"`, matching
|
||||||
|
`variables.nix`'s `lanDomain` — manually kept in sync, same pattern as
|
||||||
|
`NIX_CACHE_HOST` mirroring `nixCacheHost`), rather than Nix-level string
|
||||||
|
substitution — that's what makes it work identically whether run
|
||||||
|
straight from a git checkout or from inside the built installer image.
|
||||||
|
`common.nix` bakes `scripts/env.sh` in alongside it at a matching
|
||||||
|
relative path (`/etc/nixos-installer/env.sh` next to
|
||||||
|
`/etc/nixos-installer/installer/auto-install.sh`) so the script's own
|
||||||
|
`source "$(dirname ...)/../env.sh"` line resolves the same way in both
|
||||||
|
contexts — this is also why it's invoked from
|
||||||
|
`/etc/nixos-installer/installer/auto-install.sh` rather than a flat
|
||||||
|
`/etc/auto-install.sh`. `#!/usr/bin/env bash`, not
|
||||||
|
`#!/run/current-system/sw/bin/bash`: the latter only resolves on an
|
||||||
|
already-activated NixOS system, breaking the checked-out-file case
|
||||||
|
entirely (confirmed live: "cannot execute: required file not found" on
|
||||||
|
a non-NixOS box); `/usr/bin/env` is reliably present on both NixOS
|
||||||
|
(`environment.usrbinenv`'s own default) and any normal Linux distro.
|
||||||
|
|
||||||
### `scripts/secrets/`
|
### `scripts/secrets/`
|
||||||
|
|
||||||
@@ -256,33 +282,6 @@ lift the shared part into `scripts/lib/` instead of copying it.
|
|||||||
`/etc/ssh/ssh_known_hosts`. Idempotent (re-running replaces its own
|
`/etc/ssh/ssh_known_hosts`. Idempotent (re-running replaces its own
|
||||||
marked block rather than duplicating it); restarts `nix-daemon` by
|
marked block rather than duplicating it); restarts `nix-daemon` by
|
||||||
default so the change takes effect immediately.
|
default so the change takes effect immediately.
|
||||||
- `scripts/proxmox/clone-pve1-to-pve-test.sh --vmid <n> [options]` — ad hoc
|
|
||||||
copy of a single VM/CT from one node to another via `vzdump` +
|
|
||||||
`qmrestore`/`pct restore`, relayed through this machine
|
|
||||||
(`ssh src cat ... | ssh dst cat > ...`, no on-disk staging copy).
|
|
||||||
Defaults `pve1` → `pve-test` (see "Two Proxmox nodes" above); source is
|
|
||||||
left running throughout (`--mode snapshot` default) and untouched
|
|
||||||
beyond the vzdump + a typed-VMID confirm, restored on the target with a
|
|
||||||
fresh MAC (`--unique 1`, always) since the source is expected to still
|
|
||||||
be live on the same LAN. Backup archives are deleted from both nodes
|
|
||||||
afterward unless `--keep-backup`. `--dry-run` throughout.
|
|
||||||
- `scripts/proxmox/migrate-vm.sh --vmid <n> [options]` — moves (rather
|
|
||||||
than clones) a single VM/CT between nodes, built on the same
|
|
||||||
vzdump/relay/restore pattern as `clone-pve1-to-pve-test.sh` above, plus
|
|
||||||
a cutover step (stop the guest on the source node, start it on the
|
|
||||||
target) and, optionally, `--remove-source` to destroy it on the source
|
|
||||||
node afterward (its own separate typed confirmation, since that step is
|
|
||||||
irreversible) — the default instead leaves the stopped source guest
|
|
||||||
intact as a safety net. Restores with the *original* MAC (no
|
|
||||||
`--unique`) unless `--unique` is passed, since the source is expected to
|
|
||||||
go away rather than keep running alongside the target. **Not** a true
|
|
||||||
`qm migrate`-style live migration — pve1/pve-test aren't a clustered
|
|
||||||
pair with shared storage, so this is vzdump-based and, with the default
|
|
||||||
`--mode snapshot`, any writes on the source between the snapshot and
|
|
||||||
cutover are lost (use `--mode stop` if that's not acceptable). Generic
|
|
||||||
node-to-node (not pve1/pve-test-specific) via
|
|
||||||
`--source-node`/`--target-node`, defaulting to pve1 → pve-test like the
|
|
||||||
clone script. `--dry-run` throughout.
|
|
||||||
|
|
||||||
### `scripts/lib/`
|
### `scripts/lib/`
|
||||||
|
|
||||||
@@ -309,8 +308,9 @@ Sourced by the scripts above, never run directly:
|
|||||||
### Top level
|
### Top level
|
||||||
|
|
||||||
- `scripts/env.sh` — shared config (`PROXMOX_HOST`, storage pool, bridge,
|
- `scripts/env.sh` — shared config (`PROXMOX_HOST`, storage pool, bridge,
|
||||||
default cores/memory) sourced by `create-proxmox-resource.sh`. Add new
|
default cores/memory, `NIX_CACHE_HOST`, `LAN_DOMAIN`) sourced by
|
||||||
cross-script config here instead of duplicating it per-script.
|
`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`/
|
- `scripts/bump-nixpkgs-release.sh` — bumps `flake.nix`'s `nixpkgs.url`/
|
||||||
`home-manager.url` in place. Exists because flake input URLs can't
|
`home-manager.url` in place. Exists because flake input URLs can't
|
||||||
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
||||||
@@ -348,11 +348,14 @@ nixosSystem {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
|
Platforms: `linode`, `proxmox`, `lxc`, `baremetal`. Build types: `minimal`,
|
||||||
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`. Not
|
`nix-cache`, `server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`,
|
||||||
every combination is built — e.g. `pxe-boot` has no `linode` variant
|
`tor-relay`. Not every combination is built — e.g. `pxe-boot` has no `linode`
|
||||||
(PXE/DHCP/TFTP need LAN L2 adjacency a Linode VPS doesn't have), and
|
variant (PXE/DHCP/TFTP need LAN L2 adjacency a Linode VPS doesn't have),
|
||||||
`tor-relay` currently only exists as `lxc-tor-relay`. Treat `flake.nix`'s
|
`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
|
`generatedTargets` as the source
|
||||||
of truth for which hosts exist — `README.md`, `AGENTS.md`,
|
of truth for which hosts exist — `README.md`, `AGENTS.md`,
|
||||||
`docs/flake-lock-automation.md`, and the CI eval workflows
|
`docs/flake-lock-automation.md`, and the CI eval workflows
|
||||||
@@ -368,12 +371,16 @@ removing a host.
|
|||||||
of their own beyond narrow parameterized helpers (see
|
of their own beyond narrow parameterized helpers (see
|
||||||
`modules/beszel/host-token.nix` below) — all shared behavior comes from the
|
`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.
|
platform/build-type modules composed in `flake.nix`, not from the host file.
|
||||||
- `modules/platforms/{linode,proxmox,lxc}.nix` — platform-specific config:
|
- `modules/platforms/{linode,proxmox,lxc,baremetal}.nix` — platform-specific
|
||||||
boot method, guest tooling, and (for linode/proxmox) the hypervisor-specific
|
config: boot method, guest tooling, and the hardware config, imported
|
||||||
hardware config, imported directly by the platform module itself
|
directly by the platform module itself — **not** wired in from
|
||||||
(`../hardware-configuration/vm/{proxmox,linode}.nix`) — **not** wired in
|
`flake.nix`. VM platforms use `../hardware-configuration/vm/{proxmox,linode}.nix`;
|
||||||
from `flake.nix`. `lxc.nix` has no hardware-configuration counterpart since
|
`baremetal.nix` uses `../hardware-configuration/baremetal.nix` (adapted
|
||||||
containers share the host kernel; instead it imports nixpkgs' own
|
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
|
`virtualisation/proxmox-lxc.nix`, which gives every `lxc-*` host a
|
||||||
`config.system.build.tarball` output — a plain rootfs tarball, used as a
|
`config.system.build.tarball` output — a plain rootfs tarball, used as a
|
||||||
`pct create ... vztmpl` CT template (**not** `pct restore`, which expects
|
`pct create ... vztmpl` CT template (**not** `pct restore`, which expects
|
||||||
@@ -397,6 +404,16 @@ removing a host.
|
|||||||
boots, so this declares them with `destroy = false` (disko never wipes
|
boots, so this declares them with `destroy = false` (disko never wipes
|
||||||
them) and a bare `filesystem`/`swap` content type instead of a partition
|
them) and a bare `filesystem`/`swap` content type instead of a partition
|
||||||
table — idempotent against an already-provisioned disk, never destructive.
|
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/boot/efi.nix` — systemd-boot + EFI vars, paired with the disko module.
|
||||||
- `modules/installer/` — the auto-installer environment (ISO, also served as
|
- `modules/installer/` — the auto-installer environment (ISO, also served as
|
||||||
PXE netboot): `common.nix` (shared config + the generated
|
PXE netboot): `common.nix` (shared config + the generated
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ workstation.
|
|||||||
Targets are named `<platform>-<buildtype>`, generated from two orthogonal
|
Targets are named `<platform>-<buildtype>`, generated from two orthogonal
|
||||||
pieces composed in `flake.nix`:
|
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`,
|
- **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`,
|
||||||
`gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
|
`gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
|
||||||
|
|
||||||
Not every combination exists — `pxe-boot` has no `linode` variant, since
|
Not every combination exists — `pxe-boot` has no `linode` variant, since
|
||||||
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have, and
|
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have,
|
||||||
`tor-relay` currently only exists as `lxc-tor-relay`. The full list:
|
`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 |
|
| Target | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -25,6 +27,7 @@ PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have, and
|
|||||||
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host — previously the flat `server` target |
|
| `linode-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-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 |
|
| `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 |
|
| `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 |
|
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node |
|
||||||
| `lxc-tor-relay` | Tor middle relay |
|
| `lxc-tor-relay` | Tor middle relay |
|
||||||
@@ -64,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` |
|
| `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/<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) |
|
| `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/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/common/` | Shared NixOS config, Home Manager, aliases imported by every host |
|
||||||
| `modules/nix-cache/` | Binary cache and remote builder client/server modules |
|
| `modules/nix-cache/` | Binary cache and remote builder client/server modules |
|
||||||
|
|||||||
+26
-5
@@ -8,10 +8,13 @@ lives here.
|
|||||||
The installer provides a small NixOS install environment (ISO, or the same
|
The installer provides a small NixOS install environment (ISO, or the same
|
||||||
image netbooted via PXE) with SSH access, Git support, and an interactive
|
image netbooted via PXE) with SSH access, Git support, and an interactive
|
||||||
installation script.
|
installation script.
|
||||||
Logging in as any user (root or `nixos`) runs `/etc/auto-install.sh`,
|
Logging in as any user (root or `nixos`) runs
|
||||||
discovers available hosts from this same flake, lets the operator choose a
|
`/etc/nixos-installer/installer/auto-install.sh` (the same file as
|
||||||
target, applies that host's Disko storage configuration, installs NixOS, and
|
`scripts/installer/auto-install.sh` in this repo — see "Installer process"
|
||||||
reboots.
|
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 —
|
**This applies to every `nixosConfigurations` target except `lxc-*` hosts —
|
||||||
see "LXC hosts" immediately below for why those are different.**
|
see "LXC hosts" immediately below for why those are different.**
|
||||||
@@ -192,6 +195,10 @@ default.
|
|||||||
`auto-install.sh` still supports the older manual path as a fallback: if a
|
`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`
|
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.
|
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
|
## Storage
|
||||||
|
|
||||||
@@ -217,7 +224,21 @@ entirely (see "LXC hosts" above), so it never reaches this code path.
|
|||||||
|
|
||||||
## Installer process
|
## 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.
|
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.
|
2. Presents them as a menu; confirms the choice.
|
||||||
|
|||||||
Generated
+6
-6
@@ -95,11 +95,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783740085,
|
"lastModified": 1784350909,
|
||||||
"narHash": "sha256-qajyHfZY29G2oEQk+uHxmsJcRoBUBXP9maTpFlwP/dI=",
|
"narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "3cd22efe6471dc7365c822bd9ad73a21e55f38fb",
|
"rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -147,11 +147,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784011430,
|
"lastModified": 1784432872,
|
||||||
"narHash": "sha256-lDebytrYdd47IBLwvNOD+6AGeoqZ78CIKlp70hzW280=",
|
"narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8eeec934ae0dbeca3d7868c059568a65c08b2fc3",
|
"rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -91,13 +91,14 @@
|
|||||||
linode-gui = mkTarget { platform = "linode"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
linode-tailscale-subnet-router = mkTarget { platform = "linode"; buildType = "tailscale-subnet-router"; hostPath = ./hosts/tailscale-subnet-router/host.nix; };
|
||||||
proxmox-tailscale-exit-node = mkTarget { platform = "proxmox"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
proxmox-tailscale-subnet-router = mkTarget { platform = "proxmox"; buildType = "tailscale-subnet-router"; hostPath = ./hosts/tailscale-subnet-router/host.nix; };
|
||||||
lxc-tailscale-exit-node = mkTarget { platform = "lxc"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/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; };
|
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,11 +19,15 @@
|
|||||||
nextcloud-client
|
nextcloud-client
|
||||||
# vscode
|
# vscode
|
||||||
chromium
|
chromium
|
||||||
|
claude-code
|
||||||
|
fish
|
||||||
|
sops
|
||||||
];
|
];
|
||||||
|
|
||||||
# Optional: set environment vars
|
# Optional: set environment vars
|
||||||
sessionVariables = {
|
sessionVariables = {
|
||||||
EDITOR = "vim";
|
EDITOR = "vim";
|
||||||
|
SOPS_AGE_KEY_FILE = "/home/nixos/Nextcloud/Filing Cabinet/keys/nixos-sops-age-key-txt";
|
||||||
};
|
};
|
||||||
|
|
||||||
file = {
|
file = {
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
_:
|
_:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
imports = [
|
||||||
|
../../modules/networking/wifi.nix
|
||||||
|
];
|
||||||
|
|
||||||
networking.hostName = "nixos";
|
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
|
# Preserved from the pre-refactor `nixos` target — stateVersion must never
|
||||||
# be bumped on an already-installed machine.
|
# be bumped on an already-installed machine.
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
_:
|
|
||||||
|
|
||||||
{
|
|
||||||
networking.hostName = "exit-node";
|
|
||||||
|
|
||||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
|
||||||
# for pool-import safety, and this host does neither.
|
|
||||||
|
|
||||||
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
|
||||||
# flake's current nixpkgs release rather than being pinned to an older one.
|
|
||||||
system.stateVersion = "26.05";
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
_:
|
||||||
|
|
||||||
|
{
|
||||||
|
networking.hostName = "tailscale-router";
|
||||||
|
|
||||||
|
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
||||||
|
# for pool-import safety, and this host does neither.
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
}
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
_:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
imports = [
|
||||||
|
(import ../../modules/beszel/host-token.nix {
|
||||||
|
name = "tor-relay";
|
||||||
|
sopsFile = ../../secrets/tor-relay.yaml;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
networking.hostName = "tor-relay";
|
networking.hostName = "tor-relay";
|
||||||
|
|
||||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
||||||
# for pool-import safety, and this host does neither.
|
# for pool-import safety, and this host does neither.
|
||||||
|
|
||||||
|
services.beszel.agent.environment = {
|
||||||
|
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||||
|
};
|
||||||
|
|
||||||
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
||||||
# flake's current nixpkgs release rather than being pinned to an older one.
|
# flake's current nixpkgs release rather than being pinned to an older one.
|
||||||
system.stateVersion = "26.05";
|
system.stateVersion = "26.05";
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
boot.loader.grub.useOSProber = true;
|
boot.loader.grub.useOSProber = true;
|
||||||
|
programs.direnv.enable = true;
|
||||||
services = {
|
services = {
|
||||||
xserver = {
|
xserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|||||||
@@ -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 = [
|
imports = [
|
||||||
../beszel/enable-agent.nix
|
../beszel/enable-agent.nix
|
||||||
../services/zfs/enable-service.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 = {
|
systemd.services.nfs-server = {
|
||||||
after = [ "zfs-mount.service" ];
|
after = [ "zfs-mount.service" ];
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
{ ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../tailscale/exit-node.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
# "server", not "both": this build type only ever advertises itself as an
|
|
||||||
# exit node (see ../tailscale/exit-node.nix) -- it doesn't advertise LAN
|
|
||||||
# subnet routes, so it doesn't need the "client"-side loose reverse-path
|
|
||||||
# filtering that "both" would also turn on. Deliberately left unbundled
|
|
||||||
# from LAN-subnet-route advertisement so this build type stays valid on
|
|
||||||
# every platform, including linode (a remote VPS with no network path to
|
|
||||||
# the home LAN at all).
|
|
||||||
services.tailscale.useRoutingFeatures = "server";
|
|
||||||
|
|
||||||
# Forwarded exit-node traffic arrives on tailscale0 already
|
|
||||||
# tailscale-authenticated -- the firewall's normal per-port allow-list
|
|
||||||
# would otherwise drop it. Standard NixOS/Tailscale exit-node guidance.
|
|
||||||
networking.firewall.trustedInterfaces = [ "tailscale0" ];
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{ ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../tailscale/subnet-router.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# "server", not "both": this build type advertises LAN subnet routes but
|
||||||
|
# doesn't use another tailscale exit node itself, so it doesn't need the
|
||||||
|
# "client"-side loose reverse-path filtering that "both" would also enable.
|
||||||
|
# Deliberately kept explicit here (not just relying on subnet-router.nix's
|
||||||
|
# own setting) so the intent is clear at the build-type level.
|
||||||
|
services.tailscale.useRoutingFeatures = "server";
|
||||||
|
|
||||||
|
# Forwarded subnet-router traffic arrives on tailscale0 already
|
||||||
|
# tailscale-authenticated -- the firewall's normal per-port allow-list
|
||||||
|
# would otherwise drop it. Standard NixOS/Tailscale subnet-router guidance.
|
||||||
|
networking.firewall.trustedInterfaces = [ "tailscale0" ];
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../tor/enable-relay.nix
|
../tor/enable-relay.nix
|
||||||
|
../beszel/enable-agent.nix
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
btop
|
btop
|
||||||
git
|
git
|
||||||
gcr
|
gcr
|
||||||
|
jq
|
||||||
];
|
];
|
||||||
|
|
||||||
# Secrets shared by every host, decrypted at activation via each host's
|
# Secrets shared by every host, decrypted at activation via each host's
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
openssh.authorizedKeys.keys = [
|
openssh.authorizedKeys.keys = [
|
||||||
vars.adminSshKey
|
vars.adminSshKey
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
|
"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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
disko
|
||||||
];
|
];
|
||||||
|
|
||||||
# Write auto-install script to /root
|
# Auto-install script, kept as a real, version-controlled shell file at
|
||||||
etc."auto-install.sh" = {
|
# scripts/installer/auto-install.sh rather than an inline Nix string.
|
||||||
text = ''
|
# It sources scripts/env.sh itself (for LAN_DOMAIN, same as every other
|
||||||
#!/run/current-system/sw/bin/bash
|
# script in this repo) rather than relying on Nix-level templating, so
|
||||||
set -eux
|
# 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
|
"nixos-installer/installer/auto-install.sh" = {
|
||||||
|
source = ../../scripts/installer/auto-install.sh;
|
||||||
export FLAKE_BASE_URL="git+https://${vars.lanDomain}/beatzaplenty/nixos.git"
|
mode = "0755";
|
||||||
|
};
|
||||||
echo "Fetching available NixOS hosts from flake..."
|
|
||||||
# Two categories deliberately excluded from the menu:
|
|
||||||
# lxc-* — these build a config.system.build.tarball meant for
|
|
||||||
# `pct restore` on Proxmox directly, not an install.
|
|
||||||
# Running nixos-install against one here would
|
|
||||||
# bind-mount / onto /mnt and then refuse to touch the
|
|
||||||
# filesystem it's currently running on — see
|
|
||||||
# docs/auto-installer.md.
|
|
||||||
# installer — this *is* the installer image's own flake target,
|
|
||||||
# not a deployable host; "installing" it means
|
|
||||||
# nixos-install-ing a copy of the installer into
|
|
||||||
# itself.
|
|
||||||
mapfile -t options < <(
|
|
||||||
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
|
||||||
"''${FLAKE_BASE_URL}#nixosConfigurations" \
|
|
||||||
--apply builtins.attrNames \
|
|
||||||
| jq -r '.[]
|
|
||||||
| select(startswith("lxc-") | not)
|
|
||||||
| select(. != "installer")'
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ ''${#options[@]} -eq 0 ]]; then
|
|
||||||
echo "ERROR: No NixOS hosts found in ''${FLAKE_BASE_URL}#nixosConfigurations" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Note: lxc-* targets aren't installed this way — build them with"
|
|
||||||
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
|
|
||||||
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
|
|
||||||
|
|
||||||
echo "Choose the flake profile to install:"
|
|
||||||
select choice in "''${options[@]}"; do
|
|
||||||
if [[ -n "$choice" ]]; then
|
|
||||||
echo "You selected: $choice"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "Invalid selection. Try again."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Starting install with flake: ''${FLAKE_BASE_URL}#''${choice}"
|
|
||||||
|
|
||||||
# Optional: confirm before proceeding
|
|
||||||
read -rp "Proceed with installation? (y/N): " confirm
|
|
||||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
||||||
echo "Aborted."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# A nix-cache host is *the* substituter/remote-builder for every other
|
|
||||||
# host once installed (its own config explicitly excludes itself from
|
|
||||||
# using either — see buildType != "nix-cache" in the nixos flake.nix).
|
|
||||||
# Installing one shouldn't depend on a nix-cache substituter either,
|
|
||||||
# for the same reason — plus in practice "nix-cache" only resolves over
|
|
||||||
# Tailscale, which a fresh installer environment was never connected to
|
|
||||||
# anyway, so it's dead weight even for non-nix-cache installs until
|
|
||||||
# that's sorted out. Override it away here specifically for nix-cache
|
|
||||||
# targets to keep install-time behaviour consistent with run-time.
|
|
||||||
nix_extra_opts=()
|
|
||||||
if [[ "''${choice}" == *-nix-cache ]]; then
|
|
||||||
echo "Installing a nix-cache host — skipping the nix-cache substituter."
|
|
||||||
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Every host reachable through this menu has a Disko config (lxc-*
|
|
||||||
# is filtered out above, and is the only category that doesn't —
|
|
||||||
# see docs/auto-installer.md), so this can run unconditionally: no
|
|
||||||
# need to probe the flake first and branch on whether Disko applies.
|
|
||||||
disko --mode destroy,format,mount \
|
|
||||||
--flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes-wipe-all-disks
|
|
||||||
|
|
||||||
# sops-nix derives this host's decryption key from its own SSH host key
|
|
||||||
# at *activation* time, which runs before systemd would otherwise
|
|
||||||
# generate one on first boot. Without pre-seeding it here, secrets
|
|
||||||
# (including the login password) fail to decrypt on first boot.
|
|
||||||
# Generate the key with scripts/secrets/prepare-host-key.sh first.
|
|
||||||
#
|
|
||||||
# Two places a key can come from, checked in order:
|
|
||||||
# /etc/host-keys — baked into this image at build time (see
|
|
||||||
# modules/installer/host-keys.nix; only present
|
|
||||||
# if built with NIXOS_HOST_KEYS_DIR set)
|
|
||||||
# /root/host-keys — scp'd in manually after boot (older fallback,
|
|
||||||
# still supported for images built without keys)
|
|
||||||
mkdir -p /root/host-keys
|
|
||||||
if [[ -f "/etc/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
|
||||||
echo "Found baked-in SSH host key for ''${choice}, installing to target..."
|
|
||||||
install -D -m 0600 "/etc/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
||||||
install -D -m 0644 "/etc/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
||||||
elif [[ -f "/root/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
|
||||||
echo "Found pre-seeded SSH host key for ''${choice}, installing to target..."
|
|
||||||
install -D -m 0600 "/root/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
||||||
install -D -m 0644 "/root/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
||||||
else
|
|
||||||
echo "WARNING: no SSH host key found for ''${choice} (checked /etc/host-keys and /root/host-keys)"
|
|
||||||
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
|
|
||||||
echo "Run scripts/secrets/prepare-host-key.sh for host ''${choice} on your admin workstation first,"
|
|
||||||
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, or scp the result to"
|
|
||||||
echo "/root/host-keys/ on this machine."
|
|
||||||
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
|
|
||||||
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
|
|
||||||
echo "Aborted."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p /mnt/install-tmp
|
|
||||||
export TMPDIR=/mnt/install-tmp
|
|
||||||
|
|
||||||
nixos-install \
|
|
||||||
--flake "''${FLAKE_BASE_URL}#''${choice}" \
|
|
||||||
"''${nix_extra_opts[@]}" \
|
|
||||||
--no-root-password
|
|
||||||
|
|
||||||
|
|
||||||
rm -rf /mnt/install-tmp
|
|
||||||
# Redundant copy of the host's private key — the real one is now at
|
|
||||||
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
|
|
||||||
# up on its own since it was written imperatively, not declaratively.
|
|
||||||
rm -rf /root/host-keys
|
|
||||||
sleep 10
|
|
||||||
reboot
|
|
||||||
'';
|
|
||||||
|
|
||||||
mode = "0755";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,7 +73,7 @@
|
|||||||
# file-copying/chown.
|
# file-copying/chown.
|
||||||
programs.bash.loginShellInit = ''
|
programs.bash.loginShellInit = ''
|
||||||
if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then
|
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"
|
touch "$HOME/.auto_install_ran"
|
||||||
fi
|
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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -106,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
|
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
|
||||||
# store DB via a systemd service (register-nix-paths) -- it never runs
|
# store DB via a systemd service (register-nix-paths) -- it never runs
|
||||||
# an activation script at all. Confirmed live this means neither
|
# an activation script at all. Confirmed live this means neither
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
_:
|
|
||||||
|
|
||||||
{
|
|
||||||
imports = [ ./enable-service.nix ];
|
|
||||||
|
|
||||||
services.tailscale = {
|
|
||||||
# Enables the sysctl forwarding settings exit nodes/subnet routers need;
|
|
||||||
# without this, --advertise-exit-node has no effect.
|
|
||||||
useRoutingFeatures = "server";
|
|
||||||
|
|
||||||
# Lets peers reach this node directly over the tailscale UDP port
|
|
||||||
# instead of relaying through DERP.
|
|
||||||
openFirewall = true;
|
|
||||||
|
|
||||||
# extraSetFlags (tailscale set, via the always-on tailscaled-set
|
|
||||||
# service), not extraUpFlags -- extraUpFlags is only ever applied by
|
|
||||||
# tailscaled-autoconnect, which itself only runs when
|
|
||||||
# services.tailscale.authKeyFile is set (nothing in this repo sets one,
|
|
||||||
# so tailscale up is a manual, one-time operator step on every host that
|
|
||||||
# uses this service). extraSetFlags has no such gate, so
|
|
||||||
# --advertise-exit-node self-reapplies on every boot once the operator
|
|
||||||
# has authenticated the node once.
|
|
||||||
extraSetFlags = [
|
|
||||||
"--advertise-exit-node"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
_:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [ ./enable-service.nix ];
|
||||||
|
|
||||||
|
services.tailscale = {
|
||||||
|
# Enables the sysctl forwarding settings subnet routers need;
|
||||||
|
# without this, --advertise-routes has no effect.
|
||||||
|
useRoutingFeatures = "server";
|
||||||
|
|
||||||
|
# Lets peers reach this node directly over the tailscale UDP port
|
||||||
|
# instead of relaying through DERP.
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -16,6 +16,14 @@
|
|||||||
#
|
#
|
||||||
# --dry-run: adds `nix build --dry-run --no-link` for whatever scope is
|
# --dry-run: adds `nix build --dry-run --no-link` for whatever scope is
|
||||||
# active (changed-files scope by default, full scope under --full-check).
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -23,6 +31,8 @@ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
source "${script_dir}/lib/nix-bootstrap.sh"
|
source "${script_dir}/lib/nix-bootstrap.sh"
|
||||||
# shellcheck source=lib/nix-eval.sh
|
# shellcheck source=lib/nix-eval.sh
|
||||||
source "${script_dir}/lib/nix-eval.sh"
|
source "${script_dir}/lib/nix-eval.sh"
|
||||||
|
# shellcheck source=lib/nix-parallel.sh
|
||||||
|
source "${script_dir}/lib/nix-parallel.sh"
|
||||||
|
|
||||||
repo_root="$(cd "${script_dir}/.." && pwd)"
|
repo_root="$(cd "${script_dir}/.." && pwd)"
|
||||||
cd "$repo_root"
|
cd "$repo_root"
|
||||||
@@ -246,66 +256,64 @@ echo
|
|||||||
if [[ ${#hosts[@]} -eq 0 ]]; then
|
if [[ ${#hosts[@]} -eq 0 ]]; then
|
||||||
echo "No hosts affected by changed files; skipping host eval."
|
echo "No hosts affected by changed files; skipping host eval."
|
||||||
else
|
else
|
||||||
echo "Evaluating host toplevel derivations (${scope_desc})..."
|
echo "Evaluating host toplevel derivations (${scope_desc}, up to ${NIX_PARALLEL_JOBS} at a time)..."
|
||||||
|
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of
|
||||||
|
# nixos-install (see docs/auto-installer.md); proxmox-* hosts can
|
||||||
|
# alternatively be built as a standalone disk image (see
|
||||||
|
# docs/proxmox-images.md). Both are otherwise-unvalidated buildable
|
||||||
|
# surface, easy to silently break without this.
|
||||||
|
declare -a host_eval_jobs=()
|
||||||
for host in "${hosts[@]}"; do
|
for host in "${hosts[@]}"; do
|
||||||
echo "==> $host"
|
host_eval_jobs+=("${host}${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.toplevel.drvPath")
|
||||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
|
||||||
|
|
||||||
# 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
|
case "$host" in
|
||||||
lxc-*)
|
lxc-*)
|
||||||
echo "==> $host (tarball)"
|
host_eval_jobs+=("${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball.drvPath")
|
||||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
|
|
||||||
;;
|
;;
|
||||||
proxmox-*)
|
proxmox-*)
|
||||||
echo "==> $host (diskoImagesScript)"
|
host_eval_jobs+=("${host} (diskoImagesScript)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath")
|
||||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
run_nix_parallel host_eval_jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
if ! $eval_packages; then
|
if ! $eval_packages; then
|
||||||
echo "No packages affected by changed files; skipping package eval."
|
echo "No packages affected by changed files; skipping package eval."
|
||||||
else
|
else
|
||||||
echo "Evaluating buildable packages..."
|
echo "Evaluating buildable packages (up to ${NIX_PARALLEL_JOBS} at a time)..."
|
||||||
|
declare -a package_eval_jobs=()
|
||||||
for pkg in "${all_packages[@]}"; do
|
for pkg in "${all_packages[@]}"; do
|
||||||
echo "==> packages.x86_64-linux.${pkg}"
|
package_eval_jobs+=("packages.x86_64-linux.${pkg}${NIX_PARALLEL_SEP}.#packages.x86_64-linux.${pkg}")
|
||||||
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
|
|
||||||
done
|
done
|
||||||
|
run_nix_parallel package_eval_jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $dry_run; then
|
if $dry_run; then
|
||||||
echo
|
echo
|
||||||
echo "Running dry-run builds for the active scope. This will not create result symlinks."
|
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
|
for host in "${hosts[@]:-}"; do
|
||||||
echo "==> Dry-run build: $host"
|
host_build_jobs+=("Dry-run build: ${host}${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.toplevel")
|
||||||
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel"
|
|
||||||
|
|
||||||
case "$host" in
|
case "$host" in
|
||||||
lxc-*)
|
lxc-*)
|
||||||
echo "==> Dry-run build: $host (tarball)"
|
host_build_jobs+=("Dry-run build: ${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball")
|
||||||
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball"
|
|
||||||
;;
|
;;
|
||||||
proxmox-*)
|
proxmox-*)
|
||||||
echo "==> Dry-run build: $host (diskoImagesScript)"
|
host_build_jobs+=("Dry-run build: ${host} (diskoImagesScript)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.diskoImagesScript")
|
||||||
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
run_nix_parallel host_build_jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
|
||||||
|
|
||||||
if $eval_packages; then
|
if $eval_packages; then
|
||||||
echo
|
echo
|
||||||
echo "Running dry-run builds for packages."
|
echo "Running dry-run builds for packages."
|
||||||
|
declare -a package_build_jobs=()
|
||||||
for pkg in "${all_packages[@]}"; do
|
for pkg in "${all_packages[@]}"; do
|
||||||
echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
|
package_build_jobs+=("Dry-run build: packages.x86_64-linux.${pkg}${NIX_PARALLEL_SEP}.#packages.x86_64-linux.${pkg}")
|
||||||
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
|
|
||||||
done
|
done
|
||||||
|
run_nix_parallel package_build_jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ cat > "$HOME/.config/nix/nix.conf" <<'EOF'
|
|||||||
experimental-features = nix-command flakes
|
experimental-features = nix-command flakes
|
||||||
accept-flake-config = false
|
accept-flake-config = false
|
||||||
warn-dirty = false
|
warn-dirty = false
|
||||||
|
build-users-group =
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Nix version:"
|
echo "Nix version:"
|
||||||
|
|||||||
+8
-2
@@ -22,7 +22,7 @@
|
|||||||
: "${PVE1_HOST:=pve1.sweet.home}"
|
: "${PVE1_HOST:=pve1.sweet.home}"
|
||||||
: "${PVE_TEST_HOST:=pve-test.sweet.home}"
|
: "${PVE_TEST_HOST:=pve-test.sweet.home}"
|
||||||
: "${PROXMOX_HOST:=$PVE1_HOST}"
|
: "${PROXMOX_HOST:=$PVE1_HOST}"
|
||||||
: "${PROXMOX_SSH_USER:=root}"
|
: "${PROXMOX_SSH_USER:=wayne}"
|
||||||
|
|
||||||
# Where this flake repo lives on the Proxmox node itself.
|
# Where this flake repo lives on the Proxmox node itself.
|
||||||
# scripts/proxmox/create-proxmox-resource.sh builds images directly on the node
|
# scripts/proxmox/create-proxmox-resource.sh builds images directly on the node
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
# (from this checkout's own `origin` remote) the first time it doesn't
|
# (from this checkout's own `origin` remote) the first time it doesn't
|
||||||
# find it, installing build tooling via scripts/codex-setup.sh, then
|
# find it, installing build tooling via scripts/codex-setup.sh, then
|
||||||
# `git pull`s it before every subsequent build.
|
# `git pull`s it before every subsequent build.
|
||||||
: "${PROXMOX_REMOTE_REPO_DIR:=/root/nixos}"
|
: "${PROXMOX_REMOTE_REPO_DIR:=/home/${PROXMOX_SSH_USER}/nixos}"
|
||||||
|
|
||||||
# Storage pool names -- Proxmox's own stock-install defaults, but this
|
# Storage pool names -- Proxmox's own stock-install defaults, but this
|
||||||
# varies a lot by setup (ZFS pool name, custom LVM-thin volume, etc.).
|
# varies a lot by setup (ZFS pool name, custom LVM-thin volume, etc.).
|
||||||
@@ -82,6 +82,12 @@ export PVE1_HOST PVE_TEST_HOST PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE \
|
|||||||
: "${NIX_CACHE_HOST:=nix-cache}"
|
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||||
export NIX_CACHE_HOST
|
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 $(...)/<(...) --
|
# nix_extra_opts: call as a plain statement (NOT inside $(...)/<(...) --
|
||||||
# that forks a subshell, and the whole point is exporting a decision back
|
# 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
|
# 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,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
|
||||||
|
}
|
||||||
@@ -6,27 +6,31 @@
|
|||||||
#
|
#
|
||||||
# This is the non-NixOS equivalent of modules/nix-cache/client.nix +
|
# This is the non-NixOS equivalent of modules/nix-cache/client.nix +
|
||||||
# modules/nix-cache/remote-builder-client.nix -- those two only apply to
|
# modules/nix-cache/remote-builder-client.nix -- those two only apply to
|
||||||
# hosts built from this flake. A plain Debian box with Nix installed
|
# hosts built from this flake. A plain Debian box with Nix installed has no
|
||||||
# (single- or multi-user install, nix-daemon running) has no NixOS module
|
# NixOS module system to pick that config up, so this edits nix.conf by hand.
|
||||||
# system to pick that config up, so this edits /etc/nix/nix.conf by hand
|
#
|
||||||
# instead. Run this ON the target Debian machine, as root.
|
# 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
|
# 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
|
# 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).
|
# key or the cache signing key is rotated (see docs/nix-cache.md).
|
||||||
#
|
#
|
||||||
# REMOTE_BUILDER_KEY defaults to this machine's own default root SSH
|
# REMOTE_BUILDER_KEY defaults to the running user's default SSH identity
|
||||||
# identity (matches modules/nix-cache/remote-builder-client.nix's
|
# (root: /root/.ssh/id_ed25519, other user: ~/.ssh/id_ed25519). That key
|
||||||
# convention for real NixOS clients: authenticate as nixremote with the
|
# must be listed in vars.remoteBuilderAuthorizedKeys in this repo and
|
||||||
# host's own default key, added individually to
|
# nix-cache rebuilt before remote building works.
|
||||||
# vars.remoteBuilderAuthorizedKeys, rather than a separately-named or
|
|
||||||
# shared keypair) -- generate one with
|
|
||||||
# `ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519` if this machine
|
|
||||||
# doesn't have one yet, then add its .pub to vars.remoteBuilderAuthorizedKeys
|
|
||||||
# and rebuild nix-cache.
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# sudo ./configure-nix-cache-client.sh [--dry-run] [--no-remote-builder] [--no-restart]
|
# ./configure-nix-cache-client.sh [--dry-run] [--no-remote-builder] [--no-restart]
|
||||||
#
|
#
|
||||||
# Env overrides (defaults match variables.nix):
|
# Env overrides (defaults match variables.nix):
|
||||||
# NIX_CACHE_HOST, NIX_CACHE_HOST_KEY, REMOTE_BUILDER_USER, REMOTE_BUILDER_KEY
|
# NIX_CACHE_HOST, NIX_CACHE_HOST_KEY, REMOTE_BUILDER_USER, REMOTE_BUILDER_KEY
|
||||||
@@ -36,17 +40,28 @@ set -euo pipefail
|
|||||||
: "${NIX_CACHE_HOST:=nix-cache}"
|
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||||
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPeWgMsdaiz4axT/deFc1+0B5bN+GX/NOeW9bbQ0c/IT lxc-nix-cache}"
|
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPeWgMsdaiz4axT/deFc1+0B5bN+GX/NOeW9bbQ0c/IT lxc-nix-cache}"
|
||||||
: "${REMOTE_BUILDER_USER:=nixremote}"
|
: "${REMOTE_BUILDER_USER:=nixremote}"
|
||||||
: "${REMOTE_BUILDER_KEY:=/root/.ssh/id_ed25519}"
|
|
||||||
|
|
||||||
CACHE_PUB_KEY="cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
|
CACHE_PUB_KEY="cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
|
||||||
FALLBACK_URL="https://cache.nixos.org/"
|
FALLBACK_URL="https://cache.nixos.org/"
|
||||||
FALLBACK_PUB_KEY="cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
FALLBACK_PUB_KEY="cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
|
|
||||||
NIX_CONF="/etc/nix/nix.conf"
|
|
||||||
KNOWN_HOSTS="/etc/ssh/ssh_known_hosts"
|
|
||||||
MARKER_BEGIN="# BEGIN nix-cache client config (configure-nix-cache-client.sh)"
|
MARKER_BEGIN="# BEGIN nix-cache client config (configure-nix-cache-client.sh)"
|
||||||
MARKER_END="# END nix-cache client config"
|
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
|
dry_run=0
|
||||||
with_remote_builder=1
|
with_remote_builder=1
|
||||||
restart_daemon=1
|
restart_daemon=1
|
||||||
@@ -57,7 +72,7 @@ for arg in "$@"; do
|
|||||||
--no-remote-builder) with_remote_builder=0 ;;
|
--no-remote-builder) with_remote_builder=0 ;;
|
||||||
--no-restart) restart_daemon=0 ;;
|
--no-restart) restart_daemon=0 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
sed -n '2,20p' "$0"
|
sed -n '2,37p' "$0"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -67,21 +82,22 @@ for arg in "$@"; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "$dry_run" -eq 0 && "$EUID" -ne 0 ]]; then
|
|
||||||
echo "ERROR: must run as root (writes $NIX_CONF and, unless --no-remote-builder, $KNOWN_HOSTS)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
echo "ERROR: no 'nix' binary on PATH -- install the Nix package manager first." >&2
|
echo "ERROR: no 'nix' binary on PATH -- install the Nix package manager first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$NIX_CONF" ]]; then
|
if [[ "$install_mode" == "multi" && ! -f "$NIX_CONF" ]]; then
|
||||||
echo "ERROR: $NIX_CONF not found -- expected an existing multi-user Nix install." >&2
|
echo "ERROR: $NIX_CONF not found -- expected an existing multi-user Nix install." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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=""
|
builder_line=""
|
||||||
if [[ "$with_remote_builder" -eq 1 ]]; then
|
if [[ "$with_remote_builder" -eq 1 ]]; then
|
||||||
if [[ -f "$REMOTE_BUILDER_KEY" ]]; then
|
if [[ -f "$REMOTE_BUILDER_KEY" ]]; then
|
||||||
@@ -117,7 +133,7 @@ fi
|
|||||||
block="${block}
|
block="${block}
|
||||||
$MARKER_END"
|
$MARKER_END"
|
||||||
|
|
||||||
echo "== nix.conf block to install =="
|
echo "== nix.conf block to install ($NIX_CONF) =="
|
||||||
echo "$block"
|
echo "$block"
|
||||||
echo "================================"
|
echo "================================"
|
||||||
|
|
||||||
@@ -159,7 +175,8 @@ if [[ "$with_remote_builder" -eq 1 ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$dry_run" -eq 0 && "$restart_daemon" -eq 1 ]]; then
|
# Only restart the daemon for multi-user installs -- single-user has no daemon.
|
||||||
|
if [[ "$dry_run" -eq 0 && "$restart_daemon" -eq 1 && "$install_mode" == "multi" ]]; then
|
||||||
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nix-daemon 2>/dev/null; then
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nix-daemon 2>/dev/null; then
|
||||||
systemctl restart nix-daemon
|
systemctl restart nix-daemon
|
||||||
echo "Restarted nix-daemon to pick up the new config."
|
echo "Restarted nix-daemon to pick up the new config."
|
||||||
|
|||||||
@@ -193,6 +193,16 @@ done
|
|||||||
|
|
||||||
ssh_target="${PROXMOX_SSH_USER}@${node}"
|
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() {
|
remote() {
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] ssh ${ssh_target} -- $*"
|
echo "[dry-run] ssh ${ssh_target} -- $*"
|
||||||
@@ -214,10 +224,10 @@ cmd_modify() {
|
|||||||
|
|
||||||
echo "Looking up VMID ${vmid} on ${node}..."
|
echo "Looking up VMID ${vmid} on ${node}..."
|
||||||
local kind current_cores current_memory disk_key
|
local kind current_cores current_memory disk_key
|
||||||
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1; then
|
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1; then
|
||||||
kind="vm"
|
kind="vm"
|
||||||
disk_key="scsi0"
|
disk_key="scsi0"
|
||||||
elif ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
elif ssh "$ssh_target" "${sudo_prefix} pct status ${vmid}" >/dev/null 2>&1; then
|
||||||
kind="lxc"
|
kind="lxc"
|
||||||
disk_key="rootfs"
|
disk_key="rootfs"
|
||||||
else
|
else
|
||||||
@@ -225,8 +235,8 @@ cmd_modify() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local config_cmd="qm config ${vmid}"
|
local config_cmd="${sudo_prefix} qm config ${vmid}"
|
||||||
[[ "$kind" == "lxc" ]] && config_cmd="pct config ${vmid}"
|
[[ "$kind" == "lxc" ]] && config_cmd="${sudo_prefix} pct config ${vmid}"
|
||||||
local current_config
|
local current_config
|
||||||
current_config="$(ssh "$ssh_target" "$config_cmd")"
|
current_config="$(ssh "$ssh_target" "$config_cmd")"
|
||||||
current_cores="$(echo "$current_config" | grep -oP '^cores:\s*\K\S+' || echo '?')"
|
current_cores="$(echo "$current_config" | grep -oP '^cores:\s*\K\S+' || echo '?')"
|
||||||
@@ -250,9 +260,9 @@ cmd_modify() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local set_cmd="qm set"
|
local set_cmd="${sudo_prefix} qm set"
|
||||||
local resize_cmd="qm resize"
|
local resize_cmd="${sudo_prefix} qm resize"
|
||||||
[[ "$kind" == "lxc" ]] && set_cmd="pct set" && resize_cmd="pct resize"
|
[[ "$kind" == "lxc" ]] && set_cmd="${sudo_prefix} pct set" && resize_cmd="${sudo_prefix} pct resize"
|
||||||
|
|
||||||
if [[ -n "$cores" || -n "$memory" ]]; then
|
if [[ -n "$cores" || -n "$memory" ]]; then
|
||||||
local args=""
|
local args=""
|
||||||
@@ -359,14 +369,15 @@ else
|
|||||||
echo
|
echo
|
||||||
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
|
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
|
||||||
ssh_check_status=0
|
ssh_check_status=0
|
||||||
existing="$(ssh "$ssh_target" bash -s -- "$host" <<'REMOTE_SCRIPT'
|
existing="$(ssh "$ssh_target" bash -s -- "$host" "$sudo_prefix" <<'REMOTE_SCRIPT'
|
||||||
target="$1"
|
target="$1"
|
||||||
for id in $(qm list 2>/dev/null | awk 'NR>1{print $1}'); do
|
sudo_pfx="$2"
|
||||||
n="$(qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
|
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}"
|
[[ "$n" == "$target" ]] && echo "vm ${id} ${n}"
|
||||||
done
|
done
|
||||||
for id in $(pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
for id in $($sudo_pfx pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||||
n="$(pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
n="$($sudo_pfx pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
||||||
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
|
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
|
||||||
done
|
done
|
||||||
exit 0
|
exit 0
|
||||||
@@ -453,12 +464,12 @@ REMOTE_SCRIPT
|
|||||||
if [[ "$kind" == "vm" ]]; then
|
if [[ "$kind" == "vm" ]]; then
|
||||||
# qm destroy has no --force to stop-then-destroy in one call (pct's
|
# qm destroy has no --force to stop-then-destroy in one call (pct's
|
||||||
# does) -- stop explicitly first if it's running.
|
# does) -- stop explicitly first if it's running.
|
||||||
if ssh "$ssh_target" "qm status ${id}" 2>/dev/null | grep -q running; then
|
if ssh "$ssh_target" "${sudo_prefix} qm status ${id}" 2>/dev/null | grep -q running; then
|
||||||
ssh "$ssh_target" "qm stop ${id}"
|
ssh "$ssh_target" "${sudo_prefix} qm stop ${id}"
|
||||||
fi
|
fi
|
||||||
ssh "$ssh_target" "qm destroy ${id} --purge 1"
|
ssh "$ssh_target" "${sudo_prefix} qm destroy ${id} --purge 1"
|
||||||
else
|
else
|
||||||
ssh "$ssh_target" "pct destroy ${id} --force 1 --purge 1"
|
ssh "$ssh_target" "${sudo_prefix} pct destroy ${id} --force 1 --purge 1"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -487,7 +498,7 @@ if [[ -z "$vmid" ]]; then
|
|||||||
vmid="<next-free-vmid>"
|
vmid="<next-free-vmid>"
|
||||||
echo "[dry-run] would ask ${node} for the next free VMID (pvesh get /cluster/nextid)"
|
echo "[dry-run] would ask ${node} for the next free VMID (pvesh get /cluster/nextid)"
|
||||||
else
|
else
|
||||||
vmid="$(ssh "$ssh_target" "pvesh get /cluster/nextid" | tr -d '[:space:]')"
|
vmid="$(ssh "$ssh_target" "${sudo_prefix} pvesh get /cluster/nextid" | tr -d '[:space:]')"
|
||||||
echo "Auto-assigned VMID: ${vmid}"
|
echo "Auto-assigned VMID: ${vmid}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -501,8 +512,8 @@ if [[ "$dry_run" -eq 0 ]]; then
|
|||||||
# both. Any success here means something is already using this ID --
|
# both. Any success here means something is already using this ID --
|
||||||
# refuse to go anywhere near it. (Reconfiguring an existing resource is
|
# refuse to go anywhere near it. (Reconfiguring an existing resource is
|
||||||
# --modify's job, not this one's.)
|
# --modify's job, not this one's.)
|
||||||
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1 \
|
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1 \
|
||||||
|| ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
|| 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 "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 "existing resource here -- use --modify to reconfigure it, pick a" >&2
|
||||||
echo "different --vmid, or omit it to auto-assign." >&2
|
echo "different --vmid, or omit it to auto-assign." >&2
|
||||||
@@ -672,11 +683,11 @@ if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
|
|||||||
# hands the result to the remote shell to re-split, which would
|
# hands the result to the remote shell to re-split, which would
|
||||||
# otherwise scatter NIX_EXTRA_OPTS (itself several space-separated,
|
# otherwise scatter NIX_EXTRA_OPTS (itself several space-separated,
|
||||||
# %q-quoted tokens) across the wrong positional parameters below.
|
# %q-quoted tokens) across the wrong positional parameters below.
|
||||||
printf -v remote_cmd 'bash -s -- %q %q %q %q %q' \
|
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q' \
|
||||||
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS"
|
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
|
||||||
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"
|
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_pfx="$6"
|
||||||
declare -a NIX_OPTS=()
|
declare -a NIX_OPTS=()
|
||||||
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||||
cd "$repo_dir"
|
cd "$repo_dir"
|
||||||
@@ -694,23 +705,14 @@ if [[ -z "$built" ]]; then
|
|||||||
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
|
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p "$dest_dir"
|
$sudo_pfx mkdir -p "$dest_dir"
|
||||||
cp "$built" "${dest_dir}/${dest_name}"
|
$sudo_pfx cp "$built" "${dest_dir}/${dest_name}"
|
||||||
echo "Built and staged: ${dest_dir}/${dest_name}"
|
echo "Built and staged: ${dest_dir}/${dest_name}"
|
||||||
REMOTE_SCRIPT
|
REMOTE_SCRIPT
|
||||||
local_image="$remote_path"
|
local_image="$remote_path"
|
||||||
echo "Built on ${node}: ${remote_path}"
|
echo "Built on ${node}: ${remote_path}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# PROXMOX_SSH_USER defaults to root (env.sh), which needs no sudo and
|
|
||||||
# can't assume it's even installed on a minimal node -- only shell out
|
|
||||||
# through sudo when actually running as a non-root SSH user.
|
|
||||||
sudo_prefix="sudo"
|
|
||||||
sudo_display="sudo "
|
|
||||||
if [[ "$PROXMOX_SSH_USER" == "root" ]]; then
|
|
||||||
sudo_prefix=""
|
|
||||||
sudo_display=""
|
|
||||||
fi
|
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
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] 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] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
|
||||||
@@ -728,7 +730,7 @@ REMOTE_SCRIPT
|
|||||||
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
|
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
|
||||||
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_prefix="$6"
|
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_pfx="$6"
|
||||||
declare -a NIX_OPTS=()
|
declare -a NIX_OPTS=()
|
||||||
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||||
cd "$repo_dir"
|
cd "$repo_dir"
|
||||||
@@ -737,7 +739,7 @@ ensure_nix_profile
|
|||||||
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||||
".#nixosConfigurations.${target}.config.system.build.diskoImagesScript" \
|
".#nixosConfigurations.${target}.config.system.build.diskoImagesScript" \
|
||||||
--out-link "result-${target}"
|
--out-link "result-${target}"
|
||||||
$sudo_prefix "./result-${target}" \
|
$sudo_pfx "./result-${target}" \
|
||||||
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
||||||
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
||||||
--build-memory 2048
|
--build-memory 2048
|
||||||
@@ -746,8 +748,8 @@ if [[ -z "$built" ]]; then
|
|||||||
echo "ERROR: no .raw image found in ${repo_dir} after build." >&2
|
echo "ERROR: no .raw image found in ${repo_dir} after build." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p "$dest_dir"
|
$sudo_pfx mkdir -p "$dest_dir"
|
||||||
mv "$built" "${dest_dir}/${dest_name}"
|
$sudo_pfx mv "$built" "${dest_dir}/${dest_name}"
|
||||||
echo "Built and staged: ${dest_dir}/${dest_name}"
|
echo "Built and staged: ${dest_dir}/${dest_name}"
|
||||||
REMOTE_SCRIPT
|
REMOTE_SCRIPT
|
||||||
local_image="$remote_path"
|
local_image="$remote_path"
|
||||||
@@ -812,9 +814,9 @@ if [[ "$type" == "lxc" ]]; then
|
|||||||
# hands the whole string to `ssh` as a single command for the *remote*
|
# hands the whole string to `ssh` as a single command for the *remote*
|
||||||
# shell to parse -- unquoted, that `;` would be read as a remote
|
# shell to parse -- unquoted, that `;` would be read as a remote
|
||||||
# command separator and silently truncate this into two commands.
|
# command separator and silently truncate this into two commands.
|
||||||
create_cmd="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"
|
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 "$create_cmd"
|
||||||
remote "pct start ${vmid}"
|
remote "${sudo_prefix} pct start ${vmid}"
|
||||||
else
|
else
|
||||||
echo "==> Creating VM ${vmid} (${name})..."
|
echo "==> Creating VM ${vmid} (${name})..."
|
||||||
# pre-enrolled-keys=0 disables OVMF's Secure Boot key pre-enrollment --
|
# pre-enrolled-keys=0 disables OVMF's Secure Boot key pre-enrollment --
|
||||||
@@ -825,29 +827,29 @@ else
|
|||||||
# without this flag Proxmox never creates the channel it listens on, so
|
# 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
|
# `qm guest exec`/`qm agent` and the UI's IP-address display silently
|
||||||
# never work for any VM this script creates.
|
# never work for any VM this script creates.
|
||||||
remote "qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
|
remote "${sudo_prefix} qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
|
||||||
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
|
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
|
||||||
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0 --agent enabled=1"
|
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0 --agent enabled=1"
|
||||||
|
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] ssh ${ssh_target} -- qm importdisk ${vmid} ${remote_path} ${storage}"
|
echo "[dry-run] ssh ${ssh_target} -- ${sudo_display}qm importdisk ${vmid} ${remote_path} ${storage}"
|
||||||
echo "[dry-run] (would parse the resulting disk identifier from that output)"
|
echo "[dry-run] (would parse the resulting disk identifier from that output)"
|
||||||
echo "[dry-run] ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
|
echo "[dry-run] ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
|
||||||
else
|
else
|
||||||
importdisk_output="$(ssh "$ssh_target" "qm importdisk ${vmid} ${remote_path} ${storage}")"
|
importdisk_output="$(ssh "$ssh_target" "${sudo_prefix} qm importdisk ${vmid} ${remote_path} ${storage}")"
|
||||||
echo "$importdisk_output"
|
echo "$importdisk_output"
|
||||||
disk_id="$(echo "$importdisk_output" | grep -oP "(?<=Successfully imported disk as ')[^']+" | sed 's/^unused[0-9]*://')"
|
disk_id="$(echo "$importdisk_output" | grep -oP "(?<=Successfully imported disk as ')[^']+" | sed 's/^unused[0-9]*://')"
|
||||||
if [[ -z "$disk_id" ]]; then
|
if [[ -z "$disk_id" ]]; then
|
||||||
echo "ERROR: couldn't parse the imported disk identifier from qm importdisk's output above." >&2
|
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 "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} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<disk-id-from-output-above>" >&2
|
||||||
echo " ssh ${ssh_target} -- qm set ${vmid} --boot order=scsi0" >&2
|
echo " ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --boot order=scsi0" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
remote "qm set ${vmid} --scsi0 ${disk_id}"
|
remote "${sudo_prefix} qm set ${vmid} --scsi0 ${disk_id}"
|
||||||
fi
|
fi
|
||||||
remote "qm set ${vmid} --boot order=scsi0"
|
remote "${sudo_prefix} qm set ${vmid} --boot order=scsi0"
|
||||||
remote "qm start ${vmid}"
|
remote "${sudo_prefix} qm start ${vmid}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -1,347 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Move a single VM/CT from one Proxmox node to another via vzdump +
|
|
||||||
# qmrestore/pct restore -- the same relay pattern as
|
|
||||||
# clone-pve1-to-pve-test.sh, but for a *migration* (the guest ends up
|
|
||||||
# living on the target node only) rather than a *clone* (a disposable
|
|
||||||
# copy, source untouched, fresh MAC).
|
|
||||||
#
|
|
||||||
# There is no real `qm migrate` here: that command only works between
|
|
||||||
# nodes in the same Proxmox cluster with shared/replicated storage, which
|
|
||||||
# pve1 and pve-test are not (see CLAUDE.md's "Two Proxmox nodes" section --
|
|
||||||
# they're deliberately separate, unclustered nodes). This script is the
|
|
||||||
# closest equivalent across two independent nodes, built out of the same
|
|
||||||
# primitives as the clone script. It is NOT a true live migration -- read
|
|
||||||
# the caveat below before using it for anything where losing a few
|
|
||||||
# seconds/minutes of writes is unacceptable.
|
|
||||||
#
|
|
||||||
# Flow:
|
|
||||||
# 1. vzdump the resource on the source node (--mode snapshot by
|
|
||||||
# default, so the source keeps running throughout the bulk copy --
|
|
||||||
# this is the part that's "live": the guest stays reachable while
|
|
||||||
# its disk data is captured and shipped to the target).
|
|
||||||
# 2. Stream the archive straight from the source node to the target
|
|
||||||
# node (ssh source cat ... | ssh target cat > ...), same relay this
|
|
||||||
# machine does in the clone script -- no separate on-disk staging
|
|
||||||
# copy here either.
|
|
||||||
# 3. qmrestore / pct restore it on the target node under the same VMID
|
|
||||||
# by default (--new-vmid to pick a different one). Restored WITHOUT
|
|
||||||
# --unique (i.e. original MAC preserved) unless --unique is passed
|
|
||||||
# explicitly -- unlike the clone script, the source is being retired,
|
|
||||||
# not left running alongside a copy, so there's no address collision
|
|
||||||
# to avoid. qmrestore/pct restore don't start the guest, so nothing
|
|
||||||
# is live on the target node yet.
|
|
||||||
# 4. Cutover: stop the guest on the source node, then (unless --no-start)
|
|
||||||
# start it on the target node. This is the only real downtime window
|
|
||||||
# -- everything before this point runs with the source guest still
|
|
||||||
# up.
|
|
||||||
# 5. Unless --remove-source is passed, the now-stopped guest is LEFT ON
|
|
||||||
# the source node as a safety net (config + disk intact, just
|
|
||||||
# powered off) -- easy to start back up if the target copy turns out
|
|
||||||
# to be broken. Pass --remove-source to actually destroy it there
|
|
||||||
# once you've verified the target is good; this step gets its own
|
|
||||||
# typed confirmation since qm destroy/pct destroy is irreversible.
|
|
||||||
# 6. Delete the vzdump archive from both nodes' storage (unless
|
|
||||||
# --keep-backup), same as the clone script -- neither node
|
|
||||||
# accumulates ad hoc backup files from this script.
|
|
||||||
#
|
|
||||||
# --- IMPORTANT CAVEAT: this is not byte-perfect live migration ---
|
|
||||||
# vzdump is not incremental. With the default --mode snapshot, the source
|
|
||||||
# guest keeps running (and can keep writing to disk) for the entire time
|
|
||||||
# between when the snapshot is taken and when this script stops it at
|
|
||||||
# cutover. Any writes in that window are NOT captured in the archive and
|
|
||||||
# will NOT exist on the target. For a large disk this window can be
|
|
||||||
# minutes. If the workload can't tolerate that:
|
|
||||||
# - use --mode stop (or --mode suspend) instead, which makes the source
|
|
||||||
# guest go down *before* vzdump reads its disk, so the archive is
|
|
||||||
# exactly the state being migrated and cutover has nothing left to
|
|
||||||
# lose -- at the cost of the guest being down for the whole backup
|
|
||||||
# duration instead of just the final cutover.
|
|
||||||
# - or don't use this script -- put both nodes in an actual Proxmox
|
|
||||||
# cluster with shared storage and use `qm migrate --online` instead,
|
|
||||||
# which is the real thing this script is only approximating.
|
|
||||||
#
|
|
||||||
# This script's own defaults are pve1 -> pve-test, matching
|
|
||||||
# clone-pve1-to-pve-test.sh and CLAUDE.md's "Two Proxmox nodes" section --
|
|
||||||
# but unlike that script, this one is a generic node-to-node mover: pass
|
|
||||||
# --source-node/--target-node for any other pair. Regardless of node
|
|
||||||
# names, the source resource is only ever touched here after typing the
|
|
||||||
# source VMID back to confirm, and destroying it on the source node
|
|
||||||
# (--remove-source) requires a second, separate typed confirmation.
|
|
||||||
#
|
|
||||||
# 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 migrate.
|
|
||||||
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 until cutover;
|
|
||||||
requires snapshot-capable storage, e.g.
|
|
||||||
ZFS/LVM-thin/Ceph/qcow2). Use "stop" for a
|
|
||||||
byte-perfect migration with no post-snapshot
|
|
||||||
write gap (source goes down for the whole
|
|
||||||
backup duration instead of just cutover) --
|
|
||||||
see the caveat at the top of this script.
|
|
||||||
--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}).
|
|
||||||
--unique Restore with a fresh MAC address
|
|
||||||
(--unique 1), as the clone script always
|
|
||||||
does. Off by default here since the
|
|
||||||
source is being retired, not left
|
|
||||||
running alongside the target.
|
|
||||||
--no-start Don't start the guest on the target
|
|
||||||
node after cutover (default: start
|
|
||||||
it).
|
|
||||||
--remove-source Destroy the guest on the source
|
|
||||||
node (qm destroy/pct destroy) after
|
|
||||||
a successful cutover, instead of
|
|
||||||
just leaving it stopped there.
|
|
||||||
Irreversible -- prompts for its own
|
|
||||||
typed confirmation unless --yes.
|
|
||||||
--keep-backup Don't delete the vzdump archive from
|
|
||||||
either node afterward (debugging aid).
|
|
||||||
--yes Skip all typed confirmations (initial
|
|
||||||
backup, cutover, and --remove-source
|
|
||||||
if passed).
|
|
||||||
--dry-run Print the full plan and skip every
|
|
||||||
mutating step (vzdump, transfer,
|
|
||||||
restore, cutover, destroy, delete)
|
|
||||||
and every 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"
|
|
||||||
unique=0
|
|
||||||
start_target=1
|
|
||||||
remove_source=0
|
|
||||||
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 ;;
|
|
||||||
--unique) unique=1; shift ;;
|
|
||||||
--no-start) start_target=0; shift ;;
|
|
||||||
--remove-source) remove_source=1; shift ;;
|
|
||||||
--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, cutover, destroy, 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}," \
|
|
||||||
"$([[ "$unique" -eq 1 ]] && echo "fresh MAC via --unique" || echo "original MAC preserved"))"
|
|
||||||
echo " cutover: stop VMID ${vmid} on ${source_node}," \
|
|
||||||
"$([[ "$start_target" -eq 1 ]] && echo "then start VMID ${new_vmid} on ${target_node}" || echo "target left stopped (--no-start)")"
|
|
||||||
if [[ "$remove_source" -eq 1 ]]; then
|
|
||||||
echo " after cutover: DESTROY VMID ${vmid} on ${source_node} (--remove-source, irreversible)"
|
|
||||||
else
|
|
||||||
echo " after cutover: source VMID ${vmid} left stopped (but intact) on ${source_node}"
|
|
||||||
fi
|
|
||||||
[[ "$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, cutover, destroy, 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} for migration: "; 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})..."
|
|
||||||
restore_unique_flag=0
|
|
||||||
[[ "$unique" -eq 1 ]] && restore_unique_flag=1
|
|
||||||
if [[ "$kind" == "vm" ]]; then
|
|
||||||
ssh "$target_target" "qmrestore '${target_tmp_archive}' ${new_vmid} --storage ${target_storage} --unique ${restore_unique_flag}"
|
|
||||||
else
|
|
||||||
ssh "$target_target" "pct restore ${new_vmid} '${target_tmp_archive}' --storage ${target_storage} --unique ${restore_unique_flag}"
|
|
||||||
fi
|
|
||||||
echo "Restored on ${target_node}. Source VMID ${vmid} on ${source_node} is still up --" \
|
|
||||||
"the guest hasn't moved yet."
|
|
||||||
|
|
||||||
# --- cutover: stop source, start target -----------------------------------
|
|
||||||
if [[ "$skip_confirm" -ne 1 ]]; then
|
|
||||||
echo
|
|
||||||
if ! confirm_typed "$vmid" "Type the source VMID (${vmid}) again to confirm CUTOVER (stop it on ${source_node}$([[ "$start_target" -eq 1 ]] && echo ", start VMID ${new_vmid} on ${target_node}")): "; then
|
|
||||||
echo "Cancelled before cutover -- input didn't match ${vmid}." >&2
|
|
||||||
echo "The restored (but not started) copy remains on ${target_node} as VMID ${new_vmid};" >&2
|
|
||||||
echo "the source on ${source_node} is untouched. Re-run cutover manually, or clean up" >&2
|
|
||||||
echo "the target copy if you no longer want it." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "==> Stopping VMID ${vmid} on ${source_node}..."
|
|
||||||
if [[ "$kind" == "vm" ]]; then
|
|
||||||
ssh "$source_target" "qm stop ${vmid}"
|
|
||||||
else
|
|
||||||
ssh "$source_target" "pct stop ${vmid}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$start_target" -eq 1 ]]; then
|
|
||||||
echo
|
|
||||||
echo "==> Starting VMID ${new_vmid} on ${target_node}..."
|
|
||||||
if [[ "$kind" == "vm" ]]; then
|
|
||||||
ssh "$target_target" "qm start ${new_vmid}"
|
|
||||||
else
|
|
||||||
ssh "$target_target" "pct start ${new_vmid}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- optionally destroy the now-stopped source resource -------------------
|
|
||||||
if [[ "$remove_source" -eq 1 ]]; then
|
|
||||||
if [[ "$skip_confirm" -ne 1 ]]; then
|
|
||||||
echo
|
|
||||||
if ! confirm_typed "$vmid" "Type the source VMID (${vmid}) one more time to permanently DESTROY it on ${source_node}: "; then
|
|
||||||
echo "Cancelled -- input didn't match ${vmid}." >&2
|
|
||||||
echo "VMID ${vmid} on ${source_node} is left stopped (not destroyed). VMID ${new_vmid}" >&2
|
|
||||||
echo "on ${target_node} is up and running." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
echo "==> Destroying VMID ${vmid} on ${source_node}..."
|
|
||||||
if [[ "$kind" == "vm" ]]; then
|
|
||||||
ssh "$source_target" "qm destroy ${vmid} --purge"
|
|
||||||
else
|
|
||||||
ssh "$source_target" "pct destroy ${vmid} --purge"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- clean up backup archives now that the migration 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}, migrated from" \
|
|
||||||
"VMID ${vmid} on ${source_node}."
|
|
||||||
if [[ "$remove_source" -ne 1 ]]; then
|
|
||||||
echo "The source copy is stopped but still present on ${source_node} -- re-run with" \
|
|
||||||
"--remove-source once you've verified the target, or remove it manually."
|
|
||||||
fi
|
|
||||||
Executable
+253
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# recover-hosts.sh — Fix sops/SSH-key/GitHub-token issues on deployed NixOS hosts
|
||||||
|
# and trigger a Switch-nix rebuild on each.
|
||||||
|
#
|
||||||
|
# Run from the repo root on the workstation (nixos@nixos):
|
||||||
|
# bash scripts/recover-hosts.sh [<hostname> ...]
|
||||||
|
#
|
||||||
|
# With no args it discovers and checks every known hostname.
|
||||||
|
# With args it checks only those hostnames:
|
||||||
|
# bash scripts/recover-hosts.sh tor-relay
|
||||||
|
#
|
||||||
|
# Fixes applied automatically (then prompts before rebuilding):
|
||||||
|
# 1. SSH host key drift — live key no longer matches host-keys/<target>_ssh_host_ed25519_key
|
||||||
|
# Fix: scp the registered key back and restore it (needs sudo once per host).
|
||||||
|
# To push new keys proactively (before drift, e.g. right after
|
||||||
|
# sync-host-keys.sh --regenerate-all-keys), use instead:
|
||||||
|
# scripts/secrets/push-host-keys.sh --all
|
||||||
|
# 2. Stale/invalid GitHub access token — the rendered nix-github-token.conf has
|
||||||
|
# a token GitHub rejects (401), blocking any rebuild that fetches disko or
|
||||||
|
# other public GitHub flake inputs.
|
||||||
|
# Fix: empty the rendered file so nix makes unauthenticated requests instead.
|
||||||
|
# Public repos (disko, nixpkgs, etc.) work fine without auth. sops-nix
|
||||||
|
# re-renders the correct new token automatically after the first successful
|
||||||
|
# rebuild.
|
||||||
|
#
|
||||||
|
# Both fixes need one interactive sudo session per host. The script opens a
|
||||||
|
# single ssh -t per broken host so you enter the password once and all steps
|
||||||
|
# run in sequence.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
source scripts/env.sh 2>/dev/null || true
|
||||||
|
|
||||||
|
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5)
|
||||||
|
SSH_USER=nixos
|
||||||
|
|
||||||
|
# Known flake-target → ssh hostname map for all currently-defined hosts.
|
||||||
|
# Add new hosts here as they are deployed.
|
||||||
|
declare -A TARGET_HOST=(
|
||||||
|
[lxc-docker]=docker
|
||||||
|
[lxc-nix-cache]=nix-cache
|
||||||
|
[lxc-pxe-boot]=pxe-boot
|
||||||
|
[lxc-tor-relay]=tor-relay
|
||||||
|
[lxc-minimal]=nix-minimal
|
||||||
|
[proxmox-server]=server
|
||||||
|
[baremetal-gui]=nixos
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
info() { echo " [✓] $*"; }
|
||||||
|
warn() { echo " [!] $*"; }
|
||||||
|
step() { echo "==> $*"; }
|
||||||
|
|
||||||
|
ssh_host_age() {
|
||||||
|
ssh-keyscan -t ed25519 "$1" 2>/dev/null \
|
||||||
|
| nix shell nixpkgs#ssh-to-age --command ssh-to-age 2>/dev/null \
|
||||||
|
| head -1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
registered_age() {
|
||||||
|
local keyfile="host-keys/${1}_ssh_host_ed25519_key.pub"
|
||||||
|
[ -f "$keyfile" ] || return 0
|
||||||
|
nix shell nixpkgs#ssh-to-age --command ssh-to-age < "$keyfile" 2>/dev/null \
|
||||||
|
| head -1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
github_token_valid() {
|
||||||
|
local host=$1
|
||||||
|
local raw token code
|
||||||
|
raw=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"cat /run/secrets/rendered/nix-github-token.conf 2>/dev/null || true")
|
||||||
|
token=$(echo "$raw" | grep -oP '(?<=github\.com=)\S+' || true)
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
return 0 # no token = unauthenticated, works for public repos
|
||||||
|
fi
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
|
-H "Authorization: token $token" \
|
||||||
|
"https://api.github.com/repos/nix-community/disko" 2>/dev/null || echo 000)
|
||||||
|
[ "$code" = "200" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── discover hosts ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
HOSTNAMES=("$@")
|
||||||
|
else
|
||||||
|
HOSTNAMES=()
|
||||||
|
seen=()
|
||||||
|
for target in "${!TARGET_HOST[@]}"; do
|
||||||
|
h="${TARGET_HOST[$target]}"
|
||||||
|
# deduplicate (e.g. proxmox-server and lxc-server both map to "server")
|
||||||
|
if [[ ! " ${seen[*]:-} " =~ " $h " ]]; then
|
||||||
|
seen+=("$h")
|
||||||
|
if ssh "${SSH_OPTS[@]}" "$SSH_USER@$h" "true" 2>/dev/null; then
|
||||||
|
HOSTNAMES+=("$h")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#HOSTNAMES[@]} -eq 0 ]; then
|
||||||
|
echo "No reachable hosts found. Pass hostnames explicitly or check SSH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Hosts to check: ${HOSTNAMES[*]}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── check phase ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
NEEDS_FIX=()
|
||||||
|
|
||||||
|
for host in "${HOSTNAMES[@]}"; do
|
||||||
|
step "$host"
|
||||||
|
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" "true" 2>/dev/null; then
|
||||||
|
warn "SSH unreachable — clearing stale known_hosts entry"
|
||||||
|
ssh-keygen -R "$host" 2>/dev/null || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
flake_target=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"cat /etc/flake-target 2>/dev/null || true")
|
||||||
|
echo " flake-target: ${flake_target:-unknown}"
|
||||||
|
|
||||||
|
host_broken=false
|
||||||
|
|
||||||
|
# SSH host key
|
||||||
|
if [ -n "$flake_target" ] && [ -f "host-keys/${flake_target}_ssh_host_ed25519_key.pub" ]; then
|
||||||
|
live=$(ssh_host_age "$host")
|
||||||
|
want=$(registered_age "$flake_target")
|
||||||
|
if [ "$live" = "$want" ]; then
|
||||||
|
info "SSH host key OK"
|
||||||
|
else
|
||||||
|
warn "SSH host key MISMATCH (live ≠ host-keys/) -- use push-host-keys.sh proactively next time"
|
||||||
|
echo " live: $live"
|
||||||
|
echo " registered: $want"
|
||||||
|
host_broken=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " [~] No host-keys/ entry for ${flake_target:-unknown} — skipping key check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GitHub token
|
||||||
|
if github_token_valid "$host"; then
|
||||||
|
info "GitHub token OK"
|
||||||
|
else
|
||||||
|
warn "GitHub token invalid (rebuild will fail with 401)"
|
||||||
|
host_broken=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# sops-nix result
|
||||||
|
sops_result=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"systemctl show sops-nix --property=Result --value 2>/dev/null || echo unknown")
|
||||||
|
if [ "$sops_result" = "success" ]; then
|
||||||
|
info "sops-nix: success"
|
||||||
|
else
|
||||||
|
warn "sops-nix: $sops_result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
$host_broken && NEEDS_FIX+=("$host")
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── fix phase ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ ${#NEEDS_FIX[@]} -eq 0 ]; then
|
||||||
|
echo "All hosts healthy — nothing to fix."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Hosts needing fixes: ${NEEDS_FIX[*]}"
|
||||||
|
echo ""
|
||||||
|
echo "Each fix requires one sudo session per host. You will be prompted for"
|
||||||
|
echo "the nixos sudo password once per host; all steps run in that session."
|
||||||
|
echo ""
|
||||||
|
read -r -p "Proceed with fixes + Switch-nix on each broken host? [y/N] " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for host in "${NEEDS_FIX[@]}"; do
|
||||||
|
step "Fixing $host"
|
||||||
|
flake_target=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"cat /etc/flake-target 2>/dev/null || true")
|
||||||
|
|
||||||
|
fix_script=""
|
||||||
|
|
||||||
|
# Fix 1: restore SSH host key
|
||||||
|
live=$(ssh_host_age "$host")
|
||||||
|
want=$(registered_age "${flake_target:-}")
|
||||||
|
if [ -n "$want" ] && [ "$live" != "$want" ]; then
|
||||||
|
echo " Uploading registered SSH host key (private + public)..."
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
"host-keys/${flake_target}_ssh_host_ed25519_key" \
|
||||||
|
"$SSH_USER@$host:/tmp/recover_ed25519_key"
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
"host-keys/${flake_target}_ssh_host_ed25519_key.pub" \
|
||||||
|
"$SSH_USER@$host:/tmp/recover_ed25519_key.pub"
|
||||||
|
fix_script+='
|
||||||
|
echo "[fix] Restoring SSH host key..."
|
||||||
|
install -m 0600 /tmp/recover_ed25519_key /etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -m 0644 /tmp/recover_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
rm -f /tmp/recover_ed25519_key /tmp/recover_ed25519_key.pub
|
||||||
|
echo " Done."
|
||||||
|
'
|
||||||
|
ssh-keygen -R "$host" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fix 2: clear invalid GitHub token
|
||||||
|
if ! github_token_valid "$host"; then
|
||||||
|
fix_script+='
|
||||||
|
echo "[fix] Clearing stale GitHub token (nix will use unauthenticated access)..."
|
||||||
|
echo "" > /run/secrets/rendered/nix-github-token.conf
|
||||||
|
systemctl restart nix-daemon 2>/dev/null || true
|
||||||
|
echo " Done."
|
||||||
|
'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fix 3: rebuild
|
||||||
|
fix_script+='
|
||||||
|
echo "[fix] Running nixos-rebuild switch..."
|
||||||
|
nixos-rebuild switch \
|
||||||
|
--no-write-lock-file \
|
||||||
|
--refresh \
|
||||||
|
--flake "git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(cat /etc/flake-target)"
|
||||||
|
echo "[fix] Rebuild complete."
|
||||||
|
'
|
||||||
|
|
||||||
|
echo " Opening SSH session (enter sudo password when prompted)..."
|
||||||
|
if ssh -t -o StrictHostKeyChecking=no "$SSH_USER@$host" \
|
||||||
|
"sudo bash -s" <<< "$fix_script"; then
|
||||||
|
echo ""
|
||||||
|
info "$host fixed and rebuilt"
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
echo ""
|
||||||
|
warn "$host: rebuild exited with code $rc (may still have succeeded — check sops-nix below)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify: re-check sops-nix result post-rebuild
|
||||||
|
sops_result_after=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"systemctl show sops-nix --property=Result --value 2>/dev/null || echo unknown" 2>/dev/null || echo "ssh-failed")
|
||||||
|
if [ "$sops_result_after" = "success" ]; then
|
||||||
|
info "$host sops-nix: success post-rebuild"
|
||||||
|
else
|
||||||
|
warn "$host sops-nix: $sops_result_after post-rebuild (may need another pass)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Recovery complete."
|
||||||
Executable
+318
@@ -0,0 +1,318 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pushes newly-generated SSH host keys from host-keys/ to already-running
|
||||||
|
# NixOS hosts, so they can decrypt sops secrets after a nixos-rebuild
|
||||||
|
# following scripts/secrets/sync-host-keys.sh --regenerate-all-keys.
|
||||||
|
#
|
||||||
|
# Before pushing any key, verifies that .sops.yaml and secrets/*.yaml are
|
||||||
|
# committed and pushed to the remote -- hosts rebuild from the remote Gitea
|
||||||
|
# flake, so recipient changes must land there before any rebuild, not just
|
||||||
|
# before the key push.
|
||||||
|
#
|
||||||
|
# push-host-keys.sh --all [--dry-run] [--skip-git-check]
|
||||||
|
# push-host-keys.sh <target> [--dry-run] [--skip-git-check]
|
||||||
|
#
|
||||||
|
# --all Push to every reachable managed host. Default when no
|
||||||
|
# target is given.
|
||||||
|
# <target> Push to one flake target only (e.g. lxc-server).
|
||||||
|
# --dry-run Print what would be done; write nothing.
|
||||||
|
# --skip-git-check Skip the commit/push check. Use only when the remote
|
||||||
|
# already has the current .sops.yaml/secrets/*.yaml.
|
||||||
|
#
|
||||||
|
# SSH: connects as SSH_USER@<hostname> (default: nixos, the user with the
|
||||||
|
# admin authorized key), then installs files via sudo -S (reads the sudo
|
||||||
|
# password from stdin). The password is prompted once at startup and reused
|
||||||
|
# for every host -- no PTY or terminal required on the remote side.
|
||||||
|
# Hosts are reached at their bare hostname (relies on LAN DNS/mDNS).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
keydir="${repo_root}/host-keys"
|
||||||
|
|
||||||
|
# shellcheck source=../env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=../lib/nix-eval.sh
|
||||||
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
|
|
||||||
|
: "${SSH_USER:=nixos}"
|
||||||
|
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5)
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
skip_git_check=0
|
||||||
|
sudo_password=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 [--all | <target>] [--dry-run] [--skip-git-check]
|
||||||
|
|
||||||
|
--all Push to every reachable managed host. Default when no
|
||||||
|
target is given.
|
||||||
|
<target> Push to one flake target only (e.g. lxc-server).
|
||||||
|
--dry-run Print what would be done; write nothing.
|
||||||
|
--skip-git-check Skip the check that .sops.yaml/secrets/*.yaml are
|
||||||
|
committed and pushed to the remote repo.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
SSH_USER SSH username (default: nixos).
|
||||||
|
SUDO_PASS Sudo password (skips the interactive prompt; useful
|
||||||
|
when calling from another script).
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prompt for the sudo password once; store it for all _do_push calls.
|
||||||
|
# Accepts SUDO_PASS from the environment to allow non-interactive callers.
|
||||||
|
prompt_sudo_password() {
|
||||||
|
[[ "$dry_run" -eq 1 ]] && return
|
||||||
|
if [[ -n "${SUDO_PASS:-}" ]]; then
|
||||||
|
sudo_password="$SUDO_PASS"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# read exits non-zero when stdin is not a terminal (e.g. CI, background
|
||||||
|
# agents). Catch that and give a clear message rather than a silent exit.
|
||||||
|
if ! read -r -s -p "sudo password for ${SSH_USER} on remote hosts: " sudo_password; then
|
||||||
|
echo >&2
|
||||||
|
echo "ERROR: stdin is not a terminal -- cannot prompt for sudo password." >&2
|
||||||
|
echo " Set SUDO_PASS=<password> in the environment and re-run." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
locally_managed_hosts() {
|
||||||
|
for f in "${keydir}"/*_ssh_host_ed25519_key.pub; do
|
||||||
|
[[ -e "$f" ]] || continue
|
||||||
|
basename "$f" _ssh_host_ed25519_key.pub
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- git state check/fix --------------------------------------------------
|
||||||
|
# Hosts rebuild from the remote Gitea flake:
|
||||||
|
# nixos-rebuild switch --flake "git+https://<gitea>/nixos.git#<target>"
|
||||||
|
# so .sops.yaml (updated recipients) and secrets/*.yaml (re-encrypted DEKs)
|
||||||
|
# must be committed and pushed before any rebuild can succeed. This check
|
||||||
|
# catches the common case where --regenerate-all-keys was just run but the
|
||||||
|
# resulting diff hasn't been committed/pushed yet.
|
||||||
|
ensure_remote_current() {
|
||||||
|
[[ "$skip_git_check" -eq 1 ]] && return
|
||||||
|
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
local dirty_unstaged dirty_staged
|
||||||
|
dirty_unstaged="$(git diff --name-only -- .sops.yaml secrets/ 2>/dev/null || true)"
|
||||||
|
dirty_staged="$(git diff --cached --name-only -- .sops.yaml secrets/ 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ -n "$dirty_unstaged" || -n "$dirty_staged" ]]; then
|
||||||
|
echo "Uncommitted changes in sops-managed files:"
|
||||||
|
[[ -n "$dirty_unstaged" ]] && sed 's/^/ (unstaged) /' <<<"$dirty_unstaged"
|
||||||
|
[[ -n "$dirty_staged" ]] && sed 's/^/ (staged) /' <<<"$dirty_staged"
|
||||||
|
echo
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would prompt to commit .sops.yaml/secrets/ before continuing."
|
||||||
|
else
|
||||||
|
read -rp "Commit .sops.yaml + secrets/ now? [y/N]: " ans
|
||||||
|
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
||||||
|
git add -- .sops.yaml secrets/
|
||||||
|
git commit -m "secrets: update recipients and re-encrypt for host key changes"
|
||||||
|
echo "Committed."
|
||||||
|
else
|
||||||
|
echo "Continuing with uncommitted changes -- the remote won't have the"
|
||||||
|
echo "updated recipients until you commit and push."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we're ahead of the remote tracking branch
|
||||||
|
local ahead
|
||||||
|
ahead="$(git rev-list --count '@{upstream}..HEAD' 2>/dev/null || echo "")"
|
||||||
|
if [[ -z "$ahead" ]]; then
|
||||||
|
echo "NOTE: no remote tracking branch found -- skipping push check."
|
||||||
|
echo " Ensure the remote has the current .sops.yaml/secrets/ before"
|
||||||
|
echo " triggering nixos-rebuild on any host."
|
||||||
|
echo
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ahead" -gt 0 ]]; then
|
||||||
|
echo "Local branch is ${ahead} commit(s) ahead of remote."
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would prompt to push before continuing."
|
||||||
|
else
|
||||||
|
read -rp "Push to remote now? [y/N]: " ans
|
||||||
|
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
||||||
|
git push
|
||||||
|
echo "Pushed."
|
||||||
|
else
|
||||||
|
echo "Continuing without pushing -- remember to push before running"
|
||||||
|
echo "nixos-rebuild on any of these hosts."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- key installation (shared) -------------------------------------------
|
||||||
|
_do_push() {
|
||||||
|
local hostname="$1" target="$2"
|
||||||
|
local keyfile="${keydir}/${target}_ssh_host_ed25519_key"
|
||||||
|
local pubfile="${keyfile}.pub"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo " [dry-run] would scp host-keys/${target}_ssh_host_ed25519_key{,.pub} to /tmp/"
|
||||||
|
echo " [dry-run] would: sudo -S install -m 0600/0644 to /etc/ssh/ and rm /tmp copies"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload to /tmp (writable as nixos, no privilege needed)
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
"$keyfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key"
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
"$pubfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key.pub"
|
||||||
|
|
||||||
|
# Install via sudo -S: the password is piped via herestring so no PTY is
|
||||||
|
# needed on either side. -p '' suppresses sudo's own prompt string.
|
||||||
|
ssh -o StrictHostKeyChecking=no "${SSH_USER}@${hostname}" \
|
||||||
|
"sudo -S -p '' bash -c '
|
||||||
|
install -m 0600 /tmp/push_ed25519_key /etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -m 0644 /tmp/push_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
rm -f /tmp/push_ed25519_key /tmp/push_ed25519_key.pub
|
||||||
|
echo \" [ok] host key installed\"
|
||||||
|
'" <<< "$sudo_password"
|
||||||
|
|
||||||
|
# Drop the stale known_hosts entry for this host (public key just changed)
|
||||||
|
ssh-keygen -R "$hostname" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo " Done. Run nixos-rebuild switch on ${hostname} to activate."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- single named target --------------------------------------------------
|
||||||
|
push_target() {
|
||||||
|
local target="$1"
|
||||||
|
local keyfile="${keydir}/${target}_ssh_host_ed25519_key"
|
||||||
|
|
||||||
|
if [[ ! -f "$keyfile" ]]; then
|
||||||
|
echo "ERROR: host-keys/${target}_ssh_host_ed25519_key not found." >&2
|
||||||
|
echo " This target may not be locally managed (e.g. &${target} was" >&2
|
||||||
|
echo " registered from the host's real SSH key, not generated here)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local hostname
|
||||||
|
hostname="$(flake_target_hostname "$repo_root" "$target")"
|
||||||
|
if [[ -z "$hostname" ]]; then
|
||||||
|
echo "ERROR: cannot resolve hostname for '${target}' from the flake." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> ${target} (→ ${hostname})"
|
||||||
|
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" true 2>/dev/null; then
|
||||||
|
echo " SKIP: ${SSH_USER}@${hostname} unreachable."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sanity-check that /etc/flake-target on the host agrees
|
||||||
|
local live_target
|
||||||
|
live_target="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" \
|
||||||
|
"cat /etc/flake-target 2>/dev/null || true")"
|
||||||
|
if [[ -n "$live_target" && "$live_target" != "$target" ]]; then
|
||||||
|
echo " WARN: host reports /etc/flake-target='${live_target}', not '${target}'."
|
||||||
|
echo " Pushing the key you specified (${target}) anyway."
|
||||||
|
fi
|
||||||
|
|
||||||
|
_do_push "$hostname" "$target"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- all managed hosts ----------------------------------------------------
|
||||||
|
# For each unique hostname derived from managed targets, SSHes in and reads
|
||||||
|
# /etc/flake-target to determine which key to push -- handles the case where
|
||||||
|
# multiple targets share a hostname (e.g. lxc-server and proxmox-server both
|
||||||
|
# resolve to "server"; only one is actually running).
|
||||||
|
push_all() {
|
||||||
|
mapfile -t managed < <(locally_managed_hosts)
|
||||||
|
if [[ "${#managed[@]}" -eq 0 ]]; then
|
||||||
|
echo "No managed keys in host-keys/ -- nothing to push."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pushing to all reachable managed hosts..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
declare -A seen_hostnames=()
|
||||||
|
local t hostname
|
||||||
|
for t in "${managed[@]}"; do
|
||||||
|
hostname="$(flake_target_hostname "$repo_root" "$t" 2>/dev/null || true)"
|
||||||
|
[[ -z "$hostname" ]] && continue
|
||||||
|
[[ -n "${seen_hostnames[$hostname]+x}" ]] && continue
|
||||||
|
seen_hostnames["$hostname"]=1
|
||||||
|
|
||||||
|
echo "==> checking ${hostname}"
|
||||||
|
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" true 2>/dev/null; then
|
||||||
|
echo " SKIP: ${SSH_USER}@${hostname} unreachable."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask the host which flake target it actually is
|
||||||
|
local live_target
|
||||||
|
live_target="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" \
|
||||||
|
"cat /etc/flake-target 2>/dev/null || true")"
|
||||||
|
|
||||||
|
if [[ -z "$live_target" ]]; then
|
||||||
|
echo " SKIP: no /etc/flake-target on host -- can't determine which key to push."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local live_keyfile="${keydir}/${live_target}_ssh_host_ed25519_key"
|
||||||
|
if [[ ! -f "$live_keyfile" ]]; then
|
||||||
|
echo " SKIP: host is '${live_target}' but no host-keys/${live_target}_... (hand-registered key, not managed here)."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " target: ${live_target}"
|
||||||
|
_do_push "$hostname" "$live_target"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- main -----------------------------------------------------------------
|
||||||
|
mode="all"
|
||||||
|
target_arg=""
|
||||||
|
extra_args=()
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) dry_run=1 ;;
|
||||||
|
--skip-git-check) skip_git_check=1 ;;
|
||||||
|
--all) mode="all" ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--*) echo "Unknown option: $arg" >&2; usage >&2; exit 1 ;;
|
||||||
|
*) extra_args+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${#extra_args[@]}" -gt 1 ]]; then
|
||||||
|
echo "ERROR: specify at most one target (or --all)." >&2
|
||||||
|
usage >&2; exit 1
|
||||||
|
elif [[ "${#extra_args[@]}" -eq 1 ]]; then
|
||||||
|
mode="single"
|
||||||
|
target_arg="${extra_args[0]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$dry_run" -eq 1 ]] && { echo "[dry-run] no changes will be made"; echo; }
|
||||||
|
|
||||||
|
nix_extra_opts
|
||||||
|
ensure_remote_current
|
||||||
|
prompt_sudo_password
|
||||||
|
|
||||||
|
if [[ "$mode" == "single" ]]; then
|
||||||
|
push_target "$target_arg"
|
||||||
|
else
|
||||||
|
push_all
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply."
|
||||||
|
else
|
||||||
|
echo "Key push complete. For each updated host, run nixos-rebuild switch to"
|
||||||
|
echo "apply the config and let sops-nix decrypt secrets with the new key."
|
||||||
|
fi
|
||||||
@@ -359,8 +359,11 @@ cmd_regenerate_all() {
|
|||||||
echo "This will remove and freshly regenerate ALL locally-managed keys:"
|
echo "This will remove and freshly regenerate ALL locally-managed keys:"
|
||||||
printf ' %s\n' "${hosts[@]}"
|
printf ' %s\n' "${hosts[@]}"
|
||||||
echo
|
echo
|
||||||
echo "Every host above will need its new key baked into a rebuilt install"
|
echo "After regenerating, each host needs its new key before it can decrypt secrets:"
|
||||||
echo "image/tarball before it can decrypt secrets again."
|
echo " • Already running: push the key before rebuilding:"
|
||||||
|
echo " scripts/secrets/push-host-keys.sh --all"
|
||||||
|
echo " • Not yet deployed: rebuild the install image with the new keys baked in"
|
||||||
|
echo " (see docs/auto-installer.md)."
|
||||||
|
|
||||||
if [[ "$dry_run" -ne 1 ]]; then
|
if [[ "$dry_run" -ne 1 ]]; then
|
||||||
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
|
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
|
||||||
@@ -397,6 +400,16 @@ cmd_regenerate_all() {
|
|||||||
echo
|
echo
|
||||||
echo "Regenerating fresh keys for every current flake target..."
|
echo "Regenerating fresh keys for every current flake target..."
|
||||||
cmd_all
|
cmd_all
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Commit and push .sops.yaml + secrets/ so the remote flake is current."
|
||||||
|
echo " 2. Push the new host key to each already-running managed host:"
|
||||||
|
echo " scripts/secrets/push-host-keys.sh --all"
|
||||||
|
echo " (this also prompts to commit/push if step 1 wasn't done yet)"
|
||||||
|
echo " 3. Run nixos-rebuild switch on each updated host."
|
||||||
|
echo " 4. For hosts not yet deployed, rebuild the install image (see"
|
||||||
|
echo " docs/auto-installer.md)."
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
|||||||
+204
-69
@@ -1,126 +1,261 @@
|
|||||||
root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str]
|
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]
|
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:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQUFhWVFVlVlBnNE5FTnMz
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwU3A1MFJvZFJkL2d3UzNv
|
||||||
VkxkTmxpRXlzZ3pSNTVZWFUrSllsYWo0alRnCkJSc25TYktSTFFJdkQydHcxOUlj
|
eUF5eFhkUGIyVVpjSEovaUZKdUlVeWxncEdjCkVOQjA0Q1FtbElFdTB2Rkp5aWl6
|
||||||
ajhQU1ZIb1lodEpHTnVhQjJ6WEthaDQKLS0tIDJCY1E2UVBaU3BoMzhXUXlIdnMv
|
THdmcXVzcnUxeVZ5VUdFMmVKa0tXSzAKLS0tIG1ZcFZwUEoxRGcwakIyRVY3dzNT
|
||||||
djZTcE1rcWNTOXFPMmFDYTVoRGo4ZTQKYy8g6pqP3VpTKDIBPbnC8NzCdDvOCKnL
|
VjlMTFdXYVVaeUpzTnNXMVhwalQybnMKjEHhkLMCGy3Q5mHPg7WSp7IBncKw6cCV
|
||||||
14kSrKmKlzefTrbkVyriz2Jdl2s0F374yfQQFreZ3m4AffSACCxziQ==
|
1WD9H47oGdW0BgBdi4rK4H7fmashw6TxjHZ1JfuCKps4mvvVWb2llg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVQnpVWC9wcnIreG9GeE9U
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRM3A0b0Jub04wcFZWelRo
|
||||||
UWhuRytkc2Flc0hyQm5yMjZnelNwaXhlWWc4CjBkWnd4cHNRRXQ0UXFkZGp4QlR1
|
L1RyZmRNS2pTV1FPVXNEbVF1SncxTXFvWnhVClRjSFVtOVIrVVd6VnoxR3NvSEpY
|
||||||
eW5NNnE1WFhnb054M1pac2ZidFg4Y3MKLS0tIFdTNmk2V1l2WC9rUk8yd0ZnOEJS
|
Y2lJU01lbWlUcTVraEFiNjJTeUlOdXcKLS0tIDliMElZTElVL0VNNVBseUVhQy9u
|
||||||
VkNnejVGVUZPZkorQkltVEplN2FmdTAKRY7DPP5HeFQntn2f/fXLjU6M1V6iug86
|
YnFmM29mUFMxUXhYS1R3ZnpEZmJtSlkKWoXICCvylDiMJ/YWR8oRgIZlSYb9Ubkl
|
||||||
BD09PI+T2DbIBQPotRZisw8IzHu9gY/O3+h0TccyIsXjI9wy/XPCAQ==
|
H6BeOPVs1qegS3Hav6vJrd+OMv1rd8rxWw5B5MYhNUEG81cdX5fgkA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkczFSTVhxWHlIWjhRcWlV
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiZS9ibXZ5VzF4UEcyQmhE
|
||||||
V2JPQXd5Wnk5R3NwWC81T3Z0MW4vYnd5S1ZZClV1NlU1Tzd6UkxPQ2M4MmhLV01G
|
N1JrOFNsMUgxbC8zRmQ4MWlxbVN1Ny9JTEhrCkM1Zk1IK2JNMnlFVjNPNzBaOVNz
|
||||||
d3VIb0RhR1RiNTZqNjlQcmg2YjdPeGsKLS0tIDQ1RTFTWGN4MnEvWkRUR3VnN204
|
K0lodkVBSnhSQkFvSm5TUmVCaFFRZ3cKLS0tICtaSE5Udnp5WTNIemI2MVJWNVpq
|
||||||
WVdFOXdmNC9FVFhBSGNEUUgyYWpYYzAKfdpeaFL/RrIbqpD9hNj8L7UxpmiBjE2I
|
Nm1XOTRHbVdYczBZTDQ4a3hwRVV3MGsKmOmKG4ixQhLu+TO6bsdtD1Iwnzj45o73
|
||||||
go/dR2E1LLXsDPtnSuJb2EZYoFvSsjsIQQQDt+YwRv0fplRtssKdxQ==
|
M5cWNcPlKa4cF7JfzHMl7jzvjTzHN9NY1KjBS60EABTzvKGqYVfJng==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TjhmOTJ0bUpMQWowb0hB
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQQUh2YzJ1MTViYkNRSlFJ
|
||||||
T3Era1loU1pMdmkxdnAvRkViekpqZWZjaWgwCkFRQXhFUy9PRVBma2JMUDhqY2F1
|
OVM5cy8zcldvRHlmY2pIL0pPeWVDQitqcFVNCnhWWjYycGNaRzliZGxQUGdOUlEx
|
||||||
VVFDRFNVbWpNaEczY1JVQUMyck9XdEkKLS0tIGpxc0tGdVFKK3FteVJKM1Fxa2ky
|
Q1Y3bWplZjc3WHdYUFpOUWQ0NkZOTEEKLS0tIFB5b0pOQVVWM3pRelR0SmlwS1FW
|
||||||
a21WLy9qV05hUURCTVBvcVh3cE45Z3cKXCYfXSjhApBoLbHDu2OOd57Y1zN54yy+
|
dnUxTklVa0N3RUl3bUIxRkU2Y3FtNncK7v0Q/fyygDAUr5aSgmillRb0AEE2+JHE
|
||||||
WDQvz8PpMxhc1nU5Kw/cI+WmL1KvN0qQZfOx/7D4W+dy/ZDWX27TpA==
|
P+ae0gqe2L/i3h01XdiqxAflVXzCF8rhFIG6jf+XCmxQx0eDUSzbLA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZK0FYQnBHMHZ6dlpMYTlC
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhRzVrSDQzWGNoL21GU1FC
|
||||||
WFdOWDFkRVBuY1pmdTFiUndLV3JXcndZa3pNCnJsd0tHN0FveWV6UUNQSEdpdWw5
|
Z3ByeSsrRU42MWI1WVBjOXNFQ1RyOUluYTN3CkxLQjFBcXFYVS8wTFhkakZDSkZB
|
||||||
dWZITkxWelNIRlpKS1pnN0ZmVlQvZjAKLS0tIEUwMXdtNFdkUWdIRjlxc0owdTRr
|
MmZRS1BCNi9Kdy9wa1FRYk9RT0V5TTQKLS0tIGpTV0I4UGZXY3BNYUNZdkxzUFYv
|
||||||
c1o1TmptWWd1ZGxzcWJJNzJ0K25PTTAKoos5rnkyQBCm+ZuhCCaMJwqJBo1fpnsl
|
ZEN2L1F0ZXAyb2gwWTRNZDRwbjViMlEKUScRb9D360Ns/ofOn8voUNiJwzJxt7k8
|
||||||
G74wu5vbTBG4VjVhI5KqyiuiTRU4jPcGxysECqe7AyZUBGp7ndewgw==
|
9hwsBIiCDvNQtw5tlgEirSD0z5cZJ+6iWiyZbh1FJmFLfmlu8Y43sQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1jy444f9d9stygj4p3w9kh54cqcfr654tvr75tdvee5cxsgtdtc9q3v60ep
|
recipient: age1jy444f9d9stygj4p3w9kh54cqcfr654tvr75tdvee5cxsgtdtc9q3v60ep
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTVkVXaCtGTXVkRXRZMFZ1
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1OTNFVTdHNVlBY1dDVkFH
|
||||||
ZUdXM1hRcDhaSURqcGh2eksxTUNCckk5SVJjCnhkSDRKdDFSckxUWXd4SmVxWG5p
|
ZU4xaVlWTkNzcjFKaDBZczU3dTFrSkd1TFJ3CjVxTXpoWXNwemEyU3hhWjZqeVl2
|
||||||
bU5OV0hzaWR6VDFwcDY2WlY4WnN3cFEKLS0tIElOVzRCcXR4U0dhajJySUhaZGps
|
cEZrMWh1V3IxWnFLVE0zR1dkY0o1alEKLS0tIEhwM3ZyNHFIbU5OYzVkYnlkT204
|
||||||
MW9rQk1JVDFWRnFxVzhCUkRIS09EamsK1rVidD48PqwlEWQyjF7iQWU7aBdPqQHy
|
RG9ocGd6NWdoRHdVanJPcWFNaStFTFEKFhmCp5Dpxev/ltZaNq4MC5JcTcKJnjUD
|
||||||
z5LaSi3LvJX3rNE/+q0E8/gbZyjGpbEn3AUI5mBF64GY3IZkRxZSXQ==
|
Nqby+fqJTcu9WFGqNHHra+I0+ROSAeE9ohhfqOZZg+6HNgzdjqmCEw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvbnJCWU9UMXJXMG92b3hm
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqMUptazlMYjNwekVXRG5C
|
||||||
WWNBTFpQamVWQThITmt5QUVwR3h2OHI2SlVRCjcxOEJTVkFjN0NhamFZQ1plK29w
|
Rk5YWE9tTm5XVzFOVitjNFEzc1lrRmNqZlVFCmk5R2M3OWZ5SlBoV1YrdkVVbXpQ
|
||||||
dk5XYkYxOXQ0YkVzcVc3VnhCQWlsV0UKLS0tIHZ5cWtFZUhDKzZkOE1BK2Y5TStR
|
dU1jTTNVWmdYRGtUczBaVmpQMDdpLzAKLS0tIGVMTFRmdUlySTk5WkE2M1VDQXZm
|
||||||
SFlDRjE4ZHpiVEJOQk5TUGNEN1B4amMKUCJ8CL8QpmRpFs83HD9TUn7NrPguuP8S
|
c0xHUXVCdFJrVFA3M0dHdUROdE50QTgKq5nhd/jlJkMIPCjYsuiMgY4VsW68zEf3
|
||||||
JQH/bzPorXTXJuyOKuKAZq1hK8BmiMUFksaZ03yN6YaFVIOeelEEMg==
|
qADcbbuYWKfQi/lz92tgMP+d+3GoRjVBIQ2my0giZD8c7ZhibXSLVw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxWHV0S2dxeHZEMDNRSkhO
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4aklLVk5XaUl6dHVsNC9J
|
||||||
ZFJHaiszd0tzYzlzd3gvbXNSQmlMVlJUNGcwCnVrM29MdFZCR1NBYnpkQ1k5VFZQ
|
YUorZGlDTWs4aFIzK0o1dXJSeElLZWpvcWtrCkpOWmFuOUZSUUtNNHZ2dnpENG5v
|
||||||
b3Z2Q3ZGekVQZkZKWGlka3NDOHJ0R1EKLS0tIDlXTmNzUk0wVXo0UWhkd0ZvK3FI
|
Q1MwbVN0bXl1bDA4b2hwNzlBRnFEY1kKLS0tIFZPRHhyYTRRSG15ZU1uSXIyZmVC
|
||||||
UzJxU3RkdWs4aTZYVVkrS056bTN1ek0KgKJNz8GvynX5pK33aW9x3v6yr2Ox0LCT
|
eXA5c2FpY1BWamk3dzNXMUtteE91OWsKV+yTdt1MW/7FUxy64abvwjN4OQPaw81l
|
||||||
GGrt+ddbKLcwpBpYjfWkFhffO330EKui73S+c/qMf8N9j6wzalOTpQ==
|
NsOLGSMbZHbQFrqyW1aXTfqYkN1wSuH5rYSZwBL+8gRLBEkFwQtfGw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3ZWJlbDl1TEkvWEpBMC9Y
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwbmpEN0lrNitUcEdyNjFT
|
||||||
ejY1MnFMUExBeDhITndxNy9YUi9hU0tXM1JJCjcwNEppcDdxYzlCSVMrMExWa3A4
|
ZUFCUXA2STFTaTFxUXdrWTRuOG51cThTZ2dnCmhadmpNOTJKS0ZvNmFRdkVLWTJ4
|
||||||
cmFPSjV3LzkyMXZCUDU2QmtHRmpHRmsKLS0tIFFTdXBOaDJJTERseXlGbmdrQzhD
|
ZUpwNGFxMGp2eDdKdjlNTmVCVnc2MncKLS0tIDY3eUZ3SmZwTXhMYkZqOWVTb2Vy
|
||||||
TWtnRFdIRXpsNkY0U1BiczNsdUk1V1kKGpndKmT8kj/oIxQuxQALfzscw+CsVmnj
|
WCtxb2toY0UwRmxSemZza2VDOEk0U0EKzk4+Ik7eGUyot4/pCLV7rMY+aNpHFvM1
|
||||||
cyPC3bF+tG6LcqqoKLjPSJfcIgzhnX7cAr/wwESavemLn8L/zQMe4w==
|
jlShEeqOrPiU1VQ0t4nWSI5E00rpZv+cxs/o1ii9RSUEi7/3VDgHtQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
recipient: age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpQVVyY3UrSHFWSmpDdmRE
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJdHBScXYyZEg4Ky9qVUZn
|
||||||
dlh5akFqVFdDVFNMSHE1eVJnZzR6YzFHSVFRCko4UE9EdXNxZzF2MW5PTTN6dEdU
|
RWpMd3VPVEp0cUc2OUJmWHZwOW1DL3ByVXo0CkNWcldRNVpnVll5eVJTV3FBMTVC
|
||||||
ZHM1MGowcVB2Y1ZlOTVHdnNtY3diM2cKLS0tIDBYSmh5dVVPaTM3d0ErcC8wMDNB
|
WUJ4a3RZRUFuNFB6b01RVWJLNTZZc3cKLS0tIEF0ckhmWGZ0Y25jcnUyajNxZkMy
|
||||||
eUpHWnZlYnJsbHZuS3pwbG15UGtwN2MKVPQA1MpjIfYAsNacoAbpvZNuAIkvx7ER
|
akEyWjUrM3RUOGZOZDgxSGlIT2VrejAKXjLTsZ/Qf+77sA2d8kxNublGsCJNiNUa
|
||||||
CvWBKEHUVm6m8905BXzv8MdGTAk0EyCIP3aMmYqTIYfv2k9pP0T08A==
|
voxDqyXEYAP9JHk7LZr2iNz2j6lnrJbB80sSerZJAhok8A5cZ/RWsw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1fxxzpnfse8nd9wz78ht3m0plrmraacf4cpga0pe8fm2tdnqcgy8q7qsyvp
|
recipient: age1fxxzpnfse8nd9wz78ht3m0plrmraacf4cpga0pe8fm2tdnqcgy8q7qsyvp
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0MTQ4MzN0bDJNL1l1bmpX
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIUjFYQTIrUjFEUytDZDJj
|
||||||
ekxDUXRNa0JHWWltZFNGTVltTFdSSE82SVZvCmdUWUdja3JIajMzY09IMUE5elox
|
eGU2UlBRV3NobkdyNy9rdjkvWDhVVi84OXlFCjlXSXBmVHljMTJwa25Zc0dIdlFD
|
||||||
MDdEakFJTmtkRWF2R1BGNkQ4U3grNWsKLS0tIE9hZUhkVGI1ZEpzdDhRU21EZm91
|
RGRkaTI5UmI3ek1oTjZjc2M5UGlHYjAKLS0tIDF2QVpzajdObjhHZVV0dnJXMFZl
|
||||||
VnJNb1kyQ05MM0RJa1lLUEtjWWxkSTAKHVAKcGcWl6LncJALRBU9RKP7ot6C6GSE
|
VFlJT0M0VjBvSHlrVXoraitkd0IrTjQKU0/plWrjbN5BL782Hls0idImBUHEDg8o
|
||||||
1iZtj1SNX6wzEWrhOEnV37aQ8bKZj6u+Y/q6/vJ4qiBs78y/drdIzA==
|
9svRbtapGOqwEVuMoLhPGhJ+FjjHZxYIDVj1o3ZHzs7aVMgiODdAzQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNUEVWY2NpVEU4OTZPZTZR
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhRW8xNnNEalNmSmtvaUJM
|
||||||
UzdKcktpUGJOcnJ0aEhkQlhvUWdUYkV2SkdzCm51eGJVeHJMcVRRRld0dFRCYUxr
|
OUQzQ1M5RFQzNHBVc1NZSUJTekZ3azRwalg4Ck8xUm5HR3UrdUJaOGIyait4ZlFk
|
||||||
TTN2WEhOVjRqV0FtQXowZWNTbkJneEUKLS0tIFpUazZpTUNWZUZBSFE0VDZZbkJu
|
RFFXTlY1SlZncy9VQnR1bTVWR3ZZcmsKLS0tIHM2K0oxMWNld3NQSHJhb1ZneWxL
|
||||||
SFVlUVhySnNqUENYOG9qUm5ZMDc1ZW8Kv0lY5dhnCEheM0sttfr4p7IL+EVog16T
|
d0J1NEhlMXRnMlZMRkdEMFhwUi9vVkUKgI1e28oU9zTUzGpiZSQUT1k4HVTtPeeV
|
||||||
OapUdbuXL2l7t7URzHnvfG/nbOtJIjH8a0XFsWyJChtNXpF2d/vf2g==
|
aqVMxoomQd9Q8Hg9NFmMSqXvbAJm5NC7NVdwUzI5WrfUIGe01a1bFA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
recipient: age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvTzc2aExGTHFrc0ZtL3pq
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCSVZkRHVWOWRUSU04cEdL
|
||||||
YlZLbzd3MVZHZTB0VUQvSXZ4NGVsZk42c2hBCk1vbTk0Tnl3b01vbVZaMkJ5aldE
|
VG82aXZhTzlaSlZaME9uRDJiUE5ORmlxN244ClVsUlRDVlVhQ0FyaldRQkRLejRY
|
||||||
WmZBMGFjb2pjQXpYcnBxWmp0UUsrdXcKLS0tIG16SG9JbFdkbmVidCsxUnpBR3V3
|
dHpicE1xektRWXdELzRkZXFwOVJJbUEKLS0tIDRDRXQxbFRJMWFIckIyVnI4ZXRY
|
||||||
RzNOY3hIRWk4UXh6N3NrcjNSU3ZwWTgKaExY4U2s8E6ojljJ+4TU+YJhcLXyuVA1
|
OE85QjZOM2R6SnBjRnlYNlZsUFVUZ2cKpdpIA9FO6aX8nSxup6OSYBRGmVAw7uUA
|
||||||
ROB70jQCjFvQOeo6thjQohSSUoPKhxSl1/nr4ZiGBO3/VskzihckKg==
|
yYwMaRZTd+d/1Xec9WJI03e8oAb0u4WMbok1G3eI1FQJYU22XQ0fDg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
recipient: age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
||||||
lastmodified: "2026-07-19T02:30:40Z"
|
- enc: |
|
||||||
mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str]
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKS0hjSStKdW0walhuVDlQ
|
||||||
|
bVBkL21sVEJ5VGlEcDhsOS9IU2VOR3pmdVFZCmhjSFJac045cWNhV0IwNGRVMGxE
|
||||||
|
QS9KUHVIZGFQVVZwcFJmTXRSc2VadkUKLS0tIGlXUUZoZ1Q0K1pvRk9HRnM3SC9t
|
||||||
|
aUc3ME9IMmVteiswUzQ4dWRYTW1ZYVUKogiJ89uSOoeOVklAIox4qzsvNaOhqTIn
|
||||||
|
DFVVP+nUZuYyqvIhPStAt/O9UpZiBKsTi7rNuTCfzDF5yHMVqQRMtg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1ehkswwz2pqaz4svzh7ela5tdnssl8kn6d4vwwxd6zwg8exfpd43syyrrjp
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLUGxMZnQzN3JJY2ZENy9T
|
||||||
|
WGlZQ1htZHRjV1E0YVp6ZE9rdHZKS3AwY2p3Ck12eGF5bEkzc2IxQzFjRFVnZTRi
|
||||||
|
YzZLTlQ2dk9PVjA4ZHRpNTJ4V2U0Q3cKLS0tIFBVVG5VLzdXRjY1THN3QlJMbDNM
|
||||||
|
bzd5eERLbVVVd1dYOVdVWEFLTWdVbTgK5GAY4iM1+xMBarDrvd/I5VPZxvJJREBd
|
||||||
|
pB8G7TpkwapsXeLguoSHy8rgc9smMlb0QhpARtyoBJv2bDr0nQRUcg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1gl5ujmhd2pe37acd0eaar8tmnf2na28n8l28s8xct40vqpxvsdwqf49g4l
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0c0V0bTMzOGdmbnhUenJP
|
||||||
|
ZXZDVGh5R1pxRDBlQTlqeEpjWFE2Z2FodlFBCklxNTZtc1M2MVNRZ3p4TE5LcU5m
|
||||||
|
UzlEZkxwTU91VkJ5U2pkbHppSmNuY1kKLS0tIEFvVzl2bHNvVFhKSTJjU0c2YjZ6
|
||||||
|
UCtjRytHY2J2amdaRHlyYXJDM1JLWTAKX2hb4vJQ43uwCoSOqaofoIUCnWuDeF5t
|
||||||
|
ir8fkAjpvFFuCTzi5AbajCkncyFXVF7tRTFzWSoj/73FdT3yQ1696w==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1eu65wsmez68gegnufl0gqrs0e6w2409mypjlajlq383d7l2e3pjqy5kn6v
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4VjVuUDZvYThhelBRWXph
|
||||||
|
QmZZZkw4MFpmNWt4b0pEa1daM2NnQ0ljUXlvCmZDbkp1d1EwNDYrWXI4cDYxME96
|
||||||
|
OUJpR0VpM1BkMk1TRTIwOEJDaUxieDgKLS0tIHRmSlFBQWl4VEtVNm8wRGZtanFv
|
||||||
|
RkhyNWppNC9WZHQyajZra0lrYlUzdUEK4GRvBd6bxV5LTRMvoOXrYJJMHKUpX3Bh
|
||||||
|
oSmmcFkphujOhnZ839jt4XnjdYoTfQ1b/M380Sijn9xhOrHfTWqinA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age17pwyghxr6lq06fw46gwqzhc9ut4paz28rpwx5pmv3cxwak6rgyjsw7lk3w
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByT1pPU2RGeEJ2RHFnQ2Jk
|
||||||
|
WnRISFk4bGRVVzNxOHZKQ3A3dlA1Rno4WTNzCjlsVXRmWk1ZbVkxaTdPTkNjZWVv
|
||||||
|
QUFLcTlsOW9yZkpic0VGdWtUVHJYWUkKLS0tIFFQTlg5ODBIMG43cTgycGpGQnZx
|
||||||
|
VEJzaWtvN2NNSHFwSkQyRC8wSGJqcHMKH+UR0NyxjXoWLR6NmiGWphSrl2CgGL3Z
|
||||||
|
KuIZPGGKFELe79AlH6bOxkcJ7BcYBfVKRFrLzYLfV283DteAfxGMSw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1jukmg69cqxnjd0lp5f534jhqe65rxew2hufcmyjxa3rkw6ayef6s2ylcmy
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsNHdueS9XN20wRzMvQnoz
|
||||||
|
NUlKeThWSUMxQ2c2ampxbzEvQS9XSWMrdFN3Ckk4YUtVTXdzTmg3QUxjTXhubHA4
|
||||||
|
azVsQ21yajlteVBjUHBGanYzU2xramsKLS0tIHhBMFNvZnJRc2ROd29vZ2VmdWlI
|
||||||
|
UStoVitWNjUvYUpkRTc4VFkrV0FOSUkK48TGNM55gQ+MKV8YGIYclv/eMz0nAZ6R
|
||||||
|
ZiY0XSP/2XE06Y2f3NKBtEauCRGqgwPFt74ITjFOAwitsqIhD0N2Uw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1tzsrtwd3p3lrr9g7nv3z5nvmzsz54t2uc6tfqwutp6usmav83s2sck25cc
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYcUxHbFljWEdBdzBsdklB
|
||||||
|
VE1IVVNoWFV3YWUzd2RON2ladVVxUThHMXgwClpXVHZUbDV3SHJlSUlTVXhONDQr
|
||||||
|
aG11YXFWWk11UVRnUG1MOE5FUWRKejQKLS0tIDl2V2M4RXNGTWtIVkgxem56UnZK
|
||||||
|
RHZXRUZESDFPRzFhUGhJKzlvODJiMjgKN793k0tnuFcfs9PSgdmDXefZ1euAEfC6
|
||||||
|
DMJ4sXJgwFGX459k0sG8f64eaZrQasg//tcz07J/gRx2D3VyaC6a5g==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1rf4kj99wuq59k7w8ar326djmgmpl9hcwlnuag07f8gauq8c3y5mqne87s4
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrejFCSG5VcFgzb1VwNXNL
|
||||||
|
VnFmVHdaNk1UcnlkRFNscVJ5RFJSbXQrL2tzCllmZ0VVZUpSLyt5UEd6aFNQcnhl
|
||||||
|
QnNqT1o0Y0pIQWtyVC9OMmtOOXFQK0kKLS0tIGdvV0M0dHc0Rnd6RkJ6KzlOSEg1
|
||||||
|
YkM3Y1lpNjdBdUxLWlMzOHFwTGJ3bVUK92lLtBdwmLeRD/I+v2h7+InJA71fr6vE
|
||||||
|
5+BDHs8LAAIgu37QsQKUBKVsEp/elgm73+PQkWgW0/lbHnhevIE3NQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1d0zhx7u3mfs3nktl67npey87cze4dwsfvfvgaje4rh3gwv500yssckcf6u
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaR01RUDJhOHRtMldiMWxB
|
||||||
|
U2tSdTExWndFUU1KUmc0M0Ivb0lFRm1NckE0CmZTWERSMVl1SGtENC9keHZGQWMz
|
||||||
|
bWJnSEdCRXFiMkxESEcwMmtaaXZ5UVEKLS0tIDdsdTVseEFmVE0vbTJtVWVnUkQy
|
||||||
|
a0FuOUdvaUc5ZCtkZHBmRGFiNXEySjAK6W065nWaOSAs5HFfKRh8Dcbs4VEClXXK
|
||||||
|
TyKkVcBMxsB3axT7wE7uLi3soHWqaI/tKZceqXfrDuh3m+jNHqglYQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1jc6wx33hdhgwhk6nzy5rr8fkgmqxk9um639tk5h632nqfyaw8czskdptj9
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKQUQ4UW1GcjNDQ3BDUjVr
|
||||||
|
QzZ6L2FjN3QwZzlNMTRab1prQmZHRmNZLzM0ClZER3FwcmJwc1dhbzRWYUdONFRr
|
||||||
|
YlZRbE9CRk41UWR5emVtY204eFRxRlEKLS0tIGFoblJ1RWtuN0d1Q0pqWVZyRStZ
|
||||||
|
eGg0T0tHN0llQnl0cUw2blB4OENGRm8KHvfSTxceDjVizRzXZ/5GH5NRYzK7AH1K
|
||||||
|
AP6/JxOP2m+wXqI2xa6kna4k0xaUOZTEo3al59UM5yTcZGlBj87Y/A==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1pgykvq4pmxhhjrqupcp99fyad2uht40pt79dkzg66cfvsjy5apjqls8u68
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwMUIrTWx2QWtuOEFiQjhs
|
||||||
|
SFlLZVlJM2VTTzJhOWVWbmNkS21MbFZyWWp3Ckp4Rms3djdMWnhKVHh2aEJJN1Zw
|
||||||
|
N0ZhamZMYlV2djQ0ZGovNGFmVVJNb2cKLS0tIEQwcUlqM1NweWxuWHZBZE9sc21V
|
||||||
|
ajlqSVVEcUpvUkR0ZFQ3NmNYSjJ1b2MKc6Hj9JU+jNYNzGzzGyIRZjRVGolowl0g
|
||||||
|
2pXB3VEVTfuAzDTH0hXiKSw8AgJdmNzR0QziXRwgoRIlZqSFKiykow==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age19ht95nv8uhz2shjmakeut8mc3l5spvrcxs3thhe85an7u02r6sysyv457n
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRZkREQ3RTbDEvSm83S0tV
|
||||||
|
UXcydk5kaDhKdHN3UzZJZ3FlUDRLTkQxSEFNCkcyblo5N0U2Zm4rSW1KUloxUHFY
|
||||||
|
VlFNbDJKc2dzTEdjYU0zYXo4NW5OWGMKLS0tIFo1MDBDSlJzUVg3TzdqTU1HeVQ3
|
||||||
|
VVNhMnZ3bC9jQXpRR09wOGdjLzMyK2sKwBokd7Jvo2Vohzipi8vJPsbxoVWoI/MQ
|
||||||
|
p2K/TpeWO44A33llWVGevwo8QvGu6zR1ANgUo5b3loj553hR6l6qJw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1mhaze5tgvc9lwjpml6dnp3xc292337wgm6376yh6tq4fn492ndjq9h23dq
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3QzBuMWE0VGdTMCtIYjZZ
|
||||||
|
SEo4OWN3ZldnenNFWCs2a0VXN21CK21XWlE4ClpzWFM4dllROGJsYXRTSGhFQjFi
|
||||||
|
Z3JpYlBCMmh5bjJQekUycWxSanFoaTQKLS0tIGdTWDdRdU1jS0VablNSYWVjdCtm
|
||||||
|
K3E0Szc3Rko2aFM2b2hzR3hCcDUrS3MKk09b4/pWkp2FqQRbB/Ggf//DN2mfWHxt
|
||||||
|
ofR0g7o9hg19a9/t34BD1eBfoTtIWfrWTAsZsJ/4+9u3YhFfubeGDA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age15me6sx0f8r58xh9v7aqrj6e99n7eu555jkpw422txpkqt4r02v0qpahlyq
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQNU5Va3VHazNyNElWUVU2
|
||||||
|
Q3NCbzVucDVsNkVSMDIyZFhuYjluR0xNV25zClBTMkdlaDN1c2pOa04wVWI1NDNk
|
||||||
|
RHZBK0dNWG40TkhxM1NQOHBnektJOGcKLS0tIEp1WStuSDMxejVIeTVFYzFNZ0gx
|
||||||
|
Y2tUdHdEb3MySm5MT3k1YjNvQnpkaDQKroAPWf5iUKvdK3dHdwBtJT/jQjgi0kCe
|
||||||
|
6Yuk1ZKlS+6Scf0UkIG/NR0wvebM6O+SvNIcngz/3EVAJyU5mAgwIw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1q3hu6eh3mt4saey7dc3yu04s7knnk2ygpm9xt6mg6rqtem6l0uyq4wzul5
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJdFNBS2Z2SE9pQUliczFB
|
||||||
|
NExlWFp4ZXAxL0FabWQycVJ1eG1XRTB5RUR3CllFRFhVQjgycGxxdCtmQ21oRzly
|
||||||
|
T2FNSEd4R3R3MFRkYzYxSndWcGxNVmMKLS0tIGpvMExFeVFHZ2FMckoxYUhSWGVp
|
||||||
|
ME4xK2VSb0ZSUDBvUFA4Qm5ZeXF6bVEKLdktQF5NQUipJ7X0+d8KJiIC17i60FST
|
||||||
|
GYg9NR4NKk9m1fxs4UpZD26tzTtPF0MzCJjrzu2/bt2sx6sbOm/2cg==
|
||||||
|
-----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
|
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
|
||||||
+33
-15
@@ -4,31 +4,49 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPWE1HTUhiSUp5ZEUwWEpI
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYcFFSVFVxOWp2dCtEVzg4
|
||||||
bGpkZlBIMUo5ZlYrQ09SN3Q1a0ZkQ0ZnOEhVCnJPNEZQenVWWGZiODlzQzNEc1Zq
|
VFF3Mk94Q0xVSDZNL1lBSXBKNzVXaU05TnlzCnhueTd1ek1BZ1JpZUMwM3ZBUHFh
|
||||||
c3l4OWZJTElJc2Y2UE15OGtEUzhyY1EKLS0tIHNJUStyWnlQWjZBbEZjQ3UwdUpz
|
UVFKZjduZUpUejJ0enBXK0pwMjZKSDQKLS0tIHJ1Mi9OQThrWndiWFE3WTRJREJN
|
||||||
ZndoUDR6bisrNGJCUHk3TGI4bTZaMFUK87fFsm9ne9s+PK2pcwtrDjqyGBss2r2E
|
Z2E5WTI5dzB3WDFtaFRoUERZa3N6TDQKy2FK57wDNLSPl2/L/FUBUP8rJZuAOqAR
|
||||||
8lhqoeiKZ2j96z8kP/7ChzovwTCmqdcmAQuyNQD+ZAFijseipSvfbQ==
|
I0hBe4epAKL28PlJ4M9P77qqg893beZK3UIJQ5O6d5fGPMNTJiIzAg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUYi9SRFFGV3Z6cFd2Znk5
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoaEZ5clpxV0ZQWmhSckxY
|
||||||
b2FLbWtzTllJMDBUaGk0NTViOTNBa2hQclVZClZHKzNhbGVjQUJhWkFWdTFBMG5a
|
a3I5djhISUphMmV1RU9DREJsZFpDdktyaUJzClBQSUNvN054aXp4SzNkMmNqNVhP
|
||||||
cUFJdUdyVG5HQXJRRnJId3hqRTN2cXMKLS0tIEFMRjh3WE1ON0U2TTNTZ3hxMTR4
|
QUdnUEppMmdENzUraHAyRVJLWm95V00KLS0tIHZuVXh4SkdhVU05YzV6VlI2c2RS
|
||||||
ZGRlemlIbDZKeExmVHROc3Eyak5DdzQKaLwIVDi6BN4cxpVxJoqTYvJETPOp4thc
|
SWttRW9nS29MM0lJT2x0QS9rVU4yb2cKYZ1gts1qRFdod8PFWl6Tg0Ry0sXhKMrP
|
||||||
l9uVMvIGuEsEZgDsvShw1dYLljd+uGy/A+dXbcxIUCP/mmPkwmd1Pw==
|
VwttGQj0iDg6UudB+qa2ngFgAc0C++OJebuLMFnK1K+PvUpfw2nRQg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArOWovSW9DeFpxL0VDUDQ3
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKNXBOeS9od1lBd0VGSDRX
|
||||||
SHUwTzJVZUtPV01ZRkdCUXZGL2lTRCtCNFNnCjBSNExqRW5mTEN5SFVucHJHSzZt
|
RE5BM3g0Zk53ZzBhdW9XUFhscmpHdEV1aG1vCnU0OGZZdENBdHd0Tzd5b05BR3RR
|
||||||
cDlNc3BjY3M1c1k1Z2tkVEg4R1pacGsKLS0tIEFWbHNKZW0vbVh1Y2VhQW93OUwx
|
ZGNZRlVSVk41YzgyRnlHK1NUaXhqZVUKLS0tIGFKcWF3VGdibldnMndmSis5Skc5
|
||||||
MWV0eW9sOXdQd0l2ZjlWOEVVc1dwcTgK2s4p9xoNkawH2OkGsl80bNIo3ad5vn4W
|
aEZJVGh1UDFVUldrRndVZURGbVJMUXMKia/z+zwpfcaILCt4fpy7wsR0QkFKf07d
|
||||||
Z2w+jwppSoUmbQnD3WFbLmSSxmuobmU8HILwElv6SZu+KE3aspF6XA==
|
QwkV+gEfM3EydhcA7invDieIKbcOUoKKgOmf2sXMgW07CsjG1QXk6w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
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"
|
lastmodified: "2026-07-19T23:30:21Z"
|
||||||
mac: ENC[AES256_GCM,data:kLGE2xawQT7mx+sfw68hmGk5nCEGiEjZrqTEl9B1dtQmTrMwmoVr/1RISi4LfJrwxy31mDgff4lcIL4wIJuM373uk3X8j4RNyYQNTfKEkORT6r8NHeepNs267O77pKGd7OmcM4MT/BqOnB8ELS7Wlf2ect7CAlvUUVyc8icxgZE=,iv:EYLDsHYHZ1XOQXafOTqHHWpk/OBNq/R6IJnOBYV33E4=,tag:thxrCPC5oGvDjhK7Dz87YA==,type:str]
|
mac: ENC[AES256_GCM,data:kLGE2xawQT7mx+sfw68hmGk5nCEGiEjZrqTEl9B1dtQmTrMwmoVr/1RISi4LfJrwxy31mDgff4lcIL4wIJuM373uk3X8j4RNyYQNTfKEkORT6r8NHeepNs267O77pKGd7OmcM4MT/BqOnB8ELS7Wlf2ect7CAlvUUVyc8icxgZE=,iv:EYLDsHYHZ1XOQXafOTqHHWpk/OBNq/R6IJnOBYV33E4=,tag:thxrCPC5oGvDjhK7Dz87YA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
+38
-20
@@ -3,40 +3,58 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaYU9HR0lETDhkYXR5NWlj
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwbWFOSlB0ckJFc2hiQTlh
|
||||||
L0diSE5JQU9KWjRoU3hrVjdqZ2tPNUtOdFZvCjdUVGNFbDVYa3pVSC9ZWVNORER4
|
eEo1NzZSTWovRXNnTGJDWTVZRUsvbjQ2NGx3CithSDVSbFpCYW9KQjJndzR3aVlL
|
||||||
QWozUlhoSEtjTk9IRlM3VkZoYlc1RTAKLS0tIDlHS01WVWlOMWFEQU5GTVRLZVhV
|
MHBDRmQxSEpTZ1FsRXFWQlE2aFc1VGsKLS0tIDc5bmI4WWtvcStZVk5WNDhva1dk
|
||||||
VTcwekhrRHB3SlVYT2MzOW5GbE52dkkKKCWehPhpdGapdyzpll20NJUcZwvW/7X8
|
cEVKM2ZCTGdLM2NEbmRudk1SdDNzMTAKB6QCrxxNzazSbif28j6RNIfdfUTKxklM
|
||||||
KQ1EqAgI2fewnbwuIDYCleN0b0SLJNUeSV/tFKDDoTMnHWCdeD4ECg==
|
Xip5xoLjTRNOjq561OS34gaMA1GE27YQqsqtgiU15i7SZ7IGefWpZQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4YndybFBTQ2p4SGZ4SDJs
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2MDVmZGp0bEpwZWRLZFFM
|
||||||
a2p0eFRQOVVWcGd2a25ESW9ESGx1RWw0Zno4CnhpVVh3cGI5UjY0YmFINFFPMTh4
|
ckY1eERsSHFuMnhPK3J0eFRLY3lHYnlEK0NnCmNhQjlnUTNFOVF0UVBLendHYmdx
|
||||||
b1B5SjJ3NTNvUE1QUmJjVFozY1dYS2MKLS0tIC9HZGNpOFZhZGNFZGt5blJuZXVV
|
aXVIa2hmNENUNGdZL2FmbTlNekd5NUkKLS0tIDdwbmFGZVFlaG94UEgvOUNxdVlz
|
||||||
SXpkRzV4d2ppV3ZQZSt1dmxYNGVFMUEKmSe9dkrmkND81Hw2/ATAmFvcmhk1tUC1
|
VHJZenplYVVPa2NUSUFWeEN5bGQ1MkUKT5CqEiCN61aOpvMlBp0effS3bh345E7u
|
||||||
LxxBw54IVHUqwYKgRYUYRNu+pykDT5OnFDPiskd49Xso99LY87PyiA==
|
wfxrMwMhrEzfnmpkvcafLCn9PMicqLuz48J9ipF5IHLf7MIvbT31ow==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLd1FSSHVTSGdHWm8wMVQz
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPS05tWG41Z3BwREhxUVRY
|
||||||
d0dlOEsxeGwxdHU0eUlFSUxka0ZmcVpnOTM0CndYNUVjVy92QVhNY2orQmpSQjYy
|
ZVBMQjhxRDNwUDBjVE1jNnlVOWZtcE81Smk0CnlHaEl2OTd2UnJhWmlHNmY2V3NQ
|
||||||
VS9KM0NUTXhuM0lCSDBZMWtISEdtWTgKLS0tIExTL2wvS3FEdVViUmRYZEFsR3R0
|
SUJKakZOZkdXeFhnZTF6dmxzTGFzZjAKLS0tIFhwKy84Z0l3cEhSQ0NBbVJIKytM
|
||||||
YTJFM200RjF6MjNxOFA4eHRpWVhtRkUKOqBIT445HnPXrrH/qV6FIuAhAuJmSL6V
|
QzhxbTY0SkdkOWN5VDU2d0dTMzlGN00KtgobQ2hsilZnvoq/BesaM/RcYbVNHxSh
|
||||||
+PQopM/m3PAnK5m5Mu3cfjYfDiB8+GWTABhljfT+GbcoK7CqWLehrw==
|
vEA80nUhyEdRim8WP/yu5KrgHsEVQ9ryclU4B49F1pINdAVaGRo9LA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
recipient: age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvVXF4a2NmaW15R2VXS3FP
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5NUxUMHdrbzl0OEl2RXoy
|
||||||
SDVPUGpUWTlIWjl2N2N2SXR1UlRkdllWNVRJClZDbVo4dUhrZytqZkMrYWtpOEZx
|
S1ovL05lTXhSNXBXZEtWU2hsdXhBZHlqc21rCnY0a1Z4QXZnTnJyTDR0WVgyYk5m
|
||||||
c0dSZGViN04zQ1B1WEZEWm1QM1lsejQKLS0tIE0wa0k5Rm1xZmw3OHFESkVXc25j
|
cHJJZkhoM2lNaG51TmY3K0hZeXZKM1kKLS0tIGU2MmV1TTFTY3QyQVNEb2ZxYmlU
|
||||||
NlFpYTJSckQ4MVlZQ01reDlkaWY3TkEKsmQlreRhRAjVZ/q5x52FNATDF2sLhbHo
|
c1doTkVLZEhKeDJTWmtLWjIrclRUenMKRem1nKPK/ghuipzOLTf6V3Ke5LK5S6LW
|
||||||
djOZigZx2rs6shqQ6It/XRJ8CiPaXZBPOt529Gwmplu+hWlAU1+l2Q==
|
5ExYf7y++FC90IqkAqeIA67l2KihcPE0DqnBmwrDlNgL5tXyhYV39g==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
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"
|
lastmodified: "2026-07-19T02:30:40Z"
|
||||||
mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str]
|
mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
beszel-token: ENC[AES256_GCM,data:gjbT3uROiVKQOJaUeafTxjVknQO1Tvbyx/Pl2bTad7DezByX,iv:3ikf7OaT2omO8yd6G6UwYbaRBSzyvbn+NghxAe5bcgI=,tag:Zuc2EP8rUtdDhr5CzSW2Pw==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5QVJmWlBlNmFTQ2RUaWdI
|
||||||
|
amdKLzdRVVdxdW5ROU9CRldKanU5QXkzc3hZClBGRWp5ajNFWWcrdlJaM2hEK1B0
|
||||||
|
emllQ2JLSkZFMG03WjNvQkc3RThrUlkKLS0tIElnR0tZVTlvTk00OGhrZUorUGJo
|
||||||
|
RGYraGU1dUg4SGJGcnVqdklINTdNbHcKeAotrwsYxsE4ktOzUgEaIibRprGf7pXQ
|
||||||
|
5GttilLcmfQNBeaA8z870impTdj6EZfUVItwaIzhN0dG9TcK7oJBoA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHbFY4WVp6OVNydnJ4eFc4
|
||||||
|
OEFDTzBTSzlVQkh5Vko4Rk80VURHSjlqbzBBCnRJNTJZRUNPaGNVUm9FV0ptdjVD
|
||||||
|
d3RUV3l2VERUMXhZVGRXRmsyR05lQk0KLS0tIC9RM052SHRrVjdyZkRhMk04RWI4
|
||||||
|
UHVxYXp5N0NKaCtmZ291MjNNakFwb1kKB3RlWa0adUCgaIUxGaN8Uzwkak7EtmX2
|
||||||
|
rBnOkujIVWMy9KHPltZtfRAQrY7nS2njhK1hFfTSomqdoJMFgH/iCQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1gl5ujmhd2pe37acd0eaar8tmnf2na28n8l28s8xct40vqpxvsdwqf49g4l
|
||||||
|
lastmodified: "2026-07-23T23:32:57Z"
|
||||||
|
mac: ENC[AES256_GCM,data:l9a/yNRoxY1hvSkLuR4N7deeKue/1JPlSvZvJfCSNbQ21p1qR433BbSDYvfW+kXQXS8GVcfgXSd9ywNzgVvkA5lR1++uYsZBLbYxJ+s3TKWs6/yECAZ0eM1KBA0BEm7cLSsHTOwd+2WspvmCYir++FDO9XRuS3guiMnQBglDf/E=,iv:eE6GVqexQNSiLYfmTTUdUx5AO//wyjSIsr96xAX1pcI=,tag:CNsoyDZYLUt5Seu7W5wJrw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -45,6 +45,20 @@
|
|||||||
# the installer image's nixos/root users.
|
# 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";
|
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
|
# System
|
||||||
timeZone = "Australia/Brisbane";
|
timeZone = "Australia/Brisbane";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user