Archived
Compare commits
380
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e51168d1b | ||
|
|
ab5206b1c7 | ||
|
|
2041557ab3 | ||
|
|
2fd483697b | ||
|
|
89186b0dee | ||
|
|
8e3606cbd3 | ||
|
|
a18dfb0127 | ||
|
|
75f1342339 | ||
|
|
36ba99c9a1 | ||
|
|
0cd8f15b48 | ||
|
|
8613b93fa8 | ||
|
|
20f9475a7d | ||
|
|
6babb3eec5 | ||
|
|
33730e6ccf | ||
|
|
65f89806cb | ||
|
|
c3007097a6 | ||
|
|
9724babcea | ||
|
|
7055bcdb97 | ||
|
|
d973da487c | ||
|
|
274d54a774 | ||
|
|
ad274d99fb | ||
|
|
bd8d93d890 | ||
|
|
53b9a64826 | ||
|
|
0ba837817e | ||
|
|
80f86b086b | ||
|
|
a351cbcf80 | ||
|
|
2aa625d566 | ||
|
|
288835db29 | ||
|
|
559c538a3d | ||
|
|
feee2f1679 | ||
|
|
120240f14a | ||
|
|
b0ccbb1162 | ||
|
|
95d4db5609 | ||
|
|
6f8c6c8ef1 | ||
|
|
627aad8c29 | ||
|
|
745f4d6fb4 | ||
|
|
c5f8bb4d1d | ||
|
|
e337063a95 | ||
|
|
d8d14db505 | ||
|
|
be05c63a67 | ||
|
|
ff695c1917 | ||
|
|
fa2a9a595e | ||
|
|
a90c4909d5 | ||
|
|
649be34dcf | ||
|
|
3c3c5ae821 | ||
|
|
eadb1e35ce | ||
|
|
71d052e737 | ||
|
|
e76486efbe | ||
|
|
302c3b671f | ||
|
|
0900ff25ae | ||
|
|
79d2ed0314 | ||
|
|
8932e52d72 | ||
|
|
4822f0b290 | ||
|
|
f8de4c8abc | ||
|
|
ca18a9d29d | ||
|
|
7d113e302f | ||
|
|
bc68d8cf17 | ||
|
|
4728982c05 | ||
|
|
8734b23746 | ||
|
|
5f52cb0cd7 | ||
|
|
f4139eec13 | ||
|
|
afc57e3755 | ||
|
|
d7cad12720 | ||
|
|
bab83d1aad | ||
|
|
da0cd225be | ||
|
|
de4b4302c9 | ||
|
|
45fe770569 | ||
|
|
3797c8885e | ||
|
|
2fb6b1de00 | ||
|
|
a2f817b098 | ||
|
|
d9015e87d3 | ||
|
|
5dc41e2866 | ||
|
|
0aae9c3bcf | ||
|
|
2a426615cb | ||
|
|
7be80e7ab6 | ||
|
|
d8d2718289 | ||
|
|
e577fda538 | ||
|
|
ffd2705ee3 | ||
|
|
54e11853b5 | ||
|
|
5fcaa6ae87 | ||
|
|
a5ef1aa519 | ||
|
|
a919252a47 | ||
|
|
3fda26e46c | ||
|
|
d48e4d4f10 | ||
|
|
d5c13bc2d2 | ||
|
|
fe83640776 | ||
|
|
03066d716d | ||
|
|
bcf57246e0 | ||
|
|
9fb6d9da13 | ||
|
|
24dd5adfed | ||
|
|
489ed11406 | ||
|
|
f77a26402f | ||
|
|
9eb219360e | ||
|
|
a3fe69a89c | ||
|
|
aa7a98f884 | ||
|
|
af48b1aef6 | ||
|
|
33522a4613 | ||
|
|
217564ffc4 | ||
|
|
8e571c7592 | ||
|
|
7ea203e22d | ||
|
|
567e2536b1 | ||
|
|
f3c6f13e89 | ||
|
|
b9b3d06810 | ||
|
|
efa0ee26fe | ||
|
|
0c68f54e1f | ||
|
|
c9b449d60e | ||
|
|
a495de3f83 | ||
|
|
6cc79e0c36 | ||
|
|
4078230052 | ||
|
|
06dc25c983 | ||
|
|
aa2af2d169 | ||
|
|
1788faf093 | ||
|
|
33584d9a02 | ||
|
|
b25d9db82b | ||
|
|
94df02515d | ||
|
|
8b49deb1d7 | ||
|
|
fac343fa76 | ||
|
|
9f59ed4426 | ||
|
|
1f2dc03486 | ||
|
|
da75a23700 | ||
|
|
3d42dee0bf | ||
|
|
9fef163850 | ||
|
|
c64e4ad7d5 | ||
|
|
aeaa1f6479 | ||
|
|
bca8c2f9df | ||
|
|
153b2f0cfa | ||
|
|
c1ba633bff | ||
|
|
62edbd203d | ||
|
|
fa3d91303c | ||
|
|
59a01b35bf | ||
|
|
cf78588f00 | ||
|
|
7156c343bb | ||
|
|
32b65b0894 | ||
|
|
5046cae7b1 | ||
|
|
0286bb4dfa | ||
|
|
cf9bb14d4b | ||
|
|
6d53a62f85 | ||
|
|
4bf1ec450a | ||
|
|
8bafae6eb2 | ||
|
|
32323aee06 | ||
|
|
bd117a8dd5 | ||
|
|
12f6703e0f | ||
|
|
56bf912c4d | ||
|
|
0649c48b96 | ||
|
|
d871f2a45d | ||
|
|
74bdbd8c9a | ||
|
|
258d471b21 | ||
|
|
a86300f094 | ||
|
|
f8886b2215 | ||
|
|
e538222c24 | ||
|
|
004529aa88 | ||
|
|
35352cc9b2 | ||
|
|
70d459de15 | ||
|
|
60886c6a64 | ||
|
|
564f8548ec | ||
|
|
36c3c48474 | ||
|
|
c29d8f9df7 | ||
|
|
b82c868b7a | ||
|
|
6f6e9a5a11 | ||
|
|
1b588d0ea0 | ||
|
|
3212d80b63 | ||
|
|
e28d953b8d | ||
|
|
566135bbbb | ||
|
|
8af805241e | ||
|
|
d3c26d7e9e | ||
|
|
63f5acb18f | ||
|
|
074ed2cbe1 | ||
|
|
1cb6bdae2f | ||
|
|
bf74783623 | ||
|
|
c31606922e | ||
|
|
a2f2af77b6 | ||
|
|
7aa4c56660 | ||
|
|
100923e1a9 | ||
|
|
0e105923da | ||
|
|
2d1cd0674c | ||
|
|
abd0b83cb2 | ||
|
|
6ad9b5ffa4 | ||
|
|
88a9d4dd78 | ||
|
|
472c18922c | ||
|
|
33cd951205 | ||
|
|
903e5243a7 | ||
|
|
3c5081506b | ||
|
|
edb898d27b | ||
|
|
c32341b2d5 | ||
|
|
745bbe0732 | ||
|
|
48c5e5c094 | ||
|
|
287f42cd81 | ||
|
|
8434e90aec | ||
|
|
5141123b34 | ||
|
|
84488de426 | ||
|
|
ed2d86bead | ||
|
|
1b6ffd9117 | ||
|
|
bd9aef4624 | ||
|
|
16d345262d | ||
|
|
ee30e40ce4 | ||
|
|
79d24c7497 | ||
|
|
dcf1c8e780 | ||
|
|
27470bd854 | ||
|
|
72b58093b1 | ||
|
|
688904321a | ||
|
|
2fb0928ff5 | ||
|
|
d424a70110 | ||
|
|
60faf681b3 | ||
|
|
4fb1a6a204 | ||
|
|
a61f88d4a3 | ||
|
|
923ac6c178 | ||
|
|
cc986d1ab7 | ||
|
|
1fd0adee54 | ||
|
|
b0d480adb8 | ||
|
|
e78dd516a6 | ||
|
|
4e30618954 | ||
|
|
3259391451 | ||
|
|
31b57930c6 | ||
|
|
0e29abd0ac | ||
|
|
038103391d | ||
|
|
7f719eb6a9 | ||
|
|
47fdce6193 | ||
|
|
e68b4f8d14 | ||
|
|
c47052fc21 | ||
|
|
59a2e65bf5 | ||
|
|
4a928707f6 | ||
|
|
5be2bdb170 | ||
|
|
4abe01c0cb | ||
|
|
d838784dd5 | ||
|
|
e95a4b5f4d | ||
|
|
247d8fc96b | ||
|
|
5f63a48731 | ||
|
|
8d26f62b57 | ||
|
|
089190ef23 | ||
|
|
d36380b79a | ||
|
|
8dd20f6ced | ||
|
|
926df3d334 | ||
|
|
26eec71b8c | ||
|
|
554f484593 | ||
|
|
7de7c76597 | ||
|
|
a36805d8dd | ||
|
|
0e244b2bc7 | ||
|
|
61f8ffb35e | ||
|
|
b5c8e43688 | ||
|
|
bd674bc5d9 | ||
|
|
97fceee985 | ||
|
|
0fe04bd956 | ||
|
|
6862fb6455 | ||
|
|
4632c14191 | ||
|
|
54b795aba8 | ||
|
|
a0cc52c3cc | ||
|
|
0cdeba3b7a | ||
|
|
4bf0e14663 | ||
|
|
4444f4e70d | ||
|
|
4b4b7834d6 | ||
|
|
4ddef559a4 | ||
|
|
33b596d254 | ||
|
|
105d20b94c | ||
|
|
7d88bb7332 | ||
|
|
0711d0aae6 | ||
|
|
d08f55af3a | ||
|
|
03cd8e5755 | ||
|
|
3b15296cf0 | ||
|
|
aba8f7605d | ||
|
|
7880f54068 | ||
|
|
d4e67d5785 | ||
|
|
a73061909e | ||
|
|
d07236986c | ||
|
|
667348bc98 | ||
|
|
c5509c420b | ||
|
|
f1dcabd579 | ||
|
|
ca5914062e | ||
|
|
025937ec6d | ||
|
|
4594242d1d | ||
|
|
832e2bb39d | ||
|
|
b7f5ae83cb | ||
|
|
2dc246b4ec | ||
|
|
9deddd14e6 | ||
|
|
10ca67ac23 | ||
|
|
e4f726839f | ||
|
|
fb9f6185ec | ||
|
|
a4e303fa34 | ||
|
|
c3259ad3a2 | ||
|
|
fd2d9d8aeb | ||
|
|
bbaf012329 | ||
|
|
e18b3bb76e | ||
|
|
df6515ad6a | ||
|
|
c731705075 | ||
|
|
aa376a2448 | ||
|
|
141491233c | ||
|
|
3d6b55df79 | ||
|
|
84cd3a1a83 | ||
|
|
60f17723ba | ||
|
|
da267255b0 | ||
|
|
17b45fd23e | ||
|
|
9cb5f9627c | ||
|
|
25b4c6cdd2 | ||
|
|
9575986b93 | ||
|
|
15f90bc2f2 | ||
|
|
119be5aca0 | ||
|
|
84a9a12f4d | ||
|
|
f979f61be1 | ||
|
|
b455028547 | ||
|
|
c424cb1cb0 | ||
|
|
9d0b736a51 | ||
|
|
7a590a2d99 | ||
|
|
513c286a4a | ||
|
|
5e778c9045 | ||
|
|
274544a212 | ||
|
|
859f30a56c | ||
|
|
0b91b68333 | ||
|
|
27122baec0 | ||
|
|
0a8af8d8e4 | ||
|
|
2ca2f74bbc | ||
|
|
fa9c3f5572 | ||
|
|
be8fefd9f8 | ||
|
|
79fb3799a9 | ||
|
|
bb1a4d8a3e | ||
|
|
f5dcb13b86 | ||
|
|
41a6800c58 | ||
|
|
f9a6ada47b | ||
|
|
941aff5048 | ||
|
|
ccf11fa673 | ||
|
|
d42336b288 | ||
|
|
66453b3396 | ||
|
|
a4fc3d9b3b | ||
|
|
66d8680b26 | ||
|
|
ac21c48b52 | ||
|
|
d4d1fc795b | ||
|
|
a4566a6f80 | ||
|
|
551cf90cdd | ||
|
|
8e896e9067 | ||
|
|
b3bc217586 | ||
|
|
59f75cd31c | ||
|
|
cdfe2943ed | ||
|
|
c3ed7ccf46 | ||
|
|
61bd88fd15 | ||
|
|
b7d572c40e | ||
|
|
431f769af8 | ||
|
|
1f8dfe54da | ||
|
|
66ab3a3977 | ||
|
|
264dae9a92 | ||
|
|
da55436abb | ||
|
|
fbd36b28ea | ||
|
|
a93fe3cc3c | ||
|
|
ad82ffcfc1 | ||
|
|
4f4699d334 | ||
|
|
8667552141 | ||
|
|
059dd4c333 | ||
|
|
3c3d6ccfa7 | ||
|
|
075d1f77b7 | ||
|
|
1241a8e9ae | ||
|
|
ddfcaa0c68 | ||
|
|
49f0a156c3 | ||
|
|
f6a1bdb0cf | ||
|
|
d5d83c78e1 | ||
|
|
f385a420db | ||
|
|
b3eed0d061 | ||
|
|
7cb3429139 | ||
|
|
b3c2033fb2 | ||
|
|
ad4f69c05e | ||
|
|
bc29cfc5ef | ||
|
|
d9f520c959 | ||
|
|
7d155d6413 | ||
|
|
1a742a35c6 | ||
|
|
5ef1bef1c0 | ||
|
|
7f5d03c06c | ||
|
|
0e650af351 | ||
|
|
b4cdf89e71 | ||
|
|
9a65f15b58 | ||
|
|
1da2471a4a | ||
|
|
e171715448 | ||
|
|
3c8c57d2f9 | ||
|
|
175ea8c7c9 | ||
|
|
54dbee8543 | ||
|
|
29a5c6c4e8 | ||
|
|
9b9ec47150 | ||
|
|
8e08a5adef | ||
|
|
3eff3b000b | ||
|
|
ac40a14d79 | ||
|
|
f81abfb31d | ||
|
|
28db091ea0 | ||
|
|
8e9ba91536 | ||
|
|
fee0f1ef14 | ||
|
|
b80550d91b |
No files matched your search
+10
@@ -3,9 +3,19 @@
|
|||||||
result
|
result
|
||||||
result-*
|
result-*
|
||||||
|
|
||||||
|
# Disko's proxmox-* image-builder writes the finished .raw disk image
|
||||||
|
# directly into the current directory, not into a result-* symlink (see
|
||||||
|
# docs/proxmox-images.md, scripts/create-proxmox-resource.sh) — several GB
|
||||||
|
# each, never meant to be committed.
|
||||||
|
*.raw
|
||||||
|
|
||||||
# Ignore automatically generated direnv output
|
# Ignore automatically generated direnv output
|
||||||
.direnv
|
.direnv
|
||||||
|
|
||||||
|
# Python bytecode cache (scripts/lib/*.py)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# Locally-generated SSH host keys staged for transfer to a new machine
|
# Locally-generated SSH host keys staged for transfer to a new machine
|
||||||
# during install (see scripts/prepare-host-key.sh) — never commit these.
|
# during install (see scripts/prepare-host-key.sh) — never commit these.
|
||||||
host-keys/
|
host-keys/
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ There is no test suite — "correctness" here means the flake evaluates and
|
|||||||
sweeps: after editing one or two hosts/modules, evaluate just the
|
sweeps: after editing one or two hosts/modules, evaluate just the
|
||||||
`nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball`
|
`nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball`
|
||||||
/`diskoImagesScript`/package output affected) rather than looping over every
|
/`diskoImagesScript`/package output affected) rather than looping over every
|
||||||
host — `codex-maintenance.sh` evaluates 19 hosts plus every package/tarball/
|
host — `codex-maintenance.sh` evaluates 18 hosts plus every package/tarball/
|
||||||
image variant now and is slow to run after each small change. Reserve a full
|
image variant now and is slow to run after each small change. Reserve a full
|
||||||
`codex-maintenance.sh` run for changes that plausibly affect every host
|
`codex-maintenance.sh` run for changes that plausibly affect every host
|
||||||
(`modules/common/*`, `flake.nix`, `variables.nix`) or as a final check before
|
(`modules/common/*`, `flake.nix`, `variables.nix`) or as a final check before
|
||||||
@@ -70,6 +70,49 @@ committing. This is a session-workflow preference only — it does not apply to
|
|||||||
CI, which should keep running the full script on every push/PR regardless of
|
CI, which should keep running the full script on every push/PR regardless of
|
||||||
diff size; that's the point of it.
|
diff size; that's the point of it.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` also has:
|
||||||
|
|
||||||
|
- `scripts/sync-host-keys.sh` — generates/registers SSH host keys and their
|
||||||
|
`.sops.yaml`/`secrets/*.yaml` recipients for flake targets, idempotently
|
||||||
|
(`--all`, `<target>`, `--remove`, `--regenerate-all-keys`, all with
|
||||||
|
`--dry-run`). The primary tool for provisioning a new host's secrets
|
||||||
|
access — see "Creating a new machine" in `docs/auto-installer.md`.
|
||||||
|
- `scripts/prepare-host-key.sh` — narrower predecessor: generates a key by
|
||||||
|
an arbitrary name without touching `.sops.yaml`. Still useful to
|
||||||
|
pre-generate a key before its flake target exists yet, since
|
||||||
|
`sync-host-keys.sh` can only act on targets `nixosConfigurations` already
|
||||||
|
has.
|
||||||
|
- `scripts/create-proxmox-resource.sh` — builds a `lxc-*`/`proxmox-*`
|
||||||
|
target's tarball/disk image and creates it on a real Proxmox node
|
||||||
|
(`pct create` against the tarball as a CT template / `qm create`+
|
||||||
|
`importdisk`), or reconfigures an existing resource's cores/memory/disk
|
||||||
|
size (`--modify`, always requires typing the VMID back to confirm).
|
||||||
|
Checks for an already-uploaded image on the node before building
|
||||||
|
(`--force-rebuild` to skip that and always rebuild), and probes
|
||||||
|
nix-cache's substituter/remote-builder reachability once up front rather
|
||||||
|
than letting every `nix build` call retry against it individually.
|
||||||
|
Refuses to create a target whose host identity already exists live on
|
||||||
|
the node (checked directly via `qm`/`pct`, not any file in this repo)
|
||||||
|
unless `--allow-duplicate-host` is passed. `--dry-run` throughout both
|
||||||
|
modes.
|
||||||
|
- `scripts/env.sh` — shared config (`PROXMOX_HOST`, storage pool, bridge,
|
||||||
|
default cores/memory) sourced by `create-proxmox-resource.sh`. Add new
|
||||||
|
cross-script config here instead of duplicating it per-script.
|
||||||
|
- `scripts/bump-nixpkgs-release.sh` — bumps `flake.nix`'s `nixpkgs.url`/
|
||||||
|
`home-manager.url` in place. Exists because flake input URLs can't
|
||||||
|
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
||||||
|
errors on it), so this is the closest equivalent to a single source of
|
||||||
|
truth for the tracked release.
|
||||||
|
|
||||||
|
`sync-host-keys.sh` and `create-proxmox-resource.sh` genuinely mutate real
|
||||||
|
state when run for real (not `--dry-run`): real `secrets/*.yaml`
|
||||||
|
recipients, real Proxmox VMs/containers. They require the operator's own
|
||||||
|
SSH/sops access, which an agent session doesn't have — but don't suggest
|
||||||
|
running either non-dry-run without the operator's explicit go-ahead even
|
||||||
|
if it becomes technically reachable.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
`flake.nix` is the single entry point. It generates one
|
`flake.nix` is the single entry point. It generates one
|
||||||
@@ -91,9 +134,10 @@ nixosSystem {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
|
Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
|
||||||
`server`, `docker`, `gui`, `pxe-boot`. Not every combination is built — e.g.
|
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`. Not every
|
||||||
`pxe-boot` has no `linode` variant (PXE/DHCP/TFTP need LAN L2 adjacency a
|
combination is built — e.g. `pxe-boot` has no `linode` variant (PXE/DHCP/TFTP
|
||||||
Linode VPS doesn't have). Treat `flake.nix`'s `generatedTargets` as the source
|
need LAN L2 adjacency a Linode VPS doesn't have). Treat `flake.nix`'s
|
||||||
|
`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
|
||||||
(`.github/workflows/check-nixos.yml`, `.gitea/workflows/check-nixos.yml`) list
|
(`.github/workflows/check-nixos.yml`, `.gitea/workflows/check-nixos.yml`) list
|
||||||
@@ -115,8 +159,10 @@ removing a host.
|
|||||||
from `flake.nix`. `lxc.nix` has no hardware-configuration counterpart since
|
from `flake.nix`. `lxc.nix` has no hardware-configuration counterpart since
|
||||||
containers share the host kernel; instead it imports nixpkgs' own
|
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 (`pct restore`-ready, no install step
|
`config.system.build.tarball` output — a plain rootfs tarball, used as a
|
||||||
— see `docs/auto-installer.md`).
|
`pct create ... vztmpl` CT template (**not** `pct restore`, which expects
|
||||||
|
`vzdump` backup-archive metadata this doesn't have), no install step —
|
||||||
|
see `docs/auto-installer.md`.
|
||||||
- `modules/build-types/*.nix` — what a system is for:
|
- `modules/build-types/*.nix` — what a system is for:
|
||||||
minimal/server/docker/gui/pxe-boot/nix-cache.
|
minimal/server/docker/gui/pxe-boot/nix-cache.
|
||||||
- `modules/common/configuration.nix` — base NixOS config imported by every
|
- `modules/common/configuration.nix` — base NixOS config imported by every
|
||||||
@@ -129,11 +175,17 @@ removing a host.
|
|||||||
`lxc-*`). Also carries `imageSize`/`imageName`, letting every `proxmox-*`
|
`lxc-*`). Also carries `imageSize`/`imageName`, letting every `proxmox-*`
|
||||||
host be built as a standalone, `qm importdisk`-ready `.raw` image with no
|
host be built as a standalone, `qm importdisk`-ready `.raw` image with no
|
||||||
install step — see `docs/proxmox-images.md`.
|
install step — see `docs/proxmox-images.md`.
|
||||||
|
- `modules/disko/linode.nix` — `linode-*`'s disko config, deliberately
|
||||||
|
different in kind from the Proxmox one: Linode provisions and sizes
|
||||||
|
`/dev/sda`/`/dev/sdb` itself as whole, unpartitioned devices before the OS
|
||||||
|
boots, so this declares them with `destroy = false` (disko never wipes
|
||||||
|
them) and a bare `filesystem`/`swap` content type instead of a partition
|
||||||
|
table — idempotent against an already-provisioned disk, never destructive.
|
||||||
- `modules/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/netboot/Proxmox
|
- `modules/installer/` — the auto-installer environment (ISO, also served as
|
||||||
LXC): `common.nix` (shared config + the generated `auto-install.sh`),
|
PXE netboot): `common.nix` (shared config + the generated
|
||||||
`iso.nix`/`proxmox-lxc.nix` (per-variant), `host-keys.nix` (optionally
|
`auto-install.sh`), `iso.nix`, `host-keys.nix` (optionally bakes
|
||||||
bakes `host-keys/` into the image under `--impure`). See
|
`host-keys/` into the image under `--impure`). See
|
||||||
`docs/auto-installer.md`.
|
`docs/auto-installer.md`.
|
||||||
- `modules/pxe-boot/stage-installer-artifacts.nix` — builds the installer's
|
- `modules/pxe-boot/stage-installer-artifacts.nix` — builds the installer's
|
||||||
netboot image and stages it on the `pxe-boot` host so its iPXE menu can
|
netboot image and stages it on the `pxe-boot` host so its iPXE menu can
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pieces composed in `flake.nix`:
|
|||||||
|
|
||||||
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`
|
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`
|
||||||
- **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`
|
`gui`, `pxe-boot`, `tailscale-exit-node`
|
||||||
|
|
||||||
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. The full
|
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have. The full
|
||||||
@@ -21,11 +21,19 @@ list:
|
|||||||
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS (real, deployed) |
|
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS (real, deployed) |
|
||||||
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox (real, deployed — previously the flat `nix-minimal` target) |
|
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox (real, deployed — previously the flat `nix-minimal` target) |
|
||||||
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
|
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
|
||||||
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder (`proxmox-nix-cache` is the real, deployed one — previously the flat `nix-cache` target) |
|
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder (`lxc-nix-cache` is the real, deployed one — previously `proxmox-nix-cache`, itself previously the flat `nix-cache` target) |
|
||||||
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host (`proxmox-server` is the real, deployed one — previously the flat `server` target) |
|
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host (`proxmox-server` is the real, deployed one — previously the flat `server` target) |
|
||||||
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack (`proxmox-docker` is the real, deployed one — previously the flat `docker` target) |
|
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack (`proxmox-docker` is the real, deployed one — previously the flat `docker` target) |
|
||||||
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation (`proxmox-gui` is the real, deployed one — previously the flat `nixos` target) |
|
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation (`proxmox-gui` is the real, deployed one — previously the flat `nixos` target) |
|
||||||
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host (`proxmox-pxe-boot` is the real, deployed one — previously the flat `pxe-boot` target) |
|
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host (`proxmox-pxe-boot` is the real, deployed one — previously the flat `pxe-boot` target) |
|
||||||
|
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node (no deployed target yet; `lxc-tailscale-exit-node` is the one planned for actual use) |
|
||||||
|
|
||||||
|
This table is the only place "(real, deployed)" status is tracked — there's
|
||||||
|
no separate machine-readable copy to keep in sync. `scripts/create-proxmox-resource.sh`
|
||||||
|
guards against creating a same-identity duplicate of an already-deployed host
|
||||||
|
by checking the Proxmox node itself (live `qm`/`pct` state) rather than any
|
||||||
|
file in this repo, since a static list can't track whether a resource still
|
||||||
|
actually exists.
|
||||||
|
|
||||||
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
|
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
|
||||||
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
|
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
|
||||||
@@ -46,17 +54,17 @@ nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
|
|||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `flake.nix` | Flake inputs, the `mkTarget` platform × build-type generator, and `nixosConfigurations` outputs |
|
| `flake.nix` | Flake inputs, the `mkTarget` platform × build-type generator, and `nixosConfigurations` outputs |
|
||||||
| `variables.nix` | Single source of truth for shared values (LAN domain/CIDR, hostnames, timezone, primary username, storage root, ...) — 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`) |
|
||||||
| `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 |
|
||||||
| `modules/installer/` | Auto-installer environment (ISO/netboot/Proxmox LXC) — see `docs/auto-installer.md` |
|
| `modules/installer/` | Auto-installer environment (ISO, also served as PXE netboot) — see `docs/auto-installer.md` |
|
||||||
| `host-keys/` | Gitignored, locally-generated SSH host keys for the auto-installer — see `docs/auto-installer.md` |
|
| `host-keys/` | Gitignored, locally-generated SSH host keys for the auto-installer — see `docs/auto-installer.md` |
|
||||||
| `docs/` | Operational notes for cache, builders, lock updates, boot services, the auto-installer, and Proxmox image builds |
|
| `docs/` | Operational notes for cache, builders, lock updates, boot services, the auto-installer, and Proxmox image builds |
|
||||||
| `scripts/` | Codex setup, validation, and host-key-prep helpers |
|
| `scripts/` | Codex setup, validation, host-key, release-bump, and Proxmox resource helpers |
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
@@ -93,14 +101,22 @@ Three different paths depending on target, none of them involving a manual
|
|||||||
`nixos-rebuild switch` from this repo:
|
`nixos-rebuild switch` from this repo:
|
||||||
|
|
||||||
- Most hosts: boot the auto-installer, pick the target from its menu — see
|
- Most hosts: boot the auto-installer, pick the target from its menu — see
|
||||||
`docs/auto-installer.md`.
|
`docs/auto-installer.md`. Every menu target has a Disko config the
|
||||||
|
installer formats unconditionally (`docs/auto-installer.md`'s "Storage"
|
||||||
|
section covers how this stays non-destructive for `linode-*`, whose disks
|
||||||
|
Linode itself provisions ahead of time).
|
||||||
- `lxc-*` targets: not installed at all — build a ready-to-run container
|
- `lxc-*` targets: not installed at all — build a ready-to-run container
|
||||||
tarball and `pct restore` it directly. `docs/auto-installer.md` covers why
|
tarball and `pct create` it as a CT template directly. `docs/auto-installer.md`
|
||||||
(and the installer's menu excludes them for the same reason).
|
covers why (and the installer's menu excludes them for the same reason).
|
||||||
- `proxmox-*` targets: can alternatively be built as a standalone `.raw`
|
- `proxmox-*` targets: can alternatively be built as a standalone `.raw`
|
||||||
disk image and attached to a new VM with no install step — see
|
disk image and attached to a new VM with no install step — see
|
||||||
`docs/proxmox-images.md`.
|
`docs/proxmox-images.md`.
|
||||||
|
|
||||||
|
`scripts/create-proxmox-resource.sh --type lxc|vm --host <name>` automates
|
||||||
|
either of the last two end to end (build, host-key registration, upload,
|
||||||
|
`pct create`/`qm create`), with `--dry-run` and a guard against duplicating
|
||||||
|
an already-deployed host's identity. See its `--help`.
|
||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
|
|
||||||
Do not commit tokens, private keys, live credentials, or new password hashes
|
Do not commit tokens, private keys, live credentials, or new password hashes
|
||||||
|
|||||||
+126
-39
@@ -5,8 +5,9 @@ install any host exposed by its own `nixosConfigurations`. It was migrated
|
|||||||
from a formerly-separate `nix-auto-installer` repo — everything it did now
|
from a formerly-separate `nix-auto-installer` repo — everything it did now
|
||||||
lives here.
|
lives here.
|
||||||
|
|
||||||
The installer provides a small NixOS install environment (ISO/netboot/Proxmox
|
The installer provides a small NixOS install environment (ISO, or the same
|
||||||
LXC) with SSH access, Git support, and an interactive installation script.
|
image netbooted via PXE) with SSH access, Git support, and an interactive
|
||||||
|
installation script.
|
||||||
Logging in as any user (root or `nixos`) runs `/etc/auto-install.sh`,
|
Logging in as any user (root or `nixos`) runs `/etc/auto-install.sh`,
|
||||||
discovers available hosts from this same flake, lets the operator choose a
|
discovers available hosts from this same flake, lets the operator choose a
|
||||||
target, applies that host's Disko storage configuration, installs NixOS, and
|
target, applies that host's Disko storage configuration, installs NixOS, and
|
||||||
@@ -33,16 +34,76 @@ container image, no install step at all:
|
|||||||
nix build .#nixosConfigurations.lxc-minimal.config.system.build.tarball
|
nix build .#nixosConfigurations.lxc-minimal.config.system.build.tarball
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, on the Proxmox host, `pct restore` (or the GUI's "Create CT" → upload
|
This is a plain rootfs tarball, not a `vzdump` backup archive — restoring it
|
||||||
template flow) that tarball directly as a new container. First boot runs
|
with `pct restore` fails ("archive contains no configuration file"), since
|
||||||
`boot.postBootCommands` (registers the Nix store DB and system profile) —
|
that command expects backup-archive metadata this tarball doesn't have. Use
|
||||||
there's no separate activation step to run yourself.
|
it as a CT *template* instead: drop it under Proxmox's template storage
|
||||||
|
(conventionally `/var/lib/vz/template/cache/` for the `local` storage, or
|
||||||
|
the GUI's "Create CT" → upload-as-template flow) and create a container
|
||||||
|
from it, supplying all config on the command line since a template has none
|
||||||
|
of its own:
|
||||||
|
|
||||||
Host keys still need pre-seeding the same way as any other host (see "Host
|
```sh
|
||||||
keys" below) — the sops-nix activation-vs-first-boot race is identical
|
pct create <vmid> local:vztmpl/<file>.tar.xz \
|
||||||
regardless of how the image reaches the machine. `NIXOS_HOST_KEYS_DIR=...
|
--unprivileged 1 --features nesting=1,keyctl=1 \
|
||||||
nix build ... --impure` bakes the matching key into the tarball the same way
|
--rootfs local-lvm:8 --hostname <name> --cores 2 --memory 2048 --swap 2048 \
|
||||||
it does for the ISO/PXE installer images.
|
--net0 name=eth0,bridge=vmbr0,ip=dhcp
|
||||||
|
pct start <vmid>
|
||||||
|
```
|
||||||
|
|
||||||
|
Every one of those extra flags is load-bearing, confirmed by actually
|
||||||
|
booting one:
|
||||||
|
|
||||||
|
- `--unprivileged 1` — `modules/platforms/lxc.nix` sets
|
||||||
|
`proxmoxLXC.privileged = false`, so the image assumes it's running
|
||||||
|
unprivileged. `pct create`'s own CLI default for this flag is
|
||||||
|
privileged (unlike the web UI, whose checkbox defaults the other way)
|
||||||
|
— omit it and you get a privileged container running a NixOS config
|
||||||
|
that assumes unprivileged, a real mismatch.
|
||||||
|
- `--features nesting=1,keyctl=1` — required for a modern (v247+)
|
||||||
|
systemd guest to boot unprivileged at all. Without it, AppArmor denies
|
||||||
|
the nested user namespaces and credential mounts systemd routinely
|
||||||
|
uses (even plain getty units) — every getty crash-loops on a denied
|
||||||
|
`/run/credentials/*` mount every ~3s (this is what garbage on the
|
||||||
|
console turns out to be) while core services like `nsncd` fail the
|
||||||
|
same way, and the system never finishes activating.
|
||||||
|
- `--swap 2048` — `--memory` doesn't touch swap; it silently stays at
|
||||||
|
Proxmox's own 512M default otherwise. Match it to `--memory` unless
|
||||||
|
you deliberately want otherwise.
|
||||||
|
|
||||||
|
First boot runs `boot.postBootCommands` (registers the Nix store DB and
|
||||||
|
system profile) — there's no separate activation step to run yourself.
|
||||||
|
`scripts/create-proxmox-resource.sh --type lxc --host <name>` automates all
|
||||||
|
of this (build, host-key handling, upload, `pct create` with the flags
|
||||||
|
above) — see its `--help`.
|
||||||
|
|
||||||
|
Host keys still need pre-seeding the same way as any other host — the
|
||||||
|
sops-nix activation-vs-first-boot race is identical regardless of how the
|
||||||
|
image reaches the machine. Unlike the ISO/PXE installer (where
|
||||||
|
`modules/installer/host-keys.nix` bakes *every* `host-keys/` entry into
|
||||||
|
`/etc/host-keys/` for `auto-install.sh` to pick from and copy at install
|
||||||
|
time — see "Host keys" below), an `lxc-*` tarball has no install step to
|
||||||
|
copy anything during, so `modules/platforms/lxc.nix` bakes this *one*
|
||||||
|
target's key straight into `/etc/ssh/ssh_host_ed25519_key(.pub)` directly,
|
||||||
|
keyed by its own exact flake target name (`config.environment.etc` can't
|
||||||
|
be read back from within a module still contributing to it, so this comes
|
||||||
|
in via `specialArgs.flakeTarget`, set by `flake.nix`'s `mkTarget`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" \
|
||||||
|
nix build .#nixosConfigurations.lxc-nix-cache.config.system.build.tarball --impure
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirmed the hard way: without this, the tarball's own built-in system
|
||||||
|
just generates a fresh host key at first boot like any host would, which
|
||||||
|
can never match whatever `.sops.yaml` actually trusts for that target —
|
||||||
|
`sops-install-secrets` fails with `Error getting data key: 0 successful
|
||||||
|
groups required, got 0`, and *every* secret (including this host's own
|
||||||
|
login) permanently fails to decrypt, silently — no error in the boot log
|
||||||
|
at all, since the activation step that would install secrets only runs on
|
||||||
|
a from-scratch first activation and skips silently once `/run/current-system`
|
||||||
|
already exists. `scripts/create-proxmox-resource.sh` always builds with
|
||||||
|
`NIXOS_HOST_KEYS_DIR` set for this reason.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -53,32 +114,40 @@ it does for the ISO/PXE installer images.
|
|||||||
`installation-cd-minimal.nix` module plus `common.nix`. Also used, paired
|
`installation-cd-minimal.nix` module plus `common.nix`. Also used, paired
|
||||||
with `netboot-minimal.nix`, to build the PXE netboot variant (see
|
with `netboot-minimal.nix`, to build the PXE netboot variant (see
|
||||||
`docs/pxe-boot.md`).
|
`docs/pxe-boot.md`).
|
||||||
- `modules/installer/proxmox-lxc.nix` — Proxmox LXC-specific: disables
|
|
||||||
bootloader options (containers don't need one) plus `common.nix`.
|
|
||||||
- `modules/installer/host-keys.nix` — optionally bakes pre-generated SSH
|
- `modules/installer/host-keys.nix` — optionally bakes pre-generated SSH
|
||||||
host keys into the image; see "Host keys" below.
|
host keys into the image; see "Host keys" below.
|
||||||
- `scripts/prepare-host-key.sh` — admin-workstation pre-flight tool, see
|
- `scripts/sync-host-keys.sh` — admin-workstation tool that generates,
|
||||||
"Creating a New Machine" below.
|
registers, and (via `--remove`/`--regenerate-all-keys`) retires host
|
||||||
|
keys; see "Creating a New Machine" below.
|
||||||
|
- `scripts/prepare-host-key.sh` — narrower predecessor: generates a single
|
||||||
|
key by an arbitrary name without touching `.sops.yaml`. Still useful for
|
||||||
|
pre-generating a key *before* its flake target exists (`sync-host-keys.sh`
|
||||||
|
can only act on targets `nixosConfigurations` already has); otherwise
|
||||||
|
`sync-host-keys.sh` does the same thing and more.
|
||||||
|
|
||||||
Flake outputs:
|
Flake outputs:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
nixosConfigurations.installer # ISO/netboot installer image
|
nixosConfigurations.installer # ISO/netboot installer image
|
||||||
nixosConfigurations.proxmox-lxc # Proxmox LXC-based installer image
|
|
||||||
|
|
||||||
packages.x86_64-linux.iso # installer ISO/netboot image
|
packages.x86_64-linux.iso # installer ISO/netboot image
|
||||||
packages.x86_64-linux.lxc # Proxmox LXC installer tarball
|
|
||||||
packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
|
packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
|
||||||
packages.x86_64-linux.all # all three of the above, bundled
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix build .#iso
|
nix build .#iso
|
||||||
nix build .#lxc
|
|
||||||
nix build .#pxe
|
nix build .#pxe
|
||||||
nix build .#all
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There's no `nixosConfigurations.proxmox-lxc` (installer-boots-as-an-LXC-
|
||||||
|
container) or `packages.x86_64-linux.lxc`/`.all` anymore. Both existed only
|
||||||
|
to let the installer itself run as an LXC container so you could
|
||||||
|
`nixos-install` some *other* host from within it — but LXC targets are
|
||||||
|
excluded from the install menu (same bind-mount problem as any LXC
|
||||||
|
`nixos-install`), and now have their own direct tarball path anyway (see
|
||||||
|
"LXC hosts" above), which left the installer's own LXC form with no real
|
||||||
|
use case.
|
||||||
|
|
||||||
The `pxe` variant is also built automatically as part of the `pxe-boot` host
|
The `pxe` variant is also built automatically as part of the `pxe-boot` host
|
||||||
itself (`modules/pxe-boot/stage-installer-artifacts.nix`) and served over
|
itself (`modules/pxe-boot/stage-installer-artifacts.nix`) and served over
|
||||||
iPXE — see `docs/pxe-boot.md`.
|
iPXE — see `docs/pxe-boot.md`.
|
||||||
@@ -123,10 +192,24 @@ next, where you can `scp` a key in after boot, same as before this migration.
|
|||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
Disk partitioning is handled by Disko — the installer has no hardcoded
|
Disk partitioning is handled by Disko — the installer has no hardcoded
|
||||||
`parted`/`mkfs`/`mkswap`/`mount` commands. Each host defines its own layout
|
`parted`/`mkfs`/`mkswap`/`mount` commands, and `auto-install.sh` runs
|
||||||
via `disko.devices`; a host without one is installed in place via bind-mount
|
`disko --mode destroy,format,mount` unconditionally, no branching on whether
|
||||||
(this is how every LXC container target works, since they have no raw block
|
the target has a Disko config. Every host reachable through this menu has
|
||||||
device to partition).
|
one:
|
||||||
|
|
||||||
|
- `proxmox-*` (`modules/disko/proxmox.nix`): a real GPT partition table
|
||||||
|
(ESP + swap + root) on `/dev/sda`.
|
||||||
|
- `linode-*` (`modules/disko/linode.nix`): Linode provisions and sizes
|
||||||
|
`/dev/sda`/`/dev/sdb` itself as whole, unpartitioned block devices before
|
||||||
|
the OS ever boots, so this declares them with `destroy = false` (skips
|
||||||
|
disko's wipe stage for these disks entirely — see the option's own docs)
|
||||||
|
and a bare `filesystem`/`swap` content type with no partition table, and
|
||||||
|
the format step it does run only calls `mkfs`/`mkswap` if `blkid` shows
|
||||||
|
the device isn't already formatted — a re-run against an
|
||||||
|
already-provisioned Linode disk is a no-op, not a wipe.
|
||||||
|
|
||||||
|
`lxc-*` is the only category without one — it's excluded from this menu
|
||||||
|
entirely (see "LXC hosts" above), so it never reaches this code path.
|
||||||
|
|
||||||
## Installer process
|
## Installer process
|
||||||
|
|
||||||
@@ -135,7 +218,7 @@ device to partition).
|
|||||||
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.
|
||||||
3. Skips the `nix-cache` substituter when installing a `nix-cache` host itself (consistent with that host's own runtime config).
|
3. Skips the `nix-cache` substituter when installing a `nix-cache` host itself (consistent with that host's own runtime config).
|
||||||
4. Runs `disko --mode destroy,format,mount` if the target has a Disko config, otherwise bind-mounts `/` onto `/mnt`.
|
4. Runs `disko --mode destroy,format,mount` (see "Storage" above — every host reachable through this menu has a Disko config, so this is unconditional).
|
||||||
5. Installs the target's SSH host key from `/etc/host-keys` or `/root/host-keys` (see "Host keys" above).
|
5. Installs the target's SSH host key from `/etc/host-keys` or `/root/host-keys` (see "Host keys" above).
|
||||||
6. Runs `nixos-install --flake <url>#<choice> --no-root-password`.
|
6. Runs `nixos-install --flake <url>#<choice> --no-root-password`.
|
||||||
7. Cleans up and reboots.
|
7. Cleans up and reboots.
|
||||||
@@ -147,24 +230,28 @@ host consumes any sops-nix secret — as of this writing, that's every host
|
|||||||
(`modules/common/configuration.nix` puts the root/nixos password hash and the
|
(`modules/common/configuration.nix` puts the root/nixos password hash and the
|
||||||
GitHub token behind sops-nix for all of them).
|
GitHub token behind sops-nix for all of them).
|
||||||
|
|
||||||
1. **On your admin workstation**, decide the new machine's flake target name
|
1. **Add the flake target** — `hosts/<name>/host.nix` plus the matching
|
||||||
(`<platform>-<buildtype>`, e.g. `proxmox-server`) and generate + register
|
`mkTarget { ... }` entry in `flake.nix`'s `generatedTargets` (see
|
||||||
its host key:
|
"Composition pattern" in `CLAUDE.md`). No secrets involved yet, so this
|
||||||
|
is safe to commit on its own if you want a clean history.
|
||||||
|
|
||||||
|
2. **On your admin workstation, generate and register its host key:**
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./scripts/prepare-host-key.sh <hostname>
|
./scripts/sync-host-keys.sh <flake-target>
|
||||||
```
|
```
|
||||||
|
|
||||||
This generates `host-keys/<hostname>_ssh_host_ed25519_key(.pub)` and
|
This generates `host-keys/<flake-target>_ssh_host_ed25519_key(.pub)`,
|
||||||
prints the `.sops.yaml` snippet to add, which secrets files to
|
adds it as a new `.sops.yaml` anchor, works out which `secrets/*.yaml`
|
||||||
re-encrypt it into, and the next steps below.
|
files this specific host actually references (from its own
|
||||||
|
`config.sops.secrets`, not guessed), adds it to each one's
|
||||||
|
`key_groups`, and re-encrypts them with `sops updatekeys` — no manual
|
||||||
|
YAML editing. Safe to re-run; it only fills in what's missing.
|
||||||
|
|
||||||
2. **Edit `.sops.yaml`** with the printed snippet, then re-encrypt every
|
Doing this for every host that needs one at once — after adding several
|
||||||
secrets file you added the new host to:
|
new targets, or just to catch up any that were missed — is
|
||||||
|
`./scripts/sync-host-keys.sh --all`. See `scripts/sync-host-keys.sh --help`
|
||||||
```sh
|
for its other modes (`--remove`, `--regenerate-all-keys`).
|
||||||
nix-shell -p sops --run 'sops updatekeys secrets/common.yaml'
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Commit and push.** The flake build the installer uses has to see the
|
3. **Commit and push.** The flake build the installer uses has to see the
|
||||||
new recipient before you install, or decryption fails on first boot
|
new recipient before you install, or decryption fails on first boot
|
||||||
|
|||||||
+37
-9
@@ -11,19 +11,38 @@ This repository configures `nix-cache` as a **binary cache server** and a **remo
|
|||||||
- Client hosts import `modules/nix-cache/client.nix` and, when remote building is enabled, `modules/nix-cache/remote-builder-client.nix`.
|
- Client hosts import `modules/nix-cache/client.nix` and, when remote building is enabled, `modules/nix-cache/remote-builder-client.nix`.
|
||||||
- The `nix-cache` host imports `modules/nix-cache/server.nix`.
|
- The `nix-cache` host imports `modules/nix-cache/server.nix`.
|
||||||
|
|
||||||
## Binary cache signing keys (on nix-cache)
|
## Binary cache signing key
|
||||||
|
|
||||||
|
`modules/nix-cache/client.nix` hardcodes every client's trust in one
|
||||||
|
specific public key (`cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4=`).
|
||||||
|
That means whichever host is currently playing the `nix-cache` role has to
|
||||||
|
use that *exact* keypair — not a freshly generated one — or no client will
|
||||||
|
accept substitutes from it (they'd just silently fall back to building
|
||||||
|
from source). So unlike most per-host secrets, this one can't be
|
||||||
|
self-generated on first boot; it's managed via sops-nix like every other
|
||||||
|
secret in this repo, sourced from `secrets/nix-cache.yaml`'s
|
||||||
|
`cache-priv-key` entry (`modules/nix-cache/server.nix`).
|
||||||
|
|
||||||
|
**Adding or rotating the value:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo install -d -m 0700 /etc/nix
|
nix-shell -p sops --run 'sops secrets/nix-cache.yaml'
|
||||||
sudo nix-store --generate-binary-cache-key nix-cache-1 /etc/nix/cache-priv.pem /etc/nix/cache-pub.pem
|
|
||||||
sudo chmod 0600 /etc/nix/cache-priv.pem
|
|
||||||
sudo chmod 0644 /etc/nix/cache-pub.pem
|
|
||||||
cat /etc/nix/cache-pub.pem
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not commit private keys.
|
Add (or replace) a `cache-priv-key` entry with the private key file's exact
|
||||||
Do not commit new password hashes or live credentials. Existing committed hashes
|
contents. If you don't have it yet, generate a keypair once:
|
||||||
should be rotated and moved to host-local secret management.
|
|
||||||
|
```bash
|
||||||
|
nix-store --generate-binary-cache-key nix-cache-1 cache-priv.pem cache-pub.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
— paste `cache-priv.pem`'s contents into the `cache-priv-key` entry above,
|
||||||
|
delete both local files afterward, and update
|
||||||
|
`trusted-public-keys` in `modules/nix-cache/client.nix` (and every already-built
|
||||||
|
client) to match `cache-pub.pem` if this is a genuine rotation rather than
|
||||||
|
a first-time bootstrap. Any `nixos-configurations.*-nix-cache` host picks
|
||||||
|
the new key up automatically on next activation — no more manual
|
||||||
|
`/etc/nix/cache-priv.pem` install step.
|
||||||
|
|
||||||
## Remote builder SSH keys
|
## Remote builder SSH keys
|
||||||
|
|
||||||
@@ -40,6 +59,15 @@ On `nix-cache`, install the matching public key used by `nixremote` authorized k
|
|||||||
The committed `nixremote` authorized keys are public SSH keys only. Keep the
|
The committed `nixremote` authorized keys are public SSH keys only. Keep the
|
||||||
matching private keys on client hosts and out of the repository.
|
matching private keys on client hosts and out of the repository.
|
||||||
|
|
||||||
|
nix-cache's own SSH *host* key is trusted declaratively via
|
||||||
|
`programs.ssh.knownHosts` in `modules/nix-cache/remote-builder-client.nix`,
|
||||||
|
sourced from `vars.nixCacheHostKey` (`variables.nix`) — every client rebuild
|
||||||
|
picks it up automatically, so distributed builds don't fail with "Host key
|
||||||
|
verification failed" on a client that has never manually SSH'd to nix-cache
|
||||||
|
before. If nix-cache's host key is ever rotated or the host rebuilt from
|
||||||
|
scratch, update `vars.nixCacheHostKey` to match its new
|
||||||
|
`/etc/ssh/ssh_host_ed25519_key.pub`.
|
||||||
|
|
||||||
## Manual verification
|
## Manual verification
|
||||||
|
|
||||||
After deployment:
|
After deployment:
|
||||||
|
|||||||
+10
-3
@@ -7,6 +7,12 @@ config (`modules/disko/proxmox.nix`) already used to format a real disk on
|
|||||||
install, so there's nothing host-specific to write; it's available for every
|
install, so there's nothing host-specific to write; it's available for every
|
||||||
`proxmox-*` target automatically.
|
`proxmox-*` target automatically.
|
||||||
|
|
||||||
|
`scripts/create-proxmox-resource.sh --type vm --host <name>` automates the
|
||||||
|
whole walkthrough below (and the equivalent LXC one) end to end, including
|
||||||
|
host-key handling and upload — see its `--help`. The steps here are what it
|
||||||
|
runs under the hood, useful for doing any of it by hand or understanding
|
||||||
|
what it does before you trust it against real infrastructure.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -43,9 +49,10 @@ sudo ./result \
|
|||||||
--build-memory 2048
|
--build-memory 2048
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate the key first with `scripts/prepare-host-key.sh <hostname>`, same
|
Generate the key first with `scripts/sync-host-keys.sh <hostname>`, same
|
||||||
as any other host — see `docs/auto-installer.md` for the full sops.yaml
|
as any other host — see `docs/auto-installer.md` for the full walkthrough
|
||||||
registration walkthrough.
|
(it registers the new key in `.sops.yaml` and re-encrypts the affected
|
||||||
|
`secrets/*.yaml` files too, no manual editing needed).
|
||||||
|
|
||||||
## Deploying to Proxmox
|
## Deploying to Proxmox
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,9 @@
|
|||||||
# nix-cache itself consumes the nix-cache substituter and remote
|
# nix-cache itself consumes the nix-cache substituter and remote
|
||||||
# builder.
|
# builder.
|
||||||
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix }:
|
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix }:
|
||||||
|
let
|
||||||
|
flakeTarget = "${platform}-${buildType}";
|
||||||
|
in
|
||||||
nixpkgs.lib.nixosSystem {
|
nixpkgs.lib.nixosSystem {
|
||||||
inherit system;
|
inherit system;
|
||||||
modules = [
|
modules = [
|
||||||
@@ -42,7 +45,7 @@
|
|||||||
./modules/platforms/${platform}.nix
|
./modules/platforms/${platform}.nix
|
||||||
./modules/build-types/${buildType}.nix
|
./modules/build-types/${buildType}.nix
|
||||||
hostPath
|
hostPath
|
||||||
{ environment.etc."flake-target".text = "${platform}-${buildType}"; }
|
{ environment.etc."flake-target".text = flakeTarget; }
|
||||||
home-manager.nixosModules.home-manager
|
home-manager.nixosModules.home-manager
|
||||||
{
|
{
|
||||||
home-manager = {
|
home-manager = {
|
||||||
@@ -56,7 +59,13 @@
|
|||||||
./modules/nix-cache/client.nix
|
./modules/nix-cache/client.nix
|
||||||
./modules/nix-cache/remote-builder-client.nix
|
./modules/nix-cache/remote-builder-client.nix
|
||||||
];
|
];
|
||||||
specialArgs = { inherit inputs vars netbootSystem; };
|
# flakeTarget is passed via specialArgs (not read back from
|
||||||
|
# config.environment.etc."flake-target" above) specifically so
|
||||||
|
# modules/platforms/lxc.nix can use it to select its own host key
|
||||||
|
# file without a same-option circular dependency (a module
|
||||||
|
# contributing to environment.etc can't read the merged
|
||||||
|
# environment.etc it's itself contributing to).
|
||||||
|
specialArgs = { inherit inputs vars netbootSystem flakeTarget; };
|
||||||
};
|
};
|
||||||
|
|
||||||
# Generated platform x build-type matrix. pxe-boot has no linode
|
# Generated platform x build-type matrix. pxe-boot has no linode
|
||||||
@@ -85,6 +94,10 @@
|
|||||||
|
|
||||||
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; };
|
||||||
|
proxmox-tailscale-exit-node = mkTarget { platform = "proxmox"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
||||||
|
lxc-tailscale-exit-node = mkTarget { platform = "lxc"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
||||||
};
|
};
|
||||||
|
|
||||||
# Auto-install environments (migrated from the former nix-auto-installer
|
# Auto-install environments (migrated from the former nix-auto-installer
|
||||||
@@ -100,12 +113,6 @@
|
|||||||
modules = [ ./modules/installer/iso.nix ];
|
modules = [ ./modules/installer/iso.nix ];
|
||||||
specialArgs = { inherit vars; };
|
specialArgs = { inherit vars; };
|
||||||
};
|
};
|
||||||
|
|
||||||
proxmox-lxc = nixpkgs.lib.nixosSystem {
|
|
||||||
inherit system;
|
|
||||||
modules = [ ./modules/installer/proxmox-lxc.nix ];
|
|
||||||
specialArgs = { inherit vars; };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Same installer environment, built as netboot (kernel + initrd +
|
# Same installer environment, built as netboot (kernel + initrd +
|
||||||
@@ -128,24 +135,22 @@
|
|||||||
|
|
||||||
nixosConfigurations = generatedTargets // installerTargets;
|
nixosConfigurations = generatedTargets // installerTargets;
|
||||||
|
|
||||||
# Buildable auto-installer artifacts (`nix build .#<name>`), migrated
|
# Buildable auto-installer artifacts (`nix build .#<name>`). No `lxc`
|
||||||
# unchanged from nix-auto-installer's interface.
|
# variant (installer-boots-as-an-LXC-container) or `all` bundle
|
||||||
packages.${system} = rec {
|
# anymore — lxc-* and proxmox-* hosts deploy via their own tarball/
|
||||||
|
# disk-image outputs instead (see docs/auto-installer.md and
|
||||||
|
# docs/proxmox-images.md), which left the installer's own LXC form
|
||||||
|
# with no real use case: it's excluded from the install menu (same
|
||||||
|
# bind-mount problem as any LXC nixos-install target) and nothing
|
||||||
|
# else needed booting the installer itself as a container.
|
||||||
|
packages.${system} = {
|
||||||
iso = installerTargets.installer.config.system.build.isoImage;
|
iso = installerTargets.installer.config.system.build.isoImage;
|
||||||
|
|
||||||
lxc = installerTargets.proxmox-lxc.config.system.build.tarball;
|
|
||||||
|
|
||||||
pxe = pkgs.linkFarm "pxe" [
|
pxe = pkgs.linkFarm "pxe" [
|
||||||
{ name = "netboot.ipxe"; path = netbootSystem.config.system.build.netbootIpxeScript; }
|
{ name = "netboot.ipxe"; path = netbootSystem.config.system.build.netbootIpxeScript; }
|
||||||
{ name = "initrd"; path = netbootSystem.config.system.build.netbootRamdisk; }
|
{ name = "initrd"; path = netbootSystem.config.system.build.netbootRamdisk; }
|
||||||
{ name = "kernel"; path = netbootSystem.config.system.build.kernel; }
|
{ name = "kernel"; path = netbootSystem.config.system.build.kernel; }
|
||||||
];
|
];
|
||||||
|
|
||||||
all = pkgs.linkFarm "all" [
|
|
||||||
{ name = "iso"; path = iso; }
|
|
||||||
{ name = "lxc"; path = lxc; }
|
|
||||||
{ name = "pxe"; path = pxe; }
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Proxmox (Chromium)
|
Name=Proxmox (Chromium)
|
||||||
Exec=chromium --app=https://pve.${vars.homeDomain}:${toString vars.pveWebPort} --window-size=1920,1080 --window-position=0,0
|
Exec=chromium --app=https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --window-size=1920,1080 --window-position=0,0
|
||||||
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
|
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Hypervisor;
|
Categories=Hypervisor;
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Proxmox Backup Server (Chromium)
|
Name=Proxmox Backup Server (Chromium)
|
||||||
Exec=chromium --app=https://${vars.pbsIp}:${toString vars.pbsWebPort} --window-size=1920,1080 --window-position=0,0
|
Exec=chromium --app=https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --window-size=1920,1080 --window-position=0,0
|
||||||
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
|
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=backup;
|
Categories=backup;
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Proxmox (Firefox)
|
Name=Proxmox (Firefox)
|
||||||
Exec=firefox --new-instance https://pve.${vars.homeDomain}:${toString vars.pveWebPort} --profile ProxmoxWebApp --window-size=1920,1080 --class ProxmoxWebApp
|
Exec=firefox --new-instance https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --profile ProxmoxWebApp --window-size=1920,1080 --class ProxmoxWebApp
|
||||||
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
|
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Hypervisor;
|
Categories=Hypervisor;
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Proxmox Backup Server (Firefox)
|
Name=Proxmox Backup Server (Firefox)
|
||||||
Exec=firefox --new-window https://${vars.pbsIp}:${toString vars.pbsWebPort} --profile PbsWebApp --window-size=1920,1080 --class PbsWebApp
|
Exec=firefox --new-window https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --profile PbsWebApp --window-size=1920,1080 --class PbsWebApp
|
||||||
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
|
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=backup;
|
Categories=backup;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
services.beszel.agent.environment = {
|
services.beszel.agent.environment = {
|
||||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||||
EXTRA_FILESYSTEMS = "${vars.storageRoot}/docker/volumes";
|
EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||||
LOG_LEVEL = "debug";
|
LOG_LEVEL = "debug";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
_:
|
||||||
|
|
||||||
|
{
|
||||||
|
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";
|
||||||
|
}
|
||||||
@@ -4,6 +4,15 @@
|
|||||||
services.beszel.agent.enable = true;
|
services.beszel.agent.enable = true;
|
||||||
services.beszel.agent.environment = {
|
services.beszel.agent.environment = {
|
||||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||||
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.beszelHubPort}";
|
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# The upstream module runs beszel-agent under DynamicUser with
|
||||||
|
# ProtectSystem = "strict" and no StateDirectory, so /var/lib/beszel-agent
|
||||||
|
# (where the agent persists its hub-pairing fingerprint, per
|
||||||
|
# https://github.com/henrygd/beszel/discussions/1542) isn't writable --
|
||||||
|
# every restart silently fails to save it and regenerates a fresh one in
|
||||||
|
# memory, permanently desyncing from whatever the hub has on record after
|
||||||
|
# the very first successful pairing. Give it real persistent storage.
|
||||||
|
systemd.services.beszel-agent.serviceConfig.StateDirectory = "beszel-agent";
|
||||||
}
|
}
|
||||||
@@ -28,13 +28,18 @@
|
|||||||
boot.supportedFilesystems = [ "nfs" ];
|
boot.supportedFilesystems = [ "nfs" ];
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"L+ /home/nixos/docker - - - - /mnt/docker/config"
|
"L+ /home/${vars.primaryUser}/docker - - - - ${vars.nfsShares.dockerConfig.mountpoint}"
|
||||||
"d /mnt/docker 0755 nixos users -"
|
"d /mnt/docker 0755 ${vars.primaryUser} users -"
|
||||||
"d /mnt/raspi-backup 0755 nixos users -"
|
"d ${vars.nfsShares.raspiVolumes.mountpoint} 0755 ${vars.primaryUser} users -"
|
||||||
];
|
];
|
||||||
|
|
||||||
users.users.nixos.extraGroups = [ "docker" ];
|
users.users.${vars.primaryUser}.extraGroups = [ "docker" ];
|
||||||
services.openssh.settings.PermitRootLogin = "yes";
|
services.openssh.settings.PermitRootLogin = "yes";
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 8080 443 vars.beszelHubPort ];
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
vars.ports.dockerHttp
|
||||||
|
vars.ports.dockerExtra
|
||||||
|
vars.ports.dockerHttps
|
||||||
|
vars.ports.beszelHub
|
||||||
|
];
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{ config, pkgs, lib, inputs, ... }:
|
{ config, pkgs, lib, inputs, vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
# The networkmanager group only exists when NM is actually enabled — the
|
# The networkmanager group only exists when NM is actually enabled — the
|
||||||
# lxc platform module force-disables it, so don't add the user to a group
|
# lxc platform module force-disables it, so don't add the user to a group
|
||||||
# that won't exist there.
|
# that won't exist there.
|
||||||
users.users.nixos.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
|
users.users.${vars.primaryUser}.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
|
||||||
|
|
||||||
programs.firefox.enable = true;
|
programs.firefox.enable = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{ lib, pkgs, config, ... }:
|
{ lib, pkgs, config, vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
networking.networkmanager.enable = true;
|
networking.networkmanager.enable = true;
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# The networkmanager group only exists when NM is actually enabled — the
|
# The networkmanager group only exists when NM is actually enabled — the
|
||||||
# lxc platform module force-disables it, so don't add the user to a group
|
# lxc platform module force-disables it, so don't add the user to a group
|
||||||
# that won't exist there.
|
# that won't exist there.
|
||||||
users.users.nixos.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
|
users.users.${vars.primaryUser}.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
inetutils
|
inetutils
|
||||||
|
|||||||
@@ -155,6 +155,6 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 ];
|
networking.firewall.allowedTCPPorts = [ vars.ports.pxeBootHttp ];
|
||||||
networking.firewall.allowedUDPPorts = [ 69 ];
|
networking.firewall.allowedUDPPorts = [ vars.ports.pxeBootTftp ];
|
||||||
}
|
}
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
services.nfs.server = {
|
services.nfs.server = {
|
||||||
enable = true;
|
enable = true;
|
||||||
exports = ''
|
exports = ''
|
||||||
${vars.storageRoot}/docker/config ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
||||||
${vars.storageRoot}/docker/volumes ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
||||||
${vars.storageRoot}/docker/databases ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
||||||
${vars.storageRoot}/docker/nextcloud-data ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
||||||
${vars.storageRoot}/raspi/volumes ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 111 2049 ];
|
networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd ];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{ ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
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" ];
|
||||||
|
}
|
||||||
@@ -17,6 +17,26 @@ let
|
|||||||
--refresh \
|
--refresh \
|
||||||
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# lxc-* hosts pre-seed their SSH host key at build time (see
|
||||||
|
# modules/platforms/lxc.nix) so sops-nix's .sops.yaml recipient matches on
|
||||||
|
# first boot -- without it, secrets permanently fail to decrypt (see that
|
||||||
|
# file's comment for the confirmed failure). That requires --impure plus
|
||||||
|
# NIXOS_HOST_KEYS_DIR pointing at the repo's host-keys/ dir, same pattern
|
||||||
|
# docs/auto-installer.md uses for the installer ISO. A function, not a
|
||||||
|
# shellAlias, since the target name has to interpolate into the middle of
|
||||||
|
# the flake attribute path, not just append after it. Must be run from the
|
||||||
|
# repo root, same as every other host-keys/ command in this repo.
|
||||||
|
buildImageFn = ''
|
||||||
|
buildImage() {
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "usage: buildImage <flake-target> (e.g. lxc-docker)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||||
|
".#nixosConfigurations.$1.config.system.build.tarball"
|
||||||
|
}
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
programs.bash = {
|
programs.bash = {
|
||||||
@@ -25,5 +45,6 @@ in
|
|||||||
"Switch-nix" = mySwitchCmd;
|
"Switch-nix" = mySwitchCmd;
|
||||||
"Test-nix" = myTestCmd;
|
"Test-nix" = myTestCmd;
|
||||||
};
|
};
|
||||||
|
initExtra = buildImageFn;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
_:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Linode provisions and sizes these disks itself (via the Linode
|
||||||
|
# dashboard/API) before the OS ever boots, and presents them as whole,
|
||||||
|
# unpartitioned block devices — /dev/sda is the root filesystem directly,
|
||||||
|
# /dev/sdb is swap directly, no partition table on either. Nothing here
|
||||||
|
# should ever repartition or resize them:
|
||||||
|
# - `destroy = false` skips each disk entirely during disko's destroy
|
||||||
|
# stage (see disko's disk.destroy option) — no wipefs, ever.
|
||||||
|
# - the filesystem content type's own create step only runs mkfs if the
|
||||||
|
# device isn't already formatted (checked via `blkid`), so re-running
|
||||||
|
# this against an already-provisioned Linode disk is a no-op.
|
||||||
|
disko.devices.disk = {
|
||||||
|
main = {
|
||||||
|
device = "/dev/sda";
|
||||||
|
destroy = false;
|
||||||
|
|
||||||
|
content = {
|
||||||
|
type = "filesystem";
|
||||||
|
format = "ext4";
|
||||||
|
mountpoint = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
swap = {
|
||||||
|
device = "/dev/sdb";
|
||||||
|
destroy = false;
|
||||||
|
|
||||||
|
content = {
|
||||||
|
type = "swap";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{ config, ... }:
|
{ config, vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
disko.devices = {
|
disko.devices = {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
# whatever's left after ESP + swap within it); imageName keeps each
|
# whatever's left after ESP + swap within it); imageName keeps each
|
||||||
# host's image distinctly named instead of every proxmox-* host
|
# host's image distinctly named instead of every proxmox-* host
|
||||||
# producing an identical "main.raw".
|
# producing an identical "main.raw".
|
||||||
imageSize = "20G";
|
imageSize = vars.proxmoxImageSize;
|
||||||
imageName = config.networking.hostName;
|
imageName = config.networking.hostName;
|
||||||
|
|
||||||
content = {
|
content = {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
fileSystems = {
|
fileSystems = {
|
||||||
"/mnt/docker/config" = {
|
${vars.nfsShares.dockerConfig.mountpoint} = {
|
||||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/docker/config";
|
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
|
||||||
fsType = "nfs";
|
fsType = "nfs";
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
"/mnt/docker/databases" = {
|
${vars.nfsShares.dockerDatabases.mountpoint} = {
|
||||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/docker/databases";
|
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
|
||||||
fsType = "nfs";
|
fsType = "nfs";
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
"/mnt/docker/volumes" = {
|
${vars.nfsShares.dockerVolumes.mountpoint} = {
|
||||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/docker/volumes";
|
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||||
fsType = "nfs";
|
fsType = "nfs";
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
"/mnt/nextcloud-data" = {
|
${vars.nfsShares.nextcloudData.mountpoint} = {
|
||||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/docker/nextcloud-data";
|
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
|
||||||
fsType = "nfs";
|
fsType = "nfs";
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
@@ -50,8 +50,8 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
"/mnt/raspi-backup" = {
|
${vars.nfsShares.raspiVolumes.mountpoint} = {
|
||||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/raspi/volumes";
|
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
|
||||||
fsType = "nfs";
|
fsType = "nfs";
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
|
|||||||
@@ -33,14 +33,11 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/" =
|
# fileSystems."/" and swapDevices are now owned by disko
|
||||||
{
|
# (../disko/linode.nix, imported from ../platforms/linode.nix) — same
|
||||||
device = "/dev/sda";
|
# /dev/sda root + /dev/sdb swap layout, declared there instead so disko's
|
||||||
fsType = "ext4";
|
# (idempotent, non-destructive — see that file) format/mount scripts stay
|
||||||
};
|
# in sync with what NixOS actually mounts.
|
||||||
|
|
||||||
swapDevices =
|
|
||||||
[{ device = "/dev/sdb"; }];
|
|
||||||
|
|
||||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
}
|
}
|
||||||
@@ -57,25 +57,23 @@
|
|||||||
|
|
||||||
echo "Fetching available NixOS hosts from flake..."
|
echo "Fetching available NixOS hosts from flake..."
|
||||||
# Two categories deliberately excluded from the menu:
|
# Two categories deliberately excluded from the menu:
|
||||||
# lxc-* — these build a config.system.build.tarball
|
# lxc-* — these build a config.system.build.tarball meant for
|
||||||
# meant for `pct restore` on Proxmox
|
# `pct restore` on Proxmox directly, not an install.
|
||||||
# directly, not an install. Running
|
# Running nixos-install against one here would
|
||||||
# nixos-install against one here would
|
# bind-mount / onto /mnt and then refuse to touch the
|
||||||
# bind-mount / onto /mnt and then refuse to
|
# filesystem it's currently running on — see
|
||||||
# touch the filesystem it's currently
|
# docs/auto-installer.md.
|
||||||
# running on — see docs/auto-installer.md.
|
# installer — this *is* the installer image's own flake target,
|
||||||
# installer/proxmox-lxc — these *are* the installer image's own
|
# not a deployable host; "installing" it means
|
||||||
# flake targets, not deployable hosts;
|
# nixos-install-ing a copy of the installer into
|
||||||
# "installing" one means nixos-install-ing
|
# itself.
|
||||||
# a copy of the installer into itself.
|
|
||||||
mapfile -t options < <(
|
mapfile -t options < <(
|
||||||
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
||||||
"''${FLAKE_BASE_URL}#nixosConfigurations" \
|
"''${FLAKE_BASE_URL}#nixosConfigurations" \
|
||||||
--apply builtins.attrNames \
|
--apply builtins.attrNames \
|
||||||
| jq -r '.[]
|
| jq -r '.[]
|
||||||
| select(startswith("lxc-") | not)
|
| select(startswith("lxc-") | not)
|
||||||
| select(. != "installer")
|
| select(. != "installer")'
|
||||||
| select(. != "proxmox-lxc")'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ ''${#options[@]} -eq 0 ]]; then
|
if [[ ''${#options[@]} -eq 0 ]]; then
|
||||||
@@ -121,21 +119,12 @@
|
|||||||
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if nix eval --refresh --json --extra-experimental-features "flakes nix-command" "''${nix_extra_opts[@]}" "''${FLAKE_BASE_URL}#nixosConfigurations.''${choice}.config.disko.devices.disk.main.device" >/dev/null 2>&1; then
|
# Every host reachable through this menu has a Disko config (lxc-*
|
||||||
disko --mode destroy,format,mount \
|
# is filtered out above, and is the only category that doesn't —
|
||||||
--flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes-wipe-all-disks
|
# see docs/auto-installer.md), so this can run unconditionally: no
|
||||||
else
|
# need to probe the flake first and branch on whether Disko applies.
|
||||||
# No raw disk to partition — true for every LXC container, which has
|
disko --mode destroy,format,mount \
|
||||||
# no block device visible from inside it, only its already-mounted
|
--flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes-wipe-all-disks
|
||||||
# root filesystem. nixos-install defaults to installing into /mnt; if
|
|
||||||
# nothing points /mnt at the real root, the new system gets built and
|
|
||||||
# written to a disconnected empty directory, never actually becomes
|
|
||||||
# bootable, and the container silently keeps running this installer
|
|
||||||
# environment forever. Bind-mount / onto /mnt so the install actually
|
|
||||||
# lands on the filesystem this container boots from.
|
|
||||||
echo "Selected host has no Disko configuration — installing in place via bind mount."
|
|
||||||
mount --bind / /mnt
|
|
||||||
fi
|
|
||||||
|
|
||||||
# sops-nix derives this host's decryption key from its own SSH host key
|
# sops-nix derives this host's decryption key from its own SSH host key
|
||||||
# at *activation* time, which runs before systemd would otherwise
|
# at *activation* time, which runs before systemd would otherwise
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{ modulesPath, ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./common.nix
|
|
||||||
(modulesPath + "/virtualisation/proxmox-lxc.nix")
|
|
||||||
];
|
|
||||||
|
|
||||||
# LXC has no bootloader
|
|
||||||
boot.loader.grub.enable = false;
|
|
||||||
boot.loader.systemd-boot.enable = false;
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,14 @@
|
|||||||
# sudo install -d -m 0700 /root/.ssh
|
# sudo install -d -m 0700 /root/.ssh
|
||||||
# sudo install -m 0600 ./nixremote /root/.ssh/nixremote
|
# sudo install -m 0600 ./nixremote /root/.ssh/nixremote
|
||||||
# sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
# sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
||||||
|
# Trust nix-cache's SSH host key declaratively so the nix-daemon (root)
|
||||||
|
# can connect the first time without a manual ssh-keyscan/known_hosts
|
||||||
|
# step on every new client.
|
||||||
|
programs.ssh.knownHosts.${vars.nixCacheHost} = {
|
||||||
|
hostNames = [ vars.nixCacheHost ];
|
||||||
|
publicKey = vars.nixCacheHostKey;
|
||||||
|
};
|
||||||
|
|
||||||
nix = {
|
nix = {
|
||||||
distributedBuilds = true;
|
distributedBuilds = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
{ config, pkgs, vars, ... }:
|
{ config, pkgs, vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
# Generate the binary cache key pair on the nix-cache host:
|
# nix-serve's signing key has to be the *same* key on every host that
|
||||||
# sudo install -d -m 0700 /etc/nix
|
# ever plays the nix-cache role -- modules/nix-cache/client.nix hardcodes
|
||||||
# sudo nix-store --generate-binary-cache-key nix-cache-1 \
|
# every client's trust in one specific public key ("cache.local-1:..."),
|
||||||
# /etc/nix/cache-priv.pem \
|
# so a freshly self-generated key here wouldn't be trusted by anyone.
|
||||||
# /etc/nix/cache-pub.pem
|
# Managed via sops-nix like every other secret in this repo instead of
|
||||||
# sudo chmod 0600 /etc/nix/cache-priv.pem
|
# the old manual `nix-store --generate-binary-cache-key` step -- see
|
||||||
# sudo chmod 0644 /etc/nix/cache-pub.pem
|
# "Binary cache signing key" in docs/nix-cache.md for how to add/rotate
|
||||||
# cat /etc/nix/cache-pub.pem
|
# the value in secrets/nix-cache.yaml.
|
||||||
|
sops.secrets."cache-priv-key".sopsFile = ../../secrets/nix-cache.yaml;
|
||||||
|
|
||||||
services = {
|
services = {
|
||||||
nix-serve = {
|
nix-serve = {
|
||||||
enable = true;
|
enable = true;
|
||||||
secretKeyFile = "/etc/nix/cache-priv.pem";
|
secretKeyFile = config.sops.secrets."cache-priv-key".path;
|
||||||
};
|
};
|
||||||
|
|
||||||
nginx = {
|
nginx = {
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
openssh.enable = true;
|
openssh.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 ];
|
networking.firewall.allowedTCPPorts = [ vars.ports.nixCacheHttp ];
|
||||||
|
|
||||||
users.groups.${vars.remoteBuilderUser} = { };
|
users.groups.${vars.remoteBuilderUser} = { };
|
||||||
|
|
||||||
@@ -54,6 +56,6 @@
|
|||||||
nix.gc = {
|
nix.gc = {
|
||||||
automatic = true;
|
automatic = true;
|
||||||
dates = "weekly";
|
dates = "weekly";
|
||||||
options = "--delete-older-than 30d";
|
options = "--delete-older-than ${vars.nixCacheGcMaxAge}";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../hardware-configuration/vm/linode.nix
|
../hardware-configuration/vm/linode.nix
|
||||||
|
../disko/linode.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
networking = {
|
networking = {
|
||||||
|
|||||||
+111
-1
@@ -1,5 +1,44 @@
|
|||||||
{ lib, modulesPath, ... }:
|
{ lib, modulesPath, flakeTarget, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# Bakes this exact flake target's pre-generated SSH host key straight
|
||||||
|
# into /etc/ssh/ -- mirrors modules/installer/host-keys.nix's
|
||||||
|
# builtins.getEnv pattern (impure and empty under normal `nix
|
||||||
|
# build`/`nix eval`, so this is a no-op unless explicitly opted into
|
||||||
|
# with NIXOS_HOST_KEYS_DIR=... --impure), but places the key directly
|
||||||
|
# rather than staging it under /etc/host-keys/ for a later manual copy
|
||||||
|
# -- this is the whole system for a `lxc-*` host, built straight to a
|
||||||
|
# pct-restorable tarball with no install step, so there's no later copy
|
||||||
|
# step to stage for.
|
||||||
|
#
|
||||||
|
# Without this, config.system.build.tarball's built-in system just
|
||||||
|
# generates a fresh host key at first boot like any other host would --
|
||||||
|
# but sops-nix derives its decryption key from *this* file, and
|
||||||
|
# .sops.yaml only trusts whatever key scripts/sync-host-keys.sh already
|
||||||
|
# registered for this exact target name. A freshly-generated key can
|
||||||
|
# never match that, so every secret (including this host's own login)
|
||||||
|
# permanently fails to decrypt. Confirmed live: sops-install-secrets
|
||||||
|
# errored with "Error getting data key: 0 successful groups required,
|
||||||
|
# got 0" -- the container's actual host key's age fingerprint didn't
|
||||||
|
# match the one registered in .sops.yaml at all.
|
||||||
|
hostKeysDirStr = builtins.getEnv "NIXOS_HOST_KEYS_DIR";
|
||||||
|
hasHostKeysDir = hostKeysDirStr != "" && builtins.pathExists hostKeysDirStr;
|
||||||
|
hostKeysDir = /. + hostKeysDirStr;
|
||||||
|
|
||||||
|
# flakeTarget ("${platform}-${buildType}") comes in via specialArgs from
|
||||||
|
# flake.nix's mkTarget -- exactly the name scripts/sync-host-keys.sh
|
||||||
|
# registers keys under. Deliberately not read back from
|
||||||
|
# config.environment.etc."flake-target" (which is set to the same value)
|
||||||
|
# -- this module also *contributes* to environment.etc below, and a
|
||||||
|
# module reading the merged value of an option it's still defining is a
|
||||||
|
# circular dependency (confirmed: "infinite recursion encountered").
|
||||||
|
privKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key";
|
||||||
|
pubKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key.pub";
|
||||||
|
hasKeyForThisTarget =
|
||||||
|
hasHostKeysDir
|
||||||
|
&& builtins.pathExists privKeyFile
|
||||||
|
&& builtins.pathExists pubKeyFile;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
# LXC containers share the host kernel — Proxmox starts them by exec'ing
|
# LXC containers share the host kernel — Proxmox starts them by exec'ing
|
||||||
# /sbin/init directly, no bootloader/initrd involved — and Proxmox has its
|
# /sbin/init directly, no bootloader/initrd involved — and Proxmox has its
|
||||||
@@ -35,4 +74,75 @@
|
|||||||
# for the same reason; it just doesn't disable NetworkManager itself,
|
# for the same reason; it just doesn't disable NetworkManager itself,
|
||||||
# which modules/common/configuration.nix enables for every host.
|
# which modules/common/configuration.nix enables for every host.
|
||||||
networking.networkmanager.enable = lib.mkForce false;
|
networking.networkmanager.enable = lib.mkForce false;
|
||||||
|
|
||||||
|
environment.etc = lib.mkIf hasKeyForThisTarget {
|
||||||
|
"ssh/ssh_host_ed25519_key" = {
|
||||||
|
source = privKeyFile;
|
||||||
|
mode = "0600";
|
||||||
|
};
|
||||||
|
"ssh/ssh_host_ed25519_key.pub" = {
|
||||||
|
source = pubKeyFile;
|
||||||
|
mode = "0644";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
|
||||||
|
# store DB via a systemd service (register-nix-paths) -- it never runs
|
||||||
|
# an activation script at all. Confirmed live this means neither
|
||||||
|
# sops-nix's "for users" secrets (password hashes -- installed by the
|
||||||
|
# activation script itself, not a systemd service, since they need to
|
||||||
|
# exist *before* user creation) nor the user-creation step that
|
||||||
|
# consumes them ever run on a real lxc-* boot. Regular secrets
|
||||||
|
# (nix-serve's key, beszel's token, etc.) work anyway because sops-nix
|
||||||
|
# provides its own systemd service for those.
|
||||||
|
#
|
||||||
|
# A systemd service, not boot.postBootCommands: tried that first (it's
|
||||||
|
# a genuine, generally-invoked hook -- nixos/modules/system/boot/stage-2-init.sh,
|
||||||
|
# which becomes this container's actual /sbin/init, unconditionally
|
||||||
|
# runs it) but switch-to-configuration behaves differently that early in
|
||||||
|
# boot (raw stage-2-init.sh, before systemd itself has even started) --
|
||||||
|
# confirmed live it silently failed to rewrite /etc/shadow from there
|
||||||
|
# even in "test" mode, despite the exact same command working reliably
|
||||||
|
# every time when run post-boot (i.e. as a normal systemd service, which
|
||||||
|
# is what this is). Not fully root-caused why the early context
|
||||||
|
# specifically breaks it; a real systemd service sidesteps needing to.
|
||||||
|
#
|
||||||
|
# /etc/shadow already has PLACEHOLDER entries for every declared user
|
||||||
|
# baked in at build time (part of constructing the system closure).
|
||||||
|
# update-users-groups.pl deliberately never overwrites an *existing*
|
||||||
|
# shadow entry -- a correct safety property in general (don't clobber a
|
||||||
|
# real user's real password on a config rebuild) -- but on a genuine
|
||||||
|
# first boot that only means the real hashedPasswordFile-derived hash
|
||||||
|
# never gets the chance to be applied either, since the placeholder is
|
||||||
|
# already "seen". Safe to clear here specifically: there is no real
|
||||||
|
# password yet to protect on a first boot.
|
||||||
|
#
|
||||||
|
# "test" mode, not "boot": confirmed live "boot" mode aborts partway
|
||||||
|
# through (before rewriting /etc/shadow) on a warning that "/boot" is on
|
||||||
|
# a different filesystem -- a real check for a host with a bootloader to
|
||||||
|
# update, meaningless for a container that has none
|
||||||
|
# (boot.loader.{grub,systemd-boot}.enable are both false above), but it
|
||||||
|
# still aborts the script. "test" runs every activation step without
|
||||||
|
# touching boot-loader state at all.
|
||||||
|
#
|
||||||
|
# ConditionPathExists (systemd-native, not a bash-level check) means
|
||||||
|
# this only ever runs once, on the genuine first boot -- systemd itself
|
||||||
|
# skips even starting it on every later boot once the marker exists.
|
||||||
|
# switch-to-configuration is otherwise the operator's call per this
|
||||||
|
# repo's own safety rules, not something to run on every boot.
|
||||||
|
systemd.services.nixos-lxc-first-boot-activate = {
|
||||||
|
description = "Complete first-boot NixOS activation (users, secrets) for this LXC container";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
unitConfig.ConditionPathExists = "!/var/lib/nixos-lxc-first-boot-activated";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
rm -f /etc/shadow
|
||||||
|
/run/current-system/bin/switch-to-configuration test
|
||||||
|
mkdir -p /var/lib
|
||||||
|
touch /var/lib/nixos-lxc-first-boot-activated
|
||||||
|
'';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{ vars, ... }:
|
{ vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
fileSystems."/mnt/raspi" = {
|
fileSystems.${vars.raspiMountpoint} = {
|
||||||
device = "raspberrypi.${vars.tailnetDomain}:/home/raspi/raspi";
|
device = "${vars.raspberryPiHost}.${vars.tailnetDomain}:${vars.raspiNfsPath}";
|
||||||
fsType = "nfs4";
|
fsType = "nfs4";
|
||||||
options = [
|
options = [
|
||||||
"nofail"
|
"nofail"
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
{ vars, ... }:
|
_:
|
||||||
|
|
||||||
{
|
{
|
||||||
services.tailscale = {
|
services.tailscale = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
extraUpFlags = [
|
# 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"
|
"--advertise-exit-node"
|
||||||
"--advertise-routes=${vars.lanCidr}"
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
_:
|
{ vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
services.logrotate = {
|
services.logrotate = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
"/mnt/docker/volumes/traefik-data/logs/*.log" = {
|
"${vars.nfsShares.dockerVolumes.mountpoint}/traefik-data/logs/*.log" = {
|
||||||
daily = true;
|
daily = true;
|
||||||
size = "100M";
|
size = vars.traefikLogRotate.maxSize;
|
||||||
rotate = 20;
|
rotate = vars.traefikLogRotate.keep;
|
||||||
compress = true;
|
compress = true;
|
||||||
missingok = true;
|
missingok = true;
|
||||||
notifempty = true;
|
notifempty = true;
|
||||||
|
|||||||
Executable
+133
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bumps the NixOS release branch this flake tracks — flake.nix's
|
||||||
|
# `nixpkgs.url` and `home-manager.url` — in one place, via targeted
|
||||||
|
# substitution of just those two lines. Deliberately does NOT touch any
|
||||||
|
# `system.stateVersion` anywhere in the repo: per NixOS's own docs, that
|
||||||
|
# value must stay fixed at whatever it was on a host's first install (it
|
||||||
|
# pins on-disk data-format defaults, not "which nixpkgs release am I on"),
|
||||||
|
# so it's never something a channel bump should follow.
|
||||||
|
#
|
||||||
|
# scripts/codex-maintenance.sh's own `nixos-25.11` pin (used only to fetch
|
||||||
|
# nixpkgs-fmt/statix — see CLAUDE.md) is a separate, independently-versioned
|
||||||
|
# reference on purpose: it doesn't have to track the flake's own nixpkgs
|
||||||
|
# input, since the tooling just needs to build, not match. Bump it with
|
||||||
|
# --tooling if you want it moved too; the default run leaves it alone.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
flake_nix="${repo_root}/flake.nix"
|
||||||
|
maintenance_sh="${repo_root}/scripts/codex-maintenance.sh"
|
||||||
|
claude_md="${repo_root}/CLAUDE.md"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 <release> [--tooling <release>]
|
||||||
|
|
||||||
|
<release> New NixOS release for flake.nix's nixpkgs.url and
|
||||||
|
home-manager.url, e.g. 26.11
|
||||||
|
--tooling <release> Also bump scripts/codex-maintenance.sh's separate
|
||||||
|
nixpkgs-fmt/statix pin (and its mention in
|
||||||
|
CLAUDE.md) to this release. Independent of the
|
||||||
|
first argument — pass the same value if you want
|
||||||
|
both in sync, a different one if you don't.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$0 26.11
|
||||||
|
$0 26.11 --tooling 26.11
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
release_re='^[0-9]{2}\.(05|11)$'
|
||||||
|
|
||||||
|
if [[ $# -eq 0 || "$1" == "-h" || "$1" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
new_release="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
tooling_release=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--tooling)
|
||||||
|
tooling_release="${2:?--tooling requires a release argument}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown argument: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
check_release_format() {
|
||||||
|
local release="$1"
|
||||||
|
if [[ ! "$release" =~ $release_re ]]; then
|
||||||
|
echo "ERROR: '$release' doesn't look like a NixOS release (expected e.g. 26.11)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_branch_exists() {
|
||||||
|
local repo_url="$1" branch="$2"
|
||||||
|
echo "Checking '$branch' exists on $repo_url..."
|
||||||
|
if ! git ls-remote --exit-code --heads "$repo_url" "$branch" >/dev/null; then
|
||||||
|
echo "ERROR: branch '$branch' not found on $repo_url. Typo, or not cut yet?" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_release_format "$new_release"
|
||||||
|
|
||||||
|
current_release="$(grep -oE 'nixos-[0-9]{2}\.[0-9]{2}' "$flake_nix" | head -1 | sed 's/^nixos-//')"
|
||||||
|
if [[ -z "$current_release" ]]; then
|
||||||
|
echo "ERROR: couldn't find flake.nix's current nixpkgs release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$current_release" == "$new_release" ]]; then
|
||||||
|
echo "flake.nix is already on $new_release."
|
||||||
|
else
|
||||||
|
echo "Bumping flake.nix's nixpkgs/home-manager release: $current_release -> $new_release"
|
||||||
|
check_branch_exists "https://github.com/NixOS/nixpkgs.git" "nixos-$new_release"
|
||||||
|
check_branch_exists "https://github.com/nix-community/home-manager.git" "release-$new_release"
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
-e "s|github:NixOS/nixpkgs/nixos-${current_release}|github:NixOS/nixpkgs/nixos-${new_release}|" \
|
||||||
|
-e "s|github:nix-community/home-manager/release-${current_release}|github:nix-community/home-manager/release-${new_release}|" \
|
||||||
|
"$flake_nix"
|
||||||
|
|
||||||
|
echo "Updated:"
|
||||||
|
grep -n "nixos-${new_release}\|release-${new_release}" "$flake_nix"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$tooling_release" ]]; then
|
||||||
|
check_release_format "$tooling_release"
|
||||||
|
|
||||||
|
current_tooling_release="$(grep -oE 'nixos-[0-9]{2}\.[0-9]{2}' "$maintenance_sh" | head -1 | sed 's/^nixos-//')"
|
||||||
|
|
||||||
|
if [[ "$current_tooling_release" == "$tooling_release" ]]; then
|
||||||
|
echo "codex-maintenance.sh's tooling pin is already on $tooling_release."
|
||||||
|
else
|
||||||
|
echo "Bumping codex-maintenance.sh's nixpkgs-fmt/statix pin: $current_tooling_release -> $tooling_release"
|
||||||
|
check_branch_exists "https://github.com/NixOS/nixpkgs.git" "nixos-$tooling_release"
|
||||||
|
|
||||||
|
sed -i "s|github:NixOS/nixpkgs/nixos-${current_tooling_release}|github:NixOS/nixpkgs/nixos-${tooling_release}|g" \
|
||||||
|
"$maintenance_sh"
|
||||||
|
sed -i "s|nixos-${current_tooling_release}|nixos-${tooling_release}|g" \
|
||||||
|
"$claude_md"
|
||||||
|
|
||||||
|
echo "Updated:"
|
||||||
|
grep -n "nixos-${tooling_release}" "$maintenance_sh" "$claude_md"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "flake.lock still points at the old input revisions until refreshed. Either:"
|
||||||
|
echo " nix flake update nixpkgs home-manager # just these two inputs"
|
||||||
|
echo " nix flake update # everything — see docs/flake-lock-automation.md"
|
||||||
|
echo
|
||||||
|
echo "Then run 'bash scripts/codex-maintenance.sh dry-run' before committing —"
|
||||||
|
echo "a channel bump can shift option defaults across every host."
|
||||||
@@ -1,22 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
export NIX_CONFIG="${NIX_CONFIG:-}
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
experimental-features = nix-command flakes
|
# shellcheck source=lib/nix-bootstrap.sh
|
||||||
accept-flake-config = false
|
source "${script_dir}/lib/nix-bootstrap.sh"
|
||||||
warn-dirty = false
|
# shellcheck source=lib/nix-eval.sh
|
||||||
"
|
source "${script_dir}/lib/nix-eval.sh"
|
||||||
|
|
||||||
MODE="${1:-validate}"
|
MODE="${1:-validate}"
|
||||||
|
|
||||||
ensure_nix_profile() {
|
|
||||||
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
|
||||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
|
||||||
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
|
||||||
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_nix_profile
|
ensure_nix_profile
|
||||||
|
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
@@ -24,8 +16,7 @@ if ! command -v nix >/dev/null 2>&1; then
|
|||||||
exit 127
|
exit 127
|
||||||
fi
|
fi
|
||||||
|
|
||||||
hosts_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames)"
|
hosts="$(list_flake_targets .)"
|
||||||
hosts="$(echo "$hosts_json" | jq -r '.[]')"
|
|
||||||
|
|
||||||
echo "Hosts:"
|
echo "Hosts:"
|
||||||
echo "$hosts"
|
echo "$hosts"
|
||||||
@@ -44,44 +35,41 @@ fi
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Checking Nix formatting with nixpkgs-fmt..."
|
echo "Checking Nix formatting with nixpkgs-fmt..."
|
||||||
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
|
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Running statix lint..."
|
echo "Running statix lint..."
|
||||||
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#statix -- check .
|
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check .
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Evaluating host toplevel derivations..."
|
echo "Evaluating host toplevel derivations..."
|
||||||
for host in $hosts; do
|
for host in $hosts; do
|
||||||
echo "==> $host"
|
echo "==> $host"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#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
|
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of
|
||||||
# nixos-install (see docs/auto-installer.md); proxmox-* hosts (excluding
|
# nixos-install (see docs/auto-installer.md); proxmox-* hosts can
|
||||||
# proxmox-lxc, the installer's own LXC target, which has no disko config)
|
# alternatively be built as a standalone disk image (see
|
||||||
# can alternatively be built as a standalone disk image (see
|
|
||||||
# docs/proxmox-images.md). Both are otherwise-unvalidated buildable
|
# docs/proxmox-images.md). Both are otherwise-unvalidated buildable
|
||||||
# surface, easy to silently break without this.
|
# surface, easy to silently break without this.
|
||||||
case "$host" in
|
case "$host" in
|
||||||
lxc-*)
|
lxc-*)
|
||||||
echo "==> $host (tarball)"
|
echo "==> $host (tarball)"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
|
||||||
;;
|
;;
|
||||||
proxmox-lxc) ;;
|
|
||||||
proxmox-*)
|
proxmox-*)
|
||||||
echo "==> $host (diskoImagesScript)"
|
echo "==> $host (diskoImagesScript)"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Evaluating buildable packages..."
|
echo "Evaluating buildable packages..."
|
||||||
packages_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#packages.x86_64-linux --apply builtins.attrNames)"
|
packages="$(nix eval --json "${NIX_EVAL_FLAGS[@]}" .#packages.x86_64-linux --apply builtins.attrNames | jq -r '.[]')"
|
||||||
packages="$(echo "$packages_json" | jq -r '.[]')"
|
|
||||||
for pkg in $packages; do
|
for pkg in $packages; do
|
||||||
echo "==> packages.x86_64-linux.${pkg}"
|
echo "==> packages.x86_64-linux.${pkg}"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "$MODE" == "dry-run" ]]; then
|
if [[ "$MODE" == "dry-run" ]]; then
|
||||||
@@ -89,17 +77,16 @@ if [[ "$MODE" == "dry-run" ]]; then
|
|||||||
echo "Running dry-run builds for all hosts. This will not create result symlinks."
|
echo "Running dry-run builds for all hosts. This will not create result symlinks."
|
||||||
for host in $hosts; do
|
for host in $hosts; do
|
||||||
echo "==> Dry-run build: $host"
|
echo "==> Dry-run build: $host"
|
||||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#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)"
|
echo "==> Dry-run build: $host (tarball)"
|
||||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball"
|
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball"
|
||||||
;;
|
;;
|
||||||
proxmox-lxc) ;;
|
|
||||||
proxmox-*)
|
proxmox-*)
|
||||||
echo "==> Dry-run build: $host (diskoImagesScript)"
|
echo "==> Dry-run build: $host (diskoImagesScript)"
|
||||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
|
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -108,7 +95,7 @@ if [[ "$MODE" == "dry-run" ]]; then
|
|||||||
echo "Running dry-run builds for all packages."
|
echo "Running dry-run builds for all packages."
|
||||||
for pkg in $packages; do
|
for pkg in $packages; do
|
||||||
echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
|
echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
|
||||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}"
|
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+7
-15
@@ -1,19 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
export NIX_CONFIG="${NIX_CONFIG:-}
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
experimental-features = nix-command flakes
|
# shellcheck source=lib/nix-bootstrap.sh
|
||||||
accept-flake-config = false
|
source "${script_dir}/lib/nix-bootstrap.sh"
|
||||||
warn-dirty = false
|
# shellcheck source=lib/nix-eval.sh
|
||||||
"
|
source "${script_dir}/lib/nix-eval.sh"
|
||||||
|
|
||||||
ensure_nix_profile() {
|
|
||||||
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
|
||||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
|
||||||
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
|
||||||
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_nix_if_missing() {
|
install_nix_if_missing() {
|
||||||
if command -v nix >/dev/null 2>&1; then
|
if command -v nix >/dev/null 2>&1; then
|
||||||
@@ -79,13 +71,13 @@ if ! command -v jq >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Available NixOS hosts:"
|
echo "Available NixOS hosts:"
|
||||||
hosts="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')"
|
hosts="$(list_flake_targets .)"
|
||||||
echo "$hosts"
|
echo "$hosts"
|
||||||
|
|
||||||
echo "Evaluating all host toplevel derivations..."
|
echo "Evaluating all host toplevel derivations..."
|
||||||
for host in $hosts; do
|
for host in $hosts; do
|
||||||
echo "==> Evaluating $host"
|
echo "==> Evaluating $host"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Codex setup complete."
|
echo "Codex setup complete."
|
||||||
Executable
+560
@@ -0,0 +1,560 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Creates new Proxmox VMs/LXC containers from this flake, and reconfigures
|
||||||
|
# existing ones -- the manual workflows in docs/proxmox-images.md (VM) and
|
||||||
|
# docs/auto-installer.md's "LXC hosts" section (container), automated.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/create-proxmox-resource.sh --type lxc|vm --host <name> [options]
|
||||||
|
# scripts/create-proxmox-resource.sh --type lxc|vm --list
|
||||||
|
# scripts/create-proxmox-resource.sh --modify --vmid <n> [--cores N] [--memory MB] [--grow-disk GB]
|
||||||
|
#
|
||||||
|
# SAFETY:
|
||||||
|
# - The default (create) mode only ever creates a NEW resource -- it
|
||||||
|
# refuses to run if the target VMID already exists on the node, or if
|
||||||
|
# a VM/CT identified as --host already exists under any other VMID
|
||||||
|
# (checked live against the node; --allow-duplicate-host overrides).
|
||||||
|
# - --modify only ever touches a resource you name explicitly via
|
||||||
|
# --vmid, shows exactly what will change first, and (outside
|
||||||
|
# --dry-run) always requires typing that VMID back to confirm before
|
||||||
|
# anything is sent to the node. There is no bulk/implicit modify.
|
||||||
|
# - Neither mode can start/stop/delete a resource. Not implemented on
|
||||||
|
# purpose -- ask before adding it.
|
||||||
|
#
|
||||||
|
# See --help for the full option list.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=lib/nix-eval.sh
|
||||||
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
|
|
||||||
|
sync_keys="${repo_root}/scripts/sync-host-keys.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 --type lxc|vm --host <name> [options] (create)
|
||||||
|
$0 --type lxc|vm --list (list --host values)
|
||||||
|
$0 --modify --vmid <n> [options] (reconfigure)
|
||||||
|
|
||||||
|
Create mode (default):
|
||||||
|
--type lxc|vm lxc = container, built as a CT template tarball.
|
||||||
|
vm = VM, built as a Disko .raw disk image (UEFI/OVMF).
|
||||||
|
--host <name> Which host identity to deploy -- matches
|
||||||
|
config.networking.hostName (server, docker,
|
||||||
|
nix-cache, nixos, pxe-boot, nix-minimal). Use
|
||||||
|
--list to see what's available for --type.
|
||||||
|
--name <name> Proxmox display name/hostname (default: --host's
|
||||||
|
value, e.g. nix-cache -- for lxc this becomes the
|
||||||
|
guest's real networking.hostName too, since
|
||||||
|
proxmoxLXC.manageHostName pulls it from Proxmox's
|
||||||
|
own container config, so it must match host.nix
|
||||||
|
regardless of build type)
|
||||||
|
--vmid <n> Numeric VMID (default: next free, via
|
||||||
|
\`pvesh get /cluster/nextid\` on the node).
|
||||||
|
Refuses to run if this ID already exists.
|
||||||
|
--disk-size <GB> lxc only: rootfs size for \`pct create\`
|
||||||
|
(default: \$PROXMOX_DEFAULT_LXC_DISK_GB, ${PROXMOX_DEFAULT_LXC_DISK_GB}).
|
||||||
|
--image <path> Use this local image/tarball instead of
|
||||||
|
checking the node / building one from the flake.
|
||||||
|
--force-rebuild Skip the "does the node already have this
|
||||||
|
image" check -- always build fresh and
|
||||||
|
overwrite what's there.
|
||||||
|
--allow-duplicate-host Required if a VM/CT identified as --host
|
||||||
|
already exists on the node (checked live via
|
||||||
|
qm/pct, not any file in this repo) --
|
||||||
|
otherwise refused, since it'd share that
|
||||||
|
host's hostName/hostId.
|
||||||
|
|
||||||
|
Modify mode (reconfigure an EXISTING resource -- requires --modify):
|
||||||
|
--modify Switch to modify mode.
|
||||||
|
--vmid <n> Required: which existing resource to change.
|
||||||
|
Type/VM-vs-CT is auto-detected on the node.
|
||||||
|
--grow-disk <GB> Grow the primary disk by this many GB
|
||||||
|
(qm/pct resize; Proxmox only supports
|
||||||
|
growing, never shrinking, an existing disk).
|
||||||
|
At least one of --cores / --memory / --grow-disk is required. Always
|
||||||
|
prints the current -> new values and requires typing the VMID back to
|
||||||
|
confirm, even outside --dry-run.
|
||||||
|
|
||||||
|
Shared:
|
||||||
|
--cores <n> create: default \$PROXMOX_DEFAULT_CORES (${PROXMOX_DEFAULT_CORES}).
|
||||||
|
modify: omit to leave unchanged.
|
||||||
|
--memory <MB> create: default \$PROXMOX_DEFAULT_MEMORY_MB (${PROXMOX_DEFAULT_MEMORY_MB}).
|
||||||
|
modify: omit to leave unchanged.
|
||||||
|
--swap <MB> lxc only, create time: \`--memory\` doesn't
|
||||||
|
touch swap -- it silently stays at Proxmox's
|
||||||
|
own 512M default otherwise. (default: matches
|
||||||
|
whatever --memory resolves to)
|
||||||
|
--storage <pool> (default: \$PROXMOX_STORAGE, ${PROXMOX_STORAGE})
|
||||||
|
--iso-storage <pool> (default: \$PROXMOX_ISO_STORAGE, ${PROXMOX_ISO_STORAGE})
|
||||||
|
--bridge <bridge> (default: \$PROXMOX_BRIDGE, ${PROXMOX_BRIDGE})
|
||||||
|
--node <host> Proxmox node to SSH into (default:
|
||||||
|
\$PROXMOX_HOST, ${PROXMOX_HOST})
|
||||||
|
--dry-run Print the full plan; touch nothing
|
||||||
|
local or remote, no prompts.
|
||||||
|
-h, --help
|
||||||
|
|
||||||
|
Config for --storage/--bridge/--node/etc. lives in scripts/env.sh -- edit
|
||||||
|
that instead of passing the same flag every time.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
modify=0
|
||||||
|
type=""
|
||||||
|
host=""
|
||||||
|
name=""
|
||||||
|
vmid=""
|
||||||
|
cores=""
|
||||||
|
memory=""
|
||||||
|
swap=""
|
||||||
|
disk_size=""
|
||||||
|
grow_disk=""
|
||||||
|
image=""
|
||||||
|
storage="$PROXMOX_STORAGE"
|
||||||
|
iso_storage="$PROXMOX_ISO_STORAGE"
|
||||||
|
bridge="$PROXMOX_BRIDGE"
|
||||||
|
node="$PROXMOX_HOST"
|
||||||
|
do_list=0
|
||||||
|
allow_duplicate_host=0
|
||||||
|
force_rebuild=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--type) type="$2"; shift 2 ;;
|
||||||
|
--host) host="$2"; shift 2 ;;
|
||||||
|
--name) name="$2"; shift 2 ;;
|
||||||
|
--vmid) vmid="$2"; shift 2 ;;
|
||||||
|
--cores) cores="$2"; shift 2 ;;
|
||||||
|
--memory) memory="$2"; shift 2 ;;
|
||||||
|
--swap) swap="$2"; shift 2 ;;
|
||||||
|
--disk-size) disk_size="$2"; shift 2 ;;
|
||||||
|
--grow-disk) grow_disk="$2"; shift 2 ;;
|
||||||
|
--image) image="$2"; shift 2 ;;
|
||||||
|
--storage) storage="$2"; shift 2 ;;
|
||||||
|
--iso-storage) iso_storage="$2"; shift 2 ;;
|
||||||
|
--bridge) bridge="$2"; shift 2 ;;
|
||||||
|
--node) node="$2"; shift 2 ;;
|
||||||
|
--list) do_list=1; shift ;;
|
||||||
|
--allow-duplicate-host) allow_duplicate_host=1; shift ;;
|
||||||
|
--force-rebuild) force_rebuild=1; shift ;;
|
||||||
|
--modify) modify=1; shift ;;
|
||||||
|
--dry-run) dry_run=1; shift ;;
|
||||||
|
-h | --help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
ssh_target="${PROXMOX_SSH_USER}@${node}"
|
||||||
|
|
||||||
|
remote() {
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] ssh ${ssh_target} -- $*"
|
||||||
|
else
|
||||||
|
ssh "$ssh_target" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================ modify mode
|
||||||
|
cmd_modify() {
|
||||||
|
if [[ -z "$vmid" ]]; then
|
||||||
|
echo "ERROR: --modify requires --vmid." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$cores" && -z "$memory" && -z "$grow_disk" ]]; then
|
||||||
|
echo "ERROR: --modify needs at least one of --cores / --memory / --grow-disk." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Looking up VMID ${vmid} on ${node}..."
|
||||||
|
local kind current_cores current_memory disk_key
|
||||||
|
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1; then
|
||||||
|
kind="vm"
|
||||||
|
disk_key="scsi0"
|
||||||
|
elif ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
||||||
|
kind="lxc"
|
||||||
|
disk_key="rootfs"
|
||||||
|
else
|
||||||
|
echo "ERROR: VMID ${vmid} doesn't exist on ${node} -- nothing to modify." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local config_cmd="qm config ${vmid}"
|
||||||
|
[[ "$kind" == "lxc" ]] && config_cmd="pct config ${vmid}"
|
||||||
|
local current_config
|
||||||
|
current_config="$(ssh "$ssh_target" "$config_cmd")"
|
||||||
|
current_cores="$(echo "$current_config" | grep -oP '^cores:\s*\K\S+' || echo '?')"
|
||||||
|
current_memory="$(echo "$current_config" | grep -oP '^memory:\s*\K\S+' || echo '?')"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "VMID ${vmid} is a ${kind} on ${node}. Planned changes:"
|
||||||
|
[[ -n "$cores" ]] && echo " cores: ${current_cores} -> ${cores}"
|
||||||
|
[[ -n "$memory" ]] && echo " memory: ${current_memory} MB -> ${memory} MB"
|
||||||
|
[[ -n "$grow_disk" ]] && echo " ${disk_key}: grow by +${grow_disk}G (Proxmox can only grow, not shrink, an existing disk)"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
echo "[dry-run] Nothing was changed."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
read -rp "Type the VMID (${vmid}) to confirm these changes: " confirm
|
||||||
|
if [[ "$confirm" != "$vmid" ]]; then
|
||||||
|
echo "Cancelled -- input didn't match ${vmid}."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local set_cmd="qm set"
|
||||||
|
local resize_cmd="qm resize"
|
||||||
|
[[ "$kind" == "lxc" ]] && set_cmd="pct set" && resize_cmd="pct resize"
|
||||||
|
|
||||||
|
if [[ -n "$cores" || -n "$memory" ]]; then
|
||||||
|
local args=""
|
||||||
|
[[ -n "$cores" ]] && args="${args} --cores ${cores}"
|
||||||
|
[[ -n "$memory" ]] && args="${args} --memory ${memory}"
|
||||||
|
remote "${set_cmd} ${vmid}${args}"
|
||||||
|
fi
|
||||||
|
if [[ -n "$grow_disk" ]]; then
|
||||||
|
remote "${resize_cmd} ${vmid} ${disk_key} +${grow_disk}G"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done. VMID ${vmid} updated."
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$modify" -eq 1 ]]; then
|
||||||
|
cmd_modify
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================= create mode
|
||||||
|
if [[ "$type" != "lxc" && "$type" != "vm" ]]; then
|
||||||
|
echo "ERROR: --type must be 'lxc' or 'vm'." >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
platform_prefix="lxc"
|
||||||
|
[[ "$type" == "vm" ]] && platform_prefix="proxmox"
|
||||||
|
[[ -z "$cores" ]] && cores="$PROXMOX_DEFAULT_CORES"
|
||||||
|
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
|
||||||
|
|
||||||
|
# --- discover / resolve the flake target from --host --------------------
|
||||||
|
# Emits "<target>\t<hostName>" pairs for every ${platform_prefix}-* flake
|
||||||
|
# target -- the one source both --list and the --host lookup below read
|
||||||
|
# from, so they can never see a different set of targets from each other.
|
||||||
|
targets_for_platform() {
|
||||||
|
local target
|
||||||
|
for target in $(list_flake_targets "$repo_root" 2>/dev/null | grep -- "^${platform_prefix}-"); do
|
||||||
|
printf '%s\t%s\n' "$target" "$(flake_target_hostname "$repo_root" "$target")"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
list_hosts() {
|
||||||
|
local target hostname
|
||||||
|
while IFS=$'\t' read -r target hostname; do
|
||||||
|
printf ' %-12s -> %s\n' "$hostname" "$target"
|
||||||
|
done < <(targets_for_platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$do_list" -eq 1 ]]; then
|
||||||
|
echo "Available --host values for --type ${type}:"
|
||||||
|
list_hosts
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$host" ]]; then
|
||||||
|
echo "ERROR: --host is required (or use --list to see options)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
flake_target=""
|
||||||
|
while IFS=$'\t' read -r target hostname; do
|
||||||
|
if [[ "$hostname" == "$host" ]]; then
|
||||||
|
flake_target="$target"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done < <(targets_for_platform)
|
||||||
|
|
||||||
|
if [[ -z "$flake_target" ]]; then
|
||||||
|
echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2
|
||||||
|
echo "Available:" >&2
|
||||||
|
list_hosts >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The container/VM's real identity is --host (e.g. "nix-cache"), validated
|
||||||
|
# above against config.networking.hostName -- not the flake target name
|
||||||
|
# (e.g. "lxc-nix-cache"), which is build-type-specific and only exists to
|
||||||
|
# pick which platform variant to build. Defaulting --name to the flake
|
||||||
|
# target would make lxc's --hostname (which proxmoxLXC.manageHostName
|
||||||
|
# feeds straight into the guest's real hostname) disagree with host.nix.
|
||||||
|
[[ -z "$name" ]] && name="$host"
|
||||||
|
|
||||||
|
# --- refuse to duplicate a host that's already live on the node ---------
|
||||||
|
# Queries the node itself (qm/pct's own name/hostname config), not any
|
||||||
|
# static list in this repo -- a file can't track whether a resource still
|
||||||
|
# actually exists, and this used to be checked against variables.nix's
|
||||||
|
# deployedTargets, which drifted stale (it kept naming a VM as "the real
|
||||||
|
# deployment" well after that VM had been destroyed, blocking its own
|
||||||
|
# redeploy) until that list was dropped in favour of this live check. This
|
||||||
|
# only catches guests identified with the default --name (== --host, what
|
||||||
|
# this script itself always uses unless --name is overridden) -- a guest
|
||||||
|
# manually renamed on the node afterwards wouldn't match, but nothing here
|
||||||
|
# creates guests that way.
|
||||||
|
if [[ "$allow_duplicate_host" -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
echo "--allow-duplicate-host: skipping the check for an existing '${host}' on ${node}."
|
||||||
|
elif [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
echo "[dry-run] would check ${node} for an existing VM/CT identified as '${host}'"
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
|
||||||
|
ssh_check_status=0
|
||||||
|
existing="$(ssh "$ssh_target" bash -s -- "$host" <<'REMOTE_SCRIPT'
|
||||||
|
target="$1"
|
||||||
|
for id in $(qm list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||||
|
n="$(qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
|
||||||
|
[[ "$n" == "$target" ]] && echo "vm ${id} ${n}"
|
||||||
|
done
|
||||||
|
for id in $(pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||||
|
n="$(pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
||||||
|
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
|
||||||
|
done
|
||||||
|
REMOTE_SCRIPT
|
||||||
|
)" || ssh_check_status=$?
|
||||||
|
if [[ "$ssh_check_status" -ne 0 ]]; then
|
||||||
|
echo "ERROR: couldn't reach ${node} (ssh exited ${ssh_check_status}) to check for an" >&2
|
||||||
|
echo "existing '${host}' resource -- refusing to guess. Fix connectivity and retry," >&2
|
||||||
|
echo "or pass --allow-duplicate-host if you're sure none exists (this skips the" >&2
|
||||||
|
echo "check entirely)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$existing" ]]; then
|
||||||
|
echo "ERROR: '${host}' already exists on ${node}:" >&2
|
||||||
|
echo "$existing" | while read -r kind id n; do
|
||||||
|
echo " - ${kind} VMID ${id} (${n})" >&2
|
||||||
|
done
|
||||||
|
echo "Refusing to create a second resource sharing this identity. Pass" >&2
|
||||||
|
echo "--allow-duplicate-host to create one anyway (it gets its own distinct" >&2
|
||||||
|
echo "sops key and VMID -- the existing resource(s) above are left untouched)," >&2
|
||||||
|
echo "or use --modify to reconfigure the existing one instead." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'"
|
||||||
|
|
||||||
|
# Decide on nix-cache once, here -- this is the earliest point that needs
|
||||||
|
# it (sync-host-keys.sh below needs nix-shell packages regardless of
|
||||||
|
# whether an image ends up getting built later), and the decision is
|
||||||
|
# exported so that subprocess -- and this script's own later build step,
|
||||||
|
# if it gets there -- both reuse it instead of probing again.
|
||||||
|
nix_extra_opts
|
||||||
|
|
||||||
|
# --- make sure this target has a registered host key --------------------
|
||||||
|
echo
|
||||||
|
echo "==> Ensuring host key exists and is registered..."
|
||||||
|
sync_args=("$flake_target")
|
||||||
|
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
|
||||||
|
bash "$sync_keys" "${sync_args[@]}"
|
||||||
|
|
||||||
|
# --- VMID: pick one, and refuse to touch anything that already exists ---
|
||||||
|
echo
|
||||||
|
if [[ -z "$vmid" ]]; then
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
vmid="<next-free-vmid>"
|
||||||
|
echo "[dry-run] would ask ${node} for the next free VMID (pvesh get /cluster/nextid)"
|
||||||
|
else
|
||||||
|
vmid="$(ssh "$ssh_target" "pvesh get /cluster/nextid" | tr -d '[:space:]')"
|
||||||
|
echo "Auto-assigned VMID: ${vmid}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Requested VMID: ${vmid}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 0 ]]; then
|
||||||
|
# qm/pct status exits non-zero (and prints "does not exist") for a free
|
||||||
|
# ID on that resource type -- but a VMID could exist as the OTHER
|
||||||
|
# resource type (e.g. requested a CT id that's actually a VM), so check
|
||||||
|
# both. Any success here means something is already using this ID --
|
||||||
|
# refuse to go anywhere near it. (Reconfiguring an existing resource is
|
||||||
|
# --modify's job, not this one's.)
|
||||||
|
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1 \
|
||||||
|
|| ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: VMID ${vmid} already exists on ${node}. Refusing to touch an" >&2
|
||||||
|
echo "existing resource here -- use --modify to reconfigure it, pick a" >&2
|
||||||
|
echo "different --vmid, or omit it to auto-assign." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- resolve the remote path -- fixed naming (not the nix store's own
|
||||||
|
# derivation-hash-based filename), so a later run can check for it by name.
|
||||||
|
# lxc uploads as a CT *template* (Proxmox's "vztmpl" content type, under
|
||||||
|
# iso_storage) -- config.system.build.tarball is a plain rootfs tarball,
|
||||||
|
# not a vzdump backup archive, so it's created with `pct create ... vztmpl`,
|
||||||
|
# not restored with `pct restore` (that expects backup-archive metadata
|
||||||
|
# this tarball doesn't have, and fails with "archive contains no
|
||||||
|
# configuration file").
|
||||||
|
remote_dir="/var/lib/vz/import"
|
||||||
|
remote_filename="${flake_target}.raw"
|
||||||
|
if [[ "$type" == "lxc" ]]; then
|
||||||
|
remote_dir="/var/lib/vz/template/cache"
|
||||||
|
remote_filename="${flake_target}.tar.xz"
|
||||||
|
fi
|
||||||
|
remote_path="${remote_dir}/${remote_filename}"
|
||||||
|
|
||||||
|
# --- build (or reuse an image already on the node) ------------------------
|
||||||
|
echo
|
||||||
|
local_image=""
|
||||||
|
image_already_remote=0
|
||||||
|
|
||||||
|
if [[ -n "$image" ]]; then
|
||||||
|
[[ -f "$image" ]] || { echo "ERROR: --image '${image}' not found." >&2; exit 1; }
|
||||||
|
local_image="$image"
|
||||||
|
echo "Using provided image: ${local_image}"
|
||||||
|
elif [[ "$force_rebuild" -eq 1 ]]; then
|
||||||
|
echo "--force-rebuild: skipping the existing-image check on ${node}."
|
||||||
|
else
|
||||||
|
echo "==> Checking whether ${node} already has ${remote_path}..."
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would check: ssh ${ssh_target} -- test -f ${remote_path}"
|
||||||
|
elif ssh "$ssh_target" "test -f '${remote_path}'" 2>/dev/null; then
|
||||||
|
echo "Found it -- reusing, skipping build and upload (use --force-rebuild to override)."
|
||||||
|
image_already_remote=1
|
||||||
|
else
|
||||||
|
echo "Not found -- will build."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
|
||||||
|
# Mirrors the real build commands' "${NIX_OPTS[@]}" below -- nix_extra_opts
|
||||||
|
# (called earlier, once) has already decided whether nix-cache is in play,
|
||||||
|
# and the dry-run preview needs to reflect that decision instead of always
|
||||||
|
# printing the same command regardless of outcome.
|
||||||
|
nix_opts_display=""
|
||||||
|
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
|
||||||
|
printf -v nix_opts_display '%q ' "${NIX_OPTS[@]}"
|
||||||
|
nix_opts_display=" ${nix_opts_display% }"
|
||||||
|
fi
|
||||||
|
if [[ "$type" == "lxc" ]]; then
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would build: NIXOS_HOST_KEYS_DIR=${repo_root}/host-keys nix build --impure \\"
|
||||||
|
echo "[dry-run] --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||||
|
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.tarball"
|
||||||
|
local_image="<built-tarball>"
|
||||||
|
else
|
||||||
|
echo "==> Building LXC tarball for ${flake_target}..."
|
||||||
|
NIXOS_HOST_KEYS_DIR="${repo_root}/host-keys" nix build --impure \
|
||||||
|
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||||
|
".#nixosConfigurations.${flake_target}.config.system.build.tarball" \
|
||||||
|
--out-link "${repo_root}/result-${flake_target}"
|
||||||
|
local_image="$(find "${repo_root}/result-${flake_target}/tarball" -maxdepth 1 -type f | head -1)"
|
||||||
|
echo "Built: ${local_image}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would build: nix build --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||||
|
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
|
||||||
|
echo "[dry-run] would run: sudo ./result-${flake_target} \\"
|
||||||
|
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \\"
|
||||||
|
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \\"
|
||||||
|
echo "[dry-run] --build-memory 2048"
|
||||||
|
local_image="<built-image>.raw"
|
||||||
|
else
|
||||||
|
echo "==> Building Disko image script for ${flake_target}..."
|
||||||
|
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||||
|
".#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript" \
|
||||||
|
--out-link "${repo_root}/result-${flake_target}"
|
||||||
|
echo "==> Running it (builds the .raw image in a temporary QEMU VM, needs sudo)..."
|
||||||
|
( cd "$repo_root" && sudo "./result-${flake_target}" \
|
||||||
|
--pre-format-files "host-keys/${flake_target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
||||||
|
--pre-format-files "host-keys/${flake_target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
||||||
|
--build-memory 2048 )
|
||||||
|
local_image="$(find "$repo_root" -maxdepth 1 -name "*.raw" -newer "${repo_root}/result-${flake_target}" | head -1)"
|
||||||
|
if [[ -z "$local_image" ]]; then
|
||||||
|
echo "ERROR: expected a .raw image after the build but didn't find one in ${repo_root}." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Built: ${local_image}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- upload (skip entirely if reusing an image already on the node) ------
|
||||||
|
echo
|
||||||
|
if [[ "$image_already_remote" -eq 1 ]]; then
|
||||||
|
: # nothing to upload
|
||||||
|
elif [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would upload: scp ${local_image} ${ssh_target}:${remote_path}"
|
||||||
|
else
|
||||||
|
echo "==> Uploading to ${node}:${remote_path}..."
|
||||||
|
ssh "$ssh_target" "mkdir -p ${remote_dir}"
|
||||||
|
scp "$local_image" "${ssh_target}:${remote_path}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- create -----------------------------------------------------------------
|
||||||
|
echo
|
||||||
|
if [[ "$type" == "lxc" ]]; then
|
||||||
|
echo "==> Creating LXC container ${vmid} (${name})..."
|
||||||
|
local_disk_size="${disk_size:-$PROXMOX_DEFAULT_LXC_DISK_GB}"
|
||||||
|
# --memory doesn't touch swap -- it silently stays at Proxmox's own
|
||||||
|
# 512M default otherwise (confirmed live: --memory 2048 left swap at
|
||||||
|
# 512). Default to matching whatever --memory resolved to above.
|
||||||
|
local_swap="${swap:-$memory}"
|
||||||
|
# --unprivileged 1: modules/platforms/lxc.nix sets proxmoxLXC.privileged
|
||||||
|
# = false, so the NixOS config inside the image assumes it's running as
|
||||||
|
# an unprivileged container (cgroup/capability/mount expectations baked
|
||||||
|
# in at boot). `pct create`'s own CLI default for this flag is
|
||||||
|
# privileged (unlike the web UI, which defaults its checkbox the other
|
||||||
|
# way) -- leaving it unset creates a privileged container running a
|
||||||
|
# NixOS config that assumes unprivileged, a real mismatch.
|
||||||
|
#
|
||||||
|
# --features nesting=1,keyctl=1: required for a modern (v247+) systemd
|
||||||
|
# guest to actually boot unprivileged -- confirmed live: without this,
|
||||||
|
# AppArmor denies the nested user namespaces and credential mounts
|
||||||
|
# systemd routinely uses (even plain getty units), and every getty
|
||||||
|
# crash-loops on a denied mount every ~3s (visible as garbage on the
|
||||||
|
# console) while core services like nsncd fail the same way.
|
||||||
|
create_cmd="pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged 1 --features ${PROXMOX_DEFAULT_LXC_FEATURES} --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
|
||||||
|
remote "$create_cmd"
|
||||||
|
remote "pct start ${vmid}"
|
||||||
|
else
|
||||||
|
echo "==> Creating VM ${vmid} (${name})..."
|
||||||
|
# pre-enrolled-keys=0 disables OVMF's Secure Boot key pre-enrollment --
|
||||||
|
# required, or systemd-boot (unsigned) can't be trusted by the firmware.
|
||||||
|
remote "qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
|
||||||
|
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
|
||||||
|
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] ssh ${ssh_target} -- qm importdisk ${vmid} ${remote_path} ${storage}"
|
||||||
|
echo "[dry-run] (would parse the resulting disk identifier from that output)"
|
||||||
|
echo "[dry-run] ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
|
||||||
|
else
|
||||||
|
importdisk_output="$(ssh "$ssh_target" "qm importdisk ${vmid} ${remote_path} ${storage}")"
|
||||||
|
echo "$importdisk_output"
|
||||||
|
disk_id="$(echo "$importdisk_output" | grep -oP "(?<=Successfully imported disk as ')[^']+" | sed 's/^unused[0-9]*://')"
|
||||||
|
if [[ -z "$disk_id" ]]; then
|
||||||
|
echo "ERROR: couldn't parse the imported disk identifier from qm importdisk's output above." >&2
|
||||||
|
echo "The VM shell (${vmid}) and imported disk both exist -- finish attaching it by hand:" >&2
|
||||||
|
echo " ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<disk-id-from-output-above>" >&2
|
||||||
|
echo " ssh ${ssh_target} -- qm set ${vmid} --boot order=scsi0" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
remote "qm set ${vmid} --scsi0 ${disk_id}"
|
||||||
|
fi
|
||||||
|
remote "qm set ${vmid} --boot order=scsi0"
|
||||||
|
remote "qm start ${vmid}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] Nothing was built, uploaded, or created."
|
||||||
|
else
|
||||||
|
echo "Done. ${name} (VMID ${vmid}) should be booting on ${node}."
|
||||||
|
fi
|
||||||
Executable
+147
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared config for scripts/*.sh. Source this instead of hardcoding a
|
||||||
|
# second copy of these values in every script:
|
||||||
|
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
|
||||||
|
# Every variable can still be overridden per-invocation via the
|
||||||
|
# environment (e.g. PROXMOX_STORAGE=tank-nvme ./scripts/create-proxmox-resource.sh ...)
|
||||||
|
# since each one only sets a default if unset.
|
||||||
|
|
||||||
|
# SSH-reachable Proxmox node that scripts/create-proxmox-resource.sh runs
|
||||||
|
# pct/qm on. Matches the Proxmox web UI hostname already used in
|
||||||
|
# hosts/nixos/home.nix's desktop shortcuts (pve.<homeDomain> from
|
||||||
|
# variables.nix) -- change this if that's not actually reachable over SSH,
|
||||||
|
# or if you're targeting a different node in a multi-node cluster.
|
||||||
|
: "${PROXMOX_HOST:=pve.sweet.home}"
|
||||||
|
: "${PROXMOX_SSH_USER:=root}"
|
||||||
|
|
||||||
|
# Storage pool names -- Proxmox's own stock-install defaults, but this
|
||||||
|
# varies a lot by setup (ZFS pool name, custom LVM-thin volume, etc.).
|
||||||
|
# Verify with `pvesm status` on the node and correct these if wrong.
|
||||||
|
: "${PROXMOX_STORAGE:=local-lvm}" # VM disks / CT rootfs
|
||||||
|
: "${PROXMOX_ISO_STORAGE:=local}" # uploaded images/ISOs/CT templates
|
||||||
|
|
||||||
|
: "${PROXMOX_BRIDGE:=vmbr0}"
|
||||||
|
|
||||||
|
# Fallback resource sizing when a script doesn't get --cores/--memory.
|
||||||
|
: "${PROXMOX_DEFAULT_CORES:=2}"
|
||||||
|
: "${PROXMOX_DEFAULT_MEMORY_MB:=2048}"
|
||||||
|
|
||||||
|
# `pct create` (unlike `pct restore`) requires an explicit rootfs size --
|
||||||
|
# no backup metadata to infer it from. Matches Proxmox's own GUI default.
|
||||||
|
: "${PROXMOX_DEFAULT_LXC_DISK_GB:=8}"
|
||||||
|
|
||||||
|
# `pct create --memory` only sets RAM -- swap is a wholly separate
|
||||||
|
# parameter that otherwise silently stays at Proxmox's own 512M default
|
||||||
|
# regardless of --memory (confirmed: creating with --memory 2048 left
|
||||||
|
# swap at 512). create-proxmox-resource.sh defaults --swap to whatever
|
||||||
|
# --memory resolves to at runtime rather than a static value here, so it
|
||||||
|
# tracks a --memory picked at the CLI too, not just the default above.
|
||||||
|
|
||||||
|
# Required for a modern (v247+) systemd guest to actually boot as an
|
||||||
|
# unprivileged container: systemd's routine use of nested user namespaces
|
||||||
|
# and credential mounts (LoadCredential=, DynamicUser=, etc. -- used even
|
||||||
|
# by plain getty units) gets denied by AppArmor's default LXC confinement
|
||||||
|
# without these. Confirmed live: without them, every getty unit
|
||||||
|
# crash-loops on a denied `/run/credentials/*` mount every ~3s (visible
|
||||||
|
# as garbage on the console) and core services like nsncd fail the same
|
||||||
|
# way on userns_create; system.build.tarball never finishes activating.
|
||||||
|
: "${PROXMOX_DEFAULT_LXC_FEATURES:=nesting=1,keyctl=1}"
|
||||||
|
|
||||||
|
export PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE PROXMOX_ISO_STORAGE \
|
||||||
|
PROXMOX_BRIDGE PROXMOX_DEFAULT_CORES PROXMOX_DEFAULT_MEMORY_MB \
|
||||||
|
PROXMOX_DEFAULT_LXC_DISK_GB PROXMOX_DEFAULT_LXC_FEATURES
|
||||||
|
|
||||||
|
# Matches variables.nix's nixCacheHost -- update both if it ever changes.
|
||||||
|
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||||
|
export NIX_CACHE_HOST
|
||||||
|
|
||||||
|
# nix_extra_opts: call as a plain statement (NOT inside $(...)/<(...) --
|
||||||
|
# that forks a subshell, and the whole point is exporting a decision back
|
||||||
|
# into *this* shell) to populate the global NIX_OPTS array with whatever
|
||||||
|
# extra `nix`/`nix-shell` CLI options are needed to avoid nix-cache when
|
||||||
|
# it's unreachable:
|
||||||
|
# nix_extra_opts
|
||||||
|
# nix build "${NIX_OPTS[@]}" ...
|
||||||
|
#
|
||||||
|
# Without this, every single `nix eval`/`nix build` call retries each
|
||||||
|
# store path against a dead substituter up to 5 times with backoff
|
||||||
|
# (confirmed: ~15s+ per lookup even with a short connect-timeout, because
|
||||||
|
# nix's own retry count isn't controllable that way), and separately
|
||||||
|
# tries it as a remote builder too -- both fail independently, so both
|
||||||
|
# are checked.
|
||||||
|
#
|
||||||
|
# Checked with a single fast `curl`/TCP probe (bypassing nix's retry logic
|
||||||
|
# entirely) the first time this is called in a given process, and the
|
||||||
|
# result is exported as NIX_EXTRA_OPTS so a script that shells out to
|
||||||
|
# another script in this repo (e.g. create-proxmox-resource.sh calling
|
||||||
|
# sync-host-keys.sh) reuses the same decision instead of probing twice.
|
||||||
|
declare -a NIX_OPTS=()
|
||||||
|
|
||||||
|
nix_extra_opts() {
|
||||||
|
if [[ -n "${NIX_EXTRA_OPTS_DECIDED:-}" ]]; then
|
||||||
|
if [[ -n "${NIX_EXTRA_OPTS:-}" ]]; then
|
||||||
|
eval "NIX_OPTS=(${NIX_EXTRA_OPTS})"
|
||||||
|
else
|
||||||
|
NIX_OPTS=()
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
export NIX_EXTRA_OPTS_DECIDED=1
|
||||||
|
NIX_OPTS=()
|
||||||
|
|
||||||
|
# Retry a couple of times, 1s apart, before believing either check --
|
||||||
|
# belt-and-suspenders against a genuine multi-second blip (nix-cache
|
||||||
|
# restarting), on top of the fix below. Worst case (~11s total, host
|
||||||
|
# genuinely gone) is still nowhere near the 15s+ *per lookup* nix's own
|
||||||
|
# substituter retries would cost if this check didn't exist at all.
|
||||||
|
local attempt cache_up=0 builder_up=0
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if curl --silent --fail --max-time 3 "http://${NIX_CACHE_HOST}/nix-cache-info" >/dev/null 2>&1; then
|
||||||
|
cache_up=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
[[ "$attempt" -lt 3 ]] && sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$cache_up" -eq 0 ]]; then
|
||||||
|
echo "nix-cache (http://${NIX_CACHE_HOST}) is unreachable -- skipping it (substituter + remote builder) for the rest of this run." >&2
|
||||||
|
NIX_OPTS=(--option substituters "https://cache.nixos.org/" --builders "")
|
||||||
|
else
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
# `exec 3<>/dev/tcp/...` just opens the fd and returns -- it does NOT
|
||||||
|
# read from it. Confirmed live this is load-bearing, not stylistic:
|
||||||
|
# the previous `cat < /dev/tcp/.../22` blocked forever and always hit
|
||||||
|
# the timeout even against a perfectly healthy nix-cache, because
|
||||||
|
# sshd sends its banner and then holds the connection open waiting
|
||||||
|
# for the client to speak next -- `cat` never sees EOF, so this
|
||||||
|
# check reported "unreachable" unconditionally, 100% of the time,
|
||||||
|
# regardless of whether the remote builder was actually up.
|
||||||
|
if timeout 3 bash -c "exec 3<>/dev/tcp/${NIX_CACHE_HOST}/22" 2>/dev/null; then
|
||||||
|
builder_up=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
[[ "$attempt" -lt 3 ]] && sleep 1
|
||||||
|
done
|
||||||
|
if [[ "$builder_up" -eq 0 ]]; then
|
||||||
|
echo "nix-cache's SSH remote builder (nixremote@${NIX_CACHE_HOST}:22) is unreachable -- disabling remote builds for the rest of this run." >&2
|
||||||
|
NIX_OPTS=(--builders "")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `printf '%q '` with a genuinely empty NIX_OPTS still runs one format
|
||||||
|
# pass over a missing argument and yields the literal `'' ` rather than
|
||||||
|
# an empty string (confirmed live) -- a subprocess that later does
|
||||||
|
# `eval "NIX_OPTS=(${NIX_EXTRA_OPTS})"` (the branch above, for e.g.
|
||||||
|
# sync-host-keys.sh reusing this process's decision) would then rebuild
|
||||||
|
# a 1-element array holding an empty string instead of a 0-element
|
||||||
|
# array, and `nix-shell "${NIX_OPTS[@]}" -p <pkg>` chokes on that stray
|
||||||
|
# element as a bogus positional argument. Guard the empty case
|
||||||
|
# explicitly so nix-cache being reachable (NIX_OPTS legitimately empty)
|
||||||
|
# round-trips as truly empty instead.
|
||||||
|
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
|
||||||
|
printf -v NIX_EXTRA_OPTS '%q ' "${NIX_OPTS[@]}"
|
||||||
|
else
|
||||||
|
NIX_EXTRA_OPTS=""
|
||||||
|
fi
|
||||||
|
export NIX_EXTRA_OPTS
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared Nix bootstrap for scripts/codex-setup.sh and
|
||||||
|
# scripts/codex-maintenance.sh: the nix.conf settings both need in effect
|
||||||
|
# before a single `nix` command runs (flakes enabled, never honor a flake
|
||||||
|
# input's own nixConfig, no "dirty tree" warning spam), plus a helper to
|
||||||
|
# pull an already-installed Nix's daemon/profile script onto PATH if it
|
||||||
|
# isn't there yet. Source this instead of copying it -- see CLAUDE.md.
|
||||||
|
export NIX_CONFIG="${NIX_CONFIG:-}
|
||||||
|
experimental-features = nix-command flakes
|
||||||
|
accept-flake-config = false
|
||||||
|
warn-dirty = false
|
||||||
|
"
|
||||||
|
|
||||||
|
ensure_nix_profile() {
|
||||||
|
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
||||||
|
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||||
|
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
||||||
|
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared flake-introspection helpers for scripts/*.sh. Source alongside
|
||||||
|
# env.sh:
|
||||||
|
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-eval.sh"
|
||||||
|
#
|
||||||
|
# NIX_EVAL_FLAGS: --no-use-registries so a call here never resolves through
|
||||||
|
# the user's global flake registry (every call targets this repo's own
|
||||||
|
# flake, or an explicit github: ref, not a registry alias); --no-accept-flake-config
|
||||||
|
# so a flake input's own nixConfig (e.g. a dependency's substituters) is
|
||||||
|
# never honored -- matches accept-flake-config = false already set repo-wide
|
||||||
|
# (see lib/nix-bootstrap.sh / CLAUDE.md). Reuse this array rather than
|
||||||
|
# retyping the two flags at each call site.
|
||||||
|
declare -a NIX_EVAL_FLAGS=(--no-use-registries --no-accept-flake-config)
|
||||||
|
|
||||||
|
# list_flake_targets <flake_ref>
|
||||||
|
# Prints the attribute names under <flake_ref>#nixosConfigurations, one per
|
||||||
|
# line, e.g.:
|
||||||
|
# list_flake_targets . # from inside the repo
|
||||||
|
# list_flake_targets "$repo_root" # from anywhere
|
||||||
|
list_flake_targets() {
|
||||||
|
local flake_ref="$1"
|
||||||
|
nix eval --json "${NIX_EVAL_FLAGS[@]}" \
|
||||||
|
"${flake_ref}#nixosConfigurations" --apply builtins.attrNames \
|
||||||
|
| jq -r '.[]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# flake_target_hostname <flake_ref> <target>
|
||||||
|
# Prints one nixosConfigurations target's config.networking.hostName.
|
||||||
|
# Empty (not an error under set -e) if the target doesn't exist or the
|
||||||
|
# eval otherwise fails -- callers that need to distinguish "empty" from
|
||||||
|
# "eval failed" should check $? themselves instead of relying on this.
|
||||||
|
flake_target_hostname() {
|
||||||
|
local flake_ref="$1" target="$2"
|
||||||
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" \
|
||||||
|
"${flake_ref}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared SSH-host-key / age-conversion helpers for scripts/sync-host-keys.sh
|
||||||
|
# and scripts/prepare-host-key.sh -- both generate the same kind of key
|
||||||
|
# (ed25519, no passphrase, the sops-nix age-derivation input) and convert it
|
||||||
|
# to an age recipient the same way; kept in one place so the two can't
|
||||||
|
# drift apart.
|
||||||
|
#
|
||||||
|
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
|
||||||
|
# nix_extra_opts) if the caller has already set it, so a decision to avoid
|
||||||
|
# an unreachable nix-cache is reused here instead of probed again. Falls
|
||||||
|
# back to no extra options if the caller never sourced env.sh.
|
||||||
|
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
|
||||||
|
declare -a NIX_OPTS=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
# generate_host_ed25519_key <hostname> <keyfile>
|
||||||
|
# Writes <keyfile> and <keyfile>.pub. Caller is responsible for refusing to
|
||||||
|
# overwrite an existing keyfile -- this always runs ssh-keygen fresh.
|
||||||
|
generate_host_ed25519_key() {
|
||||||
|
local hostname="$1" keyfile="$2"
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
|
||||||
|
"ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ssh_pubkey_to_age <pubkeyfile>
|
||||||
|
# Prints the age public key derived from an ed25519 SSH public key file.
|
||||||
|
ssh_pubkey_to_age() {
|
||||||
|
local pubkeyfile="$1"
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${pubkeyfile}'"
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Companion to scripts/sync-host-keys.sh. Applies a set of additive edits
|
||||||
|
# to .sops.yaml via targeted line insertion -- not a full YAML parse and
|
||||||
|
# re-serialize -- so every untouched byte of the file is guaranteed to stay
|
||||||
|
# exactly as it was (a full round-trip through a generic YAML library was
|
||||||
|
# tested and silently reformatted the whole file's indentation style).
|
||||||
|
#
|
||||||
|
# Reads a JSON edit plan from stdin:
|
||||||
|
# {
|
||||||
|
# "add_keys": [{"host": "lxc-server", "age_key": "age1..."}],
|
||||||
|
# "add_aliases": [{"host": "lxc-server", "basename": "common.yaml"}],
|
||||||
|
# "remove_keys": ["lxc-server"],
|
||||||
|
# "remove_aliases_for_hosts": ["lxc-server"]
|
||||||
|
# }
|
||||||
|
# Idempotent: an anchor or alias that's already present/absent is left
|
||||||
|
# alone -- adding what's already there, or removing what's already gone,
|
||||||
|
# is a no-op rather than an error.
|
||||||
|
# Prints a JSON summary of what actually changed to stdout, so the caller
|
||||||
|
# knows which secrets/*.yaml files need `sops updatekeys` and which don't.
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
KEY_LINE_RE = re.compile(r"^ - &\S+ age1")
|
||||||
|
|
||||||
|
|
||||||
|
def add_keys(lines, add_keys):
|
||||||
|
existing_anchors = set()
|
||||||
|
for line in lines:
|
||||||
|
m = re.match(r"^ - &(\S+) age1", line)
|
||||||
|
if m:
|
||||||
|
existing_anchors.add(m.group(1))
|
||||||
|
|
||||||
|
new = [e for e in add_keys if e["host"] not in existing_anchors]
|
||||||
|
if not new:
|
||||||
|
return lines, []
|
||||||
|
|
||||||
|
last_idx = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if KEY_LINE_RE.match(line):
|
||||||
|
last_idx = i
|
||||||
|
if last_idx is None:
|
||||||
|
print("ERROR: no ' - &<name> age1...' line found under keys: in .sops.yaml", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
insert = [f" - &{e['host']} {e['age_key']}\n" for e in new]
|
||||||
|
lines = lines[: last_idx + 1] + insert + lines[last_idx + 1 :]
|
||||||
|
return lines, [e["host"] for e in new]
|
||||||
|
|
||||||
|
|
||||||
|
def add_aliases(lines, add_aliases):
|
||||||
|
changed_files = []
|
||||||
|
for e in add_aliases:
|
||||||
|
host = e["host"]
|
||||||
|
basename = e["basename"]
|
||||||
|
target = "path_regex: secrets/" + basename.replace(".", r"\.") + "$"
|
||||||
|
|
||||||
|
rule_start = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if target in line:
|
||||||
|
rule_start = i
|
||||||
|
break
|
||||||
|
if rule_start is None:
|
||||||
|
print(
|
||||||
|
f"WARNING: no creation_rule for secrets/{basename} in .sops.yaml "
|
||||||
|
f"(needed by {host}) -- add one manually, then re-run.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule_end = len(lines)
|
||||||
|
for i in range(rule_start + 1, len(lines)):
|
||||||
|
if re.match(r"^ - path_regex:", lines[i]):
|
||||||
|
rule_end = i
|
||||||
|
break
|
||||||
|
|
||||||
|
block = lines[rule_start:rule_end]
|
||||||
|
if any(re.search(r"\*" + re.escape(host) + r"\s*$", line) for line in block):
|
||||||
|
continue # already present
|
||||||
|
|
||||||
|
last_alias_idx = None
|
||||||
|
last_alias_indent = None
|
||||||
|
for i in range(rule_start, rule_end):
|
||||||
|
m = re.match(r"^(\s*)- \*\S+\s*$", lines[i])
|
||||||
|
if m:
|
||||||
|
last_alias_idx = i
|
||||||
|
last_alias_indent = m.group(1)
|
||||||
|
if last_alias_idx is None:
|
||||||
|
print(
|
||||||
|
f"WARNING: creation_rule for secrets/{basename} has no existing "
|
||||||
|
f"'- *alias' line to anchor the insertion point -- add {host} manually.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_line = f"{last_alias_indent}- *{host}\n"
|
||||||
|
lines = lines[: last_alias_idx + 1] + [new_line] + lines[last_alias_idx + 1 :]
|
||||||
|
changed_files.append(basename)
|
||||||
|
return lines, changed_files
|
||||||
|
|
||||||
|
|
||||||
|
def remove_keys(lines, hosts):
|
||||||
|
hosts = set(hosts)
|
||||||
|
removed = []
|
||||||
|
kept = []
|
||||||
|
for line in lines:
|
||||||
|
m = re.match(r"^ - &(\S+) age1", line)
|
||||||
|
if m and m.group(1) in hosts:
|
||||||
|
removed.append(m.group(1))
|
||||||
|
continue
|
||||||
|
kept.append(line)
|
||||||
|
return kept, removed
|
||||||
|
|
||||||
|
|
||||||
|
def remove_aliases_for_hosts(lines, hosts):
|
||||||
|
hosts = set(hosts)
|
||||||
|
if not hosts:
|
||||||
|
return lines, []
|
||||||
|
|
||||||
|
changed_files = []
|
||||||
|
result = []
|
||||||
|
current_basename = None
|
||||||
|
current_block_changed = False
|
||||||
|
|
||||||
|
def flush():
|
||||||
|
if current_block_changed and current_basename:
|
||||||
|
changed_files.append(current_basename)
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if re.match(r"^ - path_regex:", line):
|
||||||
|
flush()
|
||||||
|
current_block_changed = False
|
||||||
|
m = re.search(r"path_regex: secrets/(.+)\$", line)
|
||||||
|
current_basename = m.group(1).replace(r"\.", ".") if m else None
|
||||||
|
result.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_basename is not None and any(
|
||||||
|
re.search(r"\*" + re.escape(h) + r"\s*$", line) for h in hosts
|
||||||
|
):
|
||||||
|
current_block_changed = True
|
||||||
|
continue # drop this alias line
|
||||||
|
|
||||||
|
result.append(line)
|
||||||
|
|
||||||
|
flush()
|
||||||
|
return result, changed_files
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sops_path = sys.argv[1]
|
||||||
|
edits = json.load(sys.stdin)
|
||||||
|
|
||||||
|
with open(sops_path) as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
lines, added_keys = add_keys(lines, edits.get("add_keys", []))
|
||||||
|
lines, added_alias_files = add_aliases(lines, edits.get("add_aliases", []))
|
||||||
|
lines, removed_keys = remove_keys(lines, edits.get("remove_keys", []))
|
||||||
|
lines, removed_alias_files = remove_aliases_for_hosts(
|
||||||
|
lines, edits.get("remove_aliases_for_hosts", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(sops_path, "w") as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
changed_files = sorted(set(added_alias_files) | set(removed_alias_files))
|
||||||
|
json.dump(
|
||||||
|
{
|
||||||
|
"added_keys": added_keys,
|
||||||
|
"removed_keys": removed_keys,
|
||||||
|
"changed_secrets_files": changed_files,
|
||||||
|
},
|
||||||
|
sys.stdout,
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Generates a new machine's SSH host key ahead of installing it, so
|
# Generates a new machine's SSH host key by an arbitrary name, before it
|
||||||
# sops-nix has something to derive an age key from before the target
|
# necessarily has a flake target yet -- prints the .sops.yaml snippet to
|
||||||
# ever boots.
|
# add by hand. For any host that already has a flake target,
|
||||||
|
# scripts/sync-host-keys.sh <target> does this same job plus the
|
||||||
|
# .sops.yaml/key_groups registration and re-encryption automatically; use
|
||||||
|
# this script only to pre-generate a key ahead of adding the flake target
|
||||||
|
# itself.
|
||||||
#
|
#
|
||||||
# Why this is needed: sops-nix derives each host's decryption key from
|
# Why a host key is needed at all: sops-nix derives each host's decryption key from
|
||||||
# its own /etc/ssh/ssh_host_ed25519_key at *activation* time, but that
|
# its own /etc/ssh/ssh_host_ed25519_key at *activation* time, but that
|
||||||
# activation runs before systemd would otherwise generate this key on
|
# activation runs before systemd would otherwise generate this key on
|
||||||
# first boot (sshd-keygen is a normal systemd service gated behind
|
# first boot (sshd-keygen is a normal systemd service gated behind
|
||||||
@@ -19,6 +23,10 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=lib/ssh-host-keys.sh
|
||||||
|
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
||||||
|
|
||||||
hostname="${1:?usage: scripts/prepare-host-key.sh <hostname>}"
|
hostname="${1:?usage: scripts/prepare-host-key.sh <hostname>}"
|
||||||
sops_yaml="${repo_root}/.sops.yaml"
|
sops_yaml="${repo_root}/.sops.yaml"
|
||||||
@@ -37,9 +45,10 @@ if [[ -f "$keyfile" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
nix-shell -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
|
nix_extra_opts
|
||||||
|
generate_host_ed25519_key "$hostname" "$keyfile"
|
||||||
|
|
||||||
age_pub="$(nix-shell -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")"
|
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
@@ -62,8 +71,8 @@ Generated: ${keyfile}(.pub)
|
|||||||
a) Rebuild the installer image with all host-keys/ baked in (see
|
a) Rebuild the installer image with all host-keys/ baked in (see
|
||||||
docs/auto-installer.md):
|
docs/auto-installer.md):
|
||||||
NIXOS_HOST_KEYS_DIR="${keydir}" nix build .#iso --impure
|
NIXOS_HOST_KEYS_DIR="${keydir}" nix build .#iso --impure
|
||||||
(or .#lxc / .#pxe / .#all — --impure is required since host-keys/
|
(or .#pxe — --impure is required since host-keys/ is gitignored and
|
||||||
is gitignored and flakes can't see it otherwise)
|
flakes can't see it otherwise)
|
||||||
|
|
||||||
b) Or, for an image already built without keys, scp it in after boot:
|
b) Or, for an image already built without keys, scp it in after boot:
|
||||||
scp ${keyfile}{,.pub} root@<target-ip>:/root/host-keys/
|
scp ${keyfile}{,.pub} root@<target-ip>:/root/host-keys/
|
||||||
|
|||||||
Executable
+443
@@ -0,0 +1,443 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Manages host-keys/ + .sops.yaml + secrets/*.yaml recipients together, so
|
||||||
|
# a flake target's SSH host key and its sops registration never drift out
|
||||||
|
# of sync with each other or with the flake itself.
|
||||||
|
#
|
||||||
|
# sync-host-keys.sh --all Generate/register every flake
|
||||||
|
# target missing a key.
|
||||||
|
# sync-host-keys.sh <target> Same, for just one target.
|
||||||
|
# sync-host-keys.sh --remove Interactively remove one
|
||||||
|
# locally-managed key.
|
||||||
|
# sync-host-keys.sh --regenerate-all-keys Remove and freshly regenerate
|
||||||
|
# every locally-managed key.
|
||||||
|
#
|
||||||
|
# "Generate/register" is idempotent and additive only: an existing
|
||||||
|
# host-keys/ file is never touched, and .sops.yaml only ever gains an
|
||||||
|
# anchor/alias it doesn't already have -- safe to re-run any time, e.g.
|
||||||
|
# right after adding a new host to flake.nix.
|
||||||
|
#
|
||||||
|
# --remove and --regenerate-all-keys only ever operate on anchors that have
|
||||||
|
# a corresponding host-keys/<name>_ssh_host_ed25519_key file. Anchors
|
||||||
|
# without one (&admin, and any anchor for an already-deployed host whose
|
||||||
|
# real /etc/ssh key was registered by hand, e.g. &docker/&server/&nix-cache
|
||||||
|
# today) are never listed, removed, or regenerated -- this tooling only
|
||||||
|
# ever touches keys it itself manages.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
sops_yaml="${repo_root}/.sops.yaml"
|
||||||
|
keydir="${repo_root}/host-keys"
|
||||||
|
editor="${repo_root}/scripts/lib/sync-host-keys-edit-sops.py"
|
||||||
|
|
||||||
|
# shellcheck source=env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=lib/nix-eval.sh
|
||||||
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
|
# shellcheck source=lib/ssh-host-keys.sh
|
||||||
|
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
||||||
|
|
||||||
|
mkdir -p "$keydir"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 --all [--dry-run]
|
||||||
|
$0 <flake-target> [--dry-run]
|
||||||
|
$0 --remove [--dry-run]
|
||||||
|
$0 --regenerate-all-keys [--dry-run]
|
||||||
|
|
||||||
|
--all Generate + register a host key for every flake
|
||||||
|
target that's missing one.
|
||||||
|
<flake-target> Same, for just one target (e.g. lxc-server).
|
||||||
|
Reports if it already has one.
|
||||||
|
--remove Interactively pick one locally-managed key to
|
||||||
|
remove from .sops.yaml and host-keys/.
|
||||||
|
--regenerate-all-keys Remove every locally-managed key and generate
|
||||||
|
fresh replacements for every current flake
|
||||||
|
target. Destructive -- requires typed
|
||||||
|
confirmation.
|
||||||
|
--dry-run Combine with any of the above: print what would
|
||||||
|
change (host-keys/ files, .sops.yaml anchors and
|
||||||
|
key_groups, which secrets/*.yaml would be
|
||||||
|
re-encrypted) without touching anything. No keys
|
||||||
|
generated, no files written, no sops calls,
|
||||||
|
no prompts for confirmation.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- step 0: make sure we can actually decrypt anything at all -------------
|
||||||
|
#
|
||||||
|
# Registering a host means editing .sops.yaml and then running
|
||||||
|
# `sops updatekeys`, which has to decrypt each secrets file with an
|
||||||
|
# existing recipient's key before it can re-encrypt it for the new one.
|
||||||
|
# Check this before doing anything else, the same order sops/age itself
|
||||||
|
# resolves a usable key in: SOPS_AGE_KEY (inline), then SOPS_AGE_KEY_FILE,
|
||||||
|
# then the XDG default path.
|
||||||
|
ensure_admin_decrypt_key() {
|
||||||
|
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
|
||||||
|
echo "Using SOPS_AGE_KEY from the environment."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
||||||
|
|
||||||
|
if [[ -s "$key_file" ]]; then
|
||||||
|
echo "Found existing sops age key at ${key_file}."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] No sops age decryption key found (checked \$SOPS_AGE_KEY, \$SOPS_AGE_KEY_FILE, ${key_file})."
|
||||||
|
echo "[dry-run] Would generate a new one here -- continuing the dry run without one; any"
|
||||||
|
echo "[dry-run] 'would re-encrypt' output below couldn't actually run for real yet."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "No sops age decryption key found (checked \$SOPS_AGE_KEY, \$SOPS_AGE_KEY_FILE, ${key_file})."
|
||||||
|
echo "Generating a new one at ${key_file}..."
|
||||||
|
mkdir -p "$(dirname "$key_file")"
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -o '${key_file}'" 2>&1 | grep -v "^Public key:" || true
|
||||||
|
local new_pub
|
||||||
|
new_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '${key_file}'")"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
A brand-new age key was just generated -- it cannot decrypt anything that
|
||||||
|
already exists in secrets/*.yaml, since nothing was ever encrypted for it.
|
||||||
|
That trust can't be bootstrapped automatically (nobody can decrypt a file
|
||||||
|
for a recipient that didn't exist when it was last encrypted).
|
||||||
|
|
||||||
|
To actually use this key:
|
||||||
|
1. Have someone who currently CAN decrypt replace the &admin entry in
|
||||||
|
.sops.yaml with this public key:
|
||||||
|
${new_pub}
|
||||||
|
2. They re-encrypt every secrets/*.yaml:
|
||||||
|
sops updatekeys --yes secrets/common.yaml
|
||||||
|
sops updatekeys --yes secrets/nix-cache.yaml
|
||||||
|
sops updatekeys --yes secrets/server.yaml
|
||||||
|
3. Re-run this script.
|
||||||
|
|
||||||
|
Exiting without making any other changes.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
discover_targets() {
|
||||||
|
# installer is the one nixosConfigurations target that doesn't import
|
||||||
|
# sops-nix at all (see CLAUDE.md's "Security Notes" -- hardcoded login
|
||||||
|
# password instead) -- config.sops.secrets doesn't exist for it.
|
||||||
|
list_flake_targets "$repo_root" | grep -v '^installer$'
|
||||||
|
}
|
||||||
|
|
||||||
|
locally_managed_hosts() {
|
||||||
|
for f in "$keydir"/*_ssh_host_ed25519_key.pub; do
|
||||||
|
[[ -e "$f" ]] || continue
|
||||||
|
basename "$f" _ssh_host_ed25519_key.pub
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
add_keys_json="[]"
|
||||||
|
add_aliases_json="[]"
|
||||||
|
dry_run=0
|
||||||
|
|
||||||
|
queue_host_sync() {
|
||||||
|
local host="$1"
|
||||||
|
local keyfile="${keydir}/${host}_ssh_host_ed25519_key"
|
||||||
|
local has_local_key=0 has_anchor=0
|
||||||
|
[[ -f "$keyfile" ]] && has_local_key=1
|
||||||
|
grep -qE "^ - &${host} age1" "$sops_yaml" && has_anchor=1
|
||||||
|
|
||||||
|
if [[ "$has_local_key" -eq 0 && "$has_anchor" -eq 1 ]]; then
|
||||||
|
echo "SKIP ${host}: .sops.yaml already has an &${host} anchor, but"
|
||||||
|
echo " host-keys/${host}_ssh_host_ed25519_key is missing locally."
|
||||||
|
echo " Not generating a replacement -- it wouldn't match whatever's"
|
||||||
|
echo " already registered (and possibly deployed). Remove the"
|
||||||
|
echo " &${host} line from .sops.yaml first if you really want a"
|
||||||
|
echo " fresh key, then re-run."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$has_local_key" -eq 0 ]]; then
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] ${host}: would generate host key"
|
||||||
|
else
|
||||||
|
echo "==> ${host}: generating host key"
|
||||||
|
generate_host_ed25519_key "$host" "$keyfile"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "==> ${host}: host key already present"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$has_anchor" -eq 0 ]]; then
|
||||||
|
local age_pub
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
age_pub="dry-run-placeholder-not-a-real-key"
|
||||||
|
else
|
||||||
|
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
|
||||||
|
fi
|
||||||
|
add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \
|
||||||
|
'. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> ${host}: checking which secrets files it references"
|
||||||
|
local basenames
|
||||||
|
mapfile -t basenames < <(
|
||||||
|
nix eval --json --no-use-registries --no-accept-flake-config \
|
||||||
|
"${repo_root}#nixosConfigurations.${host}.config.sops.secrets" \
|
||||||
|
--apply 'builtins.mapAttrs (n: v: baseNameOf v.sopsFile)' \
|
||||||
|
| jq -r '[.[]] | unique | .[]'
|
||||||
|
)
|
||||||
|
local basename
|
||||||
|
for basename in "${basenames[@]}"; do
|
||||||
|
add_aliases_json="$(jq --arg host "$host" --arg basename "$basename" \
|
||||||
|
'. + [{host: $host, basename: $basename}]' <<<"$add_aliases_json")"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# In dry-run, this runs the exact same edit logic (so idempotency/what's-
|
||||||
|
# actually-new is determined for real, not guessed) but against a scratch
|
||||||
|
# copy of .sops.yaml that's discarded afterward -- the real file is never
|
||||||
|
# opened for writing, and `sops updatekeys` never runs.
|
||||||
|
apply_edit_plan() {
|
||||||
|
local plan="$1"
|
||||||
|
local target="$sops_yaml"
|
||||||
|
local tmpfile=""
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
tmpfile="$(mktemp)"
|
||||||
|
cp "$sops_yaml" "$tmpfile"
|
||||||
|
target="$tmpfile"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result
|
||||||
|
result="$(echo "$plan" | nix-shell "${NIX_OPTS[@]}" -p python3 --run "python3 '${editor}' '${target}'")"
|
||||||
|
[[ -n "$tmpfile" ]] && rm -f "$tmpfile"
|
||||||
|
|
||||||
|
local added removed changed
|
||||||
|
added="$(jq -r '.added_keys[]?' <<<"$result")"
|
||||||
|
removed="$(jq -r '.removed_keys[]?' <<<"$result")"
|
||||||
|
changed="$(jq -r '.changed_secrets_files[]?' <<<"$result")"
|
||||||
|
|
||||||
|
if [[ -z "$added" && -z "$removed" && -z "$changed" ]]; then
|
||||||
|
echo "Nothing changed in .sops.yaml."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local prefix=""
|
||||||
|
[[ "$dry_run" -eq 1 ]] && prefix="[dry-run] would "
|
||||||
|
|
||||||
|
[[ -n "$added" ]] && echo "${prefix}Add .sops.yaml anchors: $(tr '\n' ' ' <<<"$added")"
|
||||||
|
[[ -n "$removed" ]] && echo "${prefix}Remove .sops.yaml anchors: $(tr '\n' ' ' <<<"$removed")"
|
||||||
|
|
||||||
|
if [[ -n "$changed" ]]; then
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would re-encrypt:"
|
||||||
|
while IFS= read -r basename; do
|
||||||
|
[[ -z "$basename" ]] && continue
|
||||||
|
echo " secrets/${basename}"
|
||||||
|
done <<<"$changed"
|
||||||
|
else
|
||||||
|
echo "Re-encrypting affected secrets files..."
|
||||||
|
while IFS= read -r basename; do
|
||||||
|
[[ -z "$basename" ]] && continue
|
||||||
|
echo "==> secrets/${basename}"
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p sops --run "sops updatekeys --yes '${repo_root}/secrets/${basename}'"
|
||||||
|
done <<<"$changed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_additions() {
|
||||||
|
if [[ "$add_keys_json" == "[]" && "$add_aliases_json" == "[]" ]]; then
|
||||||
|
echo "Nothing to do -- every requested target already has a fully registered host key."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "Applying .sops.yaml edits..."
|
||||||
|
local plan
|
||||||
|
plan="$(jq -n --argjson add_keys "$add_keys_json" --argjson add_aliases "$add_aliases_json" \
|
||||||
|
'{add_keys: $add_keys, add_aliases: $add_aliases}')"
|
||||||
|
apply_edit_plan "$plan"
|
||||||
|
echo
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
|
||||||
|
else
|
||||||
|
echo "Done. Review the .sops.yaml / secrets/*.yaml diff, then commit and push --"
|
||||||
|
echo "the flake build the installer uses has to see the new recipient(s) before"
|
||||||
|
echo "any of these hosts can decrypt their secrets on first boot."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_all() {
|
||||||
|
echo "Discovering flake targets..."
|
||||||
|
local targets
|
||||||
|
mapfile -t targets < <(discover_targets)
|
||||||
|
local host
|
||||||
|
for host in "${targets[@]}"; do
|
||||||
|
queue_host_sync "$host" || true
|
||||||
|
done
|
||||||
|
flush_additions
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_target() {
|
||||||
|
local host="$1"
|
||||||
|
local targets
|
||||||
|
mapfile -t targets < <(discover_targets)
|
||||||
|
if ! printf '%s\n' "${targets[@]}" | grep -qxF "$host"; then
|
||||||
|
echo "ERROR: '${host}' is not a current nixosConfigurations target." >&2
|
||||||
|
echo "Current targets:" >&2
|
||||||
|
printf ' %s\n' "${targets[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
queue_host_sync "$host" || exit 1
|
||||||
|
flush_additions
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_remove() {
|
||||||
|
local hosts
|
||||||
|
mapfile -t hosts < <(locally_managed_hosts)
|
||||||
|
if [[ "${#hosts[@]}" -eq 0 ]]; then
|
||||||
|
echo "No locally-managed keys in host-keys/ -- nothing to remove."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Locally-managed keys:"
|
||||||
|
local i=1 host
|
||||||
|
for host in "${hosts[@]}"; do
|
||||||
|
local registered="not registered in .sops.yaml"
|
||||||
|
grep -qE "^ - &${host} age1" "$sops_yaml" && registered="registered in .sops.yaml"
|
||||||
|
printf ' %d) %s (%s)\n' "$i" "$host" "$registered"
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
local choice
|
||||||
|
read -rp "Remove which one? (number, or blank to cancel): " choice
|
||||||
|
if [[ -z "$choice" ]]; then
|
||||||
|
echo "Cancelled."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#hosts[@]} )); then
|
||||||
|
echo "ERROR: invalid selection." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local target="${hosts[$((choice - 1))]}"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -ne 1 ]]; then
|
||||||
|
read -rp "Really remove '${target}'? Its host-keys/ files will be deleted and it will lose access to every secrets file it can currently decrypt. (y/N): " confirm
|
||||||
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Cancelled."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local plan
|
||||||
|
plan="$(jq -n --arg host "$target" \
|
||||||
|
'{remove_keys: [$host], remove_aliases_for_hosts: [$host]}')"
|
||||||
|
apply_edit_plan "$plan"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would delete host-keys/${target}_ssh_host_ed25519_key(.pub)."
|
||||||
|
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
|
||||||
|
else
|
||||||
|
rm -f "${keydir}/${target}_ssh_host_ed25519_key" "${keydir}/${target}_ssh_host_ed25519_key.pub"
|
||||||
|
echo "Removed host-keys/${target}_ssh_host_ed25519_key(.pub)."
|
||||||
|
echo
|
||||||
|
echo "Review the diff, then commit and push."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_regenerate_all() {
|
||||||
|
local hosts
|
||||||
|
mapfile -t hosts < <(locally_managed_hosts)
|
||||||
|
if [[ "${#hosts[@]}" -eq 0 ]]; then
|
||||||
|
echo "No locally-managed keys in host-keys/ -- nothing to regenerate."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "This will remove and freshly regenerate ALL locally-managed keys:"
|
||||||
|
printf ' %s\n' "${hosts[@]}"
|
||||||
|
echo
|
||||||
|
echo "Every host above will need its new key baked into a rebuilt install"
|
||||||
|
echo "image/tarball before it can decrypt secrets again."
|
||||||
|
|
||||||
|
if [[ "$dry_run" -ne 1 ]]; then
|
||||||
|
read -rp "Type REGENERATE to confirm: " confirm
|
||||||
|
if [[ "$confirm" != "REGENERATE" ]]; then
|
||||||
|
echo "Cancelled."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
local hosts_json
|
||||||
|
hosts_json="$(printf '%s\n' "${hosts[@]}" | jq -R . | jq -s .)"
|
||||||
|
local plan
|
||||||
|
plan="$(jq -n --argjson hosts "$hosts_json" \
|
||||||
|
'{remove_keys: $hosts, remove_aliases_for_hosts: $hosts}')"
|
||||||
|
apply_edit_plan "$plan"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would delete ${#hosts[@]} host-keys/ file pair(s)."
|
||||||
|
echo "[dry-run] would then generate fresh replacements for the same hosts"
|
||||||
|
echo "[dry-run] (not simulated further here -- run without --dry-run, or"
|
||||||
|
echo "[dry-run] preview a specific target with: $0 <target> --dry-run)."
|
||||||
|
echo
|
||||||
|
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Removing existing keys..."
|
||||||
|
local host
|
||||||
|
for host in "${hosts[@]}"; do
|
||||||
|
rm -f "${keydir}/${host}_ssh_host_ed25519_key" "${keydir}/${host}_ssh_host_ed25519_key.pub"
|
||||||
|
done
|
||||||
|
echo "Removed ${#hosts[@]} host-keys/ file pair(s)."
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Regenerating fresh keys for every current flake target..."
|
||||||
|
cmd_all
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local args=()
|
||||||
|
local arg
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == "--dry-run" ]]; then
|
||||||
|
dry_run=1
|
||||||
|
else
|
||||||
|
args+=("$arg")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
set -- "${args[@]+"${args[@]}"}"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] no changes will be made"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
nix_extra_opts
|
||||||
|
ensure_admin_decrypt_key
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
--all)
|
||||||
|
cmd_all
|
||||||
|
;;
|
||||||
|
--remove)
|
||||||
|
cmd_remove
|
||||||
|
;;
|
||||||
|
--regenerate-all-keys)
|
||||||
|
cmd_regenerate_all
|
||||||
|
;;
|
||||||
|
-h | --help | "")
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
cmd_target "$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
beszel-token: ENC[AES256_GCM,data:meuzUP/6wCssJDVTgbC0XwiLZPMGyDl55HEIiON9xOXCD9k6,iv:TDqWcp+8Mxd8wN09r5otQRQXq3XTeQphaTWxvvuLTAs=,tag:cRPZQGlwB/dTguBAheWPQg==,type:str]
|
beszel-token: ENC[AES256_GCM,data:meuzUP/6wCssJDVTgbC0XwiLZPMGyDl55HEIiON9xOXCD9k6,iv:TDqWcp+8Mxd8wN09r5otQRQXq3XTeQphaTWxvvuLTAs=,tag:cRPZQGlwB/dTguBAheWPQg==,type:str]
|
||||||
|
cache-priv-key: ENC[AES256_GCM,data:6vQKIf7eS0WNL2Eptoi4VWr18SRMZfN/H/aFUUtXdMYQY5LLyBp2EHRKqZcGFuh1nZhUdAxUztq/CVXx+QFxKW+ElHxCxUSp0QqI1fdSkBkKZb8hlit5SoX9JtLzZGg0HBNM3nJu,iv:0J+xmrPJhInHhFR/c41ACjuTfaIoMkQFSfbL2KkgFa8=,tag:f4s9Szs5oprVVRSyXaX48A==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
@@ -28,7 +29,7 @@ sops:
|
|||||||
2SEMsctdGC+5E3ilPXvPpZ5RONZHbXxn6kQRBlBv6AJERpGDzsfgfA==
|
2SEMsctdGC+5E3ilPXvPpZ5RONZHbXxn6kQRBlBv6AJERpGDzsfgfA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||||
lastmodified: "2026-07-19T02:30:40Z"
|
lastmodified: "2026-07-19T23:30:21Z"
|
||||||
mac: ENC[AES256_GCM,data:7+FeT6aeCGn+JFBXbPO0qP4BJ1nHPSennewv1kWkG+hOTIqs1ymuswUK1Hyfi6Z9h2umFX9HvK+o3qtmYvk6k7BUNe6w6QUHTNwm6lmNqrb7sgAE3iFVI2p9m14NGhgoTfnXx1M4JIZ1iuNYhCukpENI4+svIe+r7x5YeE5Evac=,iv:4AKAPI6upyAvHBr8BLWX7R/NupmJdcXdqiN8e0ZQ3ls=,tag:TkpfpCaLhn+Mx5cQZZuMdA==,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
|
||||||
version: 3.13.1
|
version: 3.13.1
|
||||||
+120
-5
@@ -12,8 +12,22 @@
|
|||||||
nfsServerHost = "server"; # NFS export source hostname
|
nfsServerHost = "server"; # NFS export source hostname
|
||||||
dockerHost = "docker"; # docker-compose stack host
|
dockerHost = "docker"; # docker-compose stack host
|
||||||
|
|
||||||
|
# Raspberry Pi's own Tailscale hostname (not fronted by `server` — it
|
||||||
|
# exports its own NFS share directly). Resolved as
|
||||||
|
# "${raspberryPiHost}.${tailnetDomain}" in modules/raspi/mount-data.nix.
|
||||||
|
raspberryPiHost = "raspberrypi";
|
||||||
|
|
||||||
remoteBuilderUser = "nixremote"; # remote builder SSH user
|
remoteBuilderUser = "nixremote"; # remote builder SSH user
|
||||||
|
|
||||||
|
# nix-cache's own SSH host public key (not a secret — the private half
|
||||||
|
# never leaves the host). Wired into every client's
|
||||||
|
# programs.ssh.knownHosts by modules/nix-cache/remote-builder-client.nix
|
||||||
|
# so distributed builds don't hit "Host key verification failed" on a
|
||||||
|
# fresh client that has never manually ssh'd to nix-cache before. Update
|
||||||
|
# this if nix-cache's host key is ever rotated or the host is rebuilt
|
||||||
|
# from scratch.
|
||||||
|
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHrMKZlIGUd3pH9G3AqbsruqUGjxIXMAZw52u9MwiBCn lxc-nix-cache";
|
||||||
|
|
||||||
# Public keys authorized to SSH in as remoteBuilderUser on the nix-cache
|
# Public keys authorized to SSH in as remoteBuilderUser on the nix-cache
|
||||||
# host (modules/nix-cache/server.nix) — one per client host that's allowed
|
# host (modules/nix-cache/server.nix) — one per client host that's allowed
|
||||||
# to use it as a distributed builder.
|
# to use it as a distributed builder.
|
||||||
@@ -32,13 +46,114 @@
|
|||||||
|
|
||||||
# System
|
# System
|
||||||
timeZone = "Australia/Brisbane";
|
timeZone = "Australia/Brisbane";
|
||||||
primaryUser = "nixos"; # main interactive user on every host
|
|
||||||
|
# Main interactive user on every host. Every module that grants this user
|
||||||
|
# a group, a home directory, or tmpfiles ownership should reference
|
||||||
|
# vars.primaryUser rather than the literal "nixos", so renaming it is a
|
||||||
|
# one-line change.
|
||||||
|
primaryUser = "nixos";
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
storageRoot = "/tank"; # ZFS pool root on `server`
|
storageRoot = "/tank"; # ZFS pool root on `server`
|
||||||
|
|
||||||
# Well-known ports for LAN services referenced from multiple hosts
|
# NFS datasets exported from `storageRoot` on `nfsServerHost` and mounted
|
||||||
beszelHubPort = 8090;
|
# by client hosts. `subpath` is relative to `storageRoot` — combined with
|
||||||
pveWebPort = 8006;
|
# it to build both the export line in modules/build-types/server.nix and
|
||||||
pbsWebPort = 8007;
|
# the "<nfsServerHost>:<storageRoot>/<subpath>" device string each client
|
||||||
|
# mount uses in modules/docker/mount-data.nix. `mountpoint` is the
|
||||||
|
# absolute local path clients mount it at, referenced by that same file's
|
||||||
|
# fileSystems attribute name plus every other place that needs to know
|
||||||
|
# where the share lives locally (modules/build-types/docker.nix's
|
||||||
|
# tmpfiles rules, modules/traefik/rotate-logs.nix's log path). Renaming a
|
||||||
|
# dataset or moving where it's mounted only needs changing it here — the
|
||||||
|
# export and every client reference follow automatically.
|
||||||
|
nfsShares = {
|
||||||
|
dockerConfig = {
|
||||||
|
subpath = "docker/config";
|
||||||
|
mountpoint = "/mnt/docker/config";
|
||||||
|
};
|
||||||
|
dockerDatabases = {
|
||||||
|
subpath = "docker/databases";
|
||||||
|
mountpoint = "/mnt/docker/databases";
|
||||||
|
};
|
||||||
|
dockerVolumes = {
|
||||||
|
subpath = "docker/volumes";
|
||||||
|
mountpoint = "/mnt/docker/volumes";
|
||||||
|
};
|
||||||
|
nextcloudData = {
|
||||||
|
subpath = "docker/nextcloud-data";
|
||||||
|
mountpoint = "/mnt/nextcloud-data";
|
||||||
|
};
|
||||||
|
raspiVolumes = {
|
||||||
|
subpath = "raspi/volumes";
|
||||||
|
mountpoint = "/mnt/raspi-backup";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# The Raspberry Pi's own NFS export — not under storageRoot/nfsServerHost,
|
||||||
|
# served directly by the Pi itself over Tailscale (see raspberryPiHost
|
||||||
|
# above) and mounted at raspiMountpoint by modules/raspi/mount-data.nix.
|
||||||
|
raspiNfsPath = "/home/raspi/raspi";
|
||||||
|
raspiMountpoint = "/mnt/raspi";
|
||||||
|
|
||||||
|
# Every literal port referenced from modules/ or hosts/, grouped by the
|
||||||
|
# service/host that opens or connects to it — kept as separate entries
|
||||||
|
# even where two happen to share a number today (e.g. nixCacheHttp and
|
||||||
|
# pxeBootHttp are both 80) so changing one service's port can never
|
||||||
|
# silently change an unrelated one.
|
||||||
|
ports = {
|
||||||
|
# nix-cache's nginx reverse proxy in front of nix-serve
|
||||||
|
# (modules/nix-cache/server.nix).
|
||||||
|
nixCacheHttp = 80;
|
||||||
|
|
||||||
|
# pxe-boot's nginx asset server, also used to build pxeBaseUrl
|
||||||
|
# (modules/build-types/pxe-boot.nix).
|
||||||
|
pxeBootHttp = 80;
|
||||||
|
|
||||||
|
# pxe-boot's atftpd TFTP server — UDP, not TCP
|
||||||
|
# (modules/build-types/pxe-boot.nix).
|
||||||
|
pxeBootTftp = 69;
|
||||||
|
|
||||||
|
# `server`'s NFS exports need both the portmapper (rpcbind) and the
|
||||||
|
# NFS data port itself opened (modules/build-types/server.nix).
|
||||||
|
nfsRpcbind = 111;
|
||||||
|
nfsd = 2049;
|
||||||
|
|
||||||
|
# Opened on the docker host's firewall for the Traefik-fronted
|
||||||
|
# container stack (docker-compose config lives in the separate
|
||||||
|
# /home/debian/docker repo, not here): 80/443 are Traefik's own
|
||||||
|
# HTTP/HTTPS listeners; 8080 is an additional exposed service whose
|
||||||
|
# exact backend isn't declared in this repo (modules/build-types/docker.nix).
|
||||||
|
dockerHttp = 80;
|
||||||
|
dockerHttps = 443;
|
||||||
|
dockerExtra = 8080;
|
||||||
|
|
||||||
|
# Beszel monitoring hub, reachable at
|
||||||
|
# http://<dockerHost>.<homeDomain>:<beszelHub> from every agent
|
||||||
|
# (modules/beszel/enable-agent.nix, hosts/nixos/home.nix).
|
||||||
|
beszelHub = 8090;
|
||||||
|
|
||||||
|
# Proxmox VE and Proxmox Backup Server web UIs, opened as desktop
|
||||||
|
# shortcuts on the gui build type (hosts/nixos/home.nix).
|
||||||
|
pveWeb = 8006;
|
||||||
|
pbsWeb = 8007;
|
||||||
|
};
|
||||||
|
|
||||||
|
# .raw disk image size for every proxmox-* host's standalone Disko image
|
||||||
|
# build (modules/disko/proxmox.nix, config.system.build.diskoImagesScript
|
||||||
|
# — see docs/proxmox-images.md). Root fills whatever's left after the ESP
|
||||||
|
# and swap partitions within this total.
|
||||||
|
proxmoxImageSize = "20G";
|
||||||
|
|
||||||
|
# nix-cache's Nix store garbage collection retention
|
||||||
|
# (modules/nix-cache/server.nix).
|
||||||
|
nixCacheGcMaxAge = "30d";
|
||||||
|
|
||||||
|
# Traefik access log rotation, watched on the docker host at
|
||||||
|
# nfsShares.dockerVolumes.mountpoint (modules/traefik/rotate-logs.nix).
|
||||||
|
traefikLogRotate = {
|
||||||
|
maxSize = "100M"; # rotate once a log file exceeds this size
|
||||||
|
keep = 20; # number of rotated logs to retain before deleting the oldest
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user