Archived
Compare commits
189
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7bbf88dce | ||
|
|
d57145b31e | ||
|
|
1cbe80c0ca | ||
|
|
01ee261cf8 | ||
|
|
f6f30c675f | ||
|
|
60b80cbd96 | ||
|
|
27a8c7fad9 | ||
|
|
d9cee0a674 | ||
|
|
6c1891812e | ||
|
|
59316c982e | ||
|
|
9eca3bd719 | ||
|
|
f2f0fcf756 | ||
|
|
b4fb9c25f2 | ||
|
|
8955840f0a | ||
|
|
a4b49c9909 | ||
|
|
c0936a10e7 | ||
|
|
f4bbd6331d | ||
|
|
b99ba87cf6 | ||
|
|
2128353f9f | ||
|
|
7b4ce0ab3d | ||
|
|
3123565011 | ||
|
|
36f5ebdf86 | ||
|
|
bba054db85 | ||
|
|
b63a529a1d | ||
|
|
ee96713a50 | ||
|
|
1ece0c75d9 | ||
|
|
548c6f5041 | ||
|
|
bae4c8171f | ||
|
|
3748c86049 | ||
|
|
9dd969cf4c | ||
|
|
7e4b2d33fb | ||
|
|
288d50fd33 | ||
|
|
f0e76f8aff | ||
|
|
e80d195284 | ||
|
|
c770feebc9 | ||
|
|
7fd6d558d5 | ||
|
|
58c40292e2 | ||
|
|
94842875d0 | ||
|
|
cfa36b97fc | ||
|
|
4444398cac | ||
|
|
f80378f92f | ||
|
|
cd4997f429 | ||
|
|
6ce4784376 | ||
|
|
5856d45575 | ||
|
|
e3498b1087 | ||
|
|
724d9a45af | ||
|
|
109429c7da | ||
|
|
40856b2e5e | ||
|
|
23634134f0 | ||
|
|
34c55f27ca | ||
|
|
dc36a47ac9 | ||
|
|
750121e9dd | ||
|
|
479444d26a | ||
|
|
8c19ee9d72 | ||
|
|
2514d3bc89 | ||
|
|
48d6d6f7a2 | ||
|
|
cf0d62696f | ||
|
|
b2a3d5fbdd | ||
|
|
86e55ff954 | ||
|
|
d3d6382360 | ||
|
|
b8d21d78d9 | ||
|
|
dcce023b14 | ||
|
|
4c5ade5605 | ||
|
|
b232daf5e1 | ||
|
|
b65736c0dc | ||
|
|
4f54a1f0cd | ||
|
|
2d85ecec8f | ||
|
|
34bb14d9f6 | ||
|
|
515da66db9 | ||
|
|
2e9d3da301 | ||
|
|
321048626e | ||
|
|
16d6baea5f | ||
|
|
d082c6a084 | ||
|
|
ee93322ae2 | ||
|
|
d1ce8d3e71 | ||
|
|
f7c32aff12 | ||
|
|
bf88a6ebb0 | ||
|
|
de508141a8 | ||
|
|
18cd9c2342 | ||
|
|
997918e2f7 | ||
|
|
9872b8ff1d | ||
|
|
e9b225d2d6 | ||
|
|
2860f750b4 | ||
|
|
781b1d324e | ||
|
|
3014a45936 | ||
|
|
cda2132d6a | ||
|
|
6c1cc821a0 | ||
|
|
4be064572d | ||
|
|
89d506180d | ||
|
|
6e1e992652 | ||
|
|
5467c2e140 | ||
|
|
123cd2b3d7 | ||
|
|
006dd8097a | ||
|
|
18cd6e884e | ||
|
|
3102d66337 | ||
|
|
dfa5452af5 | ||
|
|
852ba2240f | ||
|
|
096dff4fa0 | ||
|
|
b5f749daa9 | ||
|
|
adaf53d647 | ||
|
|
01679f1639 | ||
|
|
8f4c88347d | ||
|
|
e9832d87c4 | ||
|
|
08aac4261f | ||
|
|
2526b2dca7 | ||
|
|
2df53fd5d7 | ||
|
|
5e2ff76cf7 | ||
|
|
e8c4122460 | ||
|
|
5db41b1166 | ||
|
|
ea7794dc05 | ||
|
|
67752fb1e8 | ||
|
|
1a14b1d4d3 | ||
|
|
68aea4cdcc | ||
|
|
0853952269 | ||
|
|
055577ee91 | ||
|
|
a63e1c70c3 | ||
|
|
a9745b594b | ||
|
|
78bb784265 | ||
|
|
784e064fa2 | ||
|
|
4a5afa6c1c | ||
|
|
c844ccc4e3 | ||
|
|
9a0aea8f89 | ||
|
|
b013e28dcd | ||
|
|
5a56030f6e | ||
|
|
f8719437ba | ||
|
|
9e34b9cbb9 | ||
|
|
4dabd725f0 | ||
|
|
2743d664a5 | ||
|
|
cfa34f3565 | ||
|
|
e021b49412 | ||
|
|
0c29c6a93d | ||
|
|
c329988cdd | ||
|
|
7a2b5ecf71 | ||
|
|
d74efd9f66 | ||
|
|
63a8c627f5 | ||
|
|
e4b335be23 | ||
|
|
0bf99c56cc | ||
|
|
e368f68ad7 | ||
|
|
fa52c2849a | ||
|
|
dce3788499 | ||
|
|
e00be5d2da | ||
|
|
82eea7f088 | ||
|
|
955a443b36 | ||
|
|
4800aebf43 | ||
|
|
cb737642e5 | ||
|
|
6d5670c8d2 | ||
|
|
c911a605e9 | ||
|
|
6002c5c738 | ||
|
|
92c50df2f1 | ||
|
|
45e61844d5 | ||
|
|
e72df8fed5 | ||
|
|
5497a5b0ae | ||
|
|
e10e493ddd | ||
|
|
ae9acecbf3 | ||
|
|
a3be05538b | ||
|
|
289163c712 | ||
|
|
2123e4ad69 | ||
|
|
177950dd3d | ||
|
|
cbf1239be4 | ||
|
|
8a282ee32e | ||
|
|
25079a7f0a | ||
|
|
4952e5224d | ||
|
|
98409f4502 | ||
|
|
7779f3e137 | ||
|
|
1462829aa6 | ||
|
|
48ce2c4097 | ||
|
|
bcae177d8e | ||
|
|
d35aca3138 | ||
|
|
147cb3803a | ||
|
|
98445565d6 | ||
|
|
eef4b05254 | ||
|
|
619324589a | ||
|
|
400af07154 | ||
|
|
9bb626327f | ||
|
|
1f8bf8c852 | ||
|
|
5d7a6327b7 | ||
|
|
0b9f124713 | ||
|
|
9479d56e11 | ||
|
|
c53c1940d6 | ||
|
|
79e8f9f2ce | ||
|
|
5fe575d362 | ||
|
|
b46424343f | ||
|
|
33b1d5ec79 | ||
|
|
f565e9c2a1 | ||
|
|
a91634c460 | ||
|
|
42919ea15c | ||
|
|
60c155327d | ||
|
|
0f78e96b81 | ||
|
|
12a2354fad |
+103
-28
@@ -1,18 +1,27 @@
|
|||||||
keys:
|
keys:
|
||||||
- &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
- &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
- &proxmox-minimal age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n
|
||||||
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
- &lxc-gui age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
|
||||||
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
- &baremetal-gui age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||||
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
- &linode-docker age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
|
||||||
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
- &linode-gui age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||||
- &lxc-nix-cache age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
- &linode-minimal age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
||||||
- &lxc-docker age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
- &linode-nix-cache age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||||
- &lxc-minimal age1jy444f9d9stygj4p3w9kh54cqcfr654tvr75tdvee5cxsgtdtc9q3v60ep
|
- &linode-server age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||||
- &lxc-pxe-boot age1fxxzpnfse8nd9wz78ht3m0plrmraacf4cpga0pe8fm2tdnqcgy8q7qsyvp
|
- &linode-tailscale-router age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||||
- &lxc-gui age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
- &lxc-docker age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
||||||
- &proxmox-server age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
- &lxc-minimal age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
||||||
- &vm-server age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
- &lxc-nix-cache age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||||
- &baremetal-gui age1ehkswwz2pqaz4svzh7ela5tdnssl8kn6d4vwwxd6zwg8exfpd43syyrrjp
|
- &lxc-pxe-boot age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
||||||
|
- &lxc-server age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||||
|
- &lxc-tailscale-router age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||||
|
- &lxc-tor-relay age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||||
|
- &proxmox-docker age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
||||||
|
- &proxmox-gui age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||||
|
- &proxmox-nix-cache age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||||
|
- &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||||
|
- &proxmox-server age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||||
|
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||||
|
|
||||||
creation_rules:
|
creation_rules:
|
||||||
# Shared across every currently-deployed host: root/nixos password hash,
|
# Shared across every currently-deployed host: root/nixos password hash,
|
||||||
@@ -23,40 +32,94 @@ creation_rules:
|
|||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *admin
|
- *admin
|
||||||
- *docker
|
|
||||||
- *server
|
|
||||||
- *nix-cache
|
|
||||||
- *lxc-minimal
|
|
||||||
- *nix-minimal
|
|
||||||
- *lxc-nix-cache
|
|
||||||
- *proxmox-minimal
|
- *proxmox-minimal
|
||||||
- *lxc-docker
|
|
||||||
- *lxc-pxe-boot
|
|
||||||
- *lxc-gui
|
- *lxc-gui
|
||||||
- *proxmox-server
|
|
||||||
- *vm-server
|
|
||||||
- *baremetal-gui
|
- *baremetal-gui
|
||||||
|
- *linode-docker
|
||||||
|
- *linode-gui
|
||||||
|
- *linode-minimal
|
||||||
|
- *linode-nix-cache
|
||||||
|
- *linode-server
|
||||||
|
- *linode-tailscale-router
|
||||||
|
- *lxc-docker
|
||||||
|
- *lxc-minimal
|
||||||
|
- *lxc-nix-cache
|
||||||
|
- *lxc-pxe-boot
|
||||||
|
- *lxc-server
|
||||||
|
- *lxc-tailscale-router
|
||||||
|
- *lxc-tor-relay
|
||||||
|
- *proxmox-docker
|
||||||
|
- *proxmox-gui
|
||||||
|
- *proxmox-nix-cache
|
||||||
|
- *proxmox-pxe-boot
|
||||||
|
- *proxmox-server
|
||||||
|
- *proxmox-tailscale-router
|
||||||
|
|
||||||
- path_regex: secrets/nix-cache\.yaml$
|
- path_regex: secrets/nix-cache\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *admin
|
- *admin
|
||||||
- *nix-cache
|
- *linode-nix-cache
|
||||||
- *lxc-nix-cache
|
- *lxc-nix-cache
|
||||||
|
- *proxmox-nix-cache
|
||||||
|
|
||||||
|
# Host keytab for nix-cache FreeIPA enrollment (binary sops file).
|
||||||
|
# Generate with: sops -e --input-type binary /tmp/nix-cache.keytab > secrets/nix-cache.keytab
|
||||||
|
- path_regex: secrets/nix-cache\.keytab$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
- *linode-nix-cache
|
||||||
|
- *lxc-nix-cache
|
||||||
|
- *proxmox-nix-cache
|
||||||
|
|
||||||
- path_regex: secrets/server\.yaml$
|
- path_regex: secrets/server\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *admin
|
- *admin
|
||||||
- *server
|
- *linode-server
|
||||||
|
- *lxc-server
|
||||||
- *proxmox-server
|
- *proxmox-server
|
||||||
- *vm-server
|
|
||||||
|
|
||||||
- path_regex: secrets/docker\.yaml$
|
- path_regex: secrets/tor-relay\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *admin
|
- *admin
|
||||||
- *docker
|
- *lxc-tor-relay
|
||||||
|
|
||||||
|
- path_regex: secrets/tailscale-router\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
- *linode-tailscale-router
|
||||||
|
- *lxc-tailscale-router
|
||||||
|
- *proxmox-tailscale-router
|
||||||
|
|
||||||
|
# HA file server per-node secrets (beszel-token).
|
||||||
|
# proxmox-ha-server-1 / proxmox-ha-server-2 keys are added automatically
|
||||||
|
# by scripts/secrets/sync-host-keys.sh once the hosts are provisioned;
|
||||||
|
# until then only the admin key can decrypt these files.
|
||||||
|
- path_regex: secrets/ha-server-1\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
# proxmox-ha-server-1 added by sync-host-keys.sh
|
||||||
|
|
||||||
|
- path_regex: secrets/ha-server-2\.yaml$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
# proxmox-ha-server-2 added by sync-host-keys.sh
|
||||||
|
|
||||||
|
# Shared HA cluster corosync authkey (binary sops file).
|
||||||
|
# Encrypted for both HA nodes so either can decrypt on boot.
|
||||||
|
# Both host keys added by sync-host-keys.sh; admin key allows initial creation.
|
||||||
|
- path_regex: secrets/ha-corosync-authkey$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
# proxmox-ha-server-1 added by sync-host-keys.sh
|
||||||
|
# proxmox-ha-server-2 added by sync-host-keys.sh
|
||||||
|
|
||||||
# gui-host-specific secrets (currently: wifi-password, see
|
# gui-host-specific secrets (currently: wifi-password, see
|
||||||
# modules/networking/wifi.nix). Only *lxc-gui has a registered key today
|
# modules/networking/wifi.nix). Only *lxc-gui has a registered key today
|
||||||
@@ -70,3 +133,15 @@ creation_rules:
|
|||||||
- *admin
|
- *admin
|
||||||
- *lxc-gui
|
- *lxc-gui
|
||||||
- *baremetal-gui
|
- *baremetal-gui
|
||||||
|
- *linode-gui
|
||||||
|
- *proxmox-gui
|
||||||
|
|
||||||
|
# Host keytab for tailscale-router FreeIPA enrollment (binary sops file).
|
||||||
|
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
|
||||||
|
- path_regex: secrets/tailscale-router\.keytab$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
- *lxc-tailscale-router
|
||||||
|
- *proxmox-tailscale-router
|
||||||
|
- *linode-tailscale-router
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ machines when deployed.
|
|||||||
template for a *real* host — every other host uses sops-nix
|
template for a *real* host — every other host uses sops-nix
|
||||||
(`hashedPasswordFile`, see "Security Notes" in `README.md`). Flag any *new*
|
(`hashedPasswordFile`, see "Security Notes" in `README.md`). Flag any *new*
|
||||||
secret-like string you encounter instead of committing it.
|
secret-like string you encounter instead of committing it.
|
||||||
- `host-keys/` is gitignored — locally-generated *private* SSH host keys for
|
- `host-keys/` is gitignored — used only by the auto-installer's own
|
||||||
the auto-installer (see `docs/auto-installer.md`). Never commit its
|
environment for pre-seeding non-LXC host keys before first boot (see
|
||||||
contents; if `git status` ever shows it as trackable, something is wrong.
|
`docs/auto-installer.md`). Never commit its contents; if `git status`
|
||||||
|
ever shows it as trackable, something is wrong. All deployed hosts use
|
||||||
|
clan vars (`vars/per-machine/<target>/openssh/`, committed and
|
||||||
|
sops-encrypted) for their SSH host keys — those ARE tracked by git and
|
||||||
|
belong in the repo.
|
||||||
|
|
||||||
### Two Proxmox nodes: `pve1.sweet.home` (production) and `pve-test.sweet.home` (sandbox)
|
### Two Proxmox nodes: `pve1.sweet.home` (production) and `pve-test.sweet.home` (sandbox)
|
||||||
|
|
||||||
@@ -164,21 +168,50 @@ before committing.
|
|||||||
|
|
||||||
Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` is
|
Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` is
|
||||||
organized by purpose: `scripts/secrets/` (sops/age + SSH host-key
|
organized by purpose: `scripts/secrets/` (sops/age + SSH host-key
|
||||||
management), `scripts/proxmox/` (Proxmox deployment), `scripts/lib/`
|
management), `scripts/proxmox/` (Proxmox deployment), `scripts/installer/`
|
||||||
(shared helpers, sourced by the scripts below — not run directly), and a
|
(the auto-installer's own shell script, templated into the image — see
|
||||||
handful of repo-wide scripts left at the top level (`env.sh`,
|
below), `scripts/lib/` (shared helpers, sourced by the scripts below — not
|
||||||
`bump-nixpkgs-release.sh`, plus `codex-setup.sh`/`codex-maintenance.sh`
|
run directly), and a handful of repo-wide scripts left at the top level
|
||||||
above). When adding a new script, put it in the matching subfolder rather
|
(`env.sh`, `bump-nixpkgs-release.sh`, plus `codex-setup.sh`/
|
||||||
than the top level, and if it duplicates logic another script already has,
|
`codex-maintenance.sh` above). When adding a new script, put it in the
|
||||||
lift the shared part into `scripts/lib/` instead of copying it.
|
matching subfolder rather than the top level, and if it duplicates logic
|
||||||
|
another script already has, lift the shared part into `scripts/lib/`
|
||||||
|
instead of copying it.
|
||||||
|
|
||||||
|
### `scripts/installer/`
|
||||||
|
|
||||||
|
- `scripts/installer/auto-install.sh` — the interactive install script
|
||||||
|
baked into the auto-installer image (see `docs/auto-installer.md`), kept
|
||||||
|
as a real, version-controlled shell file rather than inline in
|
||||||
|
`modules/installer/common.nix`'s Nix. It sources `scripts/env.sh` itself
|
||||||
|
for `LAN_DOMAIN` (`export LAN_DOMAIN`/`: "${LAN_DOMAIN:=...}"`, matching
|
||||||
|
`variables.nix`'s `lanDomain` — manually kept in sync, same pattern as
|
||||||
|
`NIX_CACHE_HOST` mirroring `nixCacheHost`), rather than Nix-level string
|
||||||
|
substitution — that's what makes it work identically whether run
|
||||||
|
straight from a git checkout or from inside the built installer image.
|
||||||
|
`common.nix` bakes `scripts/env.sh` in alongside it at a matching
|
||||||
|
relative path (`/etc/nixos-installer/env.sh` next to
|
||||||
|
`/etc/nixos-installer/installer/auto-install.sh`) so the script's own
|
||||||
|
`source "$(dirname ...)/../env.sh"` line resolves the same way in both
|
||||||
|
contexts — this is also why it's invoked from
|
||||||
|
`/etc/nixos-installer/installer/auto-install.sh` rather than a flat
|
||||||
|
`/etc/auto-install.sh`. `#!/usr/bin/env bash`, not
|
||||||
|
`#!/run/current-system/sw/bin/bash`: the latter only resolves on an
|
||||||
|
already-activated NixOS system, breaking the checked-out-file case
|
||||||
|
entirely (confirmed live: "cannot execute: required file not found" on
|
||||||
|
a non-NixOS box); `/usr/bin/env` is reliably present on both NixOS
|
||||||
|
(`environment.usrbinenv`'s own default) and any normal Linux distro.
|
||||||
|
|
||||||
### `scripts/secrets/`
|
### `scripts/secrets/`
|
||||||
|
|
||||||
- `scripts/secrets/sync-host-keys.sh` — generates/registers SSH host keys
|
- `scripts/secrets/sync-host-keys.sh` — generates/registers SSH host keys
|
||||||
and their `.sops.yaml`/`secrets/*.yaml` recipients for flake targets,
|
and their `.sops.yaml`/`secrets/*.yaml` recipients for flake targets,
|
||||||
idempotently (`--all`, `<target>`, `--remove`, `--regenerate-all-keys`,
|
idempotently (`--all`, `<target>`, `--remove`, `--regenerate-all-keys`,
|
||||||
all with `--dry-run`). The primary tool for provisioning a new host's
|
all with `--dry-run`). Stores keys as clan vars
|
||||||
secrets access — see "Creating a new machine" in `docs/auto-installer.md`.
|
(`vars/per-machine/<target>/openssh/`, committed and sops-encrypted) for
|
||||||
|
all flake targets. The primary tool for provisioning a new host's
|
||||||
|
secrets access — see "Creating a new machine" in
|
||||||
|
`docs/auto-installer.md`.
|
||||||
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a
|
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a
|
||||||
key by an arbitrary name without touching `.sops.yaml`. Still useful to
|
key by an arbitrary name without touching `.sops.yaml`. Still useful to
|
||||||
pre-generate a key before its flake target exists yet, since
|
pre-generate a key before its flake target exists yet, since
|
||||||
@@ -282,8 +315,9 @@ Sourced by the scripts above, never run directly:
|
|||||||
### Top level
|
### Top level
|
||||||
|
|
||||||
- `scripts/env.sh` — shared config (`PROXMOX_HOST`, storage pool, bridge,
|
- `scripts/env.sh` — shared config (`PROXMOX_HOST`, storage pool, bridge,
|
||||||
default cores/memory) sourced by `create-proxmox-resource.sh`. Add new
|
default cores/memory, `NIX_CACHE_HOST`, `LAN_DOMAIN`) sourced by
|
||||||
cross-script config here instead of duplicating it per-script.
|
`create-proxmox-resource.sh` and `scripts/installer/auto-install.sh`. Add
|
||||||
|
new cross-script config here instead of duplicating it per-script.
|
||||||
- `scripts/bump-nixpkgs-release.sh` — bumps `flake.nix`'s `nixpkgs.url`/
|
- `scripts/bump-nixpkgs-release.sh` — bumps `flake.nix`'s `nixpkgs.url`/
|
||||||
`home-manager.url` in place. Exists because flake input URLs can't
|
`home-manager.url` in place. Exists because flake input URLs can't
|
||||||
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
|
|||||||
| `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, also served as PXE netboot) — 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; only used by the auto-installer environment for pre-seeding SSH host keys before first boot — see `docs/auto-installer.md`. All deployed hosts use clan vars (`vars/per-machine/<target>/openssh/`) instead |
|
||||||
|
| `vars/per-machine/` | Clan vars: committed, sops-encrypted SSH host keys for all deployed hosts; read by `create-proxmox-resource.sh` at deploy time |
|
||||||
| `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, host-key, release-bump, and Proxmox resource helpers |
|
| `scripts/` | Codex setup, validation, host-key, release-bump, and Proxmox resource helpers |
|
||||||
|
|
||||||
@@ -162,7 +163,9 @@ sops-nix-everywhere: it has a hardcoded login password instead (no stable
|
|||||||
per-boot host key for sops-nix to derive from on ephemeral media) — see
|
per-boot host key for sops-nix to derive from on ephemeral media) — see
|
||||||
"Host keys" in `docs/auto-installer.md` for why, and how the private keys it
|
"Host keys" in `docs/auto-installer.md` for why, and how the private keys it
|
||||||
*does* pre-seed for target hosts stay out of git via the gitignored
|
*does* pre-seed for target hosts stay out of git via the gitignored
|
||||||
`host-keys/` directory.
|
`host-keys/` directory. All deployed hosts use clan vars
|
||||||
|
(`vars/per-machine/<target>/openssh/`, committed and sops-encrypted) for
|
||||||
|
their SSH host keys.
|
||||||
|
|
||||||
This repository's git *history* still contains secrets committed before this
|
This repository's git *history* still contains secrets committed before this
|
||||||
migration (see `remove-sensetive-info-refactor.md`) — those are being
|
migration (see `remove-sensetive-info-refactor.md`) — those are being
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIESDCCArCgAwIBAgIBATANBgkqhkiG9w0BAQsFADA1MRMwEQYDVQQKDApTV0VF
|
||||||
|
VC5IT01FMR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjYwNzI2
|
||||||
|
MjExMzQxWhcNNDYwNzI2MjExMzQxWjA1MRMwEQYDVQQKDApTV0VFVC5IT01FMR4w
|
||||||
|
HAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggGiMA0GCSqGSIb3DQEBAQUA
|
||||||
|
A4IBjwAwggGKAoIBgQCzljYktbHdMGVJ6Wq0XQJuHLN6dkCSOgtoIzQtriPQkkNI
|
||||||
|
uo28LwobaiQQ8sX4kGRH/BTKnH8QlId/jug4Uc+sDHnABYu++AiOhPbBX8gCpRQ0
|
||||||
|
hebBjZiktHSBUEJR31siWOVdBoKBDJEoxehx7XUXvcxIJcaRN+LHYjO86nJN55HB
|
||||||
|
VwFU2JcYDk98c+144dFJxXdr++MjWe4Z/oVVU8JHIOtNtKhVhvij6oOSWxcYoJO/
|
||||||
|
S80LRj1vx/o6o/3G6bYug7PjY7JjZk/Oj61whijZkcsoO1MXSYI6UywJZGflv+ZB
|
||||||
|
7HyufdYAsK3WhE8O2FX3/kq64Ol83HNtoR8Dt68rTg1xpW6K45jS6iDPKueYGkb0
|
||||||
|
oSx7e++90VAW2PDhj6QQ3JJ4O5VQwrrecekJzUrAean0FOEbmgyi4PsEp1Vk6LDQ
|
||||||
|
SsIn1x0euyxVivQMlzNX2XrZL3urn1BNPqAdntXQMkR0Wl8sbUiJPe0kxG52CGXs
|
||||||
|
6yfNEXbPmVGcC0TBdGECAwEAAaNjMGEwHQYDVR0OBBYEFLh5QbI1UWMH0WR4z8bG
|
||||||
|
lhrOX3X5MB8GA1UdIwQYMBaAFLh5QbI1UWMH0WR4z8bGlhrOX3X5MA8GA1UdEwEB
|
||||||
|
/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgHGMA0GCSqGSIb3DQEBCwUAA4IBgQCVodVN
|
||||||
|
owwo53OQe02QhtEbIur2PL7zIfvhvCTRD4J8gwpbMIqT7JQK0tV6Mvsg2L8yTb2O
|
||||||
|
KjrWeLKHGWaZZlhGSPTbkMFdb/Ls8M9FSnkc2bwcdWW3Z1lOiCjBYYqwLCG6JhvB
|
||||||
|
5SXVwWNJwXeasL2m7oFTSwhsqPpARJ2t25u2N35o+tqIoCjijKwkmEOT66N9EAbu
|
||||||
|
2VQjtYZWPkBtP4YCe0Ey6u4oy7sy8ThNAjOylZok+J4JW7QEFjK4Q/emhA4aQq5H
|
||||||
|
gg9qgMuG+5oi6D1g2Wy+fMTRBaukJtLYZbBpQMQhMYWg44uPp/2bbNPTID/nV1KB
|
||||||
|
GcPyHaskcVxPdYWxAPMwk3AeJXWyOq7atAPTF5sbk0kQQf2m+vyOqcli5CxRMUgV
|
||||||
|
rcyi9l6+dZW4U+38Q0ET5M3OuxNI4hA7kVY2cfTakXWNqh97+TIHnstblDhAxECK
|
||||||
|
6ZLMJQYUy7LqJTX84H27CBWLexEMjXwdr5HCV88Fj6mAK0fRufnIw5FeneA=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
+26
-5
@@ -8,10 +8,13 @@ lives here.
|
|||||||
The installer provides a small NixOS install environment (ISO, or the same
|
The installer provides a small NixOS install environment (ISO, or the same
|
||||||
image netbooted via PXE) with SSH access, Git support, and an interactive
|
image netbooted via PXE) with SSH access, Git support, and an interactive
|
||||||
installation script.
|
installation script.
|
||||||
Logging in as any user (root or `nixos`) runs `/etc/auto-install.sh`,
|
Logging in as any user (root or `nixos`) runs
|
||||||
discovers available hosts from this same flake, lets the operator choose a
|
`/etc/nixos-installer/installer/auto-install.sh` (the same file as
|
||||||
target, applies that host's Disko storage configuration, installs NixOS, and
|
`scripts/installer/auto-install.sh` in this repo — see "Installer process"
|
||||||
reboots.
|
below for why it's baked in at that path rather than a flat
|
||||||
|
`/etc/auto-install.sh`), discovers available hosts from this same flake,
|
||||||
|
lets the operator choose a target, applies that host's Disko storage
|
||||||
|
configuration, installs NixOS, and reboots.
|
||||||
|
|
||||||
**This applies to every `nixosConfigurations` target except `lxc-*` hosts —
|
**This applies to every `nixosConfigurations` target except `lxc-*` hosts —
|
||||||
see "LXC hosts" immediately below for why those are different.**
|
see "LXC hosts" immediately below for why those are different.**
|
||||||
@@ -192,6 +195,10 @@ default.
|
|||||||
`auto-install.sh` still supports the older manual path as a fallback: if a
|
`auto-install.sh` still supports the older manual path as a fallback: if a
|
||||||
host's key isn't baked in (`/etc/host-keys`), it checks `/root/host-keys`
|
host's key isn't baked in (`/etc/host-keys`), it checks `/root/host-keys`
|
||||||
next, where you can `scp` a key in after boot, same as before this migration.
|
next, where you can `scp` a key in after boot, same as before this migration.
|
||||||
|
If neither has it and the script is running interactively (an actual
|
||||||
|
operator at the other end of stdin, not an unattended run), it prompts for
|
||||||
|
an arbitrary directory to check (a mounted USB stick, another filesystem,
|
||||||
|
etc.) and copies the key pair into `/root/host-keys` from there if found.
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
@@ -217,7 +224,21 @@ entirely (see "LXC hosts" above), so it never reaches this code path.
|
|||||||
|
|
||||||
## Installer process
|
## Installer process
|
||||||
|
|
||||||
`/etc/auto-install.sh`:
|
`scripts/installer/auto-install.sh` is a real, version-controlled shell
|
||||||
|
script — not an inline Nix string. It sources `scripts/env.sh` for
|
||||||
|
`LAN_DOMAIN` itself (same as every other script in `scripts/`), so it
|
||||||
|
behaves identically whether it's run straight from a git checkout (e.g.
|
||||||
|
manually, from a stock NixOS ISO that isn't this repo's own installer
|
||||||
|
image) or from inside the built installer image. That's also why it's
|
||||||
|
baked in at `/etc/nixos-installer/installer/auto-install.sh` rather than a
|
||||||
|
flat `/etc/auto-install.sh` — `modules/installer/common.nix` bakes
|
||||||
|
`scripts/env.sh` in alongside it at `/etc/nixos-installer/env.sh`,
|
||||||
|
preserving the same relative layout (`installer/auto-install.sh` ->
|
||||||
|
`../env.sh`) the checked-out repo has, so the script's own
|
||||||
|
`source ".../env.sh"` line resolves correctly in both places without any
|
||||||
|
Nix-level templating.
|
||||||
|
|
||||||
|
Once running, it:
|
||||||
|
|
||||||
1. Queries `nixosConfigurations` from this flake over the network (`git+https://<lanDomain>/beatzaplenty/nixos.git`) — this happens at *install* time, not build time, so a generic installer image always sees whatever hosts are currently committed, without needing a rebuild.
|
1. Queries `nixosConfigurations` from this flake over the network (`git+https://<lanDomain>/beatzaplenty/nixos.git`) — this happens at *install* time, not build time, so a generic installer image always sees whatever hosts are currently committed, without needing a rebuild.
|
||||||
2. Presents them as a menu; confirms the choice.
|
2. Presents them as a menu; confirms the choice.
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# IP Addressing Scheme
|
||||||
|
|
||||||
|
## Subnets
|
||||||
|
|
||||||
|
| Subnet | CIDR | Purpose | Routed? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| LAN | `192.168.2.0/24` | General LAN — clients and infrastructure | Yes (gateway .254) |
|
||||||
|
| Storage | `192.168.4.0/29` | HA file server DRBD replication | No — internal `vmbr1` only, no uplink |
|
||||||
|
|
||||||
|
The storage subnet never leaves pve1. `vmbr1` is a Proxmox Linux bridge with no physical port
|
||||||
|
attached; traffic between the two HA file server VMs stays in-kernel.
|
||||||
|
|
||||||
|
The host octet is consistent across subnets for any host that has multiple interfaces — e.g.
|
||||||
|
ha-node1 is always `.228` (LAN: `192.168.2.228`, storage: `192.168.4.228`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LAN — 192.168.2.0/24
|
||||||
|
|
||||||
|
### Address map
|
||||||
|
|
||||||
|
| Range | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| .1–.9 | Reserved, never assign |
|
||||||
|
| .10–.59 | Client DHCP pool (router-assigned) |
|
||||||
|
| .60–.219 | Unallocated buffer |
|
||||||
|
| .220–.229 | Virtual nodes (VMs / LXC containers) |
|
||||||
|
| .230–.239 | Expansion buffer (reserved, unallocated) |
|
||||||
|
| .240–.249 | Physical nodes (bare-metal hosts) |
|
||||||
|
| .250–.253 | Network services |
|
||||||
|
| .254 | Router / gateway |
|
||||||
|
|
||||||
|
### Network services (.250–.253)
|
||||||
|
|
||||||
|
| IP | Hostname | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `192.168.2.254` | router | Gateway (TP-Link) |
|
||||||
|
| `192.168.2.253` | domain-controller | FreeIPA — authoritative DNS for `sweet.home`, Kerberos, LDAP |
|
||||||
|
| `192.168.2.250`–`.252` | — | Reserved for future network services |
|
||||||
|
|
||||||
|
### Physical nodes (.240–.249)
|
||||||
|
|
||||||
|
| IP | Hostname | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `192.168.2.245` | pve1 | Proxmox VE hypervisor |
|
||||||
|
| `192.168.2.244` | pbs | Proxmox Backup Server |
|
||||||
|
| `192.168.2.243` | nixos | Bare-metal workstation (`baremetal-gui`) |
|
||||||
|
| `192.168.2.246`–`.249` | — | Reserved — second Proxmox node and associated services |
|
||||||
|
| `192.168.2.240`–`.242` | — | Reserved |
|
||||||
|
|
||||||
|
pve1 sits mid-range deliberately so a second Proxmox node can slot in on either side.
|
||||||
|
|
||||||
|
### Virtual nodes (.220–.229)
|
||||||
|
|
||||||
|
All VMs and LXC containers run on pve1.
|
||||||
|
|
||||||
|
| IP | Hostname | Role | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `192.168.2.229` | ha-vip | HA file server iSCSI floating VIP (Pacemaker) | Future |
|
||||||
|
| `192.168.2.228` | ha-node1 | HA file server node 1 (DRBD + XFS + iSCSI) | Future |
|
||||||
|
| `192.168.2.227` | ha-node2 | HA file server node 2 (DRBD + XFS + iSCSI) | Future |
|
||||||
|
| `192.168.2.226` | server | Current NFS/ZFS file server — retires when HA is live | Retiring |
|
||||||
|
| `192.168.2.225` | docker | Docker / Traefik stack | Active |
|
||||||
|
| `192.168.2.224` | nix-cache | Nix binary cache + remote builder | Active |
|
||||||
|
| `192.168.2.223` | pxe-boot | PXE / TFTP / HTTP netboot server | Active |
|
||||||
|
| `192.168.2.222` | tailscale-router | Tailscale exit node / router | Active |
|
||||||
|
| `192.168.2.221` | tor-relay | Tor relay | Active |
|
||||||
|
| `192.168.2.220` | pdm | Proxmox Deploy Manager | Active |
|
||||||
|
|
||||||
|
### Client DHCP pool (.10–.59)
|
||||||
|
|
||||||
|
Assigned by the router. DNS option points to `192.168.2.253` (domain-controller).
|
||||||
|
|
||||||
|
Devices in this range: phones, laptops, IoT, Canon printer, any non-infrastructure host.
|
||||||
|
No static reservations for infrastructure hosts — all infra uses static IP configuration
|
||||||
|
on the guest itself (not DHCP reservations), so IPs survive VM recreation regardless of
|
||||||
|
MAC address churn.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage network — 192.168.4.0/29
|
||||||
|
|
||||||
|
Internal to pve1 only. Proxmox bridge `vmbr1`, no physical NIC attached.
|
||||||
|
|
||||||
|
| IP | Hostname | Interface role |
|
||||||
|
|---|---|---|
|
||||||
|
| `192.168.4.228` | ha-node1 | DRBD replication NIC |
|
||||||
|
| `192.168.4.227` | ha-node2 | DRBD replication NIC |
|
||||||
|
| — | no gateway | Isolated — not routed to LAN or internet |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration reference
|
||||||
|
|
||||||
|
Current → target IP for every host being renumbered.
|
||||||
|
|
||||||
|
| Host | Current IP | New IP | Config location |
|
||||||
|
|---|---|---|---|
|
||||||
|
| router | `192.168.2.254` | `192.168.2.254` | unchanged |
|
||||||
|
| domain-controller | `192.168.2.138` | `192.168.2.253` | `/etc/sysconfig/network-scripts/ifcfg-eth0` on guest |
|
||||||
|
| pve1 | `192.168.2.250` | `192.168.2.245` | `/etc/network/interfaces` on Proxmox host |
|
||||||
|
| pbs | `192.168.2.108` | `192.168.2.244` | static config on PBS host |
|
||||||
|
| nixos workstation | `192.168.2.119` | `192.168.2.243` | `networking.interfaces` / NetworkManager on guest |
|
||||||
|
| ha-node1 | — | `192.168.2.228` | future |
|
||||||
|
| ha-node2 | — | `192.168.2.227` | future |
|
||||||
|
| ha-vip | — | `192.168.2.229` | future (Pacemaker resource) |
|
||||||
|
| server | `192.168.2.252` | `192.168.2.226` | static config on guest |
|
||||||
|
| docker | `192.168.2.249` | `192.168.2.225` | static config on guest |
|
||||||
|
| nix-cache | `192.168.2.120` | `192.168.2.224` | static config on guest |
|
||||||
|
| pxe-boot | `192.168.2.247` | `192.168.2.223` | static config on guest; update `vars.pxeServerIp` in `variables.nix` ✓ |
|
||||||
|
| tailscale-router | `192.168.2.121` | `192.168.2.222` | static config on guest |
|
||||||
|
| tor-relay | `192.168.2.107` | `192.168.2.221` | static config on guest |
|
||||||
|
| pdm | `192.168.2.248` | `192.168.2.220` | static config on guest |
|
||||||
|
|
||||||
|
### Cutover notes
|
||||||
|
|
||||||
|
- **Do domain-controller first** — it becomes the DNS server; everything else depends on it
|
||||||
|
having its new IP and FreeIPA DNS configured before Pi-hole is retired.
|
||||||
|
- **pve1 last among physical hosts** — changing the Proxmox management IP drops the web UI
|
||||||
|
briefly; all guests keep running.
|
||||||
|
- **Update Pi-hole custom.list / FreeIPA DNS A records** to new IPs before flipping any host,
|
||||||
|
so name resolution stays valid throughout the migration.
|
||||||
|
- **variables.nix already updated** for `pxeServerIp` (.247→.223), `pbsIp` (.108→.244), and
|
||||||
|
new `domainControllerIp` (.253). Rebuild affected hosts after renumbering.
|
||||||
|
- **Router DHCP**: once domain-controller is at .253 and FreeIPA DNS is serving `sweet.home`,
|
||||||
|
switch router DHCP on with pool .10–.59 and DNS option pointing to .253; retire Pi-hole CT.
|
||||||
|
- **Pi-hole's iPXE dnsmasq config** (`99-ipxe-chainload.conf`) moves to the pxe-boot CT as a
|
||||||
|
dnsmasq proxy-mode config before Pi-hole is decommissioned.
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
# Network Cutover Plan
|
||||||
|
|
||||||
|
Moves the LAN from the current flat/Pi-hole-managed state to the new IP scheme
|
||||||
|
defined in `docs/ip-addressing.md`. Works in five independent stages — each
|
||||||
|
stage is safe to pause after and resume later. Rollback steps are given at
|
||||||
|
every point where something can break.
|
||||||
|
|
||||||
|
**Before starting anything:** confirm you have
|
||||||
|
- SSH access to `192.168.2.138` (domain-controller, current IP)
|
||||||
|
- SSH access to `192.168.2.250` (pve1)
|
||||||
|
- Browser access to Pi-hole admin at `http://192.168.2.253`
|
||||||
|
- Browser access to router admin at `http://192.168.2.254`
|
||||||
|
- The FreeIPA `admin` password to hand
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 1 — Prepare FreeIPA DNS (zero downtime)
|
||||||
|
|
||||||
|
Everything here is additive. Pi-hole keeps running. Nothing breaks if you stop
|
||||||
|
mid-stage.
|
||||||
|
|
||||||
|
### 1a. Add NextDNS forwarders
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh wayne@192.168.2.138
|
||||||
|
kinit admin # enter FreeIPA admin password when prompted
|
||||||
|
ipa dnsconfig-mod \
|
||||||
|
--forwarder=45.90.28.142 \
|
||||||
|
--forwarder=45.90.30.142 \
|
||||||
|
--forward-policy=only
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify external resolution works through FreeIPA before continuing:**
|
||||||
|
```bash
|
||||||
|
dig @127.0.0.1 google.com +short # must return an IP, not SERVFAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1b. Add A records for every host at their CURRENT IPs
|
||||||
|
|
||||||
|
These represent the live state now. You'll update each record to the new IP
|
||||||
|
when you renumber that host in Stage 5.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ipa dnsrecord-add sweet.home pve1 --a-rec 192.168.2.250
|
||||||
|
ipa dnsrecord-add sweet.home pbs --a-rec 192.168.2.108
|
||||||
|
ipa dnsrecord-add sweet.home nixos --a-rec 192.168.2.119
|
||||||
|
ipa dnsrecord-add sweet.home server --a-rec 192.168.2.252
|
||||||
|
ipa dnsrecord-add sweet.home docker --a-rec 192.168.2.249
|
||||||
|
ipa dnsrecord-add sweet.home nix-cache --a-rec 192.168.2.120
|
||||||
|
ipa dnsrecord-add sweet.home pxe-boot --a-rec 192.168.2.247
|
||||||
|
ipa dnsrecord-add sweet.home tailscale-router --a-rec 192.168.2.121
|
||||||
|
ipa dnsrecord-add sweet.home tor-relay --a-rec 192.168.2.107
|
||||||
|
ipa dnsrecord-add sweet.home pdm --a-rec 192.168.2.248
|
||||||
|
ipa dnsrecord-add sweet.home router --a-rec 192.168.2.254
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1c. Clean up stale reverse-zone PTR records
|
||||||
|
|
||||||
|
FreeIPA already has PTR records from an earlier import but some are wrong.
|
||||||
|
Fix them now so reverse DNS is accurate from day one.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove stale "win11" entry at .250 (should be pve1)
|
||||||
|
ipa dnsrecord-del 2.168.192.in-addr.arpa 250 --ptr-rec win11.
|
||||||
|
ipa dnsrecord-add 2.168.192.in-addr.arpa 250 --ptr-rec pve1.sweet.home.
|
||||||
|
|
||||||
|
# Fix unqualified PTR records (missing .sweet.home. suffix)
|
||||||
|
ipa dnsrecord-mod 2.168.192.in-addr.arpa 108 --ptr-rec pbs.sweet.home.
|
||||||
|
ipa dnsrecord-mod 2.168.192.in-addr.arpa 248 --ptr-rec pdm.sweet.home.
|
||||||
|
ipa dnsrecord-mod 2.168.192.in-addr.arpa 249 --ptr-rec docker.sweet.home.
|
||||||
|
ipa dnsrecord-mod 2.168.192.in-addr.arpa 252 --ptr-rec server.sweet.home.
|
||||||
|
|
||||||
|
# Add any missing PTR records
|
||||||
|
ipa dnsrecord-add 2.168.192.in-addr.arpa 119 --ptr-rec nixos.sweet.home.
|
||||||
|
ipa dnsrecord-add 2.168.192.in-addr.arpa 120 --ptr-rec nix-cache.sweet.home.
|
||||||
|
ipa dnsrecord-add 2.168.192.in-addr.arpa 121 --ptr-rec tailscale-router.sweet.home.
|
||||||
|
ipa dnsrecord-add 2.168.192.in-addr.arpa 247 --ptr-rec pxe-boot.sweet.home.
|
||||||
|
ipa dnsrecord-add 2.168.192.in-addr.arpa 254 --ptr-rec router.sweet.home.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1d. Point domain-controller's own DNS at itself
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nmcli connection modify "System eth0" ipv4.dns "127.0.0.1"
|
||||||
|
sudo nmcli connection up "System eth0"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
```bash
|
||||||
|
dig pve1.sweet.home +short # must return 192.168.2.250
|
||||||
|
dig google.com +short # must return an IP (NextDNS forwarding)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rollback 1d:** `sudo nmcli connection modify "System eth0" ipv4.dns "192.168.2.253" && sudo nmcli connection up "System eth0"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 2 — Move pxe-boot DHCP options off Pi-hole (zero downtime)
|
||||||
|
|
||||||
|
Pi-hole's dnsmasq currently serves the iPXE boot options via
|
||||||
|
`99-ipxe-chainload.conf`. Before Pi-hole is retired, that config must move to
|
||||||
|
the pxe-boot CT running dnsmasq in proxy mode so PXE boot keeps working.
|
||||||
|
|
||||||
|
### 2a. Add dnsmasq proxy config to the pxe-boot NixOS module
|
||||||
|
|
||||||
|
In `modules/build-types/pxe-boot.nix`, add:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.dnsmasq = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
# Proxy mode: respond only to PXE DHCP requests, leave normal leases to router
|
||||||
|
dhcp-range = [ "192.168.2.0,proxy" ];
|
||||||
|
# iPXE client detection
|
||||||
|
dhcp-match = [
|
||||||
|
"set:ipxe,175"
|
||||||
|
"set:efi64,option:client-arch,7"
|
||||||
|
"set:efi64,option:client-arch,9"
|
||||||
|
];
|
||||||
|
dhcp-userclass = "set:ipxe,iPXE";
|
||||||
|
# Boot file selection
|
||||||
|
dhcp-boot = [
|
||||||
|
"tag:ipxe,tag:efi64,http://${vars.pxeServerIp}/boot.ipxe"
|
||||||
|
"tag:ipxe,http://${vars.pxeServerIp}/boot.ipxe"
|
||||||
|
"tag:efi64,ipxe.efi,,${vars.pxeServerIp}"
|
||||||
|
"undionly.kpxe,,${vars.pxeServerIp}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2b. Rebuild and deploy the pxe-boot CT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On pve1 — build the new tarball
|
||||||
|
nix build .#lxc-pxe-boot.config.system.build.tarball
|
||||||
|
|
||||||
|
# Verify dnsmasq starts correctly in the CT after deploy
|
||||||
|
ssh nixos@192.168.2.247 systemctl status dnsmasq
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2c. Remove the iPXE config from Pi-hole
|
||||||
|
|
||||||
|
In the Pi-hole CT, remove `/etc/dnsmasq.d/99-ipxe-chainload.conf` and
|
||||||
|
restart the FTL service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh wayne@pve1.sweet.home \
|
||||||
|
"sudo pct exec 100 -- bash -c 'rm /etc/dnsmasq.d/99-ipxe-chainload.conf && systemctl restart pihole-FTL'"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify:** PXE boot a test machine — it should still get an iPXE response and
|
||||||
|
reach the boot menu.
|
||||||
|
|
||||||
|
**Rollback 2c:** restore the file from the Pi-hole config backup at
|
||||||
|
`/etc/pihole/config_backups/` and restart pihole-FTL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3 — DHCP migration: Pi-hole → router (brief maintenance window)
|
||||||
|
|
||||||
|
**Do this in the evening.** Existing DHCP leases stay valid during the
|
||||||
|
switchover so connected devices don't drop — only new lease requests fail
|
||||||
|
during the gap, which is under 60 seconds if you follow the steps in order.
|
||||||
|
|
||||||
|
The key: configure the router's DHCP DNS option to point at `.253` (Pi-hole's
|
||||||
|
current IP). This way, all new leases issued by the router still get the same
|
||||||
|
DNS server address — clients never need to change their DNS config. When Pi-hole
|
||||||
|
is retired and the DC takes `.253` in Stage 4, `.253` just starts answering
|
||||||
|
differently. No client reconfiguration.
|
||||||
|
|
||||||
|
### 3a. Pre-configure router DHCP (do not enable yet)
|
||||||
|
|
||||||
|
Log into `http://192.168.2.254`, find the DHCP settings and fill in — but
|
||||||
|
leave DHCP **disabled** until step 3b:
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---|---|
|
||||||
|
| Start IP | 192.168.2.10 |
|
||||||
|
| End IP | 192.168.2.59 |
|
||||||
|
| Subnet mask | 255.255.255.0 |
|
||||||
|
| Gateway | 192.168.2.254 |
|
||||||
|
| Primary DNS | 192.168.2.253 |
|
||||||
|
| Secondary DNS | *(leave blank)* |
|
||||||
|
| Lease time | 24h |
|
||||||
|
|
||||||
|
Save without enabling.
|
||||||
|
|
||||||
|
### 3b. Switchover (do steps in quick succession)
|
||||||
|
|
||||||
|
1. **Disable Pi-hole DHCP:** Pi-hole admin UI → Settings → DHCP → uncheck
|
||||||
|
"DHCP server enabled" → Save
|
||||||
|
2. **Enable router DHCP** immediately after step 1
|
||||||
|
|
||||||
|
### 3c. Verify router DHCP is working
|
||||||
|
|
||||||
|
On a phone or laptop, disconnect from WiFi and reconnect (or run
|
||||||
|
`sudo dhclient -r && sudo dhclient` on a Linux host):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ip addr show # IP should be in 192.168.2.10–59 range
|
||||||
|
dig google.com # should resolve (Pi-hole DNS still running at .253)
|
||||||
|
dig pve1.sweet.home # should resolve via FreeIPA at .138 (relayed via Pi-hole)
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait 10–15 minutes for the most active devices to renew their leases. There's
|
||||||
|
no need to wait for all leases to expire before proceeding.
|
||||||
|
|
||||||
|
**Rollback 3b:** Re-enable Pi-hole DHCP. Disable router DHCP. Done — existing
|
||||||
|
leases remain valid so most devices are unaffected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 4 — Move domain-controller from .138 to .253
|
||||||
|
|
||||||
|
Pi-hole lives at `.253`. The DC must take `.253` the moment Pi-hole stops so
|
||||||
|
clients that still have `.253` as their DNS server don't notice the change.
|
||||||
|
Script these commands in advance and run them in rapid succession.
|
||||||
|
|
||||||
|
**Pre-stage: have this SSH command ready before running step 4a:**
|
||||||
|
```bash
|
||||||
|
ssh wayne@192.168.2.138 "
|
||||||
|
sudo nmcli connection modify 'System eth0' \
|
||||||
|
ipv4.addresses '192.168.2.253/24' \
|
||||||
|
ipv4.gateway '192.168.2.254' \
|
||||||
|
ipv4.dns '127.0.0.1' \
|
||||||
|
ipv4.method manual && \
|
||||||
|
sudo nmcli connection up 'System eth0'
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Also update the Proxmox VM config to match (run from pve1):**
|
||||||
|
```bash
|
||||||
|
sudo qm set 108 \
|
||||||
|
--ipconfig0 ip=192.168.2.253/24,gw=192.168.2.254 \
|
||||||
|
--nameserver 192.168.2.253
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4a. Stop Pi-hole
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh wayne@pve1.sweet.home "sudo pct stop 100"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4b. Immediately: change DC's IP to .253
|
||||||
|
|
||||||
|
Run the pre-staged SSH command from above. You have ~30 seconds before any
|
||||||
|
client notices Pi-hole is gone. If SSH to `.138` refuses (the IP is already
|
||||||
|
changing), open a Proxmox console to VM 108 and run the `nmcli` commands
|
||||||
|
there.
|
||||||
|
|
||||||
|
### 4c. Update Proxmox VM config
|
||||||
|
|
||||||
|
Run the pre-staged `qm set 108` command from above.
|
||||||
|
|
||||||
|
### 4d. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh wayne@192.168.2.253 # must connect (new DC IP)
|
||||||
|
dig @192.168.2.253 pve1.sweet.home +short # must return 192.168.2.250
|
||||||
|
dig @192.168.2.253 google.com +short # must return an IP
|
||||||
|
```
|
||||||
|
|
||||||
|
From a client device that renewed its DHCP lease in Stage 3:
|
||||||
|
```bash
|
||||||
|
cat /etc/resolv.conf # should show 192.168.2.253
|
||||||
|
dig pve1.sweet.home # should resolve
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rollback 4:** `ssh wayne@pve1.sweet.home "sudo pct start 100"`. Change DC IP
|
||||||
|
back to .138 via Proxmox console. This restores full Pi-hole DNS/DHCP service.
|
||||||
|
Leave Pi-hole CT stopped-but-intact for 48 hours before deleting it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 5 — Host renumbering (one at a time, any order)
|
||||||
|
|
||||||
|
For each host:
|
||||||
|
1. Update FreeIPA DNS A record and PTR record to the new IP
|
||||||
|
2. Change the static IP on the host itself
|
||||||
|
3. Verify SSH to new IP
|
||||||
|
4. Update `variables.nix` if that host has an IP variable (pxe-boot, pbs — already done in this PR)
|
||||||
|
|
||||||
|
**FreeIPA record update template** (run as admin on domain-controller):
|
||||||
|
```bash
|
||||||
|
ipa dnsrecord-mod sweet.home <hostname> --a-rec <new-ip>
|
||||||
|
ipa dnsrecord-del 2.168.192.in-addr.arpa <old-last-octet> --ptr-rec <hostname>.sweet.home.
|
||||||
|
ipa dnsrecord-add 2.168.192.in-addr.arpa <new-last-octet> --ptr-rec <hostname>.sweet.home.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renumbering order
|
||||||
|
|
||||||
|
| # | Host | Old IP | New IP | How to change IP |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | nixos workstation | .119 | .243 | NetworkManager on guest; or `nmcli connection modify` |
|
||||||
|
| 2 | nix-cache | .120 | .224 | `pct set 102 --net0 name=eth0,bridge=vmbr0,ip=192.168.2.224/24,gw=192.168.2.254` then `pct reboot 102` |
|
||||||
|
| 3 | tailscale-router | .121 | .222 | Static config on guest; check Tailscale ACLs if IP is referenced there |
|
||||||
|
| 4 | tor-relay | .107 | .221 | `pct set 104 --net0 name=eth0,bridge=vmbr0,ip=192.168.2.221/24,gw=192.168.2.254` then `pct reboot 104` |
|
||||||
|
| 5 | pdm | .248 | .220 | `pct set 106 --net0 name=eth0,bridge=vmbr0,ip=192.168.2.220/24,gw=192.168.2.254` then `pct reboot 106` |
|
||||||
|
| 6 | pxe-boot | .247 | .223 | `pct set 103 --net0 name=eth0,bridge=vmbr0,ip=192.168.2.223/24,gw=192.168.2.254` then rebuild NixOS (already updated in variables.nix) |
|
||||||
|
| 7 | server | .252 | .226 | Static config on guest; NFS clients (docker) lose mounts briefly — they remount automatically |
|
||||||
|
| 8 | docker | .249 | .225 | Static config on guest; do this after server is at .226 |
|
||||||
|
| 9 | pbs | .108 | .244 | Static config on PBS host itself; update in `pbsIp` already done in variables.nix |
|
||||||
|
| 10 | pve1 | .250 | .245 | Edit `/etc/network/interfaces` on the Proxmox host — see below |
|
||||||
|
|
||||||
|
### pve1 renumber (step 10 — do last)
|
||||||
|
|
||||||
|
All guests keep running; only the Proxmox web UI is briefly unreachable.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh wayne@pve1.sweet.home
|
||||||
|
|
||||||
|
# Edit /etc/network/interfaces: change address from .250 to .245
|
||||||
|
sudo nano /etc/network/interfaces
|
||||||
|
# Change: address 192.168.2.250/24
|
||||||
|
# To: address 192.168.2.245/24
|
||||||
|
|
||||||
|
sudo systemctl restart networking
|
||||||
|
# SSH will drop here — reconnect to new IP
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh wayne@192.168.2.245 # verify
|
||||||
|
```
|
||||||
|
|
||||||
|
Update FreeIPA DNS:
|
||||||
|
```bash
|
||||||
|
ipa dnsrecord-mod sweet.home pve1 --a-rec 192.168.2.245
|
||||||
|
ipa dnsrecord-del 2.168.192.in-addr.arpa 250 --ptr-rec pve1.sweet.home.
|
||||||
|
ipa dnsrecord-add 2.168.192.in-addr.arpa 245 --ptr-rec pve1.sweet.home.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rollback any step 5 host:** change the IP back on the guest and update the
|
||||||
|
FreeIPA record back to the old IP. The old IP is unoccupied so you can
|
||||||
|
temporarily use either.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 6 — Final cleanup
|
||||||
|
|
||||||
|
Once all hosts are at their new IPs and verified:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete the Pi-hole CT (already stopped since Stage 4)
|
||||||
|
ssh wayne@pve1.sweet.home "sudo pct destroy 100"
|
||||||
|
|
||||||
|
# Remove stale FreeIPA records for retired addresses
|
||||||
|
ipa dnsrecord-del sweet.home pihole --del-all
|
||||||
|
ipa dnsrecord-del 2.168.192.in-addr.arpa 253 --ptr-rec pihole.sweet.home.
|
||||||
|
|
||||||
|
# Rebuild any NixOS hosts that reference pbsIp or pxeServerIp to pick up
|
||||||
|
# the updated variables.nix values (pxe-boot mandatory; others as convenient)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback summary
|
||||||
|
|
||||||
|
| What broke | How to roll back |
|
||||||
|
|---|---|
|
||||||
|
| FreeIPA DNS not resolving | Check `systemctl status named` on DC; restart if failed |
|
||||||
|
| FreeIPA DNS unreachable | `pct start 100` on pve1 (restores Pi-hole) |
|
||||||
|
| Router DHCP not handing out leases | Re-enable Pi-hole DHCP; disable router DHCP |
|
||||||
|
| DC unreachable after IP change | Proxmox console on VM 108 → `nmcli connection up "System eth0"` with old IP |
|
||||||
|
| Host unreachable after renumber | Proxmox console → revert IP; or `pct set <id> --net0 ...` old IP and reboot CT |
|
||||||
|
| pve1 web UI gone after renumber | SSH to .245 and check `/etc/network/interfaces`; if wrong, fix and restart networking |
|
||||||
+97
-7
@@ -15,6 +15,7 @@ rescue/inspection use.
|
|||||||
- TFTP root for first-stage bootloaders: `/srv/pxe/tftp`
|
- TFTP root for first-stage bootloaders: `/srv/pxe/tftp`
|
||||||
- iPXE entry script: `/srv/pxe/http/boot.ipxe`
|
- iPXE entry script: `/srv/pxe/http/boot.ipxe`
|
||||||
- Generated iPXE menu: `/srv/pxe/http/menu.ipxe`
|
- Generated iPXE menu: `/srv/pxe/http/menu.ipxe`
|
||||||
|
- Debian Minimal iPXE script: `/srv/pxe/http/debian.ipxe`
|
||||||
- SystemRescue iPXE script: `/srv/pxe/http/systemrescue.ipxe`
|
- SystemRescue iPXE script: `/srv/pxe/http/systemrescue.ipxe`
|
||||||
- TFTP fallback script: `/srv/pxe/tftp/autoexec.ipxe`
|
- TFTP fallback script: `/srv/pxe/tftp/autoexec.ipxe`
|
||||||
- Boot binaries copied from the Nix `ipxe` package:
|
- Boot binaries copied from the Nix `ipxe` package:
|
||||||
@@ -28,32 +29,51 @@ The host creates these directories with systemd tmpfiles:
|
|||||||
```text
|
```text
|
||||||
/srv/pxe
|
/srv/pxe
|
||||||
/srv/pxe/http
|
/srv/pxe/http
|
||||||
/srv/pxe/http/images
|
/srv/pxe/http/images -> /mnt/pxe-images (symlink to NFS share)
|
||||||
/srv/pxe/http/auto-installer
|
/srv/pxe/http/auto-installer
|
||||||
/srv/pxe/http/nixos-minimal
|
/srv/pxe/http/nixos-minimal
|
||||||
|
/srv/pxe/http/debian
|
||||||
/srv/pxe/http/systemrescue
|
/srv/pxe/http/systemrescue
|
||||||
/srv/pxe/http/ubuntu
|
/srv/pxe/http/ubuntu
|
||||||
/srv/pxe/http/rescue
|
/srv/pxe/http/rescue
|
||||||
/srv/pxe/tftp
|
/srv/pxe/tftp
|
||||||
```
|
```
|
||||||
|
|
||||||
Mount shared image storage under `/srv/pxe/http`, preferably
|
`/srv/pxe/http/images` is a symlink to `/mnt/pxe-images`, which is an NFS
|
||||||
`/srv/pxe/http/images` unless a menu entry expects files in a specific
|
mount of `server.sweet.home:/tank/pxe-boot/images`
|
||||||
directory such as `/srv/pxe/http/auto-installer`.
|
(`modules/pxe-boot/mount-pxe-images.nix`). Place large images there (ISOs,
|
||||||
|
disk images) rather than on the pxe-boot host's own root disk. For an LXC
|
||||||
|
pxe-boot container the mount uses NFSv3+nolock with `nofail` (eager,
|
||||||
|
non-blocking on server unavailability); for a Proxmox VM it uses NFSv4.2
|
||||||
|
with `x-systemd.automount` (lazy, triggered on first access).
|
||||||
|
|
||||||
|
When running as `lxc-pxe-boot`, the Proxmox container must have
|
||||||
|
`features: nesting=1,mount=nfs` (at minimum) in its Proxmox config. `nesting=1`
|
||||||
|
is required by systemd 260+ for credential isolation (user namespace creation
|
||||||
|
and internal move-mounts); without it, AppArmor denies both, and every
|
||||||
|
systemd service that uses `PrivateUsers`, `PrivateDevices`, or credential
|
||||||
|
passing fails on boot. `mount=nfs` allows the NFSv3 mount. Both are set
|
||||||
|
automatically by `scripts/proxmox/create-proxmox-resource.sh` (via
|
||||||
|
`PROXMOX_DEFAULT_LXC_FEATURES` in `scripts/env.sh` which defaults to
|
||||||
|
`nesting=1,keyctl=1,mount=nfs;nfs4`). If you ever change these features
|
||||||
|
manually via `pct set`, be sure to include both — `pct set` replaces the
|
||||||
|
entire features string, it does not append to it.
|
||||||
|
|
||||||
The HTTP iPXE chain is:
|
The HTTP iPXE chain is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
undionly.kpxe or ipxe.efi
|
undionly.kpxe or ipxe.efi
|
||||||
-> autoexec.ipxe from the TFTP root, when iPXE requests it
|
-> autoexec.ipxe from the TFTP root, when iPXE requests it
|
||||||
-> http://192.168.2.247/boot.ipxe
|
-> http://192.168.2.223/boot.ipxe
|
||||||
-> http://192.168.2.247/menu.ipxe
|
-> http://192.168.2.223/menu.ipxe
|
||||||
```
|
```
|
||||||
|
|
||||||
The generated menu currently exposes entries for:
|
The generated menu currently exposes entries for:
|
||||||
|
|
||||||
- NixOS Auto-Installer
|
- NixOS Auto-Installer
|
||||||
- NixOS Minimal
|
- NixOS Minimal
|
||||||
|
- Debian Minimal
|
||||||
|
- FreeIPA Server (Rocky Linux 9)
|
||||||
- SystemRescue environment
|
- SystemRescue environment
|
||||||
- iPXE shell
|
- iPXE shell
|
||||||
- Reboot
|
- Reboot
|
||||||
@@ -84,19 +104,82 @@ directory name (`auto-installer` / `nixos-minimal`), so each one's
|
|||||||
generated system name (`nixos-system-<name>-*`) is self-describing rather
|
generated system name (`nixos-system-<name>-*`) is self-describing rather
|
||||||
than the nixpkgs default of `nixos-system-nixos-*` for both.
|
than the nixpkgs default of `nixos-system-nixos-*` for both.
|
||||||
|
|
||||||
|
The Debian Minimal entry chains `http://<pxeServerIp>/debian.ipxe`, which loads
|
||||||
|
the Debian bookworm netboot kernel and initrd from `/srv/pxe/http/debian/`. The
|
||||||
|
`fetch-debian-netboot.service` oneshot downloads these files from
|
||||||
|
`deb.debian.org` on first boot (idempotent — skips if files are already
|
||||||
|
present):
|
||||||
|
|
||||||
|
```text
|
||||||
|
/srv/pxe/http/debian/linux (Debian bookworm netboot kernel)
|
||||||
|
/srv/pxe/http/debian/initrd.gz (Debian bookworm netboot initrd)
|
||||||
|
```
|
||||||
|
|
||||||
|
The service requires outbound internet access on the pxe-boot host. To
|
||||||
|
re-download (e.g. after a Debian point release), delete the files and restart
|
||||||
|
the service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm /srv/pxe/http/debian/linux /srv/pxe/http/debian/initrd.gz
|
||||||
|
systemctl restart fetch-debian-netboot.service
|
||||||
|
```
|
||||||
|
|
||||||
|
To update to a different Debian release, change `debianRelease` in
|
||||||
|
`modules/build-types/pxe-boot.nix` and redeploy.
|
||||||
|
|
||||||
|
The **FreeIPA Server (Rocky Linux 9)** entry chains
|
||||||
|
`http://<pxeServerIp>/rocky-freeipa.ipxe`, which boots the Rocky Linux 9
|
||||||
|
Anaconda installer with a Kickstart file (`rocky-freeipa.ks`) hosted on the
|
||||||
|
same server. The `fetch-rocky-pxeboot.service` oneshot downloads the pxeboot
|
||||||
|
kernel and initrd from the Rocky Linux mirror on first boot (idempotent):
|
||||||
|
|
||||||
|
```text
|
||||||
|
/srv/pxe/http/rocky/vmlinuz (Rocky Linux 9 Anaconda pxeboot kernel)
|
||||||
|
/srv/pxe/http/rocky/initrd.img (Rocky Linux 9 Anaconda pxeboot initrd)
|
||||||
|
```
|
||||||
|
|
||||||
|
The Kickstart file is generated from the NixOS module and staged at
|
||||||
|
`/srv/pxe/http/rocky-freeipa.ks`. It performs a fully unattended install:
|
||||||
|
|
||||||
|
1. Installs Rocky Linux 9 with `ipa-server` + `ipa-server-dns` packages
|
||||||
|
2. Configures static IP `192.168.2.138`, hostname `domain-controller.sweet.home`
|
||||||
|
3. Creates user `wayne` with the `adminSshKey` from `variables.nix`
|
||||||
|
4. Generates random IPA passwords and writes them to `/root/ipa-credentials.txt`
|
||||||
|
5. Creates a `freeipa-first-boot.service` oneshot that runs `ipa-server-install`
|
||||||
|
on first reboot (~20 minutes)
|
||||||
|
|
||||||
|
After the install completes:
|
||||||
|
- SSH in as `wayne@domain-controller` using the admin key
|
||||||
|
- Monitor FreeIPA install progress: `sudo tail -f /root/freeipa-install.log`
|
||||||
|
- Retrieve credentials: `sudo cat /root/ipa-credentials.txt` (save to password manager)
|
||||||
|
- Configure Pi-hole: `server=/sweet.home/192.168.2.138` in dnsmasq
|
||||||
|
|
||||||
|
To refresh the pxeboot files (e.g. after a Rocky point release):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm /srv/pxe/http/rocky/vmlinuz /srv/pxe/http/rocky/initrd.img
|
||||||
|
systemctl restart fetch-rocky-pxeboot.service
|
||||||
|
```
|
||||||
|
|
||||||
|
To update to a different Rocky release, change `rockyRelease` in
|
||||||
|
`modules/build-types/pxe-boot.nix` and redeploy.
|
||||||
|
|
||||||
The SystemRescue entry expects the source ISO at:
|
The SystemRescue entry expects the source ISO at:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/srv/pxe/http/images/systemrescue.iso
|
/srv/pxe/http/images/systemrescue.iso
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Since `/srv/pxe/http/images` is the NFS-backed symlink, place the ISO on the
|
||||||
|
NFS share at `server.sweet.home:/tank/pxe-boot/images/systemrescue.iso`.
|
||||||
|
|
||||||
The `stage-systemrescue.service` oneshot extracts that ISO into:
|
The `stage-systemrescue.service` oneshot extracts that ISO into:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/srv/pxe/http/systemrescue
|
/srv/pxe/http/systemrescue
|
||||||
```
|
```
|
||||||
|
|
||||||
The rescue menu entry then chains `http://192.168.2.247/systemrescue.ipxe`,
|
The rescue menu entry then chains `http://192.168.2.223/systemrescue.ipxe`,
|
||||||
which loads the SystemRescue kernel and initramfs from the extracted tree and
|
which loads the SystemRescue kernel and initramfs from the extracted tree and
|
||||||
uses `archiso_http_srv` to fetch the squashfs payload over HTTP.
|
uses `archiso_http_srv` to fetch the squashfs payload over HTTP.
|
||||||
|
|
||||||
@@ -113,6 +196,13 @@ After deployment by an operator, basic service checks are:
|
|||||||
```bash
|
```bash
|
||||||
curl http://pxe-boot/boot.ipxe
|
curl http://pxe-boot/boot.ipxe
|
||||||
curl http://pxe-boot/menu.ipxe
|
curl http://pxe-boot/menu.ipxe
|
||||||
|
curl http://pxe-boot/debian.ipxe
|
||||||
|
curl -I http://pxe-boot/debian/linux
|
||||||
|
curl -I http://pxe-boot/debian/initrd.gz
|
||||||
|
curl http://pxe-boot/rocky-freeipa.ipxe
|
||||||
|
curl http://pxe-boot/rocky-freeipa.ks
|
||||||
|
curl -I http://pxe-boot/rocky/vmlinuz
|
||||||
|
curl -I http://pxe-boot/rocky/initrd.img
|
||||||
curl http://pxe-boot/systemrescue.ipxe
|
curl http://pxe-boot/systemrescue.ipxe
|
||||||
curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/vmlinuz
|
curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/vmlinuz
|
||||||
curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/sysresccd.img
|
curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/sysresccd.img
|
||||||
|
|||||||
Generated
+151
-1
@@ -1,5 +1,62 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"clan-core": {
|
||||||
|
"inputs": {
|
||||||
|
"data-mesher": "data-mesher",
|
||||||
|
"disko": [
|
||||||
|
"disko"
|
||||||
|
],
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"nix-darwin": "nix-darwin",
|
||||||
|
"nix-select": "nix-select",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"sops-nix": [
|
||||||
|
"sops-nix"
|
||||||
|
],
|
||||||
|
"systems": "systems",
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1783497933,
|
||||||
|
"narHash": "sha256-TxmwEews6URFPqOWEHNychtXbFDgLZjbOfEXtvtOm6U=",
|
||||||
|
"rev": "3dc0221ca09033599fe98055e9bbc81bdf32732a",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/3dc0221ca09033599fe98055e9bbc81bdf32732a.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://git.clan.lol/clan/clan-core/archive/26.05.tar.gz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data-mesher": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": [
|
||||||
|
"clan-core",
|
||||||
|
"flake-parts"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"clan-core",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"treefmt-nix": [
|
||||||
|
"clan-core",
|
||||||
|
"treefmt-nix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778718524,
|
||||||
|
"narHash": "sha256-pXLoI6Ax0EnUK6r34UM1vibVC7CfTu6j72R2692ZzPs=",
|
||||||
|
"rev": "12c552ad547d87254f33f33bddd1a2cdbeac754d",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/12c552ad547d87254f33f33bddd1a2cdbeac754d.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://git.clan.lol/clan/data-mesher/archive/main.tar.gz"
|
||||||
|
}
|
||||||
|
},
|
||||||
"disko": {
|
"disko": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -51,9 +108,30 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": [
|
||||||
|
"clan-core",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778716662,
|
||||||
|
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": "systems"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1694529238,
|
"lastModified": 1694529238,
|
||||||
@@ -109,6 +187,40 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nix-darwin": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"clan-core",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1779036909,
|
||||||
|
"narHash": "sha256-zXcwYQGCT6pzinK+1dBB2ekTVtfxGZAapb3Evdcu4fY=",
|
||||||
|
"owner": "nix-darwin",
|
||||||
|
"repo": "nix-darwin",
|
||||||
|
"rev": "56c666e108467d87d13508936aade6d567f2a501",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-darwin",
|
||||||
|
"repo": "nix-darwin",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix-select": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763303120,
|
||||||
|
"narHash": "sha256-yxcNOha7Cfv2nhVpz9ZXSNKk0R7wt4AiBklJ8D24rVg=",
|
||||||
|
"rev": "3d1e3860bef36857a01a2ddecba7cdb0a14c35a9",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/3d1e3860bef36857a01a2ddecba7cdb0a14c35a9.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixos-conf-editor": {
|
"nixos-conf-editor": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
@@ -163,6 +275,7 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"clan-core": "clan-core",
|
||||||
"disko": "disko",
|
"disko": "disko",
|
||||||
"home-manager": "home-manager",
|
"home-manager": "home-manager",
|
||||||
"nixos-conf-editor": "nixos-conf-editor",
|
"nixos-conf-editor": "nixos-conf-editor",
|
||||||
@@ -214,6 +327,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774449309,
|
||||||
|
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"ref": "future-26.11",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681028828,
|
"lastModified": 1681028828,
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
@@ -227,6 +356,27 @@
|
|||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"clan-core",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1780220602,
|
||||||
|
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -16,6 +16,19 @@
|
|||||||
url = "github:Mic92/sops-nix";
|
url = "github:Mic92/sops-nix";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
clan-core = {
|
||||||
|
url = "https://git.clan.lol/clan/clan-core/archive/26.05.tar.gz";
|
||||||
|
# Deduplicate modules: clan-core bundles its own disko and sops-nix
|
||||||
|
# (both imported by nixosModules.clanCore). Without follows, we'd get
|
||||||
|
# two different versions of each, and disko's _module.args.diskoLib
|
||||||
|
# unique option would conflict. With follows, clan-core uses the same
|
||||||
|
# store paths as us, so NixOS deduplicates the imports.
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.follows = "nixpkgs";
|
||||||
|
disko.follows = "disko";
|
||||||
|
sops-nix.follows = "sops-nix";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, nixos-conf-editor, home-manager, sops-nix, ... } @ inputs:
|
outputs = { self, nixpkgs, nixos-conf-editor, home-manager, sops-nix, ... } @ inputs:
|
||||||
@@ -41,6 +54,22 @@
|
|||||||
modules = [
|
modules = [
|
||||||
inputs.disko.nixosModules.disko
|
inputs.disko.nixosModules.disko
|
||||||
sops-nix.nixosModules.sops
|
sops-nix.nixosModules.sops
|
||||||
|
inputs.clan-core.nixosModules.clanCore
|
||||||
|
{
|
||||||
|
# Required clan settings. directory is the flake root (where
|
||||||
|
# vars/ and sops/ directories live); machine.name is the flake
|
||||||
|
# target name (matches what clan vars generate uses as the key
|
||||||
|
# under vars/per-machine/). enableRecommendedDefaults = false
|
||||||
|
# is mandatory: without it, clan unconditionally enables
|
||||||
|
# networking.useNetworkd, adds packages, and tweaks nix settings
|
||||||
|
# -- none of which belong here.
|
||||||
|
clan.core = {
|
||||||
|
settings.directory = self;
|
||||||
|
settings.machine.name = flakeTarget;
|
||||||
|
enableRecommendedDefaults = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
./modules/clan/ssh-host-key.nix
|
||||||
./modules/common/configuration.nix
|
./modules/common/configuration.nix
|
||||||
./modules/platforms/${platform}.nix
|
./modules/platforms/${platform}.nix
|
||||||
./modules/build-types/${buildType}.nix
|
./modules/build-types/${buildType}.nix
|
||||||
@@ -96,11 +125,14 @@
|
|||||||
proxmox-pxe-boot = mkTarget { platform = "proxmox"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
|
proxmox-pxe-boot = mkTarget { platform = "proxmox"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
|
||||||
lxc-pxe-boot = mkTarget { platform = "lxc"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
|
lxc-pxe-boot = mkTarget { platform = "lxc"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
|
||||||
|
|
||||||
linode-tailscale-exit-node = mkTarget { platform = "linode"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
linode-tailscale-router = mkTarget { platform = "linode"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
|
||||||
proxmox-tailscale-exit-node = mkTarget { platform = "proxmox"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
proxmox-tailscale-router = mkTarget { platform = "proxmox"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
|
||||||
lxc-tailscale-exit-node = mkTarget { platform = "lxc"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
lxc-tailscale-router = mkTarget { platform = "lxc"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
|
||||||
|
|
||||||
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
|
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
|
||||||
|
|
||||||
|
proxmox-ha-server-1 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-1/host.nix; };
|
||||||
|
proxmox-ha-server-2 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-2/host.nix; };
|
||||||
};
|
};
|
||||||
|
|
||||||
# Auto-install environments (migrated from the former nix-auto-installer
|
# Auto-install environments (migrated from the former nix-auto-installer
|
||||||
|
|||||||
+12
-3
@@ -1,8 +1,17 @@
|
|||||||
_:
|
{ vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
networking.hostName = "docker";
|
networking = {
|
||||||
networking.hostId = "007f0200";
|
hostName = "docker";
|
||||||
|
hostId = "007f0200";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.${vars.vmLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.dockerIp;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
boot.zfs.forceImportRoot = false;
|
boot.zfs.forceImportRoot = false;
|
||||||
|
|
||||||
# Preserved from the pre-refactor `docker` target — stateVersion must never
|
# Preserved from the pre-refactor `docker` target — stateVersion must never
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{ vars, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(import ../../modules/beszel/host-token.nix {
|
||||||
|
name = "ha-server-1";
|
||||||
|
sopsFile = ../../secrets/ha-server-1.yaml;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
hostName = vars.haServer1Host;
|
||||||
|
hostId = "3a4b5c6d";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.${vars.vmLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haServer1Ip;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
interfaces.${vars.vmStorageInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haServer1StorageIp;
|
||||||
|
prefixLength = vars.haStoragePrefixLength;
|
||||||
|
}];
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Set KEY after pairing this host with the beszel hub; the token is sops-managed.
|
||||||
|
services.beszel.agent.environment.KEY = "";
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{ vars, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(import ../../modules/beszel/host-token.nix {
|
||||||
|
name = "ha-server-2";
|
||||||
|
sopsFile = ../../secrets/ha-server-2.yaml;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
hostName = vars.haServer2Host;
|
||||||
|
hostId = "7e8f9a0b";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.${vars.vmLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haServer2Ip;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
interfaces.${vars.vmStorageInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haServer2StorageIp;
|
||||||
|
prefixLength = vars.haStoragePrefixLength;
|
||||||
|
}];
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Set KEY after pairing this host with the beszel hub; the token is sops-managed.
|
||||||
|
services.beszel.agent.environment.KEY = "";
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
}
|
||||||
@@ -6,9 +6,23 @@
|
|||||||
name = "nix-cache";
|
name = "nix-cache";
|
||||||
sopsFile = ../../secrets/nix-cache.yaml;
|
sopsFile = ../../secrets/nix-cache.yaml;
|
||||||
})
|
})
|
||||||
|
(import ../../modules/ipa/client.nix {
|
||||||
|
keytabSopsFile = ../../secrets/nix-cache.keytab;
|
||||||
|
caCertFile = ../../certs/ipa-ca.crt;
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
networking.hostName = vars.nixCacheHost;
|
networking = {
|
||||||
|
hostName = vars.nixCacheHost;
|
||||||
|
domain = vars.homeDomain;
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.nixCacheIp;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
services.beszel.agent.environment = {
|
services.beszel.agent.environment = {
|
||||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||||
|
|||||||
@@ -19,11 +19,15 @@
|
|||||||
nextcloud-client
|
nextcloud-client
|
||||||
# vscode
|
# vscode
|
||||||
chromium
|
chromium
|
||||||
|
claude-code
|
||||||
|
fish
|
||||||
|
sops
|
||||||
];
|
];
|
||||||
|
|
||||||
# Optional: set environment vars
|
# Optional: set environment vars
|
||||||
sessionVariables = {
|
sessionVariables = {
|
||||||
EDITOR = "vim";
|
EDITOR = "vim";
|
||||||
|
SOPS_AGE_KEY_FILE = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
|
||||||
};
|
};
|
||||||
|
|
||||||
file = {
|
file = {
|
||||||
|
|||||||
+11
-2
@@ -1,7 +1,16 @@
|
|||||||
_:
|
{ vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
networking.hostName = "pxe-boot";
|
networking = {
|
||||||
|
hostName = "pxe-boot";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.pxeServerIp;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
# Preserved from the pre-refactor `pxe-boot` target — stateVersion must
|
# Preserved from the pre-refactor `pxe-boot` target — stateVersion must
|
||||||
# never be bumped on an already-installed machine.
|
# never be bumped on an already-installed machine.
|
||||||
|
|||||||
+11
-2
@@ -8,8 +8,17 @@
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
networking.hostName = vars.nfsServerHost;
|
networking = {
|
||||||
networking.hostId = "6689f93e";
|
hostName = vars.nfsServerHost;
|
||||||
|
hostId = "6689f93e";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.${vars.vmLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.serverIp;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
services.beszel.agent.environment = {
|
services.beszel.agent.environment = {
|
||||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
_:
|
|
||||||
|
|
||||||
{
|
|
||||||
networking.hostName = "exit-node";
|
|
||||||
|
|
||||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
|
||||||
# for pool-import safety, and this host does neither.
|
|
||||||
|
|
||||||
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
|
||||||
# flake's current nixpkgs release rather than being pinned to an older one.
|
|
||||||
system.stateVersion = "26.05";
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{ vars, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(import ../../modules/beszel/host-token.nix {
|
||||||
|
name = "tailscale-router";
|
||||||
|
sopsFile = ../../secrets/tailscale-router.yaml;
|
||||||
|
})
|
||||||
|
(import ../../modules/ipa/client.nix {
|
||||||
|
keytabSopsFile = ../../secrets/tailscale-router.keytab;
|
||||||
|
caCertFile = ../../certs/ipa-ca.crt;
|
||||||
|
})
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
hostName = "tailscale-router";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.tailscaleRouterIp;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.beszel.agent.environment = {
|
||||||
|
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||||
|
};
|
||||||
|
|
||||||
|
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
||||||
|
# for pool-import safety, and this host does neither.
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
}
|
||||||
@@ -1,10 +1,30 @@
|
|||||||
_:
|
{ vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
networking.hostName = "tor-relay";
|
imports = [
|
||||||
|
(import ../../modules/beszel/host-token.nix {
|
||||||
|
name = "tor-relay";
|
||||||
|
sopsFile = ../../secrets/tor-relay.yaml;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
networking = {
|
||||||
# for pool-import safety, and this host does neither.
|
hostName = "tor-relay";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.torRelayIp;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# No networking.hostId: only ZFS-touching hosts need one for pool-import
|
||||||
|
# safety, and this host does neither.
|
||||||
|
|
||||||
|
services.beszel.agent.environment = {
|
||||||
|
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||||
|
};
|
||||||
|
|
||||||
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
||||||
# flake's current nixpkgs release rather than being pinned to an older one.
|
# flake's current nixpkgs release rather than being pinned to an older one.
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
../docker/enable-service.nix
|
../docker/enable-service.nix
|
||||||
../docker/nextcloud-cron-job.nix
|
../docker/nextcloud-cron-job.nix
|
||||||
../docker/docker-health-to-gotify.nix
|
../docker/docker-health-to-gotify.nix
|
||||||
../tailscale/enable-service.nix
|
|
||||||
../traefik/rotate-logs.nix
|
../traefik/rotate-logs.nix
|
||||||
../raspi/mount-data.nix
|
../raspi/mount-data.nix
|
||||||
../services/enable-rpcbind.nix
|
../services/enable-rpcbind.nix
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
boot.loader.grub.useOSProber = true;
|
boot.loader.grub.useOSProber = true;
|
||||||
|
programs.direnv.enable = true;
|
||||||
services = {
|
services = {
|
||||||
xserver = {
|
xserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# HA file server build type: DRBD + XFS + LIO iSCSI + NFS, managed by
|
||||||
|
# Corosync + Pacemaker. Both ha-server-1 and ha-server-2 use this type.
|
||||||
|
#
|
||||||
|
# NFS start/stop:
|
||||||
|
# services.nfs.server.enable = true configures /etc/exports, wires up
|
||||||
|
# rpcbind, and loads kernel modules — but nfs-server.service.wantedBy is
|
||||||
|
# force-cleared so systemd does NOT auto-start it at boot. Pacemaker's
|
||||||
|
# ha-group resource group (configured by scripts/ha/cluster-init.sh)
|
||||||
|
# starts and stops nfs-server as part of the failover sequence after the
|
||||||
|
# XFS mount and iSCSI target are brought up on the new Active node.
|
||||||
|
#
|
||||||
|
# Beszel agent:
|
||||||
|
# Enabled here via enable-agent.nix. The agent KEY (used to pair with
|
||||||
|
# the Beszel hub) is not set yet — add it to hosts/ha-server-{1,2}/host.nix
|
||||||
|
# under services.beszel.agent.environment.KEY once the hub accepts the
|
||||||
|
# new agents, following the pattern in hosts/server/host.nix.
|
||||||
|
{ lib, vars, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../ha/pacemaker-stack.nix
|
||||||
|
../ha/iscsi-target.nix
|
||||||
|
../ha/cluster-config.nix
|
||||||
|
../beszel/enable-agent.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
services.nfs.server = {
|
||||||
|
enable = true;
|
||||||
|
exports = ''
|
||||||
|
${vars.haStorageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.haStorageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.haStorageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.haStorageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.haStorageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.haStorageRoot}/${vars.nfsShares.proxmoxIsos.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.haStorageRoot}/${vars.nfsShares.proxmoxLxcImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.haStorageRoot}/${vars.nfsShares.pxebootImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Pacemaker controls nfs-server — prevent systemd from starting it at boot
|
||||||
|
# on both nodes (only the Active node should be serving NFS).
|
||||||
|
systemd.services.nfs-server.wantedBy = lib.mkForce [ ];
|
||||||
|
}
|
||||||
@@ -21,6 +21,216 @@ let
|
|||||||
chain ${pxeBaseUrl}/boot.ipxe
|
chain ${pxeBaseUrl}/boot.ipxe
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
debianRelease = "bookworm";
|
||||||
|
debianMirror = "https://deb.debian.org/debian";
|
||||||
|
debianNetbootBase = "${debianMirror}/dists/${debianRelease}/main/installer-amd64/current/images/netboot/debian-installer/amd64";
|
||||||
|
|
||||||
|
rockyRelease = "9";
|
||||||
|
rockyArch = "x86_64";
|
||||||
|
rockyMirror = "https://dl.rockylinux.org/pub/rocky/${rockyRelease}";
|
||||||
|
rockyPxebootBase = "${rockyMirror}/BaseOS/${rockyArch}/os/images/pxeboot";
|
||||||
|
|
||||||
|
debianIpxe = pkgs.writeText "debian.ipxe" ''
|
||||||
|
#!ipxe
|
||||||
|
|
||||||
|
set base ${pxeBaseUrl}
|
||||||
|
|
||||||
|
kernel ''${base}/debian/linux
|
||||||
|
initrd ''${base}/debian/initrd.gz
|
||||||
|
boot
|
||||||
|
'';
|
||||||
|
|
||||||
|
fetchDebianNetboot = pkgs.writeShellScript "fetch-debian-netboot" ''
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
dir="${httpRoot}/debian"
|
||||||
|
mirror="${debianNetbootBase}"
|
||||||
|
|
||||||
|
if [ -f "$dir/linux" ] && [ -f "$dir/initrd.gz" ]; then
|
||||||
|
echo "Debian ${debianRelease} netboot files already present; skipping download."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Downloading Debian ${debianRelease} netboot kernel and initrd from $mirror ..."
|
||||||
|
${pkgs.curl}/bin/curl -fsSL -o "$dir/linux.tmp" "$mirror/linux"
|
||||||
|
${pkgs.curl}/bin/curl -fsSL -o "$dir/initrd.gz.tmp" "$mirror/initrd.gz"
|
||||||
|
mv "$dir/linux.tmp" "$dir/linux"
|
||||||
|
mv "$dir/initrd.gz.tmp" "$dir/initrd.gz"
|
||||||
|
echo "Debian ${debianRelease} netboot files staged."
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Rocky Linux 9 iPXE script — boots vmlinuz+initrd.img from the staged
|
||||||
|
# /rocky/ directory and hands Anaconda the hosted Kickstart URL.
|
||||||
|
# net.ifnames=0 biosdevname=0 ensures the NIC is eth0 in both the
|
||||||
|
# installer and the installed system (matches the Kickstart NM config).
|
||||||
|
rockyFreeIpaIpxe = pkgs.writeText "rocky-freeipa.ipxe" ''
|
||||||
|
#!ipxe
|
||||||
|
|
||||||
|
set base ${pxeBaseUrl}
|
||||||
|
|
||||||
|
kernel ''${base}/rocky/vmlinuz inst.ks=''${base}/rocky-freeipa.ks inst.repo=${rockyMirror}/BaseOS/${rockyArch}/os/ net.ifnames=0 biosdevname=0 ip=dhcp quiet
|
||||||
|
initrd ''${base}/rocky/initrd.img
|
||||||
|
boot
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Kickstart file for domain-controller.sweet.home.
|
||||||
|
# Installs Rocky Linux 9, sets a static IP, creates wayne with the
|
||||||
|
# admin SSH key, then on first reboot runs ipa-server-install via a
|
||||||
|
# systemd oneshot service. Passwords are generated at %post time,
|
||||||
|
# written to /root/ipa-credentials.txt (chmod 600), and read back by
|
||||||
|
# the first-boot script — never hardcoded here or in the repo.
|
||||||
|
rockyFreeIpaKs = pkgs.writeText "rocky-freeipa.ks" ''
|
||||||
|
#version=RHEL9
|
||||||
|
# Unattended Rocky Linux 9 + FreeIPA install
|
||||||
|
# Target: domain-controller.${vars.homeDomain} ${vars.domainControllerIp}
|
||||||
|
|
||||||
|
url --url=${rockyMirror}/BaseOS/${rockyArch}/os/
|
||||||
|
repo --name=appstream --baseurl=${rockyMirror}/AppStream/${rockyArch}/os/
|
||||||
|
|
||||||
|
lang en_US.UTF-8
|
||||||
|
keyboard us
|
||||||
|
timezone UTC --utc
|
||||||
|
|
||||||
|
# DHCP during install; static IP configured in %post via NM config file
|
||||||
|
network --bootproto=dhcp --device=link --activate
|
||||||
|
network --hostname=domain-controller.sweet.home
|
||||||
|
|
||||||
|
selinux --enforcing
|
||||||
|
firewall --enabled --service=ssh
|
||||||
|
|
||||||
|
rootpw --lock
|
||||||
|
user --name=wayne --groups=wheel --shell=/bin/bash
|
||||||
|
sshkey --username=wayne "${vars.adminSshKey}"
|
||||||
|
|
||||||
|
zerombr
|
||||||
|
clearpart --all --initlabel --drives=sda
|
||||||
|
# Keep net.ifnames=0 biosdevname=0 in the installed GRUB so the NIC
|
||||||
|
# stays eth0 after reboot (matches the NM connection file below).
|
||||||
|
bootloader --location=mbr --boot-drive=sda --append="net.ifnames=0 biosdevname=0"
|
||||||
|
|
||||||
|
part /boot --fstype=xfs --size=1024 --ondisk=sda
|
||||||
|
part swap --fstype=swap --size=2048 --ondisk=sda
|
||||||
|
part / --fstype=xfs --grow --size=1 --ondisk=sda --asprimary
|
||||||
|
|
||||||
|
%packages
|
||||||
|
@^minimal-environment
|
||||||
|
ipa-server
|
||||||
|
ipa-server-dns
|
||||||
|
%end
|
||||||
|
|
||||||
|
reboot
|
||||||
|
|
||||||
|
%post --log=/root/ks-post.log
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# -- Static IP: write NM connection file directly (NM not running in chroot) --
|
||||||
|
mkdir -p /etc/NetworkManager/system-connections
|
||||||
|
cat > /etc/NetworkManager/system-connections/eth0.nmconnection << 'NMCONN'
|
||||||
|
[connection]
|
||||||
|
id=eth0
|
||||||
|
type=ethernet
|
||||||
|
interface-name=eth0
|
||||||
|
autoconnect=true
|
||||||
|
|
||||||
|
[ethernet]
|
||||||
|
|
||||||
|
[ipv4]
|
||||||
|
method=manual
|
||||||
|
addresses=${vars.domainControllerIp}/${toString vars.lanPrefixLength}
|
||||||
|
gateway=${vars.lanGateway}
|
||||||
|
dns=${vars.domainControllerIp};
|
||||||
|
dns-search=${vars.homeDomain};
|
||||||
|
|
||||||
|
[ipv6]
|
||||||
|
method=auto
|
||||||
|
NMCONN
|
||||||
|
chmod 600 /etc/NetworkManager/system-connections/eth0.nmconnection
|
||||||
|
|
||||||
|
# -- /etc/hosts: FQDN must resolve to the real IP (not loopback) for IPA --
|
||||||
|
sed -i '/domain-controller/d' /etc/hosts
|
||||||
|
echo '${vars.domainControllerIp} domain-controller.${vars.homeDomain} domain-controller' >> /etc/hosts
|
||||||
|
|
||||||
|
# -- Generate IPA passwords and store securely --
|
||||||
|
DM_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)
|
||||||
|
ADMIN_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)
|
||||||
|
printf 'Directory Manager: %s\nIPA Admin: %s\n' "$DM_PASS" "$ADMIN_PASS" \
|
||||||
|
> /root/ipa-credentials.txt
|
||||||
|
chmod 600 /root/ipa-credentials.txt
|
||||||
|
|
||||||
|
# -- First-boot script: reads passwords back, runs ipa-server-install --
|
||||||
|
cat > /usr/local/sbin/freeipa-first-boot.sh << 'FIRSTBOOT'
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
exec >> /root/freeipa-install.log 2>&1
|
||||||
|
echo "=== FreeIPA first-boot install started at $(date) ==="
|
||||||
|
|
||||||
|
DM_PASS=$(grep '^Directory Manager:' /root/ipa-credentials.txt | awk '{print $NF}')
|
||||||
|
ADMIN_PASS=$(grep '^IPA Admin:' /root/ipa-credentials.txt | awk '{print $NF}')
|
||||||
|
|
||||||
|
ipa-server-install \
|
||||||
|
--realm=SWEET.HOME \
|
||||||
|
--domain=sweet.home \
|
||||||
|
--hostname=domain-controller.sweet.home \
|
||||||
|
--ds-password="$DM_PASS" \
|
||||||
|
--admin-password="$ADMIN_PASS" \
|
||||||
|
--setup-dns \
|
||||||
|
--forwarder=192.168.2.253 \
|
||||||
|
--no-dnssec-validation \
|
||||||
|
--no-ntp \
|
||||||
|
--unattended
|
||||||
|
|
||||||
|
echo "=== FreeIPA install complete at $(date) ==="
|
||||||
|
echo "Credentials: /root/ipa-credentials.txt (save to password manager)"
|
||||||
|
echo "CA backup: /root/cacert.p12 (encrypted with Directory Manager password)"
|
||||||
|
systemctl disable freeipa-first-boot.service
|
||||||
|
FIRSTBOOT
|
||||||
|
chmod 700 /usr/local/sbin/freeipa-first-boot.sh
|
||||||
|
|
||||||
|
# -- Systemd oneshot service: runs freeipa-first-boot.sh on first real boot --
|
||||||
|
cat > /etc/systemd/system/freeipa-first-boot.service << 'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=FreeIPA first-boot installation
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
ConditionPathExists=/root/ipa-credentials.txt
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/freeipa-first-boot.sh
|
||||||
|
TimeoutStartSec=1800
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
UNIT
|
||||||
|
|
||||||
|
mkdir -p /etc/systemd/system/multi-user.target.wants
|
||||||
|
ln -sf /etc/systemd/system/freeipa-first-boot.service \
|
||||||
|
/etc/systemd/system/multi-user.target.wants/freeipa-first-boot.service
|
||||||
|
|
||||||
|
echo "Kickstart %post complete. FreeIPA installs on first reboot (~20 min)."
|
||||||
|
%end
|
||||||
|
'';
|
||||||
|
|
||||||
|
fetchRockyPxeboot = pkgs.writeShellScript "fetch-rocky-pxeboot" ''
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
dir="${httpRoot}/rocky"
|
||||||
|
base="${rockyPxebootBase}"
|
||||||
|
|
||||||
|
if [ -f "$dir/vmlinuz" ] && [ -f "$dir/initrd.img" ]; then
|
||||||
|
echo "Rocky Linux ${rockyRelease} pxeboot files already present; skipping download."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Downloading Rocky Linux ${rockyRelease} pxeboot kernel and initrd from $base ..."
|
||||||
|
${pkgs.curl}/bin/curl -fsSL -o "$dir/vmlinuz.tmp" "$base/vmlinuz"
|
||||||
|
${pkgs.curl}/bin/curl -fsSL -o "$dir/initrd.img.tmp" "$base/initrd.img"
|
||||||
|
mv "$dir/vmlinuz.tmp" "$dir/vmlinuz"
|
||||||
|
mv "$dir/initrd.img.tmp" "$dir/initrd.img"
|
||||||
|
echo "Rocky Linux ${rockyRelease} pxeboot files staged."
|
||||||
|
'';
|
||||||
|
|
||||||
systemRescueIpxe = pkgs.writeText "systemrescue.ipxe" ''
|
systemRescueIpxe = pkgs.writeText "systemrescue.ipxe" ''
|
||||||
#!ipxe
|
#!ipxe
|
||||||
|
|
||||||
@@ -70,6 +280,8 @@ let
|
|||||||
menu PXE Boot Menu
|
menu PXE Boot Menu
|
||||||
item auto-installer NixOS Auto-Installer
|
item auto-installer NixOS Auto-Installer
|
||||||
item nixos-minimal NixOS Minimal
|
item nixos-minimal NixOS Minimal
|
||||||
|
item debian Debian Minimal
|
||||||
|
item rocky-freeipa FreeIPA Server (Rocky Linux 9)
|
||||||
item rescue Rescue Environment
|
item rescue Rescue Environment
|
||||||
item shell iPXE Shell
|
item shell iPXE Shell
|
||||||
item reboot Reboot
|
item reboot Reboot
|
||||||
@@ -82,6 +294,12 @@ let
|
|||||||
:nixos-minimal
|
:nixos-minimal
|
||||||
chain ''${base}/nixos-minimal/netboot.ipxe
|
chain ''${base}/nixos-minimal/netboot.ipxe
|
||||||
|
|
||||||
|
:debian
|
||||||
|
chain ''${base}/debian.ipxe
|
||||||
|
|
||||||
|
:rocky-freeipa
|
||||||
|
chain ''${base}/rocky-freeipa.ipxe
|
||||||
|
|
||||||
:rescue
|
:rescue
|
||||||
chain ''${base}/systemrescue.ipxe
|
chain ''${base}/systemrescue.ipxe
|
||||||
|
|
||||||
@@ -95,6 +313,7 @@ in
|
|||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../pxe-boot/stage-installer-artifacts.nix
|
../pxe-boot/stage-installer-artifacts.nix
|
||||||
|
../pxe-boot/mount-pxe-images.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
@@ -129,25 +348,64 @@ in
|
|||||||
openssh.settings.PermitRootLogin = "yes";
|
openssh.settings.PermitRootLogin = "yes";
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd = {
|
||||||
|
tmpfiles.rules = [
|
||||||
"d ${pxeRoot} 0755 root root -"
|
"d ${pxeRoot} 0755 root root -"
|
||||||
"d ${httpRoot} 0755 root root -"
|
"d ${httpRoot} 0755 root root -"
|
||||||
"d ${httpRoot}/images 0755 root root -"
|
"L+ ${httpRoot}/images - - - - ${vars.nfsShares.pxebootImages.mountpoint}"
|
||||||
"d ${httpRoot}/auto-installer 0755 root root -"
|
"d ${httpRoot}/auto-installer 0755 root root -"
|
||||||
"d ${httpRoot}/nixos-minimal 0755 root root -"
|
"d ${httpRoot}/nixos-minimal 0755 root root -"
|
||||||
"d ${httpRoot}/systemrescue 0755 root root -"
|
"d ${httpRoot}/systemrescue 0755 root root -"
|
||||||
|
"d ${httpRoot}/debian 0755 root root -"
|
||||||
"d ${httpRoot}/ubuntu 0755 root root -"
|
"d ${httpRoot}/ubuntu 0755 root root -"
|
||||||
"d ${httpRoot}/rescue 0755 root root -"
|
"d ${httpRoot}/rescue 0755 root root -"
|
||||||
|
"d ${httpRoot}/rocky 0755 root root -"
|
||||||
"d ${tftpRoot} 0755 root root -"
|
"d ${tftpRoot} 0755 root root -"
|
||||||
"C+ ${httpRoot}/boot.ipxe 0644 root root - ${bootIpxe}"
|
"C+ ${httpRoot}/boot.ipxe 0644 root root - ${bootIpxe}"
|
||||||
"C+ ${httpRoot}/menu.ipxe 0644 root root - ${menuIpxe}"
|
"C+ ${httpRoot}/menu.ipxe 0644 root root - ${menuIpxe}"
|
||||||
|
"C+ ${httpRoot}/debian.ipxe 0644 root root - ${debianIpxe}"
|
||||||
|
"C+ ${httpRoot}/rocky-freeipa.ipxe 0644 root root - ${rockyFreeIpaIpxe}"
|
||||||
|
"C+ ${httpRoot}/rocky-freeipa.ks 0644 root root - ${rockyFreeIpaKs}"
|
||||||
"C+ ${httpRoot}/systemrescue.ipxe 0644 root root - ${systemRescueIpxe}"
|
"C+ ${httpRoot}/systemrescue.ipxe 0644 root root - ${systemRescueIpxe}"
|
||||||
"C+ ${tftpRoot}/autoexec.ipxe 0644 root root - ${autoexecIpxe}"
|
"C+ ${tftpRoot}/autoexec.ipxe 0644 root root - ${autoexecIpxe}"
|
||||||
"C+ ${tftpRoot}/ipxe.efi 0644 root root - ${pkgs.ipxe}/ipxe.efi"
|
"C+ ${tftpRoot}/ipxe.efi 0644 root root - ${pkgs.ipxe}/ipxe.efi"
|
||||||
"C+ ${tftpRoot}/undionly.kpxe 0644 root root - ${pkgs.ipxe}/undionly.kpxe"
|
"C+ ${tftpRoot}/undionly.kpxe 0644 root root - ${pkgs.ipxe}/undionly.kpxe"
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.stage-systemrescue = {
|
services = {
|
||||||
|
fetch-debian-netboot = {
|
||||||
|
description = "Download Debian ${debianRelease} netboot kernel and initrd for HTTP PXE boot";
|
||||||
|
after = [
|
||||||
|
"local-fs.target"
|
||||||
|
"systemd-tmpfiles-setup.service"
|
||||||
|
"network-online.target"
|
||||||
|
];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = fetchDebianNetboot;
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch-rocky-pxeboot = {
|
||||||
|
description = "Download Rocky Linux ${rockyRelease} pxeboot kernel and initrd for HTTP PXE boot";
|
||||||
|
after = [
|
||||||
|
"local-fs.target"
|
||||||
|
"systemd-tmpfiles-setup.service"
|
||||||
|
"network-online.target"
|
||||||
|
];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = fetchRockyPxeboot;
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
stage-systemrescue = {
|
||||||
description = "Stage SystemRescue ISO contents for HTTP PXE boot";
|
description = "Stage SystemRescue ISO contents for HTTP PXE boot";
|
||||||
after = [
|
after = [
|
||||||
"local-fs.target"
|
"local-fs.target"
|
||||||
@@ -159,7 +417,32 @@ in
|
|||||||
ExecStart = stageSystemRescue;
|
ExecStart = stageSystemRescue;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.dnsmasq = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
# Disable DNS listener — only proxy DHCP is needed here.
|
||||||
|
# Without this dnsmasq tries to bind port 53 which systemd-resolved
|
||||||
|
# already owns, causing startup failure.
|
||||||
|
port = 0;
|
||||||
|
dhcp-range = [ "192.168.2.0,proxy" ];
|
||||||
|
dhcp-match = [
|
||||||
|
"set:ipxe,175"
|
||||||
|
"set:efi64,option:client-arch,7"
|
||||||
|
"set:efi64,option:client-arch,9"
|
||||||
|
];
|
||||||
|
dhcp-userclass = "set:ipxe,iPXE";
|
||||||
|
dhcp-boot = [
|
||||||
|
"tag:ipxe,tag:efi64,http://${vars.pxeServerIp}/boot.ipxe"
|
||||||
|
"tag:ipxe,http://${vars.pxeServerIp}/boot.ipxe"
|
||||||
|
"tag:efi64,ipxe.efi,,${vars.pxeServerIp}"
|
||||||
|
"undionly.kpxe,,${vars.pxeServerIp}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ vars.ports.pxeBootHttp ];
|
networking.firewall.allowedTCPPorts = [ vars.ports.pxeBootHttp ];
|
||||||
networking.firewall.allowedUDPPorts = [ vars.ports.pxeBootTftp ];
|
networking.firewall.allowedUDPPorts = [ vars.ports.pxeBootTftp 67 ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,92 @@
|
|||||||
{ vars, lib, ... }:
|
{ vars, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
poolName = lib.removePrefix "/" vars.storageRoot;
|
||||||
|
|
||||||
|
# For each NFS share subpath, generate every ancestor path so ZFS datasets
|
||||||
|
# are created parent-first. e.g. "docker/config" → ["docker" "docker/config"]
|
||||||
|
ancestors = path:
|
||||||
|
let parts = lib.splitString "/" path;
|
||||||
|
in lib.imap1 (i: _: lib.concatStringsSep "/" (lib.take i parts)) parts;
|
||||||
|
|
||||||
|
poolDatasets = lib.unique (
|
||||||
|
lib.concatMap (share: ancestors share.subpath)
|
||||||
|
(lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares))
|
||||||
|
);
|
||||||
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../beszel/enable-agent.nix
|
../beszel/enable-agent.nix
|
||||||
../services/zfs/enable-service.nix
|
../services/zfs/enable-service.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.zfs.extraPools = [ (lib.removePrefix "/" vars.storageRoot) ];
|
boot.zfs.extraPools = [ poolName ];
|
||||||
|
|
||||||
|
# On a fresh image deploy the data disk (scsi1) starts blank — no pool
|
||||||
|
# exists yet, so zfs-import-tank.service would spin for 60 s and fail.
|
||||||
|
# This service runs first: if the pool is already present it exits instantly;
|
||||||
|
# otherwise it creates it (with all required datasets) so the standard
|
||||||
|
# import service finds it ready on the very first boot.
|
||||||
|
systemd.services."zfs-init-${poolName}" = {
|
||||||
|
description = "Initialize '${poolName}' ZFS pool on first boot if not present";
|
||||||
|
wantedBy = [ "zfs-import-${poolName}.service" ];
|
||||||
|
before = [ "zfs-import-${poolName}.service" ];
|
||||||
|
after = [ "systemd-udev-settle.service" ];
|
||||||
|
unitConfig.DefaultDependencies = false;
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
path = [ pkgs.zfs_unstable ];
|
||||||
|
script = ''
|
||||||
|
# Already imported — nothing to do.
|
||||||
|
if zpool list "${poolName}" >/dev/null 2>&1; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Locate the data disk first — used for both the fallback import
|
||||||
|
# attempt and, only if the disk is genuinely blank, pool creation.
|
||||||
|
DATA_DISK=""
|
||||||
|
for candidate in /dev/disk/by-id/scsi-*drive-scsi1; do
|
||||||
|
[[ "$candidate" == *-part* ]] && continue
|
||||||
|
[ -b "$candidate" ] && DATA_DISK="$candidate" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$DATA_DISK" ]; then
|
||||||
|
echo "zfs-init-${poolName}: no data disk found (expected /dev/disk/by-id/scsi-*drive-scsi1)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try importing via the by-id symlink directory first (normal path),
|
||||||
|
# then fall back to scanning the disk directly. The two-step exists
|
||||||
|
# because of a udev race: systemd-udev-settle.service can clear before
|
||||||
|
# /dev/disk/by-id/ entries are fully populated, causing the first
|
||||||
|
# import to fail even when the pool is intact on the disk.
|
||||||
|
if zpool import -d /dev/disk/by-id -N "${poolName}" 2>/dev/null; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if zpool import -d "$DATA_DISK" -N "${poolName}" 2>/dev/null; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Both import attempts failed. Before creating a new pool, verify the
|
||||||
|
# disk is genuinely blank — if ZFS label metadata is present the import
|
||||||
|
# failed for some other reason and we must not clobber existing data.
|
||||||
|
if zdb -l "$DATA_DISK" 2>/dev/null | grep -q "name: '${poolName}'"; then
|
||||||
|
echo "zfs-init-${poolName}: $DATA_DISK has ZFS pool '${poolName}' metadata but import failed — refusing to overwrite existing data. Run 'zpool import -d $DATA_DISK ${poolName}' manually to investigate." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disk is genuinely blank: create the pool. -f is intentionally
|
||||||
|
# omitted so that if we somehow reach this point with an existing pool
|
||||||
|
# on the disk, zpool refuses rather than silently destroying data.
|
||||||
|
echo "zfs-init-${poolName}: creating pool on $DATA_DISK"
|
||||||
|
zpool create "${poolName}" "$DATA_DISK"
|
||||||
|
${lib.concatMapStrings (ds: ''
|
||||||
|
zfs create "${poolName}/${ds}"
|
||||||
|
'') poolDatasets}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
systemd.services.nfs-server = {
|
systemd.services.nfs-server = {
|
||||||
after = [ "zfs-mount.service" ];
|
after = [ "zfs-mount.service" ];
|
||||||
@@ -16,13 +96,20 @@
|
|||||||
services.nfs.server = {
|
services.nfs.server = {
|
||||||
enable = true;
|
enable = true;
|
||||||
exports = ''
|
exports = ''
|
||||||
${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
|
${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.storageRoot}/${vars.nfsShares.proxmoxIsos.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.storageRoot}/${vars.nfsShares.proxmoxLxcImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
|
${vars.storageRoot}/${vars.nfsShares.pxebootImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd ];
|
# mountd (20048) is needed for showmount/NFSv3 mount protocol — without it
|
||||||
|
# clients can reach portmapper (111) and get the mountd port back, then
|
||||||
|
# time out trying to connect to it. All three ports need TCP and UDP.
|
||||||
|
networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd vars.ports.nfsMountd ];
|
||||||
|
networking.firewall.allowedUDPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd vars.ports.nfsMountd ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
{ ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../tailscale/exit-node.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
# "server", not "both": this build type only ever advertises itself as an
|
|
||||||
# exit node (see ../tailscale/exit-node.nix) -- it doesn't advertise LAN
|
|
||||||
# subnet routes, so it doesn't need the "client"-side loose reverse-path
|
|
||||||
# filtering that "both" would also turn on. Deliberately left unbundled
|
|
||||||
# from LAN-subnet-route advertisement so this build type stays valid on
|
|
||||||
# every platform, including linode (a remote VPS with no network path to
|
|
||||||
# the home LAN at all).
|
|
||||||
services.tailscale.useRoutingFeatures = "server";
|
|
||||||
|
|
||||||
# Forwarded exit-node traffic arrives on tailscale0 already
|
|
||||||
# tailscale-authenticated -- the firewall's normal per-port allow-list
|
|
||||||
# would otherwise drop it. Standard NixOS/Tailscale exit-node guidance.
|
|
||||||
networking.firewall.trustedInterfaces = [ "tailscale0" ];
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{ vars, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../tailscale/subnet-router.nix
|
||||||
|
../tailscale/ts-dns-forwarder.nix
|
||||||
|
../beszel/enable-agent.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# "server", not "both": this build type advertises LAN subnet routes but
|
||||||
|
# doesn't use another tailscale exit node itself, so it doesn't need the
|
||||||
|
# "client"-side loose reverse-path filtering that "both" would also enable.
|
||||||
|
# Deliberately kept explicit here (not just relying on subnet-router.nix's
|
||||||
|
# own setting) so the intent is clear at the build-type level.
|
||||||
|
services.tailscale.useRoutingFeatures = "server";
|
||||||
|
|
||||||
|
# Advertise the LAN subnet so Tailscale peers can route back to LAN machines.
|
||||||
|
# Must also be approved in the Tailscale admin console (Machines → Edit route settings).
|
||||||
|
services.tailscale.extraUpFlags = [ "--advertise-routes=${vars.lanCidr}" ];
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
# Forwarded subnet-router traffic arrives on tailscale0 already
|
||||||
|
# tailscale-authenticated -- the firewall's normal per-port allow-list
|
||||||
|
# would otherwise drop it. Standard NixOS/Tailscale subnet-router guidance.
|
||||||
|
trustedInterfaces = [ "tailscale0" ];
|
||||||
|
|
||||||
|
# SNAT LAN traffic going into Tailscale so the remote peer sees it as
|
||||||
|
# coming from this router's Tailscale IP rather than a raw LAN IP.
|
||||||
|
# Without this, Tailscale drops forwarded packets whose source is not a
|
||||||
|
# recognised Tailscale address.
|
||||||
|
#
|
||||||
|
# We target POSTROUTING directly (always-existing built-in chain) rather
|
||||||
|
# than nixos-nat-post: extraCommands runs after the old nixos-nat-post is
|
||||||
|
# deleted but before the new one is created, so -A nixos-nat-post silently
|
||||||
|
# fails. The -C check makes the rule idempotent across firewall reloads.
|
||||||
|
extraCommands = ''
|
||||||
|
iptables -t nat -C POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE 2>/dev/null || \
|
||||||
|
iptables -t nat -A POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE
|
||||||
|
'';
|
||||||
|
extraStopCommands = ''
|
||||||
|
iptables -t nat -D POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE 2>/dev/null || true
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../tor/enable-relay.nix
|
../tor/enable-relay.nix
|
||||||
|
../beszel/enable-agent.nix
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{ pkgs, ... }: {
|
||||||
|
# Defines the SSH host key as a clan vars generator so that:
|
||||||
|
# - `clan vars generate <target>` creates and encrypts the key pair
|
||||||
|
# - The private key lives at vars/per-machine/<target>/openssh/ssh_host_ed25519_key/secret
|
||||||
|
# (sops binary-encrypted, admin-key-only; decrypted by the build script)
|
||||||
|
# - The public key lives at vars/per-machine/<target>/openssh/ssh_host_ed25519_key.pub/value
|
||||||
|
# (plaintext; used by sync-host-keys.sh to derive the sops age fingerprint)
|
||||||
|
#
|
||||||
|
# neededFor = "activation" means clan's deployment tool would upload this
|
||||||
|
# before running nixos-rebuild/nixos-install (for VM/baremetal via
|
||||||
|
# nixos-anywhere). For lxc-* hosts, the build script bakes it into the
|
||||||
|
# tarball directly via NIXOS_HOST_KEYS_DIR -- the neededFor value here
|
||||||
|
# simply ensures it is NOT mapped to sops.secrets (which would try to
|
||||||
|
# decrypt it at runtime as a regular service secret, which is wrong: the
|
||||||
|
# SSH host key reaches the container via the tarball, not sops).
|
||||||
|
clan.core.vars.generators.openssh = {
|
||||||
|
files."ssh_host_ed25519_key" = {
|
||||||
|
secret = true;
|
||||||
|
neededFor = "activation";
|
||||||
|
};
|
||||||
|
files."ssh_host_ed25519_key.pub" = {
|
||||||
|
secret = false;
|
||||||
|
neededFor = "activation";
|
||||||
|
};
|
||||||
|
runtimeInputs = [ pkgs.openssh ];
|
||||||
|
script = ''
|
||||||
|
ssh-keygen -t ed25519 -N "" -C "" -f "$out/ssh_host_ed25519_key"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
btop
|
btop
|
||||||
git
|
git
|
||||||
gcr
|
gcr
|
||||||
|
jq
|
||||||
];
|
];
|
||||||
|
|
||||||
# Secrets shared by every host, decrypted at activation via each host's
|
# Secrets shared by every host, decrypted at activation via each host's
|
||||||
@@ -60,13 +61,20 @@
|
|||||||
!include ${config.sops.templates."nix-github-token.conf".path}
|
!include ${config.sops.templates."nix-github-token.conf".path}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
#Set root password
|
users = {
|
||||||
users.users.root = {
|
# With mutableUsers = false, update-users-groups.pl enforces hashedPasswordFile
|
||||||
|
# on every activation regardless of whether the account already exists in
|
||||||
|
# /etc/shadow. The default (true) only applies hashedPasswordFile to newly-
|
||||||
|
# created accounts — which means a freshly-built proxmox disk image (where
|
||||||
|
# activation runs without a usable sops key, so both accounts land in shadow
|
||||||
|
# with ‘!’) will never have its passwords fixed by subsequent boots.
|
||||||
|
mutableUsers = false;
|
||||||
|
|
||||||
|
users.root = {
|
||||||
hashedPasswordFile = config.sops.secrets."root-hashedPassword".path;
|
hashedPasswordFile = config.sops.secrets."root-hashedPassword".path;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Define a user account. Don't forget to set a password with ‘passwd’.
|
users.${vars.primaryUser} = {
|
||||||
users.users.${vars.primaryUser} = {
|
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
|
extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
@@ -76,8 +84,10 @@
|
|||||||
openssh.authorizedKeys.keys = [
|
openssh.authorizedKeys.keys = [
|
||||||
vars.adminSshKey
|
vars.adminSshKey
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGygkCljN6uKpdJbHTOQtn8ZnH+wKXDLAwrDFbLrE/65 nixos@nixos"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
# Enable the OpenSSH daemon.
|
# Enable the OpenSSH daemon.
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Cluster-wide HA config shared by both ha-server nodes.
|
||||||
|
#
|
||||||
|
# Covers everything that is identical on both nodes and references cluster
|
||||||
|
# topology (node IPs, hostnames, DRBD resource). Per-node identity
|
||||||
|
# (hostname, static IP, stateVersion) lives in hosts/ha-server-{1,2}/host.nix.
|
||||||
|
#
|
||||||
|
# Corosync authkey:
|
||||||
|
# /etc/corosync/authkey (mode 0400) is managed by sops-nix below.
|
||||||
|
# Bootstrap: run scripts/ha/cluster-init.sh on node1 to generate the key,
|
||||||
|
# then encrypt it with: sops -e --input-type binary /etc/corosync/authkey > secrets/ha-corosync-authkey
|
||||||
|
# Both host keys must be registered via sync-host-keys.sh first so both nodes can decrypt it.
|
||||||
|
#
|
||||||
|
# DRBD fencing:
|
||||||
|
# Production setting is resource-only: DRBD waits for the STONITH fence
|
||||||
|
# agent to confirm the peer is dead before promoting to Primary. This
|
||||||
|
# requires a working fence_pve_ssh STONITH resource in Pacemaker
|
||||||
|
# (see scripts/ha/cluster-enable-stonith.sh). On a fresh cluster with
|
||||||
|
# no fence device yet, temporarily change to dont-care and run
|
||||||
|
# cluster-enable-stonith.sh once the fence key is deployed.
|
||||||
|
{ lib, vars, ... }:
|
||||||
|
{
|
||||||
|
services.drbd = {
|
||||||
|
enable = true;
|
||||||
|
config = ''
|
||||||
|
global {
|
||||||
|
usage-count yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
common {
|
||||||
|
net {
|
||||||
|
protocol C;
|
||||||
|
ping-int 1;
|
||||||
|
verify-alg sha256;
|
||||||
|
after-sb-0pri discard-zero-changes;
|
||||||
|
after-sb-1pri discard-secondary;
|
||||||
|
}
|
||||||
|
disk {
|
||||||
|
fencing resource-only;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource ha-data {
|
||||||
|
volume 0 {
|
||||||
|
device /dev/drbd0;
|
||||||
|
disk /dev/sdb;
|
||||||
|
meta-disk internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
on ${vars.haServer1Host} {
|
||||||
|
address ${vars.haServer1StorageIp}:${toString vars.ports.haServerDrbd};
|
||||||
|
}
|
||||||
|
|
||||||
|
on ${vars.haServer2Host} {
|
||||||
|
address ${vars.haServer2StorageIp}:${toString vars.ports.haServerDrbd};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# /etc/corosync/authkey — sops binary secret, identical on both nodes.
|
||||||
|
# Decryptable by both ha-server host keys (added by sync-host-keys.sh).
|
||||||
|
sops.secrets.corosync_authkey = {
|
||||||
|
sopsFile = ../../secrets/ha-corosync-authkey;
|
||||||
|
format = "binary";
|
||||||
|
path = "/etc/corosync/authkey";
|
||||||
|
mode = "0400";
|
||||||
|
restartUnits = [ "corosync.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# NixOS common config enables NetworkManager by default; HA cluster nodes
|
||||||
|
# need stable static IPs with predictable interface names — NM is not suitable.
|
||||||
|
networking.networkmanager.enable = lib.mkForce false;
|
||||||
|
|
||||||
|
# services.corosync.enable is set by modules/ha/pacemaker-stack.nix.
|
||||||
|
services.corosync = {
|
||||||
|
clusterName = "ha-cluster";
|
||||||
|
nodelist = [
|
||||||
|
{ nodeid = 1; name = vars.haServer1Host; ring_addrs = [ vars.haServer1StorageIp ]; }
|
||||||
|
{ nodeid = 2; name = vars.haServer2Host; ring_addrs = [ vars.haServer2StorageIp ]; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
allowedTCPPorts = [
|
||||||
|
vars.ports.haServerIscsi
|
||||||
|
vars.ports.haServerPacemakerRemoted
|
||||||
|
vars.ports.haServerPcsd
|
||||||
|
vars.ports.haServerDrbd
|
||||||
|
vars.ports.nfsRpcbind
|
||||||
|
vars.ports.nfsd
|
||||||
|
vars.ports.nfsMountd
|
||||||
|
];
|
||||||
|
allowedUDPPorts = [
|
||||||
|
vars.ports.haServerCorosync1
|
||||||
|
vars.ports.haServerCorosync2
|
||||||
|
vars.ports.haServerCorosyncCrypto
|
||||||
|
vars.ports.nfsRpcbind
|
||||||
|
vars.ports.nfsd
|
||||||
|
vars.ports.nfsMountd
|
||||||
|
];
|
||||||
|
extraCommands = ''
|
||||||
|
iptables -A INPUT -s ${vars.haServer1Ip}/32 -j ACCEPT
|
||||||
|
iptables -A INPUT -s ${vars.haServer2Ip}/32 -j ACCEPT
|
||||||
|
iptables -A INPUT -s ${vars.haStorageCidr} -j ACCEPT
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# LIO iSCSI target service (targetctl) for NixOS HA clusters.
|
||||||
|
#
|
||||||
|
# Provides the targetctl.service that saves/restores LIO configuration from
|
||||||
|
# /etc/target/saveconfig.json. Pacemaker manages this service via its
|
||||||
|
# systemd resource agent (class="systemd" type="targetctl").
|
||||||
|
#
|
||||||
|
# Why ExecStop is not simply "targetctl save":
|
||||||
|
# targetctl save writes the LIO config to JSON but does NOT remove the LIO
|
||||||
|
# target from the kernel's configfs. As a result, any fileio backing store
|
||||||
|
# that LIO has open (e.g. iscsi-lun.img on an XFS-over-DRBD filesystem)
|
||||||
|
# stays referenced in the kernel. The subsequent XFS umount from the
|
||||||
|
# Filesystem OCF resource then returns EBUSY and either hangs for the full
|
||||||
|
# op-stop timeout or fails outright, blocking the entire failover.
|
||||||
|
#
|
||||||
|
# The ExecStop script here additionally tears down the kernel LIO state
|
||||||
|
# via rtslib_fb after saving, so the backing-store file descriptor is
|
||||||
|
# released and umount succeeds immediately.
|
||||||
|
#
|
||||||
|
# Empty-config guard:
|
||||||
|
# The save step is skipped when no iSCSI targets are currently active.
|
||||||
|
# This prevents the secondary node (where LIO was never started) from
|
||||||
|
# overwriting a valid saveconfig.json with an empty one when Pacemaker
|
||||||
|
# stops the iscsi-target resource as part of a failover or cleanup.
|
||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]);
|
||||||
|
targetctl = "${pkgs.targetcli-fb}/bin/targetctl";
|
||||||
|
|
||||||
|
targetctlStop = pkgs.writeScript "targetctl-stop" ''
|
||||||
|
#!${python3}/bin/python3
|
||||||
|
import subprocess, sys
|
||||||
|
import rtslib_fb
|
||||||
|
|
||||||
|
root = rtslib_fb.RTSRoot()
|
||||||
|
targets = list(root.targets)
|
||||||
|
if targets:
|
||||||
|
subprocess.run(
|
||||||
|
["${targetctl}", "save", "/etc/target/saveconfig.json"],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
print(f"saved {len(targets)} iSCSI target(s)")
|
||||||
|
else:
|
||||||
|
print("no active LIO targets — saveconfig.json unchanged")
|
||||||
|
|
||||||
|
for target in targets:
|
||||||
|
try:
|
||||||
|
for tpg in list(target.tpgs):
|
||||||
|
tpg.enable = False
|
||||||
|
target.delete()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"warn (target): {e}", file=sys.stderr)
|
||||||
|
for so in list(root.storage_objects):
|
||||||
|
try:
|
||||||
|
so.delete()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"warn (backstore): {e}", file=sys.stderr)
|
||||||
|
print("LIO kernel target cleared")
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
boot.kernelModules = [
|
||||||
|
"target_core_mod"
|
||||||
|
"iscsi_target_mod"
|
||||||
|
"target_core_file"
|
||||||
|
"target_core_pscsi"
|
||||||
|
"target_core_user"
|
||||||
|
"configfs"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
mounts = [{
|
||||||
|
where = "/sys/kernel/config";
|
||||||
|
what = "configfs";
|
||||||
|
type = "configfs";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
before = [ "targetctl.service" ];
|
||||||
|
}];
|
||||||
|
services.targetctl = {
|
||||||
|
description = "LIO iSCSI target config save/restore";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "sys-kernel-config.mount" "network.target" ];
|
||||||
|
requires = [ "sys-kernel-config.mount" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
ExecStart = "${targetctl} restore /etc/target/saveconfig.json";
|
||||||
|
ExecStop = "${targetctlStop}";
|
||||||
|
};
|
||||||
|
unitConfig.ConditionFileNotEmpty = "/etc/target/saveconfig.json";
|
||||||
|
};
|
||||||
|
tmpfiles.rules = [
|
||||||
|
"d /etc/target 0750 root root -"
|
||||||
|
"f /etc/target/saveconfig.json 0640 root root -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.targetcli-fb ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Pacemaker + Corosync HA stack for NixOS with known-good workarounds.
|
||||||
|
#
|
||||||
|
# Issues fixed here (confirmed through live testing on NixOS 25.11):
|
||||||
|
#
|
||||||
|
# 1. StateDirectory ownership reset: systemd's StateDirectory=pacemaker
|
||||||
|
# creates /var/lib/pacemaker owned root:root. pacemaker-based (the CIB
|
||||||
|
# daemon) runs as the hacluster user and calls pcmk__daemon_can_write,
|
||||||
|
# which requires the CIB directory to be owned by hacluster or be
|
||||||
|
# group-writable by haclient. Workaround: remove StateDirectory and let
|
||||||
|
# ExecStartPre create every required subdirectory with correct ownership.
|
||||||
|
#
|
||||||
|
# 2. HA_SBIN_DIR wrong path: ocf-shellfuncs sets HA_SBIN_DIR to the Nix
|
||||||
|
# store path of the resource-agents derivation's /sbin, which doesn't
|
||||||
|
# exist. The DRBD OCF agent uses ${HA_SBIN_DIR}/crm_master, so it exits
|
||||||
|
# 127 without this override. Fix: export HA_SBIN_DIR=/run/current-system/sw/bin.
|
||||||
|
#
|
||||||
|
# 3. Broad PATH for OCF agents: the resource executor (pacemaker-execd) runs
|
||||||
|
# OCF agent scripts as children. NixOS provides no implicit PATH for
|
||||||
|
# system services; without an explicit PATH the agents can't find ip, ss,
|
||||||
|
# mount, umount, drbdadm, etc.
|
||||||
|
#
|
||||||
|
# 4. FUSER=true: the Filesystem OCF agent calls check_binary $FUSER (default:
|
||||||
|
# fuser from psmisc), which is not installed. Setting FUSER=true makes
|
||||||
|
# check_binary succeed (true is always in PATH) and the subsequent
|
||||||
|
# "$FUSER -km $mountpoint" becomes a no-op. Pair with force_unmount=false
|
||||||
|
# on each Filesystem resource unless you want lazy unmount behaviour.
|
||||||
|
{ lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
ocfBinPath = lib.concatStringsSep ":" [
|
||||||
|
"${pkgs.iproute2}/bin"
|
||||||
|
"${pkgs.iproute2}/sbin"
|
||||||
|
"${pkgs.iputils}/bin"
|
||||||
|
"${pkgs.util-linux}/bin"
|
||||||
|
"${pkgs.util-linux}/sbin"
|
||||||
|
"${pkgs.gawk}/bin"
|
||||||
|
"${pkgs.gnugrep}/bin"
|
||||||
|
"${pkgs.gnused}/bin"
|
||||||
|
"${pkgs.coreutils}/bin"
|
||||||
|
"${pkgs.bash}/bin"
|
||||||
|
"${pkgs.procps}/bin"
|
||||||
|
"${pkgs.xfsprogs}/bin"
|
||||||
|
"${pkgs.drbd}/bin"
|
||||||
|
"${pkgs.python3}/bin"
|
||||||
|
"/run/current-system/sw/bin"
|
||||||
|
"/run/current-system/sw/sbin"
|
||||||
|
"/usr/local/sbin"
|
||||||
|
"/usr/local/bin"
|
||||||
|
"/usr/sbin"
|
||||||
|
"/usr/bin"
|
||||||
|
"/sbin"
|
||||||
|
"/bin"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Single pre-start script: schemas symlink + directory ownership.
|
||||||
|
# Runs before pacemakerd so pacemaker-based finds hacluster-owned dirs.
|
||||||
|
preStartCmd = "${pkgs.bash}/bin/bash -c '"
|
||||||
|
+ "ln -sfn ${pkgs.pacemaker}/share/pacemaker /var/lib/pacemaker/schemas; "
|
||||||
|
+ "for d in /var/lib/pacemaker /var/lib/pacemaker/cib /var/lib/pacemaker/cores "
|
||||||
|
+ "/var/lib/pacemaker/pengine /var/lib/pacemaker/blackbox "
|
||||||
|
+ "/var/lib/pacemaker/hostcache; do "
|
||||||
|
+ "mkdir -p \"\\$d\" && chown hacluster:pacemaker \"\\$d\" && chmod 2770 \"\\$d\"; "
|
||||||
|
+ "done'";
|
||||||
|
|
||||||
|
ocfEnv = {
|
||||||
|
PATH = lib.mkForce ocfBinPath;
|
||||||
|
OCF_ROOT = "${pkgs.ocf-resource-agents}/usr/lib/ocf";
|
||||||
|
HA_SBIN_DIR = "/run/current-system/sw/bin";
|
||||||
|
FUSER = "true";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
users.groups.haclient = { };
|
||||||
|
|
||||||
|
services.corosync.enable = true;
|
||||||
|
services.pacemaker.enable = true;
|
||||||
|
|
||||||
|
systemd.services = {
|
||||||
|
pacemaker = {
|
||||||
|
serviceConfig = {
|
||||||
|
StateDirectory = lib.mkForce "";
|
||||||
|
ExecStartPre = lib.mkBefore [ preStartCmd ];
|
||||||
|
};
|
||||||
|
environment = ocfEnv;
|
||||||
|
};
|
||||||
|
pacemaker-execd.environment = ocfEnv;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
corosync
|
||||||
|
pacemaker
|
||||||
|
ocf-resource-agents
|
||||||
|
];
|
||||||
|
}
|
||||||
+14
-133
@@ -45,142 +45,23 @@
|
|||||||
disko
|
disko
|
||||||
];
|
];
|
||||||
|
|
||||||
# Write auto-install script to /root
|
# Auto-install script, kept as a real, version-controlled shell file at
|
||||||
etc."auto-install.sh" = {
|
# scripts/installer/auto-install.sh rather than an inline Nix string.
|
||||||
text = ''
|
# It sources scripts/env.sh itself (for LAN_DOMAIN, same as every other
|
||||||
#!/run/current-system/sw/bin/bash
|
# script in this repo) rather than relying on Nix-level templating, so
|
||||||
set -eux
|
# it behaves identically whether it's run straight from a git checkout
|
||||||
|
# or from here -- baking scripts/env.sh in alongside it at a matching
|
||||||
set -euo pipefail
|
# relative path (installer/auto-install.sh -> ../env.sh) is what makes
|
||||||
|
# that resolve correctly in both places.
|
||||||
export FLAKE_BASE_URL="git+https://${vars.lanDomain}/beatzaplenty/nixos.git"
|
etc = {
|
||||||
|
"nixos-installer/env.sh".source = ../../scripts/env.sh;
|
||||||
echo "Fetching available NixOS hosts from flake..."
|
|
||||||
# Two categories deliberately excluded from the menu:
|
|
||||||
# lxc-* — these build a config.system.build.tarball meant for
|
|
||||||
# `pct restore` on Proxmox directly, not an install.
|
|
||||||
# Running nixos-install against one here would
|
|
||||||
# bind-mount / onto /mnt and then refuse to touch the
|
|
||||||
# filesystem it's currently running on — see
|
|
||||||
# docs/auto-installer.md.
|
|
||||||
# installer — this *is* the installer image's own flake target,
|
|
||||||
# not a deployable host; "installing" it means
|
|
||||||
# nixos-install-ing a copy of the installer into
|
|
||||||
# itself.
|
|
||||||
mapfile -t options < <(
|
|
||||||
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
|
||||||
"''${FLAKE_BASE_URL}#nixosConfigurations" \
|
|
||||||
--apply builtins.attrNames \
|
|
||||||
| jq -r '.[]
|
|
||||||
| select(startswith("lxc-") | not)
|
|
||||||
| select(. != "installer")'
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ ''${#options[@]} -eq 0 ]]; then
|
|
||||||
echo "ERROR: No NixOS hosts found in ''${FLAKE_BASE_URL}#nixosConfigurations" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Note: lxc-* targets aren't installed this way — build them with"
|
|
||||||
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
|
|
||||||
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
|
|
||||||
|
|
||||||
echo "Choose the flake profile to install:"
|
|
||||||
select choice in "''${options[@]}"; do
|
|
||||||
if [[ -n "$choice" ]]; then
|
|
||||||
echo "You selected: $choice"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "Invalid selection. Try again."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Starting install with flake: ''${FLAKE_BASE_URL}#''${choice}"
|
|
||||||
|
|
||||||
# Optional: confirm before proceeding
|
|
||||||
read -rp "Proceed with installation? (y/N): " confirm
|
|
||||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
||||||
echo "Aborted."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# A nix-cache host is *the* substituter/remote-builder for every other
|
|
||||||
# host once installed (its own config explicitly excludes itself from
|
|
||||||
# using either — see buildType != "nix-cache" in the nixos flake.nix).
|
|
||||||
# Installing one shouldn't depend on a nix-cache substituter either,
|
|
||||||
# for the same reason — plus in practice "nix-cache" only resolves over
|
|
||||||
# Tailscale, which a fresh installer environment was never connected to
|
|
||||||
# anyway, so it's dead weight even for non-nix-cache installs until
|
|
||||||
# that's sorted out. Override it away here specifically for nix-cache
|
|
||||||
# targets to keep install-time behaviour consistent with run-time.
|
|
||||||
nix_extra_opts=()
|
|
||||||
if [[ "''${choice}" == *-nix-cache ]]; then
|
|
||||||
echo "Installing a nix-cache host — skipping the nix-cache substituter."
|
|
||||||
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Every host reachable through this menu has a Disko config (lxc-*
|
|
||||||
# is filtered out above, and is the only category that doesn't —
|
|
||||||
# see docs/auto-installer.md), so this can run unconditionally: no
|
|
||||||
# need to probe the flake first and branch on whether Disko applies.
|
|
||||||
disko --mode destroy,format,mount \
|
|
||||||
--flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes-wipe-all-disks
|
|
||||||
|
|
||||||
# sops-nix derives this host's decryption key from its own SSH host key
|
|
||||||
# at *activation* time, which runs before systemd would otherwise
|
|
||||||
# generate one on first boot. Without pre-seeding it here, secrets
|
|
||||||
# (including the login password) fail to decrypt on first boot.
|
|
||||||
# Generate the key with scripts/secrets/prepare-host-key.sh first.
|
|
||||||
#
|
|
||||||
# Two places a key can come from, checked in order:
|
|
||||||
# /etc/host-keys — baked into this image at build time (see
|
|
||||||
# modules/installer/host-keys.nix; only present
|
|
||||||
# if built with NIXOS_HOST_KEYS_DIR set)
|
|
||||||
# /root/host-keys — scp'd in manually after boot (older fallback,
|
|
||||||
# still supported for images built without keys)
|
|
||||||
mkdir -p /root/host-keys
|
|
||||||
if [[ -f "/etc/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
|
||||||
echo "Found baked-in SSH host key for ''${choice}, installing to target..."
|
|
||||||
install -D -m 0600 "/etc/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
||||||
install -D -m 0644 "/etc/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
||||||
elif [[ -f "/root/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
|
||||||
echo "Found pre-seeded SSH host key for ''${choice}, installing to target..."
|
|
||||||
install -D -m 0600 "/root/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
|
||||||
install -D -m 0644 "/root/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
|
||||||
else
|
|
||||||
echo "WARNING: no SSH host key found for ''${choice} (checked /etc/host-keys and /root/host-keys)"
|
|
||||||
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
|
|
||||||
echo "Run scripts/secrets/prepare-host-key.sh for host ''${choice} on your admin workstation first,"
|
|
||||||
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, or scp the result to"
|
|
||||||
echo "/root/host-keys/ on this machine."
|
|
||||||
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
|
|
||||||
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
|
|
||||||
echo "Aborted."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p /mnt/install-tmp
|
|
||||||
export TMPDIR=/mnt/install-tmp
|
|
||||||
|
|
||||||
nixos-install \
|
|
||||||
--flake "''${FLAKE_BASE_URL}#''${choice}" \
|
|
||||||
"''${nix_extra_opts[@]}" \
|
|
||||||
--no-root-password
|
|
||||||
|
|
||||||
|
|
||||||
rm -rf /mnt/install-tmp
|
|
||||||
# Redundant copy of the host's private key — the real one is now at
|
|
||||||
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
|
|
||||||
# up on its own since it was written imperatively, not declaratively.
|
|
||||||
rm -rf /root/host-keys
|
|
||||||
sleep 10
|
|
||||||
reboot
|
|
||||||
'';
|
|
||||||
|
|
||||||
|
"nixos-installer/installer/auto-install.sh" = {
|
||||||
|
source = ../../scripts/installer/auto-install.sh;
|
||||||
mode = "0755";
|
mode = "0755";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
programs.git.enable = true;
|
programs.git.enable = true;
|
||||||
|
|
||||||
@@ -192,7 +73,7 @@
|
|||||||
# file-copying/chown.
|
# file-copying/chown.
|
||||||
programs.bash.loginShellInit = ''
|
programs.bash.loginShellInit = ''
|
||||||
if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then
|
if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then
|
||||||
sudo /etc/auto-install.sh
|
sudo /etc/nixos-installer/installer/auto-install.sh
|
||||||
touch "$HOME/.auto_install_ran"
|
touch "$HOME/.auto_install_ran"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Fully declarative FreeIPA domain membership.
|
||||||
|
#
|
||||||
|
# Configures security.ipa (SSSD, Kerberos, PAM, NSSwitch) and places a
|
||||||
|
# pre-provisioned host keytab via sops-nix so no imperative ipa-client-install
|
||||||
|
# step is needed after deployment.
|
||||||
|
#
|
||||||
|
# Usage (in a host.nix imports list):
|
||||||
|
# (import ../../modules/ipa/client.nix {
|
||||||
|
# keytabSopsFile = ../../secrets/nix-cache.keytab;
|
||||||
|
# caCertFile = ../../certs/ipa-ca.crt;
|
||||||
|
# })
|
||||||
|
#
|
||||||
|
# One-time operator setup per host (do this BEFORE deploying):
|
||||||
|
#
|
||||||
|
# 1. Fetch the IPA CA certificate (public — safe to commit):
|
||||||
|
# curl -o certs/ipa-ca.crt http://<ipa-server>/ipa/config/ca.crt
|
||||||
|
# Replace the placeholder at certs/ipa-ca.crt and commit it.
|
||||||
|
#
|
||||||
|
# 2. On the FreeIPA server, add the host and generate a keytab:
|
||||||
|
# ipa host-add <fqdn> --ip-address=<ip>
|
||||||
|
# ipa-getkeytab -s <ipa-server> -p host/<fqdn> -k /tmp/<host>.keytab
|
||||||
|
#
|
||||||
|
# 3. sops-encrypt the keytab as a binary secret from your admin machine
|
||||||
|
# (must run from repo root; sops matches creation rules against the file
|
||||||
|
# path, so copy to secrets/ first and encrypt in-place):
|
||||||
|
# cp /tmp/<host>.keytab secrets/<host>.keytab
|
||||||
|
# sops -e --input-type binary -i secrets/<host>.keytab
|
||||||
|
# Add secrets/<host>.keytab to .sops.yaml with the host's age key as a
|
||||||
|
# recipient (see the nix-cache.keytab entry for the pattern), then run:
|
||||||
|
# scripts/secrets/sync-host-keys.sh <target> # if not done yet
|
||||||
|
# sops updatekeys secrets/<host>.keytab
|
||||||
|
# Commit the encrypted file.
|
||||||
|
#
|
||||||
|
# 4. nixos-rebuild switch (or create-proxmox-resource.sh) — no further
|
||||||
|
# manual enrollment steps required.
|
||||||
|
#
|
||||||
|
# vars dependencies: homeDomain, ipaServer
|
||||||
|
|
||||||
|
{ keytabSopsFile, caCertFile }:
|
||||||
|
{ config, lib, pkgs, vars, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
realm = lib.strings.toUpper vars.homeDomain;
|
||||||
|
fqdn = "${config.networking.hostName}.${vars.homeDomain}";
|
||||||
|
# "sweet.home" -> "dc=sweet,dc=home"
|
||||||
|
basedn = lib.strings.concatMapStringsSep "," (c: "dc=${c}") (lib.strings.splitString "." vars.homeDomain);
|
||||||
|
# security.ipa.certificate expects a derivation (package), not a raw path.
|
||||||
|
caCertPkg = pkgs.writeText "ipa-ca.crt" (builtins.readFile caCertFile);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
security.ipa = {
|
||||||
|
enable = true;
|
||||||
|
domain = vars.homeDomain;
|
||||||
|
realm = realm;
|
||||||
|
server = vars.ipaServer;
|
||||||
|
certificate = caCertPkg;
|
||||||
|
basedn = basedn;
|
||||||
|
ipaHostname = fqdn;
|
||||||
|
offlinePasswords = true;
|
||||||
|
cacheCredentials = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Host keytab: pre-provisioned on the IPA server, sops-encrypted binary.
|
||||||
|
# Placed at /etc/krb5.keytab before SSSD starts so the host authenticates
|
||||||
|
# to IPA without running ipa-client-install.
|
||||||
|
sops.secrets."ipa-host-keytab" = {
|
||||||
|
sopsFile = keytabSopsFile;
|
||||||
|
format = "binary";
|
||||||
|
path = "/etc/krb5.keytab";
|
||||||
|
owner = "root";
|
||||||
|
group = "root";
|
||||||
|
mode = "0600";
|
||||||
|
restartUnits = [ "sssd.service" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{
|
{
|
||||||
nix.settings = {
|
nix.settings = {
|
||||||
substituters = [
|
substituters = [
|
||||||
"http://${vars.nixCacheHost}"
|
"http://${vars.nixCacheHost}.${vars.homeDomain}"
|
||||||
"https://cache.nixos.org/"
|
"https://cache.nixos.org/"
|
||||||
];
|
];
|
||||||
trusted-public-keys = [
|
trusted-public-keys = [
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
# dedicated keypair). If this host doesn't have one yet:
|
# dedicated keypair). If this host doesn't have one yet:
|
||||||
# sudo -u root ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
|
# sudo -u root ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
|
||||||
# # then add its .pub to vars.remoteBuilderAuthorizedKeys and rebuild nix-cache
|
# # then add its .pub to vars.remoteBuilderAuthorizedKeys and rebuild nix-cache
|
||||||
# sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache nix-store --version
|
# sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache.sweet.home nix-store --version
|
||||||
# Trust nix-cache's SSH host key declaratively so the nix-daemon (root)
|
# 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
|
# can connect the first time without a manual ssh-keyscan/known_hosts
|
||||||
# step on every new client.
|
# step on every new client.
|
||||||
programs.ssh.knownHosts.${vars.nixCacheHost} = {
|
programs.ssh.knownHosts."${vars.nixCacheHost}.${vars.homeDomain}" = {
|
||||||
hostNames = [ vars.nixCacheHost ];
|
hostNames = [ "${vars.nixCacheHost}.${vars.homeDomain}" ];
|
||||||
publicKey = vars.nixCacheHostKey;
|
publicKey = vars.nixCacheHostKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
buildMachines = [
|
buildMachines = [
|
||||||
{
|
{
|
||||||
hostName = vars.nixCacheHost;
|
hostName = "${vars.nixCacheHost}.${vars.homeDomain}";
|
||||||
sshUser = vars.remoteBuilderUser;
|
sshUser = vars.remoteBuilderUser;
|
||||||
sshKey = "/root/.ssh/id_ed25519";
|
sshKey = "/root/.ssh/id_ed25519";
|
||||||
inherit (pkgs.stdenv.hostPlatform) system;
|
inherit (pkgs.stdenv.hostPlatform) system;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
nginx = {
|
nginx = {
|
||||||
enable = true;
|
enable = true;
|
||||||
recommendedProxySettings = true;
|
recommendedProxySettings = true;
|
||||||
virtualHosts.${vars.nixCacheHost} = {
|
virtualHosts."${vars.nixCacheHost}.${vars.homeDomain}" = {
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
proxyPass = "http://${config.services.nix-serve.bindAddress}:${toString config.services.nix-serve.port}";
|
proxyPass = "http://${config.services.nix-serve.bindAddress}:${toString config.services.nix-serve.port}";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,4 +25,14 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
enable32Bit = true;
|
enable32Bit = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# The systemd-based initrd (default here since this host has a ZFS root --
|
||||||
|
# see modules/disko/baremetal.nix) locks the root account by default, so
|
||||||
|
# sulogin refuses to hand over a shell if something in the initrd (e.g.
|
||||||
|
# the ZFS pool import) fails and it drops to emergency mode -- confirmed
|
||||||
|
# live: it just loops re-entering the target instead of prompting. This
|
||||||
|
# only affects the pre-switch-root initrd shell, not the installed
|
||||||
|
# system's own login, and is worth the tradeoff on a box already reachable
|
||||||
|
# at the physical console.
|
||||||
|
boot.initrd.systemd.emergencyAccess = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-20
@@ -63,23 +63,22 @@ in
|
|||||||
# back to decide `pct create`'s --unprivileged flag, so the two stay
|
# back to decide `pct create`'s --unprivileged flag, so the two stay
|
||||||
# in sync).
|
# in sync).
|
||||||
#
|
#
|
||||||
# lxc-docker is the one exception: the kernel's NFS client doesn't set
|
# Any lxc-* host with an NFS fileSystem must be privileged: the kernel's
|
||||||
# FS_USERNS_MOUNT, so mounting NFS from inside *any* non-init user
|
# NFS client doesn't set FS_USERNS_MOUNT, so mounting NFS from inside
|
||||||
# namespace -- which is exactly what an unprivileged container's
|
# *any* non-init user namespace -- which is exactly what an unprivileged
|
||||||
# UID-mapped root runs in -- is rejected at the VFS layer with EPERM,
|
# container's UID-mapped root runs in -- is rejected at the VFS layer
|
||||||
# no matter what Proxmox's own `mount=nfs;nfs4` container feature
|
# with EPERM, no matter what Proxmox's own `mount=nfs;nfs4` container
|
||||||
# allows at the AppArmor layer (confirmed live: TCP to the NFS server
|
# feature allows at the AppArmor layer (confirmed live: TCP to the NFS
|
||||||
# succeeds, the server's export table matches the container's IP, and
|
# server succeeds, the server's export table matches the container's IP,
|
||||||
# `mount.nfs: Operation not permitted` still fires immediately with no
|
# and `mount.nfs: Operation not permitted` still fires immediately with
|
||||||
# corresponding denial anywhere in the server's logs -- a kernel-level
|
# no corresponding denial anywhere in the server's logs -- a kernel-level
|
||||||
# rejection, not a network or export-permission one). Keying off
|
# rejection, not a network or export-permission one). Deriving this from
|
||||||
# hostName rather than something docker-build-type-specific because
|
# fileSystems rather than a per-host override keeps it self-consistent:
|
||||||
# modules/build-types/docker.nix is also composed for linode-docker/
|
# any new lxc-* host that declares an NFS mount automatically gets the
|
||||||
# proxmox-docker, which don't import proxmox-lxc.nix at all --setting
|
# privilege level it needs without a separate manual flag.
|
||||||
# this option there would break their eval with "option does not
|
privileged = builtins.any
|
||||||
# exist" regardless of any mkIf guard, since mkIf only makes a value
|
(fs: fs.fsType == "nfs" || fs.fsType == "nfs4")
|
||||||
# conditional, not whether the option needs to exist somewhere.
|
(builtins.attrValues config.fileSystems);
|
||||||
privileged = config.networking.hostName == "docker";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
boot.loader = {
|
boot.loader = {
|
||||||
@@ -106,15 +105,61 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# NixOS's etc activation removes any /etc file that was in the previous
|
||||||
|
# generation's environment.etc but is absent from the current one — even
|
||||||
|
# real (non-symlink) copies. On every routine nixos-rebuild switch/test that
|
||||||
|
# lacks NIXOS_HOST_KEYS_DIR the key is absent from environment.etc, so it
|
||||||
|
# gets removed as "obsolete". sops-nix derives its age decryption key from
|
||||||
|
# /etc/ssh/ssh_host_ed25519_key; deletion cascades into every sops secret
|
||||||
|
# failing with "Error getting data key: 0 successful groups required, got 0".
|
||||||
|
#
|
||||||
|
# Fix: activation scripts that bracket the etc step, with explicit deps
|
||||||
|
# to enforce the correct ordering. Without deps the topological sort places
|
||||||
|
# preserveSshHostKey AFTER etc (confirmed live on a deployed lxc-tor-relay:
|
||||||
|
# position 7 vs etc's position 5) -- the key is already gone by the time it
|
||||||
|
# tries to save it. The etc/setupSecrets entries ADD to existing deps
|
||||||
|
# (types.listOf concatenates across module definitions).
|
||||||
|
system.activationScripts = {
|
||||||
|
# Saves the live key to /run before etc can delete it.
|
||||||
|
preserveSshHostKey = ''
|
||||||
|
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
|
||||||
|
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
|
||||||
|
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Reinstalls the key after etc runs if it was removed as "obsolete".
|
||||||
|
# The resulting file is not registered in environment.etc for either
|
||||||
|
# generation, so subsequent rebuilds leave it alone permanently.
|
||||||
|
restoreSshHostKey = {
|
||||||
|
deps = [ "etc" ];
|
||||||
|
text = ''
|
||||||
|
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
|
||||||
|
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
fi
|
||||||
|
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Force etc to wait until the key is saved, and sops to wait until the
|
||||||
|
# key is restored. Without these the topological sort breaks the chain.
|
||||||
|
etc = { deps = [ "preserveSshHostKey" ]; };
|
||||||
|
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
|
||||||
|
};
|
||||||
|
|
||||||
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
|
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
|
||||||
# store DB via a systemd service (register-nix-paths) -- it never runs
|
# store DB via a systemd service (register-nix-paths) -- it never runs
|
||||||
# an activation script at all. Confirmed live this means neither
|
# an activation script at all. Confirmed live this means neither
|
||||||
# sops-nix's "for users" secrets (password hashes -- installed by the
|
# sops-nix's "for users" secrets (password hashes -- installed by the
|
||||||
# activation script itself, not a systemd service, since they need to
|
# activation script itself, not a systemd service, since they need to
|
||||||
# exist *before* user creation) nor the user-creation step that
|
# exist *before* user creation) nor the user-creation step that
|
||||||
# consumes them ever run on a real lxc-* boot. Regular secrets
|
# consumes them ever run on a real lxc-* boot. In this config sops-nix
|
||||||
# (nix-serve's key, beszel's token, etc.) work anyway because sops-nix
|
# does NOT generate its own boot-time service (confirmed live: no
|
||||||
# provides its own systemd service for those.
|
# sops-nix.service in systemctl list-unit-files on a deployed
|
||||||
|
# lxc-tor-relay container); /run/secrets is a tmpfs cleared on every
|
||||||
|
# reboot, so secrets must be reinstalled on each non-first boot by
|
||||||
|
# nixos-lxc-sops-reinstall (below).
|
||||||
#
|
#
|
||||||
# A systemd service, not boot.postBootCommands: tried that first (it's
|
# A systemd service, not boot.postBootCommands: tried that first (it's
|
||||||
# a genuine, generally-invoked hook -- nixos/modules/system/boot/stage-2-init.sh,
|
# a genuine, generally-invoked hook -- nixos/modules/system/boot/stage-2-init.sh,
|
||||||
@@ -165,4 +210,41 @@ in
|
|||||||
touch /var/lib/nixos-lxc-first-boot-activated
|
touch /var/lib/nixos-lxc-first-boot-activated
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Reinstalls sops secrets on every non-first boot. /run/secrets is a
|
||||||
|
# tmpfs that is cleared on each reboot; without this service, secrets
|
||||||
|
# are permanently absent after the first boot and every service that
|
||||||
|
# reads from /run/secrets fails on start.
|
||||||
|
#
|
||||||
|
# wantedBy/before network.target: switch-to-configuration test requires
|
||||||
|
# D-Bus to restart systemd targets after running activation scripts. D-Bus
|
||||||
|
# is available once basic.target completes (the default After=basic.target
|
||||||
|
# that DefaultDependencies would otherwise add). Placing the service before
|
||||||
|
# network.target ensures secrets are ready before any network-dependent
|
||||||
|
# service (including beszel-agent and nix-serve) starts, while running late
|
||||||
|
# enough that D-Bus is already up.
|
||||||
|
#
|
||||||
|
# ConditionPathExists=... skips this service on the genuine first boot
|
||||||
|
# (the marker doesn't exist yet); nixos-lxc-first-boot-activate handles
|
||||||
|
# that case. On every subsequent boot the condition passes and secrets
|
||||||
|
# are reinstalled before user services start.
|
||||||
|
#
|
||||||
|
# SuccessExitStatus=11: switch-to-configuration exits 11 when it cannot
|
||||||
|
# acquire the activation lock (another switch is already in progress).
|
||||||
|
# During a nixos-rebuild switch the activation already installs secrets, so
|
||||||
|
# treating the lock-held case as success is correct.
|
||||||
|
systemd.services.nixos-lxc-sops-reinstall = {
|
||||||
|
description = "Reinstall sops secrets on each non-first boot (LXC, /run is tmpfs)";
|
||||||
|
wantedBy = [ "network.target" ];
|
||||||
|
before = [ "network.target" ];
|
||||||
|
unitConfig.ConditionPathExists = "/var/lib/nixos-lxc-first-boot-activated";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
SuccessExitStatus = "11";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
/run/current-system/bin/switch-to-configuration test
|
||||||
|
'';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,81 @@
|
|||||||
{ ... }:
|
{ lib, flakeTarget, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# Bakes this exact flake target's pre-generated SSH host key straight
|
||||||
|
# into /etc/ssh/ -- mirrors lxc.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).
|
||||||
|
#
|
||||||
|
# Unlike --pre-format-files (which places files on the QEMU builder VM's
|
||||||
|
# rootfs, not the target disk), embedding via environment.etc here means
|
||||||
|
# nixos-install's own activation step installs the key onto the target
|
||||||
|
# disk. sshd-keygen then finds it already present and skips generation,
|
||||||
|
# so the disk image boots with the clan-registered key and sops can
|
||||||
|
# decrypt on first boot.
|
||||||
|
#
|
||||||
|
# Without this, nixos-install's sshd-keygen activation generates a fresh
|
||||||
|
# key (unregistered in .sops.yaml), sops decryption fails permanently,
|
||||||
|
# and password hashes are never applied -- confirmed live: passwords
|
||||||
|
# stayed '!' even with mutableUsers = false because hashedPasswordFile
|
||||||
|
# pointed to a path that sops never wrote.
|
||||||
|
hostKeysDirStr = builtins.getEnv "NIXOS_HOST_KEYS_DIR";
|
||||||
|
hasHostKeysDir = hostKeysDirStr != "" && builtins.pathExists hostKeysDirStr;
|
||||||
|
hostKeysDir = /. + hostKeysDirStr;
|
||||||
|
|
||||||
|
privKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key";
|
||||||
|
pubKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key.pub";
|
||||||
|
hasKeyForThisTarget =
|
||||||
|
hasHostKeysDir
|
||||||
|
&& builtins.pathExists privKeyFile
|
||||||
|
&& builtins.pathExists pubKeyFile;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../hardware-configuration/vm/proxmox.nix
|
../hardware-configuration/vm/proxmox.nix
|
||||||
../boot/efi.nix
|
../boot/efi.nix
|
||||||
../disko/proxmox.nix
|
../disko/proxmox.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
environment.etc = lib.mkIf hasKeyForThisTarget {
|
||||||
|
"ssh/ssh_host_ed25519_key" = {
|
||||||
|
source = privKeyFile;
|
||||||
|
mode = "0600";
|
||||||
|
};
|
||||||
|
"ssh/ssh_host_ed25519_key.pub" = {
|
||||||
|
source = pubKeyFile;
|
||||||
|
mode = "0644";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# NixOS's etc activation removes any /etc file that was in the previous
|
||||||
|
# generation's environment.etc but is absent from the current one. Since
|
||||||
|
# the SSH key is only in environment.etc during the --impure build (when
|
||||||
|
# NIXOS_HOST_KEYS_DIR is set), normal rebuilds would remove it as
|
||||||
|
# "obsolete". These scripts mirror lxc.nix's approach: save the live key
|
||||||
|
# before etc runs, restore it after. Without the explicit deps, the
|
||||||
|
# topological sort places preserveSshHostKey after etc (confirmed live on
|
||||||
|
# lxc-tor-relay: position 7 vs etc's position 5), so the key is gone
|
||||||
|
# before it can be saved.
|
||||||
|
system.activationScripts = {
|
||||||
|
preserveSshHostKey = ''
|
||||||
|
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
|
||||||
|
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
|
||||||
|
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
restoreSshHostKey = {
|
||||||
|
deps = [ "etc" ];
|
||||||
|
text = ''
|
||||||
|
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
|
||||||
|
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
fi
|
||||||
|
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
etc = { deps = [ "preserveSshHostKey" ]; };
|
||||||
|
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{ config, lib, vars, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# Use the same FQDN approach as docker/mount-data.nix — a bare hostname is
|
||||||
|
# unreliable: systemd-resolved only tries LLMNR for single-label names, and
|
||||||
|
# a global search domain causes it to skip the interface-scoped LAN DNS.
|
||||||
|
nfsServer = "${vars.nfsServerHost}.${vars.homeDomain}";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
fileSystems.${vars.nfsShares.pxebootImages.mountpoint} = {
|
||||||
|
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.pxebootImages.subpath}";
|
||||||
|
fsType = "nfs";
|
||||||
|
options = [
|
||||||
|
"_netdev"
|
||||||
|
"noatime"
|
||||||
|
] ++ (if config.boot.isContainer
|
||||||
|
# NFSv4 requires rpc_pipefs (sunrpc filesystem), which Proxmox LXC
|
||||||
|
# containers block unless `features: mount=nfs` is set. Use NFSv3+nolock
|
||||||
|
# instead: no rpc_pipefs dependency at the protocol level, and rpcbind
|
||||||
|
# on the server handles port resolution without needing client-side
|
||||||
|
# sunrpc infrastructure. nofail keeps boot clean if server is unreachable.
|
||||||
|
then [ "nfsvers=3" "proto=tcp" "nolock" "nofail" ]
|
||||||
|
else [ "nfsvers=4.2" "x-systemd.automount" ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
# NixOS pulls var-lib-nfs-rpc_pipefs.mount (the sunrpc filesystem) into
|
||||||
|
# nfs-client.target for any nfs fileSystems entry. In LXC containers the
|
||||||
|
# sunrpc mount is blocked by Proxmox's AppArmor profile, causing it to fail
|
||||||
|
# and the activation to report an error even though our mount uses nofail.
|
||||||
|
# Add ConditionVirtualization=!container via drop-in so systemd skips the
|
||||||
|
# unit entirely in containers (skip = inactive, not failed), which keeps
|
||||||
|
# nfs-client.target green and activation clean.
|
||||||
|
systemd.units = lib.mkIf config.boot.isContainer {
|
||||||
|
"var-lib-nfs-rpc_pipefs.mount" = {
|
||||||
|
overrideStrategy = "asDropin";
|
||||||
|
text = ''
|
||||||
|
[Unit]
|
||||||
|
ConditionVirtualization=!container
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
_:
|
|
||||||
|
|
||||||
{
|
|
||||||
imports = [ ./enable-service.nix ];
|
|
||||||
|
|
||||||
services.tailscale = {
|
|
||||||
# Enables the sysctl forwarding settings exit nodes/subnet routers need;
|
|
||||||
# without this, --advertise-exit-node has no effect.
|
|
||||||
useRoutingFeatures = "server";
|
|
||||||
|
|
||||||
# Lets peers reach this node directly over the tailscale UDP port
|
|
||||||
# instead of relaying through DERP.
|
|
||||||
openFirewall = true;
|
|
||||||
|
|
||||||
# extraSetFlags (tailscale set, via the always-on tailscaled-set
|
|
||||||
# service), not extraUpFlags -- extraUpFlags is only ever applied by
|
|
||||||
# tailscaled-autoconnect, which itself only runs when
|
|
||||||
# services.tailscale.authKeyFile is set (nothing in this repo sets one,
|
|
||||||
# so tailscale up is a manual, one-time operator step on every host that
|
|
||||||
# uses this service). extraSetFlags has no such gate, so
|
|
||||||
# --advertise-exit-node self-reapplies on every boot once the operator
|
|
||||||
# has authenticated the node once.
|
|
||||||
extraSetFlags = [
|
|
||||||
"--advertise-exit-node"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [ ./enable-service.nix ];
|
||||||
|
|
||||||
|
services.tailscale = {
|
||||||
|
# Enables the sysctl forwarding settings subnet routers need;
|
||||||
|
# without this, --advertise-routes has no effect.
|
||||||
|
useRoutingFeatures = "server";
|
||||||
|
|
||||||
|
# Lets peers reach this node directly over the tailscale UDP port
|
||||||
|
# instead of relaying through DERP.
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Tailscale recommends these ethtool flags on the uplink interface to get
|
||||||
|
# full UDP GRO throughput on subnet routers (https://tailscale.com/s/ethtool-config-udp-gro).
|
||||||
|
# The interface is derived from the default route so it works regardless of
|
||||||
|
# what the NIC is named on a given host.
|
||||||
|
systemd.services.tailscale-udp-gro = {
|
||||||
|
description = "Enable UDP GRO forwarding on uplink for Tailscale subnet router";
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
path = [ pkgs.ethtool pkgs.iproute2 ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
ExecStart = pkgs.writeShellScript "tailscale-udp-gro" ''
|
||||||
|
NETDEV=$(ip -o route get 8.8.8.8 | cut -f 5 -d " ")
|
||||||
|
ethtool -K "$NETDEV" rx-udp-gro-forwarding on rx-gro-list off
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{ vars, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Run dnsmasq on the LAN interface as a forwarding-only resolver for
|
||||||
|
# *.ts.net (Tailscale MagicDNS names). FreeIPA's bind-dyndb-ldap
|
||||||
|
# cannot reach 100.100.100.100 (Tailscale's internal resolver) directly
|
||||||
|
# because the DC is not a Tailscale node. This host IS a Tailscale node
|
||||||
|
# and can reach 100.100.100.100 via its tailscale0 interface, so it
|
||||||
|
# acts as an intermediary: FreeIPA has a conditional forward zone for
|
||||||
|
# ts.net pointing here (vars.tailscaleRouterIp), and this dnsmasq
|
||||||
|
# instance forwards those queries onward to Tailscale's resolver.
|
||||||
|
#
|
||||||
|
# Configure FreeIPA once after deploying this host:
|
||||||
|
# kinit admin
|
||||||
|
# ipa dnsforwardzone-add ${vars.tailnetDomain} \
|
||||||
|
# --forwarder=${vars.tailscaleRouterIp} \
|
||||||
|
# --forward-policy=only
|
||||||
|
# Note: IPA refuses to shadow ts.net (a real public TLD); use the
|
||||||
|
# tailnet-specific subdomain (vars.tailnetDomain) instead.
|
||||||
|
services.dnsmasq = {
|
||||||
|
enable = true;
|
||||||
|
# NixOS's dnsmasq module defaults resolveLocalQueries to true, which adds
|
||||||
|
# 127.0.0.1 to networking.nameservers and makes dnsmasq bind to
|
||||||
|
# listen-address=127.0.0.1. This instance is not the host's local
|
||||||
|
# resolver — it only serves IPA's conditional forwarder for tailnet names.
|
||||||
|
# The host uses domainControllerIp directly (networking.nameservers in
|
||||||
|
# host.nix). Without this, all host DNS goes through dnsmasq, which has
|
||||||
|
# no upstream for general queries (no-resolv=true), breaking resolution.
|
||||||
|
resolveLocalQueries = false;
|
||||||
|
settings = {
|
||||||
|
# Listen only on the LAN interface — not tailscale0 or loopback.
|
||||||
|
# bind-interfaces prevents dnsmasq from binding to 0.0.0.0 and
|
||||||
|
# then filtering by interface later; combined with `interface` this
|
||||||
|
# ensures it genuinely listens only on eth0.
|
||||||
|
bind-interfaces = true;
|
||||||
|
interface = [ vars.lxcLanInterface ];
|
||||||
|
|
||||||
|
# Forward-only: no local /etc/hosts or /etc/resolv.conf reading,
|
||||||
|
# no negative caching of NXDOMAIN for names this instance doesn't
|
||||||
|
# serve. All ts.net queries come from FreeIPA's conditional forwarder
|
||||||
|
# and must be answered by Tailscale's resolver.
|
||||||
|
no-hosts = true;
|
||||||
|
no-resolv = true;
|
||||||
|
|
||||||
|
# Tailscale's internal "Quad100" resolver — reachable from any
|
||||||
|
# Tailscale node via the tailscale0 interface. Scoped to the
|
||||||
|
# specific tailnet subdomain (vars.tailnetDomain) rather than
|
||||||
|
# all of ts.net: FreeIPA refuses to shadow ts.net (a real public
|
||||||
|
# TLD with DNSimple nameservers) so the conditional forward zone
|
||||||
|
# in FreeIPA must use the tailnet-specific subdomain instead:
|
||||||
|
# ipa dnsforwardzone-add ${vars.tailnetDomain} \
|
||||||
|
# --forwarder=${vars.tailscaleRouterIp} \
|
||||||
|
# --forward-policy=only
|
||||||
|
server = [ "/${vars.tailnetDomain}/100.100.100.100" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedUDPPorts = [ 53 ];
|
||||||
|
networking.firewall.allowedTCPPorts = [ 53 ];
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -68,6 +68,7 @@ cat > "$HOME/.config/nix/nix.conf" <<'EOF'
|
|||||||
experimental-features = nix-command flakes
|
experimental-features = nix-command flakes
|
||||||
accept-flake-config = false
|
accept-flake-config = false
|
||||||
warn-dirty = false
|
warn-dirty = false
|
||||||
|
build-users-group =
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Nix version:"
|
echo "Nix version:"
|
||||||
|
|||||||
+19
-2
@@ -22,7 +22,7 @@
|
|||||||
: "${PVE1_HOST:=pve1.sweet.home}"
|
: "${PVE1_HOST:=pve1.sweet.home}"
|
||||||
: "${PVE_TEST_HOST:=pve-test.sweet.home}"
|
: "${PVE_TEST_HOST:=pve-test.sweet.home}"
|
||||||
: "${PROXMOX_HOST:=$PVE1_HOST}"
|
: "${PROXMOX_HOST:=$PVE1_HOST}"
|
||||||
: "${PROXMOX_SSH_USER:=root}"
|
: "${PROXMOX_SSH_USER:=wayne}"
|
||||||
|
|
||||||
# Where this flake repo lives on the Proxmox node itself.
|
# Where this flake repo lives on the Proxmox node itself.
|
||||||
# scripts/proxmox/create-proxmox-resource.sh builds images directly on the node
|
# scripts/proxmox/create-proxmox-resource.sh builds images directly on the node
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
# (from this checkout's own `origin` remote) the first time it doesn't
|
# (from this checkout's own `origin` remote) the first time it doesn't
|
||||||
# find it, installing build tooling via scripts/codex-setup.sh, then
|
# find it, installing build tooling via scripts/codex-setup.sh, then
|
||||||
# `git pull`s it before every subsequent build.
|
# `git pull`s it before every subsequent build.
|
||||||
: "${PROXMOX_REMOTE_REPO_DIR:=/root/nixos}"
|
: "${PROXMOX_REMOTE_REPO_DIR:=/home/${PROXMOX_SSH_USER}/nixos}"
|
||||||
|
|
||||||
# Storage pool names -- Proxmox's own stock-install defaults, but this
|
# Storage pool names -- Proxmox's own stock-install defaults, but this
|
||||||
# varies a lot by setup (ZFS pool name, custom LVM-thin volume, etc.).
|
# varies a lot by setup (ZFS pool name, custom LVM-thin volume, etc.).
|
||||||
@@ -82,6 +82,23 @@ export PVE1_HOST PVE_TEST_HOST PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE \
|
|||||||
: "${NIX_CACHE_HOST:=nix-cache}"
|
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||||
export NIX_CACHE_HOST
|
export NIX_CACHE_HOST
|
||||||
|
|
||||||
|
# Matches variables.nix's lanDomain (the Gitea host this flake's own repo
|
||||||
|
# is served from -- see scripts/installer/auto-install.sh's FLAKE_BASE_URL)
|
||||||
|
# -- update both if it ever changes.
|
||||||
|
: "${LAN_DOMAIN:=gitea.lan.ddnsgeek.com}"
|
||||||
|
export LAN_DOMAIN
|
||||||
|
|
||||||
|
# Matches variables.nix's homeDomain -- the base LAN domain for service
|
||||||
|
# subdomains, FreeIPA Kerberos realm, and host FQDNs.
|
||||||
|
: "${HOME_DOMAIN:=sweet.home}"
|
||||||
|
export HOME_DOMAIN
|
||||||
|
|
||||||
|
# Matches variables.nix's ipaServer -- the FreeIPA server hostname.
|
||||||
|
# scripts/ipa/create-nixos-ipa-host-account.sh SSHes here to run
|
||||||
|
# ipa host-add and ipa-getkeytab.
|
||||||
|
: "${IPA_SERVER:=domain-controller.sweet.home}"
|
||||||
|
export IPA_SERVER
|
||||||
|
|
||||||
# nix_extra_opts: call as a plain statement (NOT inside $(...)/<(...) --
|
# nix_extra_opts: call as a plain statement (NOT inside $(...)/<(...) --
|
||||||
# that forks a subshell, and the whole point is exporting a decision back
|
# that forks a subshell, and the whole point is exporting a decision back
|
||||||
# into *this* shell) to populate the global NIX_OPTS array with whatever
|
# into *this* shell) to populate the global NIX_OPTS array with whatever
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# acceptance-tests.sh — HA cluster acceptance tests (T1–T7)
|
||||||
|
#
|
||||||
|
# Run from a host with SSH access to both HA nodes (or from node1 itself).
|
||||||
|
# All 7 tests must pass before considering the cluster production-ready.
|
||||||
|
# Test values below must match variables.nix haServer* values.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Configuration ─────────────────────────────────────────────────────────
|
||||||
|
NODE1="ha-server-1"
|
||||||
|
NODE2="ha-server-2"
|
||||||
|
NODE1_IP="192.168.2.200" # vars.haServer1Ip
|
||||||
|
NODE2_IP="192.168.2.201" # vars.haServer2Ip
|
||||||
|
VIP="192.168.2.202" # vars.haServerVip
|
||||||
|
XFS_MOUNT="/srv/ha-data" # vars.haStorageRoot
|
||||||
|
ISCSI_IQN="iqn.2026-01.home.sweet:ha-storage" # vars.haIscsiIqn
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
RESULTS=()
|
||||||
|
|
||||||
|
pass() { echo " PASS: $1"; ((PASS++)); RESULTS+=("PASS $1"); }
|
||||||
|
fail() { echo " FAIL: $1"; ((FAIL++)); RESULTS+=("FAIL $1"); }
|
||||||
|
|
||||||
|
n1() { ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "root@${NODE1_IP}" "$@" 2>/dev/null; }
|
||||||
|
n2() { ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "root@${NODE2_IP}" "$@" 2>/dev/null; }
|
||||||
|
|
||||||
|
echo "════════════════════════════════════════════════════"
|
||||||
|
echo " HA Cluster Acceptance Tests — $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
echo "════════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
# ── T1: Corosync quorum established ──────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[T1] Corosync quorum"
|
||||||
|
if n1 "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then
|
||||||
|
pass "cluster has quorum"
|
||||||
|
else
|
||||||
|
fail "cluster does not have quorum — check corosync on both nodes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── T2: DRBD Primary on node1, Secondary on node2 ────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[T2] DRBD roles"
|
||||||
|
DRBD_ROLE=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown")
|
||||||
|
if [[ "$DRBD_ROLE" == "Primary/Secondary" || "$DRBD_ROLE" == "Primary" ]]; then
|
||||||
|
pass "DRBD Primary on $NODE1 ($DRBD_ROLE)"
|
||||||
|
else
|
||||||
|
fail "unexpected DRBD role on $NODE1: $DRBD_ROLE (expected Primary/Secondary)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DRBD_DSTATE=$(n1 "drbdadm dstate ha-data" 2>/dev/null || echo "unknown")
|
||||||
|
if echo "$DRBD_DSTATE" | grep -q "UpToDate"; then
|
||||||
|
pass "DRBD disk state UpToDate ($DRBD_DSTATE)"
|
||||||
|
else
|
||||||
|
fail "DRBD disk not UpToDate: $DRBD_DSTATE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── T3: XFS mounted at haStorageRoot on the Active node ──────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[T3] XFS mount"
|
||||||
|
if n1 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||||
|
pass "XFS mounted at ${XFS_MOUNT} on $NODE1"
|
||||||
|
else
|
||||||
|
fail "XFS not mounted at ${XFS_MOUNT} on $NODE1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||||
|
fail "XFS unexpectedly mounted on $NODE2 (should only be on Active node)"
|
||||||
|
else
|
||||||
|
pass "XFS not mounted on $NODE2 (correct — Secondary)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── T4: iSCSI target visible on both nodes ────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[T4] iSCSI target"
|
||||||
|
IQN_COUNT=$(n1 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0")
|
||||||
|
if [[ "$IQN_COUNT" -ge 1 ]]; then
|
||||||
|
pass "iSCSI IQN active on $NODE1 ($IQN_COUNT target(s))"
|
||||||
|
else
|
||||||
|
fail "no iSCSI IQN active on $NODE1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# iSCSI discovery from node2 via VIP
|
||||||
|
if n2 "iscsiadm -m discovery -t sendtargets -p '${VIP}' 2>/dev/null | grep -q '${ISCSI_IQN}'"; then
|
||||||
|
pass "iSCSI target discoverable from $NODE2 via VIP ${VIP}"
|
||||||
|
else
|
||||||
|
fail "iSCSI target not discoverable from $NODE2 via ${VIP}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── T5: Failover — standby node1, verify resources move to node2 ──────────
|
||||||
|
echo ""
|
||||||
|
echo "[T5] Failover (standby $NODE1)"
|
||||||
|
MYNODE=$(n1 "crm_node -n" 2>/dev/null || echo "")
|
||||||
|
n1 "crm_standby -N '${MYNODE}' -v on" 2>/dev/null || true
|
||||||
|
echo " Waiting up to 30 s for resources to move to $NODE2..."
|
||||||
|
MOVED=false
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||||
|
MOVED=true
|
||||||
|
echo " Resources moved in ${i}s"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if $MOVED; then
|
||||||
|
pass "XFS mounted on $NODE2 after failover"
|
||||||
|
IQN_ON_N2=$(n2 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0")
|
||||||
|
[[ "$IQN_ON_N2" -ge 1 ]] \
|
||||||
|
&& pass "iSCSI target active on $NODE2 after failover" \
|
||||||
|
|| fail "iSCSI target NOT active on $NODE2 after failover"
|
||||||
|
else
|
||||||
|
fail "XFS did not mount on $NODE2 within 30 s — failover incomplete"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── T6: Data integrity — file written pre-failover readable post-failover ─
|
||||||
|
echo ""
|
||||||
|
echo "[T6] Data integrity"
|
||||||
|
# Write a test file on node2 (now Active) and verify its content
|
||||||
|
TEST_FILE="${XFS_MOUNT}/.acceptance-test-$$"
|
||||||
|
TEST_CONTENT="ha-acceptance-test-$(date +%s)"
|
||||||
|
n2 "echo '${TEST_CONTENT}' > '${TEST_FILE}'" 2>/dev/null || true
|
||||||
|
READBACK=$(n2 "cat '${TEST_FILE}' 2>/dev/null" || echo "")
|
||||||
|
if [[ "$READBACK" == "$TEST_CONTENT" ]]; then
|
||||||
|
pass "test file written and read back correctly on $NODE2"
|
||||||
|
else
|
||||||
|
fail "data integrity check failed (wrote: '$TEST_CONTENT', read: '$READBACK')"
|
||||||
|
fi
|
||||||
|
n2 "rm -f '${TEST_FILE}'" 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── T7: Node rejoin — un-standby node1, verify cluster is healthy ─────────
|
||||||
|
echo ""
|
||||||
|
echo "[T7] Node rejoin"
|
||||||
|
n1 "crm_standby -N '${MYNODE}' -v off" 2>/dev/null || true
|
||||||
|
n1 "crm_resource --cleanup" 2>/dev/null || true
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
ONLINE_NODES=$(n2 "crm_mon -1 2>/dev/null | grep -c 'Online:'" || echo "0")
|
||||||
|
if n1 "corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'"; then
|
||||||
|
pass "$NODE1 rejoined — cluster has quorum"
|
||||||
|
else
|
||||||
|
fail "$NODE1 did not rejoin with quorum"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DRBD_ROLE_AFTER=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown")
|
||||||
|
if echo "$DRBD_ROLE_AFTER" | grep -q "Secondary"; then
|
||||||
|
pass "$NODE1 is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)"
|
||||||
|
else
|
||||||
|
fail "unexpected DRBD role on $NODE1 after rejoin: $DRBD_ROLE_AFTER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════════════════"
|
||||||
|
echo " Results: ${PASS} PASS, ${FAIL} FAIL"
|
||||||
|
echo "════════════════════════════════════════════════════"
|
||||||
|
for r in "${RESULTS[@]}"; do echo " $r"; done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$FAIL" -eq 0 ]]; then
|
||||||
|
echo "ALL PASS — cluster is production-ready."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "SOME TESTS FAILED — investigate before deploying."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# cluster-enable-stonith.sh — enable STONITH fence agent after the fence SSH
|
||||||
|
# key is deployed to both nodes and authorised on the Proxmox host.
|
||||||
|
#
|
||||||
|
# Run from ha-server-1 as root AFTER:
|
||||||
|
# - /etc/pacemaker/fence_pve_ssh exists on both nodes (chmod +x)
|
||||||
|
# (copy from scripts/ha/fence-pve-ssh.py)
|
||||||
|
# - /etc/fence-pve-ssh-key (SSH private key) exists on both nodes
|
||||||
|
# - The corresponding public key is in authorized_keys on PVE_HOST
|
||||||
|
# - VMID_NODE1 / VMID_NODE2 filled in below
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Configuration ─────────────────────────────────────────────────────────
|
||||||
|
NODE1="ha-server-1"
|
||||||
|
NODE2="ha-server-2"
|
||||||
|
VMID_NODE1="" # FILL IN: Proxmox VMID for ha-server-1
|
||||||
|
VMID_NODE2="" # FILL IN: Proxmox VMID for ha-server-2
|
||||||
|
PVE_HOST="pve1.sweet.home"
|
||||||
|
PVE_USER="wayne"
|
||||||
|
FENCE_KEY="/etc/fence-pve-ssh-key"
|
||||||
|
FENCE_SCRIPT="/etc/pacemaker/fence_pve_ssh"
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log() { echo "[stonith-setup] $*"; }
|
||||||
|
die() { echo "[stonith-setup] ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
[[ $(id -u) -eq 0 ]] || die "must run as root"
|
||||||
|
[[ -n "$VMID_NODE1" ]] || die "VMID_NODE1 not set — edit this script"
|
||||||
|
[[ -n "$VMID_NODE2" ]] || die "VMID_NODE2 not set — edit this script"
|
||||||
|
[[ -f "$FENCE_KEY" ]] || die "fence key not found at $FENCE_KEY"
|
||||||
|
[[ -f "$FENCE_SCRIPT" ]] || die "fence script not found at $FENCE_SCRIPT"
|
||||||
|
|
||||||
|
log "Verifying fence agent can reach ${PVE_HOST}..."
|
||||||
|
ssh -i "$FENCE_KEY" -o BatchMode=yes -o ConnectTimeout=10 \
|
||||||
|
-o StrictHostKeyChecking=no "${PVE_USER}@${PVE_HOST}" \
|
||||||
|
"sudo /usr/sbin/qm list" &>/dev/null \
|
||||||
|
|| die "Cannot SSH to ${PVE_USER}@${PVE_HOST} — check authorized_keys and sudo"
|
||||||
|
log "Fence agent SSH connectivity confirmed"
|
||||||
|
|
||||||
|
log "Creating Pacemaker STONITH resources..."
|
||||||
|
cibadmin --create --scope resources --xml-text "
|
||||||
|
<primitive id=\"stonith-${NODE1}\" class=\"stonith\" type=\"external/fence_pve_ssh\">
|
||||||
|
<instance_attributes id=\"stonith-${NODE1}-attrs\">
|
||||||
|
<nvpair id=\"stonith-${NODE1}-plug\" name=\"plug\" value=\"${NODE1}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE1}-pve-host\" name=\"pve_host\" value=\"${PVE_HOST}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE1}-pve-user\" name=\"pve_user\" value=\"${PVE_USER}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE1}-key-file\" name=\"key_file\" value=\"${FENCE_KEY}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE1}-vmid1\" name=\"vmid_node1\" value=\"${VMID_NODE1}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE1}-vmid2\" name=\"vmid_node2\" value=\"${VMID_NODE2}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE1}-host-list\" name=\"pcmk_host_list\" value=\"${NODE1}\"/>
|
||||||
|
</instance_attributes>
|
||||||
|
<operations>
|
||||||
|
<op id=\"stonith-${NODE1}-monitor\" name=\"monitor\" interval=\"30s\" timeout=\"30s\"/>
|
||||||
|
</operations>
|
||||||
|
</primitive>
|
||||||
|
" 2>/dev/null || true
|
||||||
|
|
||||||
|
cibadmin --create --scope resources --xml-text "
|
||||||
|
<primitive id=\"stonith-${NODE2}\" class=\"stonith\" type=\"external/fence_pve_ssh\">
|
||||||
|
<instance_attributes id=\"stonith-${NODE2}-attrs\">
|
||||||
|
<nvpair id=\"stonith-${NODE2}-plug\" name=\"plug\" value=\"${NODE2}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE2}-pve-host\" name=\"pve_host\" value=\"${PVE_HOST}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE2}-pve-user\" name=\"pve_user\" value=\"${PVE_USER}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE2}-key-file\" name=\"key_file\" value=\"${FENCE_KEY}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE2}-vmid1\" name=\"vmid_node1\" value=\"${VMID_NODE1}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE2}-vmid2\" name=\"vmid_node2\" value=\"${VMID_NODE2}\"/>
|
||||||
|
<nvpair id=\"stonith-${NODE2}-host-list\" name=\"pcmk_host_list\" value=\"${NODE2}\"/>
|
||||||
|
</instance_attributes>
|
||||||
|
<operations>
|
||||||
|
<op id=\"stonith-${NODE2}-monitor\" name=\"monitor\" interval=\"30s\" timeout=\"30s\"/>
|
||||||
|
</operations>
|
||||||
|
</primitive>
|
||||||
|
" 2>/dev/null || true
|
||||||
|
|
||||||
|
log "Enabling STONITH and restoring quorum policy..."
|
||||||
|
crm_attribute -t crm_config -n stonith-enabled -v true
|
||||||
|
crm_attribute -t crm_config -n no-quorum-policy -v stop
|
||||||
|
|
||||||
|
log "DRBD fencing mode must also be updated to resource-only (already the"
|
||||||
|
log "default in cluster-config.nix; confirm with: cat /etc/drbd.d/ha-data.conf)"
|
||||||
|
|
||||||
|
log "Testing fence agent..."
|
||||||
|
stonith_admin --list-devices && log "Fence devices listed successfully." \
|
||||||
|
|| warn "stonith_admin --list-devices failed — check config"
|
||||||
|
|
||||||
|
log "STONITH enabled. Cluster is now fully HA."
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# cluster-init.sh — one-time HA cluster initialisation script
|
||||||
|
#
|
||||||
|
# Run ONCE from ha-server-1 as root AFTER both VMs are booted and have SSH
|
||||||
|
# access. It:
|
||||||
|
# 1. Generates and distributes the corosync authkey
|
||||||
|
# 2. Waits for corosync quorum and pacemaker
|
||||||
|
# 3. Initialises DRBD metadata, promotes node1 to primary
|
||||||
|
# 4. Creates XFS on /dev/drbd0 and mounts it
|
||||||
|
# 5. Creates the directory tree and iSCSI LUN backing file
|
||||||
|
# 6. Configures LIO iSCSI target (file-backed LUN)
|
||||||
|
# 7. Configures Pacemaker resources: DRBD → XFS → iSCSI → NFS → VIP
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - Both VMs booted with the ha-server config (nixos-rebuild done)
|
||||||
|
# - SSH key access from node1 to root@NODE2_IP
|
||||||
|
# - VMID_NODE1 / VMID_NODE2 filled in below (needed for STONITH setup;
|
||||||
|
# cluster starts without STONITH, which you enable separately via
|
||||||
|
# scripts/ha/cluster-enable-stonith.sh)
|
||||||
|
# - Run as root on ha-server-1
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Configuration ─────────────────────────────────────────────────────────
|
||||||
|
# These must match variables.nix haServer* values and the Proxmox VMID
|
||||||
|
# assignments. Update before running.
|
||||||
|
NODE1="ha-server-1"
|
||||||
|
NODE2="ha-server-2"
|
||||||
|
NODE1_IP="192.168.2.200" # vars.haServer1Ip
|
||||||
|
NODE2_IP="192.168.2.201" # vars.haServer2Ip
|
||||||
|
VIP="192.168.2.202" # vars.haServerVip
|
||||||
|
XFS_MOUNT="/srv/ha-data" # vars.haStorageRoot
|
||||||
|
ISCSI_IQN="iqn.2026-01.home.sweet:ha-storage" # vars.haIscsiIqn
|
||||||
|
ISCSI_LUN_FILE="${XFS_MOUNT}/iscsi-lun.img"
|
||||||
|
ISCSI_LUN_SIZE="10G"
|
||||||
|
DRBD_DEVICE="/dev/drbd0"
|
||||||
|
VMID_NODE1="" # FILL IN: Proxmox VMID for ha-server-1
|
||||||
|
VMID_NODE2="" # FILL IN: Proxmox VMID for ha-server-2
|
||||||
|
PVE_HOST="pve1.sweet.home"
|
||||||
|
PVE_USER="wayne"
|
||||||
|
|
||||||
|
# NFS dataset subdirectories to create under XFS_MOUNT.
|
||||||
|
# Must mirror vars.nfsShares subpath values in variables.nix.
|
||||||
|
NFS_SUBDIRS=(
|
||||||
|
"docker/config"
|
||||||
|
"docker/volumes"
|
||||||
|
"docker/databases"
|
||||||
|
"docker/nextcloud-data"
|
||||||
|
"raspi/volumes"
|
||||||
|
"proxmox/iso"
|
||||||
|
"proxmox/lxc"
|
||||||
|
"pxe-boot/images"
|
||||||
|
)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
log() { echo "[cluster-init] $*"; }
|
||||||
|
die() { echo "[cluster-init] ERROR: $*" >&2; exit 1; }
|
||||||
|
warn() { echo "[cluster-init] WARNING: $*" >&2; }
|
||||||
|
|
||||||
|
[[ $(id -u) -eq 0 ]] || die "must run as root"
|
||||||
|
[[ "$(hostname)" == "$NODE1" ]] || die "must run on $NODE1"
|
||||||
|
|
||||||
|
# ── 0. Corosync authkey ───────────────────────────────────────────────────
|
||||||
|
AUTHKEY="/etc/corosync/authkey"
|
||||||
|
mkdir -p /etc/corosync
|
||||||
|
if [[ ! -f "$AUTHKEY" ]]; then
|
||||||
|
log "Generating corosync authkey..."
|
||||||
|
corosync-keygen -k "$AUTHKEY"
|
||||||
|
chmod 0400 "$AUTHKEY"
|
||||||
|
fi
|
||||||
|
log "Distributing authkey to $NODE2..."
|
||||||
|
ssh "root@${NODE2_IP}" "mkdir -p /etc/corosync"
|
||||||
|
scp -q "$AUTHKEY" "root@${NODE2_IP}:${AUTHKEY}"
|
||||||
|
ssh "root@${NODE2_IP}" "chmod 0400 '${AUTHKEY}'"
|
||||||
|
|
||||||
|
log "Restarting corosync on both nodes..."
|
||||||
|
systemctl restart corosync
|
||||||
|
ssh "root@${NODE2_IP}" "systemctl restart corosync"
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# ── 1. Corosync quorum ────────────────────────────────────────────────────
|
||||||
|
log "Waiting for corosync quorum..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'; then
|
||||||
|
log "Quorum established"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
[[ $i -eq 30 ]] && die "corosync quorum not established after 60 s"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Waiting for pacemaker..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if crm_mon -1 &>/dev/null; then
|
||||||
|
log "Pacemaker running"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
[[ $i -eq 30 ]] && die "pacemaker not running after 60 s"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 2. DRBD initialisation ────────────────────────────────────────────────
|
||||||
|
log "Initialising DRBD metadata on $NODE1..."
|
||||||
|
if ! drbdadm dstate ha-data 2>/dev/null | grep -q "UpToDate\|Inconsistent\|Diskless"; then
|
||||||
|
drbdadm create-md ha-data --force
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Initialising DRBD metadata on $NODE2..."
|
||||||
|
ssh "root@${NODE2_IP}" "
|
||||||
|
if ! drbdadm dstate ha-data 2>/dev/null | grep -q 'UpToDate\|Inconsistent\|Diskless'; then
|
||||||
|
drbdadm create-md ha-data --force
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Bringing up DRBD on both nodes..."
|
||||||
|
drbdadm up ha-data 2>/dev/null || true
|
||||||
|
ssh "root@${NODE2_IP}" "drbdadm up ha-data 2>/dev/null" || true
|
||||||
|
|
||||||
|
log "Forcing $NODE1 to DRBD Primary for initial sync..."
|
||||||
|
drbdadm primary ha-data --force
|
||||||
|
|
||||||
|
log "Waiting for DRBD to finish initial sync (this may take several minutes)..."
|
||||||
|
for i in $(seq 1 300); do
|
||||||
|
state=$(drbdadm dstate ha-data 2>/dev/null || echo "unknown")
|
||||||
|
if echo "$state" | grep -q "UpToDate/UpToDate"; then
|
||||||
|
log "DRBD sync complete: $state"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
[[ $i -eq 300 ]] && warn "DRBD not UpToDate after 300 s — continuing anyway (check drbdadm status)"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 3. XFS filesystem ─────────────────────────────────────────────────────
|
||||||
|
log "Creating XFS on ${DRBD_DEVICE}..."
|
||||||
|
if ! xfs_info "${DRBD_DEVICE}" &>/dev/null; then
|
||||||
|
mkfs.xfs -f "${DRBD_DEVICE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Mounting ${DRBD_DEVICE} at ${XFS_MOUNT}..."
|
||||||
|
mkdir -p "${XFS_MOUNT}"
|
||||||
|
mount "${DRBD_DEVICE}" "${XFS_MOUNT}"
|
||||||
|
|
||||||
|
# ── 4. NFS dataset directories ────────────────────────────────────────────
|
||||||
|
log "Creating NFS dataset directories..."
|
||||||
|
for subdir in "${NFS_SUBDIRS[@]}"; do
|
||||||
|
mkdir -p "${XFS_MOUNT}/${subdir}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 5. iSCSI LUN backing file ─────────────────────────────────────────────
|
||||||
|
log "Creating iSCSI LUN backing file ${ISCSI_LUN_FILE} (${ISCSI_LUN_SIZE})..."
|
||||||
|
if [[ ! -f "${ISCSI_LUN_FILE}" ]]; then
|
||||||
|
fallocate -l "${ISCSI_LUN_SIZE}" "${ISCSI_LUN_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6. LIO iSCSI target ───────────────────────────────────────────────────
|
||||||
|
log "Configuring LIO iSCSI target via targetcli..."
|
||||||
|
targetcli <<EOF
|
||||||
|
/backstores/fileio create name=ha-lun0 file_or_dev=${ISCSI_LUN_FILE} size=0 write_back=false
|
||||||
|
/iscsi create ${ISCSI_IQN}
|
||||||
|
/iscsi/${ISCSI_IQN}/tpg1/luns create /backstores/fileio/ha-lun0
|
||||||
|
/iscsi/${ISCSI_IQN}/tpg1/portals create ${VIP}
|
||||||
|
/iscsi/${ISCSI_IQN}/tpg1 set attribute authentication=0
|
||||||
|
/iscsi/${ISCSI_IQN}/tpg1 set attribute demo_mode_write_protect=0
|
||||||
|
saveconfig /etc/target/saveconfig.json
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "Distributing iSCSI saveconfig to $NODE2..."
|
||||||
|
scp -q /etc/target/saveconfig.json "root@${NODE2_IP}:/etc/target/saveconfig.json"
|
||||||
|
|
||||||
|
log "Unmounting ${XFS_MOUNT} — Pacemaker manages it..."
|
||||||
|
umount "${XFS_MOUNT}"
|
||||||
|
|
||||||
|
log "Demoting DRBD to Secondary — Pacemaker manages primary role..."
|
||||||
|
drbdadm secondary ha-data
|
||||||
|
|
||||||
|
# ── 7. Pacemaker resources ────────────────────────────────────────────────
|
||||||
|
log "Configuring Pacemaker cluster properties..."
|
||||||
|
crm_attribute -t crm_config -n stonith-enabled -v false
|
||||||
|
crm_attribute -t crm_config -n no-quorum-policy -v ignore
|
||||||
|
|
||||||
|
log "Creating DRBD promotable clone resource..."
|
||||||
|
cibadmin --replace --scope resources --xml-text "
|
||||||
|
<resources>
|
||||||
|
<clone id=\"ms-drbd0\" globally-unique=\"false\">
|
||||||
|
<meta_attributes id=\"ms-drbd0-meta\">
|
||||||
|
<nvpair id=\"ms-drbd0-promotable\" name=\"promotable\" value=\"true\"/>
|
||||||
|
<nvpair id=\"ms-drbd0-master-max\" name=\"master-max\" value=\"1\"/>
|
||||||
|
<nvpair id=\"ms-drbd0-master-node-max\" name=\"master-node-max\" value=\"1\"/>
|
||||||
|
<nvpair id=\"ms-drbd0-clone-max\" name=\"clone-max\" value=\"2\"/>
|
||||||
|
<nvpair id=\"ms-drbd0-clone-node-max\" name=\"clone-node-max\" value=\"1\"/>
|
||||||
|
<nvpair id=\"ms-drbd0-notify\" name=\"notify\" value=\"true\"/>
|
||||||
|
<nvpair id=\"ms-drbd0-interleave\" name=\"interleave\" value=\"true\"/>
|
||||||
|
</meta_attributes>
|
||||||
|
<primitive id=\"drbd0\" class=\"ocf\" type=\"drbd\" provider=\"linbit\">
|
||||||
|
<instance_attributes id=\"drbd0-attrs\">
|
||||||
|
<nvpair id=\"drbd0-resource\" name=\"drbd_resource\" value=\"ha-data\"/>
|
||||||
|
</instance_attributes>
|
||||||
|
<operations>
|
||||||
|
<op id=\"drbd0-start\" name=\"start\" interval=\"0\" timeout=\"240s\"/>
|
||||||
|
<op id=\"drbd0-stop\" name=\"stop\" interval=\"0\" timeout=\"120s\"/>
|
||||||
|
<op id=\"drbd0-promote\" name=\"promote\" interval=\"0\" timeout=\"90s\"/>
|
||||||
|
<op id=\"drbd0-demote\" name=\"demote\" interval=\"0\" timeout=\"90s\"/>
|
||||||
|
<op id=\"drbd0-monitor-master\" name=\"monitor\" interval=\"20s\" timeout=\"20s\" role=\"Promoted\"/>
|
||||||
|
<op id=\"drbd0-monitor-slave\" name=\"monitor\" interval=\"30s\" timeout=\"20s\" role=\"Unpromoted\"/>
|
||||||
|
</operations>
|
||||||
|
</primitive>
|
||||||
|
</clone>
|
||||||
|
<group id=\"ha-group\">
|
||||||
|
<primitive id=\"xfs-data\" class=\"ocf\" type=\"Filesystem\" provider=\"heartbeat\">
|
||||||
|
<instance_attributes id=\"xfs-data-attrs\">
|
||||||
|
<nvpair id=\"xfs-data-device\" name=\"device\" value=\"${DRBD_DEVICE}\"/>
|
||||||
|
<nvpair id=\"xfs-data-directory\" name=\"directory\" value=\"${XFS_MOUNT}\"/>
|
||||||
|
<nvpair id=\"xfs-data-fstype\" name=\"fstype\" value=\"xfs\"/>
|
||||||
|
<nvpair id=\"xfs-data-options\" name=\"options\" value=\"defaults\"/>
|
||||||
|
<nvpair id=\"xfs-data-force_unmount\" name=\"force_unmount\" value=\"false\"/>
|
||||||
|
</instance_attributes>
|
||||||
|
<operations>
|
||||||
|
<op id=\"xfs-data-start\" name=\"start\" interval=\"0\" timeout=\"60s\"/>
|
||||||
|
<op id=\"xfs-data-stop\" name=\"stop\" interval=\"0\" timeout=\"60s\"/>
|
||||||
|
<op id=\"xfs-data-monitor\" name=\"monitor\" interval=\"20s\" timeout=\"40s\"/>
|
||||||
|
</operations>
|
||||||
|
</primitive>
|
||||||
|
<primitive id=\"iscsi-target\" class=\"systemd\" type=\"targetctl\">
|
||||||
|
<operations>
|
||||||
|
<op id=\"iscsi-start\" name=\"start\" interval=\"0\" timeout=\"60s\"/>
|
||||||
|
<op id=\"iscsi-stop\" name=\"stop\" interval=\"0\" timeout=\"60s\"/>
|
||||||
|
<op id=\"iscsi-monitor\" name=\"monitor\" interval=\"20s\" timeout=\"40s\"/>
|
||||||
|
</operations>
|
||||||
|
</primitive>
|
||||||
|
<primitive id=\"nfs-server\" class=\"systemd\" type=\"nfs-server\">
|
||||||
|
<operations>
|
||||||
|
<op id=\"nfs-start\" name=\"start\" interval=\"0\" timeout=\"60s\"/>
|
||||||
|
<op id=\"nfs-stop\" name=\"stop\" interval=\"0\" timeout=\"60s\"/>
|
||||||
|
<op id=\"nfs-monitor\" name=\"monitor\" interval=\"30s\" timeout=\"40s\"/>
|
||||||
|
</operations>
|
||||||
|
</primitive>
|
||||||
|
<primitive id=\"vip\" class=\"ocf\" type=\"IPaddr2\" provider=\"heartbeat\">
|
||||||
|
<instance_attributes id=\"vip-attrs\">
|
||||||
|
<nvpair id=\"vip-ip\" name=\"ip\" value=\"${VIP}\"/>
|
||||||
|
<nvpair id=\"vip-cidr\" name=\"cidr_netmask\" value=\"24\"/>
|
||||||
|
</instance_attributes>
|
||||||
|
<operations>
|
||||||
|
<op id=\"vip-start\" name=\"start\" interval=\"0\" timeout=\"20s\"/>
|
||||||
|
<op id=\"vip-stop\" name=\"stop\" interval=\"0\" timeout=\"20s\"/>
|
||||||
|
<op id=\"vip-monitor\" name=\"monitor\" interval=\"10s\" timeout=\"20s\"/>
|
||||||
|
</operations>
|
||||||
|
</primitive>
|
||||||
|
</group>
|
||||||
|
</resources>
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Adding ordering and colocation constraints..."
|
||||||
|
cibadmin --create --scope constraints --xml-text "
|
||||||
|
<constraints>
|
||||||
|
<rsc_order id=\"order-drbd-group\" first=\"ms-drbd0\" first-action=\"promote\" then=\"ha-group\" then-action=\"start\"/>
|
||||||
|
<rsc_colocation id=\"coloc-group-with-drbd\" rsc=\"ha-group\" with-rsc=\"ms-drbd0\" with-rsc-role=\"Master\" score=\"INFINITY\"/>
|
||||||
|
</constraints>
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Waiting for resources to start..."
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if crm_resource -r vip --locate 2>/dev/null | grep -q "running on"; then
|
||||||
|
log "VIP is up: $(crm_resource -r vip --locate)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
[[ $i -eq 60 ]] && { warn "VIP not up after 120 s — check: crm_mon -1"; break; }
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "═══════════════════════════════════════════════════════════════"
|
||||||
|
log " HA cluster initialised."
|
||||||
|
log ""
|
||||||
|
log " crm_mon -1 — cluster status"
|
||||||
|
log " iscsiadm -m discovery -t st -p ${VIP} — verify iSCSI target"
|
||||||
|
log " showmount -e ${VIP} — verify NFS exports"
|
||||||
|
log ""
|
||||||
|
log " To enable STONITH (after deploying fence SSH key):"
|
||||||
|
log " 1. Fill in VMID_NODE1 / VMID_NODE2 in cluster-enable-stonith.sh"
|
||||||
|
log " 2. Copy scripts/ha/fence-pve-ssh.py to /etc/pacemaker/fence_pve_ssh"
|
||||||
|
log " on both nodes (chmod +x)"
|
||||||
|
log " 3. Generate and distribute the fence SSH key"
|
||||||
|
log " (see docs or cluster-enable-stonith.sh header)"
|
||||||
|
log " 4. bash scripts/ha/cluster-enable-stonith.sh"
|
||||||
|
log "═══════════════════════════════════════════════════════════════"
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fence_pve_ssh - Proxmox VE SSH fence agent for Pacemaker.
|
||||||
|
|
||||||
|
Uses SSH to reach the Proxmox host and run 'qm stop/start <vmid>'.
|
||||||
|
Deploy to /etc/pacemaker/fence_pve_ssh on both HA nodes (chmod +x).
|
||||||
|
|
||||||
|
Configuration (as pacemaker stonith resource attributes):
|
||||||
|
pve_host Proxmox host to SSH to (default: pve1.sweet.home)
|
||||||
|
pve_user SSH user (default: wayne)
|
||||||
|
key_file SSH private key path (default: /etc/fence-pve-ssh-key)
|
||||||
|
vmid_node1 VMID for ha-server-1
|
||||||
|
vmid_node2 VMID for ha-server-2
|
||||||
|
plug Node name to act on (set by pacemaker: ha-server-1 or ha-server-2)
|
||||||
|
action Action: off|on|reboot|status|list|metadata
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
METADATA = """<?xml version="1.0" ?>
|
||||||
|
<resource-agent name="fence_pve_ssh" shortdesc="Proxmox VE SSH fence agent (test lab)">
|
||||||
|
<longdesc>Fences a VM on a Proxmox VE host by SSHing to the PVE host and
|
||||||
|
running qm stop/start. For test use only.</longdesc>
|
||||||
|
<vendor-url>https://proxmox.com</vendor-url>
|
||||||
|
<parameters>
|
||||||
|
<parameter name="action" required="1" unique="0">
|
||||||
|
<getopt mixed="-a, --action=[action]"/>
|
||||||
|
<content type="string" default="reboot"/>
|
||||||
|
<shortdesc lang="en">Fencing action: off|on|reboot|status|list</shortdesc>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="plug" required="0" unique="0">
|
||||||
|
<getopt mixed="-n, --plug=[nodename]"/>
|
||||||
|
<content type="string"/>
|
||||||
|
<shortdesc lang="en">Cluster node name to fence</shortdesc>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="pve_host" required="0" unique="0">
|
||||||
|
<getopt mixed="--pve-host=[host]"/>
|
||||||
|
<content type="string" default="pve1.sweet.home"/>
|
||||||
|
<shortdesc lang="en">Proxmox VE host to SSH to</shortdesc>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="pve_user" required="0" unique="0">
|
||||||
|
<getopt mixed="--pve-user=[user]"/>
|
||||||
|
<content type="string" default="wayne"/>
|
||||||
|
<shortdesc lang="en">SSH user on the Proxmox host</shortdesc>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="key_file" required="0" unique="0">
|
||||||
|
<getopt mixed="--key-file=[path]"/>
|
||||||
|
<content type="string" default="/etc/fence-pve-ssh-key"/>
|
||||||
|
<shortdesc lang="en">SSH private key file path</shortdesc>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="vmid_node1" required="1" unique="0">
|
||||||
|
<getopt mixed="--vmid-node1=[vmid]"/>
|
||||||
|
<content type="string"/>
|
||||||
|
<shortdesc lang="en">VMID for ha-test-node1</shortdesc>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="vmid_node2" required="1" unique="0">
|
||||||
|
<getopt mixed="--vmid-node2=[vmid]"/>
|
||||||
|
<content type="string"/>
|
||||||
|
<shortdesc lang="en">VMID for ha-test-node2</shortdesc>
|
||||||
|
</parameter>
|
||||||
|
</parameters>
|
||||||
|
<actions>
|
||||||
|
<action name="off" timeout="60s"/>
|
||||||
|
<action name="on" timeout="60s"/>
|
||||||
|
<action name="reboot" timeout="60s"/>
|
||||||
|
<action name="status" timeout="30s"/>
|
||||||
|
<action name="list" timeout="10s"/>
|
||||||
|
<action name="metadata" timeout="5s"/>
|
||||||
|
</actions>
|
||||||
|
</resource-agent>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
p = argparse.ArgumentParser(add_help=False)
|
||||||
|
p.add_argument("-a", "--action", default="reboot")
|
||||||
|
p.add_argument("-n", "--plug")
|
||||||
|
p.add_argument("--pve-host", default="pve1.sweet.home")
|
||||||
|
p.add_argument("--pve-user", default="wayne")
|
||||||
|
p.add_argument("--key-file", default="/etc/fence-pve-ssh-key")
|
||||||
|
p.add_argument("--vmid-node1")
|
||||||
|
p.add_argument("--vmid-node2")
|
||||||
|
# Allow remaining unknown args (pacemaker may pass extra ones)
|
||||||
|
return p.parse_known_args()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def ssh(pve_host, pve_user, key_file, cmd):
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"ssh",
|
||||||
|
"-i", key_file,
|
||||||
|
"-o", "StrictHostKeyChecking=no",
|
||||||
|
"-o", "BatchMode=yes",
|
||||||
|
"-o", "ConnectTimeout=10",
|
||||||
|
f"{pve_user}@{pve_host}",
|
||||||
|
cmd,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_vmid(args):
|
||||||
|
node = args.plug
|
||||||
|
if not node:
|
||||||
|
print("ERROR: --plug not specified", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
mapping = {
|
||||||
|
"ha-server-1": args.vmid_node1,
|
||||||
|
"ha-server-2": args.vmid_node2,
|
||||||
|
}
|
||||||
|
vmid = mapping.get(node)
|
||||||
|
if not vmid:
|
||||||
|
print(f"ERROR: unknown node '{node}'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return vmid
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
action = args.action.lower()
|
||||||
|
|
||||||
|
if action == "metadata":
|
||||||
|
print(METADATA)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if action == "list":
|
||||||
|
if args.vmid_node1:
|
||||||
|
print("ha-server-1")
|
||||||
|
if args.vmid_node2:
|
||||||
|
print("ha-server-2")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
vmid = get_vmid(args)
|
||||||
|
|
||||||
|
if not os.path.exists(args.key_file):
|
||||||
|
print(f"ERROR: SSH key not found at {args.key_file}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if action in ("off", "reboot"):
|
||||||
|
print(f"Stopping VM {vmid} ({args.plug}) on {args.pve_host}...")
|
||||||
|
r = ssh(args.pve_host, args.pve_user, args.key_file,
|
||||||
|
f"sudo /usr/sbin/qm stop {vmid}")
|
||||||
|
if r.returncode != 0:
|
||||||
|
print(f"ERROR stopping VM: {r.stderr}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"VM {vmid} stopped")
|
||||||
|
|
||||||
|
if action in ("on", "reboot"):
|
||||||
|
print(f"Starting VM {vmid} ({args.plug}) on {args.pve_host}...")
|
||||||
|
r = ssh(args.pve_host, args.pve_user, args.key_file,
|
||||||
|
f"sudo /usr/sbin/qm start {vmid}")
|
||||||
|
if r.returncode != 0:
|
||||||
|
print(f"ERROR starting VM: {r.stderr}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"VM {vmid} started")
|
||||||
|
|
||||||
|
if action == "status":
|
||||||
|
r = ssh(args.pve_host, args.pve_user, args.key_file,
|
||||||
|
f"sudo /usr/sbin/qm status {vmid}")
|
||||||
|
if r.returncode != 0:
|
||||||
|
print(f"ERROR querying VM status: {r.stderr}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
# qm status returns "status: running" or "status: stopped"
|
||||||
|
status_line = r.stdout.strip()
|
||||||
|
print(status_line)
|
||||||
|
if "stopped" in status_line:
|
||||||
|
sys.exit(2) # pacemaker interprets exit 2 as "off"
|
||||||
|
sys.exit(0) # running = exit 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+206
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env nix-shell
|
||||||
|
#!nix-shell -i bash -p jq disko nixos-install-tools zfs
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# The only genuinely external tools this script calls directly: `jq`
|
||||||
|
# (parsing the `nix eval` host list), `disko`/`nixos-install` (the
|
||||||
|
# install itself), and `zpool` (exporting a ZFS root pool before reboot,
|
||||||
|
# see the comment above that call below). Everything disko shells out to
|
||||||
|
# internally (parted/sgdisk/mkfs.*/zfs/...) is self-contained -- disko's
|
||||||
|
# own generated scripts hardcode absolute Nix store paths for those, they
|
||||||
|
# don't rely on this script's PATH at all (confirmed by inspecting a
|
||||||
|
# generated system.build.formatScript). The built installer image
|
||||||
|
# (modules/installer/common.nix, plus the upstream
|
||||||
|
# installation-cd-minimal.nix it imports via iso.nix) already has all
|
||||||
|
# four in environment.systemPackages, so this nix-shell wrapper is a
|
||||||
|
# fast no-op there; it's what makes the script also work standalone
|
||||||
|
# (e.g. run directly from a checkout on a stock ISO), where they aren't
|
||||||
|
# guaranteed.
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# shellcheck source=../env.sh
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/env.sh"
|
||||||
|
|
||||||
|
export FLAKE_BASE_URL="git+https://${LAN_DOMAIN}/beatzaplenty/nixos.git"
|
||||||
|
|
||||||
|
echo "Fetching available NixOS hosts from flake..."
|
||||||
|
# Two categories deliberately excluded from the menu:
|
||||||
|
# lxc-* — these build a config.system.build.tarball meant for
|
||||||
|
# `pct restore` on Proxmox directly, not an install.
|
||||||
|
# Running nixos-install against one here would
|
||||||
|
# bind-mount / onto /mnt and then refuse to touch the
|
||||||
|
# filesystem it's currently running on — see
|
||||||
|
# docs/auto-installer.md.
|
||||||
|
# installer — this *is* the installer image's own flake target,
|
||||||
|
# not a deployable host; "installing" it means
|
||||||
|
# nixos-install-ing a copy of the installer into
|
||||||
|
# itself.
|
||||||
|
mapfile -t options < <(
|
||||||
|
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
|
||||||
|
"${FLAKE_BASE_URL}#nixosConfigurations" \
|
||||||
|
--apply builtins.attrNames \
|
||||||
|
| jq -r '.[]
|
||||||
|
| select(startswith("lxc-") | not)
|
||||||
|
| select(. != "installer")'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ ${#options[@]} -eq 0 ]]; then
|
||||||
|
echo "ERROR: No NixOS hosts found in ${FLAKE_BASE_URL}#nixosConfigurations" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Note: lxc-* targets aren't installed this way — build them with"
|
||||||
|
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
|
||||||
|
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
|
||||||
|
|
||||||
|
echo "Choose the flake profile to install:"
|
||||||
|
select choice in "${options[@]}"; do
|
||||||
|
if [[ -n "$choice" ]]; then
|
||||||
|
echo "You selected: $choice"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Invalid selection. Try again."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Starting install with flake: ${FLAKE_BASE_URL}#${choice}"
|
||||||
|
|
||||||
|
# Optional: confirm before proceeding
|
||||||
|
read -rp "Proceed with installation? (y/N): " confirm
|
||||||
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A nix-cache host is *the* substituter/remote-builder for every other
|
||||||
|
# host once installed (its own config explicitly excludes itself from
|
||||||
|
# using either — see buildType != "nix-cache" in the nixos flake.nix).
|
||||||
|
# Installing one shouldn't depend on a nix-cache substituter either,
|
||||||
|
# for the same reason — plus in practice "nix-cache" only resolves over
|
||||||
|
# Tailscale, which a fresh installer environment was never connected to
|
||||||
|
# anyway, so it's dead weight even for non-nix-cache installs until
|
||||||
|
# that's sorted out. Override it away here specifically for nix-cache
|
||||||
|
# targets to keep install-time behaviour consistent with run-time.
|
||||||
|
nix_extra_opts=()
|
||||||
|
if [[ "${choice}" == *-nix-cache ]]; then
|
||||||
|
echo "Installing a nix-cache host — skipping the nix-cache substituter."
|
||||||
|
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Every host reachable through this menu has a Disko config (lxc-*
|
||||||
|
# is filtered out above, and is the only category that doesn't —
|
||||||
|
# see docs/auto-installer.md), so this can run unconditionally: no
|
||||||
|
# need to probe the flake first and branch on whether Disko applies.
|
||||||
|
disko --mode destroy,format,mount \
|
||||||
|
--flake "${FLAKE_BASE_URL}#${choice}" "${nix_extra_opts[@]}" --yes-wipe-all-disks
|
||||||
|
|
||||||
|
# sops-nix derives this host's decryption key from its own SSH host key
|
||||||
|
# at *activation* time, which runs before systemd would otherwise
|
||||||
|
# generate one on first boot. Without pre-seeding it here, secrets
|
||||||
|
# (including the login password) fail to decrypt on first boot.
|
||||||
|
# Generate the key with scripts/secrets/prepare-host-key.sh first.
|
||||||
|
#
|
||||||
|
# Two places a key can come from, checked in order:
|
||||||
|
# /etc/host-keys — baked into this image at build time (see
|
||||||
|
# modules/installer/host-keys.nix; only present
|
||||||
|
# if built with NIXOS_HOST_KEYS_DIR set)
|
||||||
|
# /root/host-keys — scp'd in manually after boot (older fallback,
|
||||||
|
# still supported for images built without keys)
|
||||||
|
mkdir -p /root/host-keys
|
||||||
|
if [[ -f "/etc/host-keys/${choice}_ssh_host_ed25519_key" ]]; then
|
||||||
|
echo "Found baked-in SSH host key for ${choice}, installing to target..."
|
||||||
|
install -D -m 0600 "/etc/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -D -m 0644 "/etc/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
elif [[ -f "/root/host-keys/${choice}_ssh_host_ed25519_key" ]]; then
|
||||||
|
echo "Found pre-seeded SSH host key for ${choice}, installing to target..."
|
||||||
|
install -D -m 0600 "/root/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -D -m 0644 "/root/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
else
|
||||||
|
# Third place a key can come from: an arbitrary path the operator
|
||||||
|
# points at interactively (e.g. a USB stick, a mount from another
|
||||||
|
# machine) -- only offered when there's an actual human at the other
|
||||||
|
# end of stdin to ask, never in a non-interactive run.
|
||||||
|
key_copied=0
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
echo "No SSH host key found for ${choice} (checked /etc/host-keys and /root/host-keys)."
|
||||||
|
read -rp "Path to a directory containing ${choice}_ssh_host_ed25519_key(.pub) (blank to skip): " key_src_dir
|
||||||
|
if [[ -n "$key_src_dir" && -f "${key_src_dir}/${choice}_ssh_host_ed25519_key" && -f "${key_src_dir}/${choice}_ssh_host_ed25519_key.pub" ]]; then
|
||||||
|
cp "${key_src_dir}/${choice}_ssh_host_ed25519_key" "${key_src_dir}/${choice}_ssh_host_ed25519_key.pub" /root/host-keys/
|
||||||
|
key_copied=1
|
||||||
|
elif [[ -n "$key_src_dir" ]]; then
|
||||||
|
echo "WARNING: ${choice}_ssh_host_ed25519_key(.pub) not found in ${key_src_dir}."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$key_copied" -eq 1 ]]; then
|
||||||
|
echo "Copied SSH host key for ${choice} from ${key_src_dir}, installing to target..."
|
||||||
|
install -D -m 0600 "/root/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -D -m 0644 "/root/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
else
|
||||||
|
echo "WARNING: no SSH host key found for ${choice} (checked /etc/host-keys and /root/host-keys)"
|
||||||
|
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
|
||||||
|
echo "Run scripts/secrets/prepare-host-key.sh for host ${choice} on your admin workstation first,"
|
||||||
|
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, scp the result to"
|
||||||
|
echo "/root/host-keys/ on this machine, or point at it when prompted above."
|
||||||
|
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
|
||||||
|
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Aborted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /mnt/install-tmp
|
||||||
|
export TMPDIR=/mnt/install-tmp
|
||||||
|
|
||||||
|
nixos-install \
|
||||||
|
--flake "${FLAKE_BASE_URL}#${choice}" \
|
||||||
|
"${nix_extra_opts[@]}" \
|
||||||
|
--no-root-password
|
||||||
|
|
||||||
|
|
||||||
|
rm -rf /mnt/install-tmp
|
||||||
|
# Redundant copy of the host's private key — the real one is now at
|
||||||
|
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
|
||||||
|
# up on its own since it was written imperatively, not declaratively.
|
||||||
|
rm -rf /root/host-keys
|
||||||
|
|
||||||
|
# disko's --mode ...,mount left any ZFS root pool imported (that's what
|
||||||
|
# let nixos-install write into /mnt). If we reboot with it still
|
||||||
|
# imported, it isn't just "not exported" -- it's stamped with *this*
|
||||||
|
# live installer environment's hostid, which almost never matches the
|
||||||
|
# target's own networking.hostId (see hosts/*/host.nix; the installer
|
||||||
|
# itself sets none). modules/services/zfs/enable-service.nix and
|
||||||
|
# modules/common/configuration.nix both set boot.zfs.forceImportRoot =
|
||||||
|
# false deliberately (the safe option per that setting's own docs), so
|
||||||
|
# the freshly-installed system's first real boot sees a pool "in use by
|
||||||
|
# another system" and refuses to import it without -f -- which is what
|
||||||
|
# makes boot stall waiting on the ZFS import. Exporting here (a no-op
|
||||||
|
# if the chosen host has no ZFS root, e.g. proxmox-*/linode-*) clears
|
||||||
|
# that in-use state so the next import, from any hostid, succeeds.
|
||||||
|
#
|
||||||
|
# Anything still mounted under /mnt -- nixos-install's own leftover
|
||||||
|
# chroot bind mounts for running the target's activation script
|
||||||
|
# (/mnt/dev, /mnt/proc, /mnt/sys, /mnt/run), and disko's own /mnt/boot
|
||||||
|
# ESP mount (modules/disko/baremetal.nix) -- blocks ZFS from unmounting
|
||||||
|
# its root dataset at /mnt, the same way any nested mount blocks
|
||||||
|
# unmounting its parent. Confirmed live: zpool export failed with
|
||||||
|
# "cannot unmount '/mnt': pool or dataset busy" even after handling the
|
||||||
|
# chroot mounts alone, because /mnt/boot was still mounted too. Because
|
||||||
|
# of this script's `set -e`, that killed the script before it ever
|
||||||
|
# reached reboot, silently defeating the whole point of exporting first.
|
||||||
|
# Unmounting everything under /mnt up front (recursively, so nested
|
||||||
|
# mounts like /mnt/dev/pts come along for free) sidesteps needing to
|
||||||
|
# enumerate every mount disko/nixos-install might leave behind.
|
||||||
|
if mountpoint -q /mnt; then
|
||||||
|
umount -R /mnt
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$(zpool list -H -o name 2>/dev/null)" ]]; then
|
||||||
|
echo "Exporting ZFS pool(s) before reboot..."
|
||||||
|
zpool export -a
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 10
|
||||||
|
reboot
|
||||||
Executable
+258
@@ -0,0 +1,258 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Add a NixOS host to the FreeIPA domain and produce a sops-encrypted keytab
|
||||||
|
# at secrets/<hostname>.keytab, ready for modules/ipa/client.nix.
|
||||||
|
#
|
||||||
|
# One command replaces three error-prone manual steps:
|
||||||
|
# 1. ipa host-add on the domain controller
|
||||||
|
# 2. ipa-getkeytab on the domain controller + SCP back
|
||||||
|
# 3. sops encrypt in-place (must be at secrets/<hostname>.keytab for
|
||||||
|
# the creation rule to match -- the common mistake that breaks sops)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/ipa/create-nixos-ipa-host-account.sh [options] <hostname>
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# <hostname> Short hostname, e.g. "tailscale-router". The FQDN is
|
||||||
|
# derived as <hostname>.<HOME_DOMAIN>.
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --ip <addr> Register this IP with the IPA host record (optional).
|
||||||
|
# --dc <host> SSH to this host for ipa-getkeytab.
|
||||||
|
# Default: $IPA_SERVER (from env.sh / environment).
|
||||||
|
# --dc-user <u> SSH user on the domain controller. Default: wayne.
|
||||||
|
# --dry-run Print what would be done without making any changes.
|
||||||
|
# -h, --help Show this message.
|
||||||
|
#
|
||||||
|
# Prereqs:
|
||||||
|
# 1. Run from the repo root (so .sops.yaml and secrets/ are found).
|
||||||
|
# 2. SSH access to the domain controller as --dc-user (default: wayne)
|
||||||
|
# with passwordless sudo (or sudo cached). IPA commands and kinit run
|
||||||
|
# as root via sudo so the Kerberos ticket is in root's cache where all
|
||||||
|
# ipa tools expect it. If there's no valid ticket, the script runs
|
||||||
|
# `sudo kinit admin` interactively — you'll be prompted for the IPA
|
||||||
|
# admin password once. The password never touches this script.
|
||||||
|
# 3. The host's age key(s) must already be in .sops.yaml. Run
|
||||||
|
# scripts/secrets/sync-host-keys.sh <flake-target> first so the host
|
||||||
|
# can decrypt its own keytab on boot. This script adds the .sops.yaml
|
||||||
|
# creation rule for secrets/<hostname>.keytab automatically, but the
|
||||||
|
# host age key anchor (&lxc-<hostname> etc.) must already exist.
|
||||||
|
# 4. sops in PATH, or Nix available to run it via `nix run`.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
# shellcheck source=../env.sh
|
||||||
|
source "${SCRIPT_DIR}/../env.sh"
|
||||||
|
|
||||||
|
# --- Argument parsing ---
|
||||||
|
|
||||||
|
DC_HOST="${IPA_SERVER}"
|
||||||
|
DC_USER="wayne"
|
||||||
|
IP_ADDR=""
|
||||||
|
DRY_RUN=false
|
||||||
|
HOSTNAME=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '/^# Usage:/,/^[^#]/{ /^#/{ s/^# \?//; p } }' "$0"
|
||||||
|
exit "${1:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--ip) IP_ADDR="$2"; shift 2 ;;
|
||||||
|
--dc) DC_HOST="$2"; shift 2 ;;
|
||||||
|
--dc-user) DC_USER="$2"; shift 2 ;;
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
-h|--help) usage 0 ;;
|
||||||
|
-*) echo "Unknown flag: $1" >&2; usage 1 ;;
|
||||||
|
*)
|
||||||
|
if [[ -n "${HOSTNAME}" ]]; then echo "Unexpected argument: $1" >&2; usage 1; fi
|
||||||
|
HOSTNAME="$1"; shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "${HOSTNAME}" ]]; then
|
||||||
|
echo "Error: hostname required." >&2
|
||||||
|
usage 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FQDN="${HOSTNAME}.${HOME_DOMAIN}"
|
||||||
|
REALM="${HOME_DOMAIN^^}" # uppercase: SWEET.HOME
|
||||||
|
KEYTAB_SECRET="${REPO_ROOT}/secrets/${HOSTNAME}.keytab"
|
||||||
|
# Temp path on the domain controller — use a name that won't collide.
|
||||||
|
DC_TMP="/tmp/nixos-keytab-${HOSTNAME}-$$.keytab"
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
log() { echo "==> $*"; }
|
||||||
|
logn() { echo " $*"; }
|
||||||
|
|
||||||
|
run() {
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo "[dry-run] $*"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dc_run() {
|
||||||
|
# Run a command string on the domain controller via SSH.
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo "[dry-run] ssh ${DC_USER}@${DC_HOST} $*"
|
||||||
|
else
|
||||||
|
ssh "${DC_USER}@${DC_HOST}" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Locate sops ---
|
||||||
|
|
||||||
|
if command -v sops &>/dev/null; then
|
||||||
|
SOPS_CMD=(sops)
|
||||||
|
else
|
||||||
|
log "sops not in PATH — will use 'nix run github:NixOS/nixpkgs/nixos-25.11#sops'"
|
||||||
|
SOPS_CMD=(nix run "github:NixOS/nixpkgs/nixos-25.11#sops" --)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Preflight checks ---
|
||||||
|
|
||||||
|
cd "${REPO_ROOT}"
|
||||||
|
|
||||||
|
[[ -f .sops.yaml ]] || { echo "Error: .sops.yaml not found — run from repo root." >&2; exit 1; }
|
||||||
|
[[ -d secrets ]] || { echo "Error: secrets/ not found — run from repo root." >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- Step 1: Ensure .sops.yaml has a creation rule for this keytab ---
|
||||||
|
#
|
||||||
|
# sops matches creation rules against the PATH of the file being encrypted,
|
||||||
|
# not the output path. To match secrets/<hostname>.keytab, the file must
|
||||||
|
# already be at that path when sops -e -i is called. The creation rule must
|
||||||
|
# also exist at that point or sops will refuse with "no matching creation
|
||||||
|
# rules found."
|
||||||
|
|
||||||
|
log "Checking .sops.yaml for creation rule: secrets/${HOSTNAME}.keytab"
|
||||||
|
|
||||||
|
RULE_EXISTS=false
|
||||||
|
# Match "path_regex: secrets/<hostname>...keytab" — using .*keytab rather
|
||||||
|
# than \.keytab because the file stores the regex verbatim (\.keytab = two
|
||||||
|
# chars: backslash + dot), which a BRE \. (= escaped literal dot) won't span.
|
||||||
|
if grep -q "path_regex: secrets/${HOSTNAME}.*keytab" .sops.yaml 2>/dev/null; then
|
||||||
|
RULE_EXISTS=true
|
||||||
|
logn "Rule already exists — skipping addition."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $RULE_EXISTS; then
|
||||||
|
# Collect which platform-variant age anchors exist in .sops.yaml for this
|
||||||
|
# hostname. The keytab is platform-agnostic (same FQDN regardless of
|
||||||
|
# whether lxc/proxmox/linode variant is deployed), so all platform anchors
|
||||||
|
# that have been registered get added as recipients.
|
||||||
|
RECIPIENTS=("*admin")
|
||||||
|
for platform in lxc proxmox linode; do
|
||||||
|
anchor="${platform}-${HOSTNAME}"
|
||||||
|
if grep -q "^ - &${anchor} " .sops.yaml; then
|
||||||
|
RECIPIENTS+=("*${anchor}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Build the indented recipient list for the YAML block.
|
||||||
|
RECIPIENT_YAML=""
|
||||||
|
for r in "${RECIPIENTS[@]}"; do
|
||||||
|
RECIPIENT_YAML+=" - ${r}"$'\n'
|
||||||
|
done
|
||||||
|
RECIPIENT_YAML="${RECIPIENT_YAML%$'\n'}" # strip trailing newline
|
||||||
|
|
||||||
|
NEW_RULE="
|
||||||
|
# Host keytab for ${HOSTNAME} FreeIPA enrollment (binary sops file).
|
||||||
|
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
|
||||||
|
- path_regex: secrets/${HOSTNAME}\\.keytab\$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
${RECIPIENT_YAML}"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo "[dry-run] Would append to .sops.yaml:"
|
||||||
|
echo "${NEW_RULE}"
|
||||||
|
else
|
||||||
|
logn "Adding creation rule (recipients: ${RECIPIENTS[*]})"
|
||||||
|
printf '%s\n' "${NEW_RULE}" >> .sops.yaml
|
||||||
|
logn "Added."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Step 2: Add IPA host account (idempotent) ---
|
||||||
|
|
||||||
|
log "Adding FreeIPA host account: ${FQDN}"
|
||||||
|
|
||||||
|
# Ensure there's a valid admin Kerberos ticket on the DC.
|
||||||
|
# ipa host-add and ipa-getkeytab both need one. All IPA commands run via
|
||||||
|
# sudo so the ticket must be in root's cache — check and refresh as root.
|
||||||
|
# ssh -t allocates a PTY so kinit (and sudo if needed) can prompt normally;
|
||||||
|
# no password ever touches this script or the shell history.
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
if ! ssh "${DC_USER}@${DC_HOST}" "sudo klist -s" &>/dev/null; then
|
||||||
|
log "No valid Kerberos ticket on ${DC_HOST} — running sudo kinit admin"
|
||||||
|
ssh -t "${DC_USER}@${DC_HOST}" "sudo kinit admin"
|
||||||
|
if ! ssh "${DC_USER}@${DC_HOST}" "sudo klist -s" &>/dev/null; then
|
||||||
|
echo "Error: kinit admin failed or produced no valid ticket." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
logn "Kerberos ticket on ${DC_HOST} is valid."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
IP_FLAG=""
|
||||||
|
[[ -n "${IP_ADDR}" ]] && IP_FLAG="--ip-address=${IP_ADDR}"
|
||||||
|
|
||||||
|
# --force: create the host record even if DNS doesn't resolve it yet.
|
||||||
|
# Pipe through grep to suppress the "already exists" warning without
|
||||||
|
# hiding real errors (ipa exits 1 for real errors, 0 for already-exists).
|
||||||
|
HOST_ADD_CMD="sudo ipa host-add '${FQDN}' ${IP_FLAG} --force 2>&1 | \
|
||||||
|
tee /dev/stderr | grep -q 'already exists' && echo '(host already registered)' || true"
|
||||||
|
dc_run "bash -c \"${HOST_ADD_CMD}\""
|
||||||
|
|
||||||
|
# --- Step 3: Fetch the keytab from the domain controller ---
|
||||||
|
|
||||||
|
log "Fetching keytab for host/${FQDN}"
|
||||||
|
|
||||||
|
dc_run "sudo ipa-getkeytab -s '${DC_HOST}' -p 'host/${FQDN}' -k '${DC_TMP}'"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo "[dry-run] Would stream ${DC_USER}@${DC_HOST}:${DC_TMP} → secrets/${HOSTNAME}.keytab"
|
||||||
|
else
|
||||||
|
logn "Streaming keytab from ${DC_HOST}:${DC_TMP} → secrets/${HOSTNAME}.keytab"
|
||||||
|
# scp can't read a root-owned temp file as wayne; pipe through sudo cat instead.
|
||||||
|
ssh "${DC_USER}@${DC_HOST}" "sudo cat '${DC_TMP}'" > "${KEYTAB_SECRET}"
|
||||||
|
|
||||||
|
logn "Removing temp file on ${DC_HOST}"
|
||||||
|
dc_run "sudo rm -f '${DC_TMP}'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Step 4: Encrypt in-place ---
|
||||||
|
#
|
||||||
|
# The file must already be at secrets/<hostname>.keytab (done above) so
|
||||||
|
# sops matches the creation rule by path. Using -i (in-place) rather than
|
||||||
|
# stdout redirect keeps the path intact through the encrypt call.
|
||||||
|
|
||||||
|
log "Encrypting secrets/${HOSTNAME}.keytab in-place with sops"
|
||||||
|
run "${SOPS_CMD[@]}" -e --input-type binary -i "${KEYTAB_SECRET}"
|
||||||
|
|
||||||
|
# --- Done ---
|
||||||
|
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
echo ""
|
||||||
|
echo "Done. secrets/${HOSTNAME}.keytab is sops-encrypted and ready."
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Verify: grep '\"data\": \"ENC' secrets/${HOSTNAME}.keytab"
|
||||||
|
echo " 2. Stage and commit:"
|
||||||
|
echo " git add secrets/${HOSTNAME}.keytab .sops.yaml"
|
||||||
|
echo " git commit -m 'secrets: add IPA keytab for ${HOSTNAME}'"
|
||||||
|
echo " 3. Add the module to hosts/${HOSTNAME}/host.nix:"
|
||||||
|
echo " (import ../../modules/ipa/client.nix {"
|
||||||
|
echo " keytabSopsFile = ../../secrets/${HOSTNAME}.keytab;"
|
||||||
|
echo " caCertFile = ../../certs/ipa-ca.crt;"
|
||||||
|
echo " })"
|
||||||
|
echo " 4. Deploy: nixos-rebuild switch (or create-proxmox-resource.sh)"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Clan vars helpers: manage SSH host keys stored as clan vars (sops-encrypted
|
||||||
|
# binary files under vars/per-machine/<target>/openssh/) instead of the
|
||||||
|
# gitignored host-keys/ directory.
|
||||||
|
#
|
||||||
|
# Layout (per clan's convention):
|
||||||
|
# vars/per-machine/<target>/openssh/ssh_host_ed25519_key/secret -- sops binary (admin-encrypted)
|
||||||
|
# vars/per-machine/<target>/openssh/ssh_host_ed25519_key.pub/value -- plaintext SSH pubkey
|
||||||
|
#
|
||||||
|
# Sourced by create-proxmox-resource.sh and sync-host-keys.sh.
|
||||||
|
# Depends on sops-age.sh and ssh-host-keys.sh being sourced first (for
|
||||||
|
# sops_yaml_admin_pubkey, ssh_pubkey_to_age, and NIX_OPTS).
|
||||||
|
|
||||||
|
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
|
||||||
|
declare -a NIX_OPTS=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
# clan_ssh_key_exists <target> <repo_root>
|
||||||
|
# Returns 0 if clan vars hold a SSH host key for <target>, 1 otherwise.
|
||||||
|
clan_ssh_key_exists() {
|
||||||
|
local target="$1" repo_root="$2"
|
||||||
|
[[ -f "${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key/secret" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# clan_ssh_pubkey_path <target> <repo_root>
|
||||||
|
# Prints the path to the plaintext SSH public key value file.
|
||||||
|
clan_ssh_pubkey_path() {
|
||||||
|
local target="$1" repo_root="$2"
|
||||||
|
echo "${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key.pub/value"
|
||||||
|
}
|
||||||
|
|
||||||
|
# clan_decrypt_ssh_key <target> <repo_root> <dest_dir>
|
||||||
|
# Decrypts the sops-encrypted SSH host private key for <target> into <dest_dir>,
|
||||||
|
# naming it <target>_ssh_host_ed25519_key (to match NIXOS_HOST_KEYS_DIR
|
||||||
|
# conventions that lxc.nix and the disko build already expect). Also copies
|
||||||
|
# the plaintext public key. The caller is responsible for protecting and
|
||||||
|
# cleaning up <dest_dir>.
|
||||||
|
clan_decrypt_ssh_key() {
|
||||||
|
local target="$1" repo_root="$2" dest_dir="$3"
|
||||||
|
local secret="${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key/secret"
|
||||||
|
local pubval="${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key.pub/value"
|
||||||
|
local dest_priv="${dest_dir}/${target}_ssh_host_ed25519_key"
|
||||||
|
local dest_pub="${dest_dir}/${target}_ssh_host_ed25519_key.pub"
|
||||||
|
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||||
|
"sops -d --output-type binary '${secret}'" > "$dest_priv"
|
||||||
|
chmod 0600 "$dest_priv"
|
||||||
|
cp "$pubval" "$dest_pub"
|
||||||
|
}
|
||||||
|
|
||||||
|
# clan_generate_ssh_key <target> <repo_root>
|
||||||
|
# Generates a new SSH host key pair and stores it in clan vars format:
|
||||||
|
# - private key: sops binary-encrypted for the admin age key
|
||||||
|
# - public key: plaintext value file
|
||||||
|
# Idempotent: if the secret already exists, prints a note and returns 0.
|
||||||
|
# Requires sops_yaml_admin_pubkey (from sops-age.sh) to be available.
|
||||||
|
clan_generate_ssh_key() {
|
||||||
|
local target="$1" repo_root="$2"
|
||||||
|
local var_base="${repo_root}/vars/per-machine/${target}/openssh"
|
||||||
|
local secret_dir="${var_base}/ssh_host_ed25519_key"
|
||||||
|
local pubval_dir="${var_base}/ssh_host_ed25519_key.pub"
|
||||||
|
|
||||||
|
if [[ -f "${secret_dir}/secret" ]]; then
|
||||||
|
echo "Clan SSH host key for ${target} already exists -- skipping generation."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve admin age public key from .sops.yaml
|
||||||
|
local admin_pubkey
|
||||||
|
admin_pubkey="$(sops_yaml_admin_pubkey "${repo_root}/.sops.yaml")"
|
||||||
|
if [[ -z "$admin_pubkey" ]]; then
|
||||||
|
echo "ERROR: Could not find &admin age key in ${repo_root}/.sops.yaml" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate the SSH key pair in a secure temp directory
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
local priv_tmp="${tmpdir}/ssh_host_ed25519_key"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2064
|
||||||
|
trap "rm -rf '${tmpdir}'" RETURN
|
||||||
|
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
|
||||||
|
"ssh-keygen -t ed25519 -N '' -C '${target}' -f '${priv_tmp}'" >/dev/null
|
||||||
|
|
||||||
|
# Create a minimal sops config that uses only the admin age key -- this
|
||||||
|
# prevents sops from merging in ALL recipients from .sops.yaml (which
|
||||||
|
# would unnecessarily encrypt for every host's key, not just admin).
|
||||||
|
local sops_cfg="${tmpdir}/sops-config.json"
|
||||||
|
printf '{"creation_rules":[{"key_groups":[{"age":["%s"]}]}]}\n' \
|
||||||
|
"$admin_pubkey" > "$sops_cfg"
|
||||||
|
|
||||||
|
# Encrypt the private key in sops binary format (admin-only recipient)
|
||||||
|
mkdir -p "$secret_dir" "$pubval_dir"
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||||
|
"sops -e --config '${sops_cfg}' --input-type binary '${priv_tmp}'" \
|
||||||
|
> "${secret_dir}/secret"
|
||||||
|
|
||||||
|
# Store the public key as a plaintext value file
|
||||||
|
cp "${priv_tmp}.pub" "${pubval_dir}/value"
|
||||||
|
|
||||||
|
echo "Generated and stored clan SSH host key for ${target}."
|
||||||
|
echo " Private key: ${secret_dir}/secret (sops binary, admin-key encrypted)"
|
||||||
|
echo " Public key: ${pubval_dir}/value"
|
||||||
|
}
|
||||||
@@ -6,27 +6,31 @@
|
|||||||
#
|
#
|
||||||
# This is the non-NixOS equivalent of modules/nix-cache/client.nix +
|
# This is the non-NixOS equivalent of modules/nix-cache/client.nix +
|
||||||
# modules/nix-cache/remote-builder-client.nix -- those two only apply to
|
# modules/nix-cache/remote-builder-client.nix -- those two only apply to
|
||||||
# hosts built from this flake. A plain Debian box with Nix installed
|
# hosts built from this flake. A plain Debian box with Nix installed has no
|
||||||
# (single- or multi-user install, nix-daemon running) has no NixOS module
|
# NixOS module system to pick that config up, so this edits nix.conf by hand.
|
||||||
# system to pick that config up, so this edits /etc/nix/nix.conf by hand
|
#
|
||||||
# instead. Run this ON the target Debian machine, as root.
|
# Two modes depending on who runs it:
|
||||||
|
#
|
||||||
|
# root (multi-user / daemon install):
|
||||||
|
# Writes /etc/nix/nix.conf, /etc/ssh/ssh_known_hosts, restarts nix-daemon.
|
||||||
|
# Requires /etc/nix/nix.conf to already exist (i.e. nix-daemon is set up).
|
||||||
|
# Run as: sudo ./configure-nix-cache-client.sh [options]
|
||||||
|
#
|
||||||
|
# non-root (single-user install):
|
||||||
|
# Writes ~/.config/nix/nix.conf, ~/.ssh/known_hosts. No daemon to restart.
|
||||||
|
# Run as: ./configure-nix-cache-client.sh [options]
|
||||||
#
|
#
|
||||||
# The values below mirror variables.nix / modules/nix-cache/client.nix in
|
# The values below mirror variables.nix / modules/nix-cache/client.nix in
|
||||||
# this repo -- update both if nix-cache is ever rebuilt with a new host
|
# this repo -- update both if nix-cache is ever rebuilt with a new host
|
||||||
# key or the cache signing key is rotated (see docs/nix-cache.md).
|
# key or the cache signing key is rotated (see docs/nix-cache.md).
|
||||||
#
|
#
|
||||||
# REMOTE_BUILDER_KEY defaults to this machine's own default root SSH
|
# REMOTE_BUILDER_KEY defaults to the running user's default SSH identity
|
||||||
# identity (matches modules/nix-cache/remote-builder-client.nix's
|
# (root: /root/.ssh/id_ed25519, other user: ~/.ssh/id_ed25519). That key
|
||||||
# convention for real NixOS clients: authenticate as nixremote with the
|
# must be listed in vars.remoteBuilderAuthorizedKeys in this repo and
|
||||||
# host's own default key, added individually to
|
# nix-cache rebuilt before remote building works.
|
||||||
# vars.remoteBuilderAuthorizedKeys, rather than a separately-named or
|
|
||||||
# shared keypair) -- generate one with
|
|
||||||
# `ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519` if this machine
|
|
||||||
# doesn't have one yet, then add its .pub to vars.remoteBuilderAuthorizedKeys
|
|
||||||
# and rebuild nix-cache.
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# sudo ./configure-nix-cache-client.sh [--dry-run] [--no-remote-builder] [--no-restart]
|
# ./configure-nix-cache-client.sh [--dry-run] [--no-remote-builder] [--no-restart]
|
||||||
#
|
#
|
||||||
# Env overrides (defaults match variables.nix):
|
# Env overrides (defaults match variables.nix):
|
||||||
# NIX_CACHE_HOST, NIX_CACHE_HOST_KEY, REMOTE_BUILDER_USER, REMOTE_BUILDER_KEY
|
# NIX_CACHE_HOST, NIX_CACHE_HOST_KEY, REMOTE_BUILDER_USER, REMOTE_BUILDER_KEY
|
||||||
@@ -34,19 +38,30 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
: "${NIX_CACHE_HOST:=nix-cache}"
|
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||||
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPeWgMsdaiz4axT/deFc1+0B5bN+GX/NOeW9bbQ0c/IT lxc-nix-cache}"
|
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuHUxGNH6ei3BZD+EfZs3l4X8uJNcjQiOsM/G4yo4O/ lxc-nix-cache}"
|
||||||
: "${REMOTE_BUILDER_USER:=nixremote}"
|
: "${REMOTE_BUILDER_USER:=nixremote}"
|
||||||
: "${REMOTE_BUILDER_KEY:=/root/.ssh/id_ed25519}"
|
|
||||||
|
|
||||||
CACHE_PUB_KEY="cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
|
CACHE_PUB_KEY="cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
|
||||||
FALLBACK_URL="https://cache.nixos.org/"
|
FALLBACK_URL="https://cache.nixos.org/"
|
||||||
FALLBACK_PUB_KEY="cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
FALLBACK_PUB_KEY="cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||||
|
|
||||||
NIX_CONF="/etc/nix/nix.conf"
|
|
||||||
KNOWN_HOSTS="/etc/ssh/ssh_known_hosts"
|
|
||||||
MARKER_BEGIN="# BEGIN nix-cache client config (configure-nix-cache-client.sh)"
|
MARKER_BEGIN="# BEGIN nix-cache client config (configure-nix-cache-client.sh)"
|
||||||
MARKER_END="# END nix-cache client config"
|
MARKER_END="# END nix-cache client config"
|
||||||
|
|
||||||
|
# Mode: root uses system-wide paths and restarts the daemon; non-root uses
|
||||||
|
# user-level paths and has no daemon to restart.
|
||||||
|
if [[ "$EUID" -eq 0 ]]; then
|
||||||
|
install_mode="multi"
|
||||||
|
NIX_CONF="/etc/nix/nix.conf"
|
||||||
|
KNOWN_HOSTS="/etc/ssh/ssh_known_hosts"
|
||||||
|
: "${REMOTE_BUILDER_KEY:=/root/.ssh/id_ed25519}"
|
||||||
|
else
|
||||||
|
install_mode="single"
|
||||||
|
NIX_CONF="${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf"
|
||||||
|
KNOWN_HOSTS="$HOME/.ssh/known_hosts"
|
||||||
|
: "${REMOTE_BUILDER_KEY:=$HOME/.ssh/id_ed25519}"
|
||||||
|
fi
|
||||||
|
|
||||||
dry_run=0
|
dry_run=0
|
||||||
with_remote_builder=1
|
with_remote_builder=1
|
||||||
restart_daemon=1
|
restart_daemon=1
|
||||||
@@ -57,7 +72,7 @@ for arg in "$@"; do
|
|||||||
--no-remote-builder) with_remote_builder=0 ;;
|
--no-remote-builder) with_remote_builder=0 ;;
|
||||||
--no-restart) restart_daemon=0 ;;
|
--no-restart) restart_daemon=0 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
sed -n '2,20p' "$0"
|
sed -n '2,37p' "$0"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -67,21 +82,22 @@ for arg in "$@"; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "$dry_run" -eq 0 && "$EUID" -ne 0 ]]; then
|
|
||||||
echo "ERROR: must run as root (writes $NIX_CONF and, unless --no-remote-builder, $KNOWN_HOSTS)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
echo "ERROR: no 'nix' binary on PATH -- install the Nix package manager first." >&2
|
echo "ERROR: no 'nix' binary on PATH -- install the Nix package manager first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$NIX_CONF" ]]; then
|
if [[ "$install_mode" == "multi" && ! -f "$NIX_CONF" ]]; then
|
||||||
echo "ERROR: $NIX_CONF not found -- expected an existing multi-user Nix install." >&2
|
echo "ERROR: $NIX_CONF not found -- expected an existing multi-user Nix install." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Single-user: create the config file if it doesn't exist yet.
|
||||||
|
if [[ "$install_mode" == "single" && "$dry_run" -eq 0 ]]; then
|
||||||
|
mkdir -p "$(dirname "$NIX_CONF")"
|
||||||
|
[[ -f "$NIX_CONF" ]] || touch "$NIX_CONF"
|
||||||
|
fi
|
||||||
|
|
||||||
builder_line=""
|
builder_line=""
|
||||||
if [[ "$with_remote_builder" -eq 1 ]]; then
|
if [[ "$with_remote_builder" -eq 1 ]]; then
|
||||||
if [[ -f "$REMOTE_BUILDER_KEY" ]]; then
|
if [[ -f "$REMOTE_BUILDER_KEY" ]]; then
|
||||||
@@ -117,7 +133,7 @@ fi
|
|||||||
block="${block}
|
block="${block}
|
||||||
$MARKER_END"
|
$MARKER_END"
|
||||||
|
|
||||||
echo "== nix.conf block to install =="
|
echo "== nix.conf block to install ($NIX_CONF) =="
|
||||||
echo "$block"
|
echo "$block"
|
||||||
echo "================================"
|
echo "================================"
|
||||||
|
|
||||||
@@ -159,7 +175,8 @@ if [[ "$with_remote_builder" -eq 1 ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$dry_run" -eq 0 && "$restart_daemon" -eq 1 ]]; then
|
# Only restart the daemon for multi-user installs -- single-user has no daemon.
|
||||||
|
if [[ "$dry_run" -eq 0 && "$restart_daemon" -eq 1 && "$install_mode" == "multi" ]]; then
|
||||||
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nix-daemon 2>/dev/null; then
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nix-daemon 2>/dev/null; then
|
||||||
systemctl restart nix-daemon
|
systemctl restart nix-daemon
|
||||||
echo "Restarted nix-daemon to pick up the new config."
|
echo "Restarted nix-daemon to pick up the new config."
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
# script -- there's no multi-gigabyte image to transfer afterward. The first
|
# script -- there's no multi-gigabyte image to transfer afterward. The first
|
||||||
# time a node doesn't have that repo path yet, it's bootstrapped: cloned from
|
# time a node doesn't have that repo path yet, it's bootstrapped: cloned from
|
||||||
# this checkout's own `origin` remote, then scripts/codex-setup.sh installs
|
# this checkout's own `origin` remote, then scripts/codex-setup.sh installs
|
||||||
# the build tooling (Nix, etc.). Every run after that just `git pull`s it and
|
# the build tooling (Nix, etc.). Every run after that just `git pull`s it.
|
||||||
# copies over the locally-managed host-keys/ (gitignored, so a git pull
|
# SSH host keys are stored as clan vars (vars/per-machine/<target>/openssh/,
|
||||||
# alone wouldn't carry it) before building.
|
# committed and sops-encrypted) -- the script decrypts them locally and
|
||||||
|
# copies only the two files for this target to the node's host-keys/ before
|
||||||
|
# building. A target with no clan var is an error (generate one first with
|
||||||
|
# scripts/secrets/sync-host-keys.sh <target>).
|
||||||
#
|
#
|
||||||
# --node (default: $PROXMOX_HOST, see scripts/env.sh) picks which of the two
|
# --node (default: $PROXMOX_HOST, see scripts/env.sh) picks which of the two
|
||||||
# LAN Proxmox nodes this runs against: production, pve1.sweet.home
|
# LAN Proxmox nodes this runs against: production, pve1.sweet.home
|
||||||
@@ -59,6 +62,10 @@ source "${repo_root}/scripts/env.sh"
|
|||||||
source "${repo_root}/scripts/lib/nix-eval.sh"
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
# shellcheck source=../lib/confirm.sh
|
# shellcheck source=../lib/confirm.sh
|
||||||
source "${repo_root}/scripts/lib/confirm.sh"
|
source "${repo_root}/scripts/lib/confirm.sh"
|
||||||
|
# shellcheck source=../lib/sops-age.sh
|
||||||
|
source "${repo_root}/scripts/lib/sops-age.sh"
|
||||||
|
# shellcheck source=../lib/clan-vars.sh
|
||||||
|
source "${repo_root}/scripts/lib/clan-vars.sh"
|
||||||
|
|
||||||
sync_keys="${repo_root}/scripts/secrets/sync-host-keys.sh"
|
sync_keys="${repo_root}/scripts/secrets/sync-host-keys.sh"
|
||||||
|
|
||||||
@@ -193,6 +200,16 @@ done
|
|||||||
|
|
||||||
ssh_target="${PROXMOX_SSH_USER}@${node}"
|
ssh_target="${PROXMOX_SSH_USER}@${node}"
|
||||||
|
|
||||||
|
# Proxmox tools (pvesh, qm, pct) require root access to the cluster IPC
|
||||||
|
# socket. When SSH-ing as a non-root user with sudo, prefix every remote
|
||||||
|
# Proxmox command with sudo.
|
||||||
|
sudo_prefix=""
|
||||||
|
sudo_display=""
|
||||||
|
if [[ "$PROXMOX_SSH_USER" != "root" ]]; then
|
||||||
|
sudo_prefix="sudo"
|
||||||
|
sudo_display="sudo "
|
||||||
|
fi
|
||||||
|
|
||||||
remote() {
|
remote() {
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] ssh ${ssh_target} -- $*"
|
echo "[dry-run] ssh ${ssh_target} -- $*"
|
||||||
@@ -214,10 +231,10 @@ cmd_modify() {
|
|||||||
|
|
||||||
echo "Looking up VMID ${vmid} on ${node}..."
|
echo "Looking up VMID ${vmid} on ${node}..."
|
||||||
local kind current_cores current_memory disk_key
|
local kind current_cores current_memory disk_key
|
||||||
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1; then
|
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1; then
|
||||||
kind="vm"
|
kind="vm"
|
||||||
disk_key="scsi0"
|
disk_key="scsi0"
|
||||||
elif ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
elif ssh "$ssh_target" "${sudo_prefix} pct status ${vmid}" >/dev/null 2>&1; then
|
||||||
kind="lxc"
|
kind="lxc"
|
||||||
disk_key="rootfs"
|
disk_key="rootfs"
|
||||||
else
|
else
|
||||||
@@ -225,8 +242,8 @@ cmd_modify() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local config_cmd="qm config ${vmid}"
|
local config_cmd="${sudo_prefix} qm config ${vmid}"
|
||||||
[[ "$kind" == "lxc" ]] && config_cmd="pct config ${vmid}"
|
[[ "$kind" == "lxc" ]] && config_cmd="${sudo_prefix} pct config ${vmid}"
|
||||||
local current_config
|
local current_config
|
||||||
current_config="$(ssh "$ssh_target" "$config_cmd")"
|
current_config="$(ssh "$ssh_target" "$config_cmd")"
|
||||||
current_cores="$(echo "$current_config" | grep -oP '^cores:\s*\K\S+' || echo '?')"
|
current_cores="$(echo "$current_config" | grep -oP '^cores:\s*\K\S+' || echo '?')"
|
||||||
@@ -250,9 +267,9 @@ cmd_modify() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local set_cmd="qm set"
|
local set_cmd="${sudo_prefix} qm set"
|
||||||
local resize_cmd="qm resize"
|
local resize_cmd="${sudo_prefix} qm resize"
|
||||||
[[ "$kind" == "lxc" ]] && set_cmd="pct set" && resize_cmd="pct resize"
|
[[ "$kind" == "lxc" ]] && set_cmd="${sudo_prefix} pct set" && resize_cmd="${sudo_prefix} pct resize"
|
||||||
|
|
||||||
if [[ -n "$cores" || -n "$memory" ]]; then
|
if [[ -n "$cores" || -n "$memory" ]]; then
|
||||||
local args=""
|
local args=""
|
||||||
@@ -285,6 +302,12 @@ platform_prefix="lxc"
|
|||||||
[[ -z "$cores" ]] && cores="$PROXMOX_DEFAULT_CORES"
|
[[ -z "$cores" ]] && cores="$PROXMOX_DEFAULT_CORES"
|
||||||
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
|
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
|
||||||
|
|
||||||
|
if [[ "$type" == "vm" && -n "$disk_size" ]]; then
|
||||||
|
echo "WARNING: --disk-size is LXC-only for create mode and is ignored for VMs." >&2
|
||||||
|
echo " VM disk size comes from proxmoxImageSize in variables.nix (currently ${disk_size}G was requested)." >&2
|
||||||
|
echo " To expand after creation, use: --modify --vmid <n> --grow-disk <GB>" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# --- discover / resolve the flake target from --host --------------------
|
# --- discover / resolve the flake target from --host --------------------
|
||||||
# Emits "<target>\t<hostName>" pairs for every ${platform_prefix}-* flake
|
# Emits "<target>\t<hostName>" pairs for every ${platform_prefix}-* flake
|
||||||
# target -- the one source both --list and the --host lookup below read
|
# target -- the one source both --list and the --host lookup below read
|
||||||
@@ -337,6 +360,14 @@ fi
|
|||||||
# feeds straight into the guest's real hostname) disagree with host.nix.
|
# feeds straight into the guest's real hostname) disagree with host.nix.
|
||||||
[[ -z "$name" ]] && name="$host"
|
[[ -z "$name" ]] && name="$host"
|
||||||
|
|
||||||
|
# For VM builds: the diskoImagesScript (run via QEMU on the node) writes the
|
||||||
|
# raw disk image as <hostname>.raw into the CWD it was called from (the remote
|
||||||
|
# repo dir), not to /var/lib/vz/import/ or anywhere else. Import directly from
|
||||||
|
# there -- no intermediate mv that can fail crossing filesystem boundaries or
|
||||||
|
# leave a stale file on error.
|
||||||
|
vm_built_raw=""
|
||||||
|
[[ "$type" == "vm" ]] && vm_built_raw="${remote_repo_dir}/${host}.raw"
|
||||||
|
|
||||||
# --- refuse to duplicate a host that's already live on the node ---------
|
# --- 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
|
# 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
|
# static list in this repo -- a file can't track whether a resource still
|
||||||
@@ -359,14 +390,15 @@ else
|
|||||||
echo
|
echo
|
||||||
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
|
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
|
||||||
ssh_check_status=0
|
ssh_check_status=0
|
||||||
existing="$(ssh "$ssh_target" bash -s -- "$host" <<'REMOTE_SCRIPT'
|
existing="$(ssh "$ssh_target" bash -s -- "$host" "$sudo_prefix" <<'REMOTE_SCRIPT'
|
||||||
target="$1"
|
target="$1"
|
||||||
for id in $(qm list 2>/dev/null | awk 'NR>1{print $1}'); do
|
sudo_pfx="$2"
|
||||||
n="$(qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
|
for id in $($sudo_pfx qm list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||||
|
n="$($sudo_pfx qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
|
||||||
[[ "$n" == "$target" ]] && echo "vm ${id} ${n}"
|
[[ "$n" == "$target" ]] && echo "vm ${id} ${n}"
|
||||||
done
|
done
|
||||||
for id in $(pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
for id in $($sudo_pfx pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||||
n="$(pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
n="$($sudo_pfx pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
||||||
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
|
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
|
||||||
done
|
done
|
||||||
exit 0
|
exit 0
|
||||||
@@ -453,12 +485,12 @@ REMOTE_SCRIPT
|
|||||||
if [[ "$kind" == "vm" ]]; then
|
if [[ "$kind" == "vm" ]]; then
|
||||||
# qm destroy has no --force to stop-then-destroy in one call (pct's
|
# qm destroy has no --force to stop-then-destroy in one call (pct's
|
||||||
# does) -- stop explicitly first if it's running.
|
# does) -- stop explicitly first if it's running.
|
||||||
if ssh "$ssh_target" "qm status ${id}" 2>/dev/null | grep -q running; then
|
if ssh "$ssh_target" "${sudo_prefix} qm status ${id}" 2>/dev/null | grep -q running; then
|
||||||
ssh "$ssh_target" "qm stop ${id}"
|
ssh "$ssh_target" "${sudo_prefix} qm stop ${id}"
|
||||||
fi
|
fi
|
||||||
ssh "$ssh_target" "qm destroy ${id} --purge 1"
|
ssh "$ssh_target" "${sudo_prefix} qm destroy ${id} --purge 1"
|
||||||
else
|
else
|
||||||
ssh "$ssh_target" "pct destroy ${id} --force 1 --purge 1"
|
ssh "$ssh_target" "${sudo_prefix} pct destroy ${id} --force 1 --purge 1"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -474,12 +506,37 @@ echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '
|
|||||||
nix_extra_opts
|
nix_extra_opts
|
||||||
|
|
||||||
# --- make sure this target has a registered host key --------------------
|
# --- make sure this target has a registered host key --------------------
|
||||||
|
# sync-host-keys.sh is idempotent and generates the key (via clan vars) if
|
||||||
|
# no key exists yet -- the old inline prepare-host-key.sh call is gone.
|
||||||
echo
|
echo
|
||||||
echo "==> Ensuring host key exists and is registered..."
|
echo "==> Ensuring host key exists and is registered..."
|
||||||
sync_args=("$flake_target")
|
sync_args=("$flake_target")
|
||||||
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
|
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
|
||||||
bash "$sync_keys" "${sync_args[@]}"
|
bash "$sync_keys" "${sync_args[@]}"
|
||||||
|
|
||||||
|
# If sync-host-keys.sh changed .sops.yaml, secrets/, or vars/per-machine/,
|
||||||
|
# those changes must be committed and pushed before the remote `git pull`
|
||||||
|
# below picks them up -- the PVE node builds from whatever HEAD is checked
|
||||||
|
# out there, not the local working tree. Uncommitted clan vars or sops
|
||||||
|
# recipients mean the image builds fine but the host cannot decrypt its
|
||||||
|
# secrets on first boot. Block until the operator confirms they've pushed.
|
||||||
|
if [[ "$dry_run" -eq 0 ]]; then
|
||||||
|
_dirty="$(git -C "$repo_root" status --porcelain -- .sops.yaml secrets/ vars/per-machine/ 2>/dev/null || true)"
|
||||||
|
if [[ -n "$_dirty" ]]; then
|
||||||
|
echo
|
||||||
|
echo "==> COMMIT + PUSH REQUIRED before the remote build can succeed:"
|
||||||
|
echo " Uncommitted changes in .sops.yaml, secrets/, or vars/per-machine/."
|
||||||
|
echo " The PVE node builds from the git-tracked flake, so these changes"
|
||||||
|
echo " must be committed and pushed first -- otherwise the image build will"
|
||||||
|
echo " succeed but the host cannot decrypt its secrets on first boot."
|
||||||
|
echo
|
||||||
|
git -C "$repo_root" status --short -- .sops.yaml secrets/ vars/per-machine/ || true
|
||||||
|
echo
|
||||||
|
read -rp " Commit and push those changes, then press Enter to continue (Ctrl-C to abort): "
|
||||||
|
fi
|
||||||
|
unset _dirty
|
||||||
|
fi
|
||||||
|
|
||||||
# --- VMID: pick one, and refuse to touch anything that already exists ---
|
# --- VMID: pick one, and refuse to touch anything that already exists ---
|
||||||
echo
|
echo
|
||||||
if [[ -z "$vmid" ]]; then
|
if [[ -z "$vmid" ]]; then
|
||||||
@@ -487,7 +544,7 @@ if [[ -z "$vmid" ]]; then
|
|||||||
vmid="<next-free-vmid>"
|
vmid="<next-free-vmid>"
|
||||||
echo "[dry-run] would ask ${node} for the next free VMID (pvesh get /cluster/nextid)"
|
echo "[dry-run] would ask ${node} for the next free VMID (pvesh get /cluster/nextid)"
|
||||||
else
|
else
|
||||||
vmid="$(ssh "$ssh_target" "pvesh get /cluster/nextid" | tr -d '[:space:]')"
|
vmid="$(ssh "$ssh_target" "${sudo_prefix} pvesh get /cluster/nextid" | tr -d '[:space:]')"
|
||||||
echo "Auto-assigned VMID: ${vmid}"
|
echo "Auto-assigned VMID: ${vmid}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -501,8 +558,8 @@ if [[ "$dry_run" -eq 0 ]]; then
|
|||||||
# both. Any success here means something is already using this ID --
|
# both. Any success here means something is already using this ID --
|
||||||
# refuse to go anywhere near it. (Reconfiguring an existing resource is
|
# refuse to go anywhere near it. (Reconfiguring an existing resource is
|
||||||
# --modify's job, not this one's.)
|
# --modify's job, not this one's.)
|
||||||
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1 \
|
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1 \
|
||||||
|| ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
|
|| ssh "$ssh_target" "${sudo_prefix} pct status ${vmid}" >/dev/null 2>&1; then
|
||||||
echo "ERROR: VMID ${vmid} already exists on ${node}. Refusing to touch an" >&2
|
echo "ERROR: VMID ${vmid} already exists on ${node}. Refusing to touch an" >&2
|
||||||
echo "existing resource here -- use --modify to reconfigure it, pick a" >&2
|
echo "existing resource here -- use --modify to reconfigure it, pick a" >&2
|
||||||
echo "different --vmid, or omit it to auto-assign." >&2
|
echo "different --vmid, or omit it to auto-assign." >&2
|
||||||
@@ -602,21 +659,35 @@ ensure_remote_repo() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- sync locally-managed host-keys/ to the node ---------------------------
|
# --- sync host key to the node ---------------------------------------------
|
||||||
# Gitignored (see .gitignore), so `git pull` above never carries it -- both
|
# SSH host keys are stored as clan vars (vars/per-machine/<target>/openssh/).
|
||||||
# build paths need it present as NIXOS_HOST_KEYS_DIR / --pre-format-files
|
# Decrypt locally and scp just the two files for this target to the node's
|
||||||
# input on the node itself now that the build runs there. scp (not rsync,
|
# host-keys/ directory, where the remote build script picks them up via
|
||||||
# not already a dependency anywhere else in this repo) mirrors how this
|
# NIXOS_HOST_KEYS_DIR (LXC) or --pre-format-files (VM). A target with no
|
||||||
# script already transfers the --image case below.
|
# clan var is an error -- generate one first with sync-host-keys.sh.
|
||||||
sync_remote_host_keys() {
|
sync_remote_host_keys() {
|
||||||
echo
|
echo
|
||||||
echo "==> Syncing host-keys/ to ${node}..."
|
echo "==> Syncing host key for ${flake_target} to ${node}..."
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] would copy ${repo_root}/host-keys/ to ${ssh_target}:${remote_repo_dir}/host-keys/"
|
echo "[dry-run] would decrypt clan SSH key for ${flake_target} and copy to ${ssh_target}:${remote_repo_dir}/host-keys/"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
if ! clan_ssh_key_exists "$flake_target" "$repo_root"; then
|
||||||
|
echo "ERROR: no clan SSH key found for ${flake_target}" >&2
|
||||||
|
echo " (expected: ${repo_root}/vars/per-machine/${flake_target}/openssh/ssh_host_ed25519_key/secret)" >&2
|
||||||
|
echo " Generate one first: bash scripts/secrets/sync-host-keys.sh ${flake_target}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
# shellcheck disable=SC2064
|
||||||
|
trap "rm -rf '${tmpdir}'" RETURN
|
||||||
|
echo " Decrypting clan SSH key for ${flake_target}..."
|
||||||
|
clan_decrypt_ssh_key "$flake_target" "$repo_root" "$tmpdir"
|
||||||
ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'"
|
ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'"
|
||||||
scp -pr "${repo_root}/host-keys/." "${ssh_target}:${remote_repo_dir}/host-keys/"
|
scp -p "${tmpdir}/${flake_target}_ssh_host_ed25519_key" \
|
||||||
|
"${tmpdir}/${flake_target}_ssh_host_ed25519_key.pub" \
|
||||||
|
"${ssh_target}:${remote_repo_dir}/host-keys/"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- build (or reuse an image already on the node) ------------------------
|
# --- build (or reuse an image already on the node) ------------------------
|
||||||
@@ -631,10 +702,14 @@ if [[ -n "$image" ]]; then
|
|||||||
elif [[ "$force_rebuild" -eq 1 ]]; then
|
elif [[ "$force_rebuild" -eq 1 ]]; then
|
||||||
echo "--force-rebuild: skipping the existing-image check on ${node}."
|
echo "--force-rebuild: skipping the existing-image check on ${node}."
|
||||||
else
|
else
|
||||||
echo "==> Checking whether ${node} already has ${remote_path}..."
|
# VMs: check for the raw image in the remote repo dir (where disko writes it).
|
||||||
|
# LXC: check for the tarball in iso_storage (where the LXC build stages it).
|
||||||
|
_check_path="$remote_path"
|
||||||
|
[[ "$type" == "vm" ]] && _check_path="$vm_built_raw"
|
||||||
|
echo "==> Checking whether ${node} already has ${_check_path}..."
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] would check: ssh ${ssh_target} -- test -f ${remote_path}"
|
echo "[dry-run] would check: ssh ${ssh_target} -- test -f ${_check_path}"
|
||||||
elif ssh "$ssh_target" "test -f '${remote_path}'" 2>/dev/null; then
|
elif ssh "$ssh_target" "test -f '${_check_path}'" 2>/dev/null; then
|
||||||
echo "Found it -- reusing, skipping build (use --force-rebuild to override)."
|
echo "Found it -- reusing, skipping build (use --force-rebuild to override)."
|
||||||
image_already_remote=1
|
image_already_remote=1
|
||||||
else
|
else
|
||||||
@@ -672,11 +747,11 @@ if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
|
|||||||
# hands the result to the remote shell to re-split, which would
|
# hands the result to the remote shell to re-split, which would
|
||||||
# otherwise scatter NIX_EXTRA_OPTS (itself several space-separated,
|
# otherwise scatter NIX_EXTRA_OPTS (itself several space-separated,
|
||||||
# %q-quoted tokens) across the wrong positional parameters below.
|
# %q-quoted tokens) across the wrong positional parameters below.
|
||||||
printf -v remote_cmd 'bash -s -- %q %q %q %q %q' \
|
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q' \
|
||||||
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS"
|
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
|
||||||
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"
|
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_pfx="$6"
|
||||||
declare -a NIX_OPTS=()
|
declare -a NIX_OPTS=()
|
||||||
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||||
cd "$repo_dir"
|
cd "$repo_dir"
|
||||||
@@ -685,6 +760,12 @@ cd "$repo_dir"
|
|||||||
# right after a successful install.
|
# right after a successful install.
|
||||||
. scripts/lib/nix-bootstrap.sh
|
. scripts/lib/nix-bootstrap.sh
|
||||||
ensure_nix_profile
|
ensure_nix_profile
|
||||||
|
if [[ ! -f "host-keys/${target}_ssh_host_ed25519_key" ]]; then
|
||||||
|
echo "ERROR: host-keys/${target}_ssh_host_ed25519_key not found in ${repo_dir}." >&2
|
||||||
|
echo "Generate the key locally (scripts/secrets/sync-host-keys.sh ${target})" >&2
|
||||||
|
echo "and ensure it was synced here before starting the build." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||||
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||||
".#nixosConfigurations.${target}.config.system.build.tarball" \
|
".#nixosConfigurations.${target}.config.system.build.tarball" \
|
||||||
@@ -694,64 +775,69 @@ if [[ -z "$built" ]]; then
|
|||||||
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
|
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p "$dest_dir"
|
$sudo_pfx mkdir -p "$dest_dir"
|
||||||
cp "$built" "${dest_dir}/${dest_name}"
|
$sudo_pfx cp "$built" "${dest_dir}/${dest_name}"
|
||||||
echo "Built and staged: ${dest_dir}/${dest_name}"
|
echo "Built and staged: ${dest_dir}/${dest_name}"
|
||||||
REMOTE_SCRIPT
|
REMOTE_SCRIPT
|
||||||
local_image="$remote_path"
|
local_image="$remote_path"
|
||||||
echo "Built on ${node}: ${remote_path}"
|
echo "Built on ${node}: ${remote_path}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# PROXMOX_SSH_USER defaults to root (env.sh), which needs no sudo and
|
|
||||||
# can't assume it's even installed on a minimal node -- only shell out
|
|
||||||
# through sudo when actually running as a non-root SSH user.
|
|
||||||
sudo_prefix="sudo"
|
|
||||||
sudo_display="sudo "
|
|
||||||
if [[ "$PROXMOX_SSH_USER" == "root" ]]; then
|
|
||||||
sudo_prefix=""
|
|
||||||
sudo_display=""
|
|
||||||
fi
|
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] would build on ${node}: nix build --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
echo "[dry-run] would build on ${node}: NIXOS_HOST_KEYS_DIR=\$(pwd)/host-keys nix build --impure --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||||
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
|
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
|
||||||
echo "[dry-run] would run: ${sudo_display}./result-${flake_target} \\"
|
echo "[dry-run] would run: ${sudo_display}./result-${flake_target} --build-memory 2048"
|
||||||
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \\"
|
echo "[dry-run] image will be at ${vm_built_raw} (imported from there; no mv to /var/lib/vz/import/)"
|
||||||
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \\"
|
|
||||||
echo "[dry-run] --build-memory 2048"
|
|
||||||
echo "[dry-run] would stage the result at ${remote_path}"
|
|
||||||
local_image="<built-image>.raw"
|
local_image="<built-image>.raw"
|
||||||
else
|
else
|
||||||
echo "==> Building Disko image for ${flake_target} on ${node}..."
|
echo "==> Building Disko image for ${flake_target} on ${node}..."
|
||||||
# See the LXC branch above for why this is one %q-quoted command
|
# See the LXC branch above for why this is one %q-quoted command
|
||||||
# string rather than separate ssh argv elements.
|
# string rather than separate ssh argv elements.
|
||||||
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q' \
|
# $7 = image_name (hostname, the diskoImagesScript's own output filename).
|
||||||
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
|
#
|
||||||
|
# NIXOS_HOST_KEYS_DIR + --impure: modules/platforms/proxmox.nix reads
|
||||||
|
# this env var at eval time (like lxc.nix) to embed the clan SSH host
|
||||||
|
# key in environment.etc. nixos-install's own activation then places the
|
||||||
|
# key on the target disk, so sshd-keygen finds it already present and
|
||||||
|
# skips generation. --pre-format-files put the key on the QEMU builder
|
||||||
|
# VM's rootfs (not the target disk), so sshd-keygen regenerated a fresh
|
||||||
|
# key -- one not registered in .sops.yaml -- and sops could never decrypt.
|
||||||
|
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q %q' \
|
||||||
|
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix" "$host"
|
||||||
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_prefix="$6"
|
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_pfx="$6"; image_name="$7"
|
||||||
declare -a NIX_OPTS=()
|
declare -a NIX_OPTS=()
|
||||||
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||||
cd "$repo_dir"
|
cd "$repo_dir"
|
||||||
. scripts/lib/nix-bootstrap.sh
|
. scripts/lib/nix-bootstrap.sh
|
||||||
ensure_nix_profile
|
ensure_nix_profile
|
||||||
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
if [[ ! -f "host-keys/${target}_ssh_host_ed25519_key" ]]; then
|
||||||
".#nixosConfigurations.${target}.config.system.build.diskoImagesScript" \
|
echo "ERROR: host-keys/${target}_ssh_host_ed25519_key not found in ${repo_dir}." >&2
|
||||||
--out-link "result-${target}"
|
echo "Generate the key locally (scripts/secrets/sync-host-keys.sh ${target})" >&2
|
||||||
$sudo_prefix "./result-${target}" \
|
echo "and ensure it was synced here before starting the build." >&2
|
||||||
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
|
||||||
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
|
||||||
--build-memory 2048
|
|
||||||
built="$(find . -maxdepth 1 -name '*.raw' -newer "result-${target}" | head -1)"
|
|
||||||
if [[ -z "$built" ]]; then
|
|
||||||
echo "ERROR: no .raw image found in ${repo_dir} after build." >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p "$dest_dir"
|
# Build diskoImagesScript with NIXOS_HOST_KEYS_DIR so proxmox.nix embeds the
|
||||||
mv "$built" "${dest_dir}/${dest_name}"
|
# clan SSH key in environment.etc (same as lxc.nix). This causes nixos-install
|
||||||
echo "Built and staged: ${dest_dir}/${dest_name}"
|
# to place the key on the target disk, so sshd-keygen finds it and skips
|
||||||
|
# generation -- the disk image boots with the registered key, sops decrypts.
|
||||||
|
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||||
|
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||||
|
".#nixosConfigurations.${target}.config.system.build.diskoImagesScript" \
|
||||||
|
--out-link "result-${target}"
|
||||||
|
# Remove any stale .raw from a previous failed build so the post-build check
|
||||||
|
# below is unambiguous (diskoImagesScript writes to CWD as ${image_name}.raw).
|
||||||
|
$sudo_pfx rm -f "${image_name}.raw" 2>/dev/null || true
|
||||||
|
$sudo_pfx "./result-${target}" --build-memory 2048
|
||||||
|
if [[ ! -f "${image_name}.raw" ]]; then
|
||||||
|
echo "ERROR: ${image_name}.raw not found in ${repo_dir} after build -- disko/QEMU may have failed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Built image: ${repo_dir}/${image_name}.raw"
|
||||||
REMOTE_SCRIPT
|
REMOTE_SCRIPT
|
||||||
local_image="$remote_path"
|
local_image="$vm_built_raw"
|
||||||
echo "Built on ${node}: ${remote_path}"
|
echo "Built on ${node}: ${vm_built_raw}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -781,17 +867,17 @@ if [[ "$type" == "lxc" ]]; then
|
|||||||
local_swap="${swap:-$memory}"
|
local_swap="${swap:-$memory}"
|
||||||
# --unprivileged: read back from modules/platforms/lxc.nix's own
|
# --unprivileged: read back from modules/platforms/lxc.nix's own
|
||||||
# proxmoxLXC.privileged (via flake_target_lxc_privileged) rather than
|
# proxmoxLXC.privileged (via flake_target_lxc_privileged) rather than
|
||||||
# hardcoded, since that's no longer the same for every lxc-* target --
|
# hardcoded. lxc.nix derives this automatically: any lxc-* host whose
|
||||||
# lxc-docker sets it true so the container's NFS mounts work at all (the
|
# config.fileSystems has an NFS entry gets privileged=true, because the
|
||||||
# kernel's NFS client can't mount from inside any unprivileged
|
# kernel's NFS client (FS_USERNS_MOUNT not set) rejects NFS mounts from
|
||||||
# container's user namespace, no matter what AppArmor allows -- see that
|
# inside any non-init user namespace -- exactly what an unprivileged
|
||||||
# option's own comment). The NixOS config inside the image bakes in
|
# container's UID-mapped root lives in -- with EPERM at the VFS layer,
|
||||||
# cgroup/capability/mount expectations matching whichever value it was
|
# regardless of AppArmor (see lxc.nix's own comment). The NixOS config
|
||||||
# built with, so this must stay in sync with it -- `pct create`'s own
|
# bakes in cgroup/capability/mount expectations matching whichever value
|
||||||
# CLI default for this flag is privileged (unlike the web UI, which
|
# it was built with, so this must stay in sync -- `pct create`'s CLI
|
||||||
# defaults its checkbox the other way), so leaving it unset would create
|
# default is privileged (unlike the web UI, which defaults the other
|
||||||
# a privileged container running a NixOS config that assumes
|
# way), so leaving it unset would create a privileged container running
|
||||||
# unprivileged for every target except lxc-docker, a real mismatch.
|
# a NixOS config that assumes unprivileged, a real mismatch.
|
||||||
privileged_eval="$(flake_target_lxc_privileged "$repo_root" "$flake_target")"
|
privileged_eval="$(flake_target_lxc_privileged "$repo_root" "$flake_target")"
|
||||||
unprivileged_flag=1
|
unprivileged_flag=1
|
||||||
[[ "$privileged_eval" == "true" ]] && unprivileged_flag=0
|
[[ "$privileged_eval" == "true" ]] && unprivileged_flag=0
|
||||||
@@ -812,9 +898,9 @@ if [[ "$type" == "lxc" ]]; then
|
|||||||
# hands the whole string to `ssh` as a single command for the *remote*
|
# hands the whole string to `ssh` as a single command for the *remote*
|
||||||
# shell to parse -- unquoted, that `;` would be read as a remote
|
# shell to parse -- unquoted, that `;` would be read as a remote
|
||||||
# command separator and silently truncate this into two commands.
|
# command separator and silently truncate this into two commands.
|
||||||
create_cmd="pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged ${unprivileged_flag} --features '${PROXMOX_DEFAULT_LXC_FEATURES}' --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
|
create_cmd="${sudo_prefix} pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged ${unprivileged_flag} --features '${PROXMOX_DEFAULT_LXC_FEATURES}' --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
|
||||||
remote "$create_cmd"
|
remote "$create_cmd"
|
||||||
remote "pct start ${vmid}"
|
remote "${sudo_prefix} pct start ${vmid}"
|
||||||
else
|
else
|
||||||
echo "==> Creating VM ${vmid} (${name})..."
|
echo "==> Creating VM ${vmid} (${name})..."
|
||||||
# pre-enrolled-keys=0 disables OVMF's Secure Boot key pre-enrollment --
|
# pre-enrolled-keys=0 disables OVMF's Secure Boot key pre-enrollment --
|
||||||
@@ -825,29 +911,56 @@ else
|
|||||||
# without this flag Proxmox never creates the channel it listens on, so
|
# without this flag Proxmox never creates the channel it listens on, so
|
||||||
# `qm guest exec`/`qm agent` and the UI's IP-address display silently
|
# `qm guest exec`/`qm agent` and the UI's IP-address display silently
|
||||||
# never work for any VM this script creates.
|
# never work for any VM this script creates.
|
||||||
remote "qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
|
remote "${sudo_prefix} qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
|
||||||
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
|
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
|
||||||
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0 --agent enabled=1"
|
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0 --agent enabled=1"
|
||||||
|
|
||||||
|
# VMs built on the node: import from the repo dir (where disko/QEMU wrote it).
|
||||||
|
# VMs from --image: import from remote_path (where scp uploaded it).
|
||||||
|
_import_path="${remote_path}"
|
||||||
|
[[ -z "$image" ]] && _import_path="${vm_built_raw}"
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] ssh ${ssh_target} -- qm importdisk ${vmid} ${remote_path} ${storage}"
|
echo "[dry-run] ssh ${ssh_target} -- ${sudo_display}qm importdisk ${vmid} ${_import_path} ${storage}"
|
||||||
echo "[dry-run] (would parse the resulting disk identifier from that output)"
|
echo "[dry-run] (would parse the resulting disk identifier from that output)"
|
||||||
echo "[dry-run] ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
|
echo "[dry-run] ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
|
||||||
else
|
else
|
||||||
importdisk_output="$(ssh "$ssh_target" "qm importdisk ${vmid} ${remote_path} ${storage}")"
|
if ! importdisk_output="$(ssh "$ssh_target" "${sudo_prefix} qm importdisk ${vmid} ${_import_path} ${storage}" 2>&1)"; then
|
||||||
|
echo "ERROR: qm importdisk failed:" >&2
|
||||||
|
echo "${importdisk_output}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "$importdisk_output"
|
echo "$importdisk_output"
|
||||||
disk_id="$(echo "$importdisk_output" | grep -oP "(?<=Successfully imported disk as ')[^']+" | sed 's/^unused[0-9]*://')"
|
# PVE output format: "unusedN: successfully imported disk '<storage>:<vol>'"
|
||||||
|
# (lowercase "successfully", no "as"; the primary regex targets this form; the
|
||||||
|
# || true inside the substitution prevents set -e from aborting when grep finds
|
||||||
|
# no match -- without it the script would silently exit before reaching the
|
||||||
|
# fallback whenever the PVE format doesn't match).
|
||||||
|
disk_id="$(echo "$importdisk_output" | grep -oP "successfully imported disk '\\K[^']+" || true)"
|
||||||
|
if [[ -z "$disk_id" ]]; then
|
||||||
|
# Fallback for other PVE output variants: read qm config directly.
|
||||||
|
unused_line="$(ssh "$ssh_target" "${sudo_prefix} qm config ${vmid}" | grep '^unused[0-9]*:' | head -1 || true)"
|
||||||
|
if [[ -n "$unused_line" ]]; then
|
||||||
|
disk_id="${unused_line#*: }"
|
||||||
|
echo "Note: disk ID resolved from qm config: ${disk_id}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
if [[ -z "$disk_id" ]]; then
|
if [[ -z "$disk_id" ]]; then
|
||||||
echo "ERROR: couldn't parse the imported disk identifier from qm importdisk's output above." >&2
|
echo "ERROR: couldn't parse the imported disk identifier from qm importdisk's output above." >&2
|
||||||
echo "The VM shell (${vmid}) and imported disk both exist -- finish attaching it by hand:" >&2
|
echo "The VM shell (${vmid}) and imported disk both exist -- finish attaching it by hand:" >&2
|
||||||
echo " ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<disk-id-from-output-above>" >&2
|
echo " ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<disk-id-from-output-above>" >&2
|
||||||
echo " ssh ${ssh_target} -- qm set ${vmid} --boot order=scsi0" >&2
|
echo " ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --boot order=scsi0" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
remote "qm set ${vmid} --scsi0 ${disk_id}"
|
remote "${sudo_prefix} qm set ${vmid} --scsi0 ${disk_id}"
|
||||||
|
# The disk data is now in ZFS; remove the source raw file (only for images
|
||||||
|
# we built on the node -- --image uploads are the operator's to manage).
|
||||||
|
if [[ -z "$image" ]]; then
|
||||||
|
ssh "$ssh_target" "${sudo_prefix} rm -f '${_import_path}'" 2>/dev/null || \
|
||||||
|
echo "Warning: couldn't remove ${_import_path} from ${node} -- you can delete it manually" >&2
|
||||||
fi
|
fi
|
||||||
remote "qm set ${vmid} --boot order=scsi0"
|
fi
|
||||||
remote "qm start ${vmid}"
|
remote "${sudo_prefix} qm set ${vmid} --boot order=scsi0"
|
||||||
|
remote "${sudo_prefix} qm start ${vmid}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
Executable
+253
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# recover-hosts.sh — Fix sops/SSH-key/GitHub-token issues on deployed NixOS hosts
|
||||||
|
# and trigger a Switch-nix rebuild on each.
|
||||||
|
#
|
||||||
|
# Run from the repo root on the workstation (nixos@nixos):
|
||||||
|
# bash scripts/recover-hosts.sh [<hostname> ...]
|
||||||
|
#
|
||||||
|
# With no args it discovers and checks every known hostname.
|
||||||
|
# With args it checks only those hostnames:
|
||||||
|
# bash scripts/recover-hosts.sh tor-relay
|
||||||
|
#
|
||||||
|
# Fixes applied automatically (then prompts before rebuilding):
|
||||||
|
# 1. SSH host key drift — live key no longer matches host-keys/<target>_ssh_host_ed25519_key
|
||||||
|
# Fix: scp the registered key back and restore it (needs sudo once per host).
|
||||||
|
# To push new keys proactively (before drift, e.g. right after
|
||||||
|
# sync-host-keys.sh --regenerate-all-keys), use instead:
|
||||||
|
# scripts/secrets/push-host-keys.sh --all
|
||||||
|
# 2. Stale/invalid GitHub access token — the rendered nix-github-token.conf has
|
||||||
|
# a token GitHub rejects (401), blocking any rebuild that fetches disko or
|
||||||
|
# other public GitHub flake inputs.
|
||||||
|
# Fix: empty the rendered file so nix makes unauthenticated requests instead.
|
||||||
|
# Public repos (disko, nixpkgs, etc.) work fine without auth. sops-nix
|
||||||
|
# re-renders the correct new token automatically after the first successful
|
||||||
|
# rebuild.
|
||||||
|
#
|
||||||
|
# Both fixes need one interactive sudo session per host. The script opens a
|
||||||
|
# single ssh -t per broken host so you enter the password once and all steps
|
||||||
|
# run in sequence.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
source scripts/env.sh 2>/dev/null || true
|
||||||
|
|
||||||
|
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5)
|
||||||
|
SSH_USER=nixos
|
||||||
|
|
||||||
|
# Known flake-target → ssh hostname map for all currently-defined hosts.
|
||||||
|
# Add new hosts here as they are deployed.
|
||||||
|
declare -A TARGET_HOST=(
|
||||||
|
[lxc-docker]=docker
|
||||||
|
[lxc-nix-cache]=nix-cache
|
||||||
|
[lxc-pxe-boot]=pxe-boot
|
||||||
|
[lxc-tor-relay]=tor-relay
|
||||||
|
[lxc-minimal]=nix-minimal
|
||||||
|
[proxmox-server]=server
|
||||||
|
[baremetal-gui]=nixos
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
info() { echo " [✓] $*"; }
|
||||||
|
warn() { echo " [!] $*"; }
|
||||||
|
step() { echo "==> $*"; }
|
||||||
|
|
||||||
|
ssh_host_age() {
|
||||||
|
ssh-keyscan -t ed25519 "$1" 2>/dev/null \
|
||||||
|
| nix shell nixpkgs#ssh-to-age --command ssh-to-age 2>/dev/null \
|
||||||
|
| head -1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
registered_age() {
|
||||||
|
local keyfile="host-keys/${1}_ssh_host_ed25519_key.pub"
|
||||||
|
[ -f "$keyfile" ] || return 0
|
||||||
|
nix shell nixpkgs#ssh-to-age --command ssh-to-age < "$keyfile" 2>/dev/null \
|
||||||
|
| head -1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
github_token_valid() {
|
||||||
|
local host=$1
|
||||||
|
local raw token code
|
||||||
|
raw=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"cat /run/secrets/rendered/nix-github-token.conf 2>/dev/null || true")
|
||||||
|
token=$(echo "$raw" | grep -oP '(?<=github\.com=)\S+' || true)
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
return 0 # no token = unauthenticated, works for public repos
|
||||||
|
fi
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
|
-H "Authorization: token $token" \
|
||||||
|
"https://api.github.com/repos/nix-community/disko" 2>/dev/null || echo 000)
|
||||||
|
[ "$code" = "200" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── discover hosts ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
HOSTNAMES=("$@")
|
||||||
|
else
|
||||||
|
HOSTNAMES=()
|
||||||
|
seen=()
|
||||||
|
for target in "${!TARGET_HOST[@]}"; do
|
||||||
|
h="${TARGET_HOST[$target]}"
|
||||||
|
# deduplicate (e.g. proxmox-server and lxc-server both map to "server")
|
||||||
|
if [[ ! " ${seen[*]:-} " =~ " $h " ]]; then
|
||||||
|
seen+=("$h")
|
||||||
|
if ssh "${SSH_OPTS[@]}" "$SSH_USER@$h" "true" 2>/dev/null; then
|
||||||
|
HOSTNAMES+=("$h")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#HOSTNAMES[@]} -eq 0 ]; then
|
||||||
|
echo "No reachable hosts found. Pass hostnames explicitly or check SSH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Hosts to check: ${HOSTNAMES[*]}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── check phase ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
NEEDS_FIX=()
|
||||||
|
|
||||||
|
for host in "${HOSTNAMES[@]}"; do
|
||||||
|
step "$host"
|
||||||
|
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" "true" 2>/dev/null; then
|
||||||
|
warn "SSH unreachable — clearing stale known_hosts entry"
|
||||||
|
ssh-keygen -R "$host" 2>/dev/null || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
flake_target=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"cat /etc/flake-target 2>/dev/null || true")
|
||||||
|
echo " flake-target: ${flake_target:-unknown}"
|
||||||
|
|
||||||
|
host_broken=false
|
||||||
|
|
||||||
|
# SSH host key
|
||||||
|
if [ -n "$flake_target" ] && [ -f "host-keys/${flake_target}_ssh_host_ed25519_key.pub" ]; then
|
||||||
|
live=$(ssh_host_age "$host")
|
||||||
|
want=$(registered_age "$flake_target")
|
||||||
|
if [ "$live" = "$want" ]; then
|
||||||
|
info "SSH host key OK"
|
||||||
|
else
|
||||||
|
warn "SSH host key MISMATCH (live ≠ host-keys/) -- use push-host-keys.sh proactively next time"
|
||||||
|
echo " live: $live"
|
||||||
|
echo " registered: $want"
|
||||||
|
host_broken=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " [~] No host-keys/ entry for ${flake_target:-unknown} — skipping key check"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GitHub token
|
||||||
|
if github_token_valid "$host"; then
|
||||||
|
info "GitHub token OK"
|
||||||
|
else
|
||||||
|
warn "GitHub token invalid (rebuild will fail with 401)"
|
||||||
|
host_broken=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# sops-nix result
|
||||||
|
sops_result=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"systemctl show sops-nix --property=Result --value 2>/dev/null || echo unknown")
|
||||||
|
if [ "$sops_result" = "success" ]; then
|
||||||
|
info "sops-nix: success"
|
||||||
|
else
|
||||||
|
warn "sops-nix: $sops_result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
$host_broken && NEEDS_FIX+=("$host")
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── fix phase ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ ${#NEEDS_FIX[@]} -eq 0 ]; then
|
||||||
|
echo "All hosts healthy — nothing to fix."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Hosts needing fixes: ${NEEDS_FIX[*]}"
|
||||||
|
echo ""
|
||||||
|
echo "Each fix requires one sudo session per host. You will be prompted for"
|
||||||
|
echo "the nixos sudo password once per host; all steps run in that session."
|
||||||
|
echo ""
|
||||||
|
read -r -p "Proceed with fixes + Switch-nix on each broken host? [y/N] " confirm
|
||||||
|
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for host in "${NEEDS_FIX[@]}"; do
|
||||||
|
step "Fixing $host"
|
||||||
|
flake_target=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"cat /etc/flake-target 2>/dev/null || true")
|
||||||
|
|
||||||
|
fix_script=""
|
||||||
|
|
||||||
|
# Fix 1: restore SSH host key
|
||||||
|
live=$(ssh_host_age "$host")
|
||||||
|
want=$(registered_age "${flake_target:-}")
|
||||||
|
if [ -n "$want" ] && [ "$live" != "$want" ]; then
|
||||||
|
echo " Uploading registered SSH host key (private + public)..."
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
"host-keys/${flake_target}_ssh_host_ed25519_key" \
|
||||||
|
"$SSH_USER@$host:/tmp/recover_ed25519_key"
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
"host-keys/${flake_target}_ssh_host_ed25519_key.pub" \
|
||||||
|
"$SSH_USER@$host:/tmp/recover_ed25519_key.pub"
|
||||||
|
fix_script+='
|
||||||
|
echo "[fix] Restoring SSH host key..."
|
||||||
|
install -m 0600 /tmp/recover_ed25519_key /etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -m 0644 /tmp/recover_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
rm -f /tmp/recover_ed25519_key /tmp/recover_ed25519_key.pub
|
||||||
|
echo " Done."
|
||||||
|
'
|
||||||
|
ssh-keygen -R "$host" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fix 2: clear invalid GitHub token
|
||||||
|
if ! github_token_valid "$host"; then
|
||||||
|
fix_script+='
|
||||||
|
echo "[fix] Clearing stale GitHub token (nix will use unauthenticated access)..."
|
||||||
|
echo "" > /run/secrets/rendered/nix-github-token.conf
|
||||||
|
systemctl restart nix-daemon 2>/dev/null || true
|
||||||
|
echo " Done."
|
||||||
|
'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fix 3: rebuild
|
||||||
|
fix_script+='
|
||||||
|
echo "[fix] Running nixos-rebuild switch..."
|
||||||
|
nixos-rebuild switch \
|
||||||
|
--no-write-lock-file \
|
||||||
|
--refresh \
|
||||||
|
--flake "git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(cat /etc/flake-target)"
|
||||||
|
echo "[fix] Rebuild complete."
|
||||||
|
'
|
||||||
|
|
||||||
|
echo " Opening SSH session (enter sudo password when prompted)..."
|
||||||
|
if ssh -t -o StrictHostKeyChecking=no "$SSH_USER@$host" \
|
||||||
|
"sudo bash -s" <<< "$fix_script"; then
|
||||||
|
echo ""
|
||||||
|
info "$host fixed and rebuilt"
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
echo ""
|
||||||
|
warn "$host: rebuild exited with code $rc (may still have succeeded — check sops-nix below)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify: re-check sops-nix result post-rebuild
|
||||||
|
sops_result_after=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
|
||||||
|
"systemctl show sops-nix --property=Result --value 2>/dev/null || echo unknown" 2>/dev/null || echo "ssh-failed")
|
||||||
|
if [ "$sops_result_after" = "success" ]; then
|
||||||
|
info "$host sops-nix: success post-rebuild"
|
||||||
|
else
|
||||||
|
warn "$host sops-nix: $sops_result_after post-rebuild (may need another pass)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Recovery complete."
|
||||||
@@ -41,8 +41,9 @@ mkdir -p "$keydir"
|
|||||||
keyfile="${keydir}/${hostname}_ssh_host_ed25519_key"
|
keyfile="${keydir}/${hostname}_ssh_host_ed25519_key"
|
||||||
|
|
||||||
if [[ -f "$keyfile" ]]; then
|
if [[ -f "$keyfile" ]]; then
|
||||||
echo "ERROR: $keyfile already exists. Remove it first if you want to regenerate." >&2
|
echo "Key already exists: ${keyfile}"
|
||||||
exit 1
|
echo "Reusing the existing key. Remove it first if you want to regenerate."
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
nix_extra_opts
|
nix_extra_opts
|
||||||
|
|||||||
Executable
+318
@@ -0,0 +1,318 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pushes newly-generated SSH host keys from host-keys/ to already-running
|
||||||
|
# NixOS hosts, so they can decrypt sops secrets after a nixos-rebuild
|
||||||
|
# following scripts/secrets/sync-host-keys.sh --regenerate-all-keys.
|
||||||
|
#
|
||||||
|
# Before pushing any key, verifies that .sops.yaml and secrets/*.yaml are
|
||||||
|
# committed and pushed to the remote -- hosts rebuild from the remote Gitea
|
||||||
|
# flake, so recipient changes must land there before any rebuild, not just
|
||||||
|
# before the key push.
|
||||||
|
#
|
||||||
|
# push-host-keys.sh --all [--dry-run] [--skip-git-check]
|
||||||
|
# push-host-keys.sh <target> [--dry-run] [--skip-git-check]
|
||||||
|
#
|
||||||
|
# --all Push to every reachable managed host. Default when no
|
||||||
|
# target is given.
|
||||||
|
# <target> Push to one flake target only (e.g. lxc-server).
|
||||||
|
# --dry-run Print what would be done; write nothing.
|
||||||
|
# --skip-git-check Skip the commit/push check. Use only when the remote
|
||||||
|
# already has the current .sops.yaml/secrets/*.yaml.
|
||||||
|
#
|
||||||
|
# SSH: connects as SSH_USER@<hostname> (default: nixos, the user with the
|
||||||
|
# admin authorized key), then installs files via sudo -S (reads the sudo
|
||||||
|
# password from stdin). The password is prompted once at startup and reused
|
||||||
|
# for every host -- no PTY or terminal required on the remote side.
|
||||||
|
# Hosts are reached at their bare hostname (relies on LAN DNS/mDNS).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
keydir="${repo_root}/host-keys"
|
||||||
|
|
||||||
|
# shellcheck source=../env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=../lib/nix-eval.sh
|
||||||
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
|
|
||||||
|
: "${SSH_USER:=nixos}"
|
||||||
|
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5)
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
skip_git_check=0
|
||||||
|
sudo_password=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 [--all | <target>] [--dry-run] [--skip-git-check]
|
||||||
|
|
||||||
|
--all Push to every reachable managed host. Default when no
|
||||||
|
target is given.
|
||||||
|
<target> Push to one flake target only (e.g. lxc-server).
|
||||||
|
--dry-run Print what would be done; write nothing.
|
||||||
|
--skip-git-check Skip the check that .sops.yaml/secrets/*.yaml are
|
||||||
|
committed and pushed to the remote repo.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
SSH_USER SSH username (default: nixos).
|
||||||
|
SUDO_PASS Sudo password (skips the interactive prompt; useful
|
||||||
|
when calling from another script).
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prompt for the sudo password once; store it for all _do_push calls.
|
||||||
|
# Accepts SUDO_PASS from the environment to allow non-interactive callers.
|
||||||
|
prompt_sudo_password() {
|
||||||
|
[[ "$dry_run" -eq 1 ]] && return
|
||||||
|
if [[ -n "${SUDO_PASS:-}" ]]; then
|
||||||
|
sudo_password="$SUDO_PASS"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# read exits non-zero when stdin is not a terminal (e.g. CI, background
|
||||||
|
# agents). Catch that and give a clear message rather than a silent exit.
|
||||||
|
if ! read -r -s -p "sudo password for ${SSH_USER} on remote hosts: " sudo_password; then
|
||||||
|
echo >&2
|
||||||
|
echo "ERROR: stdin is not a terminal -- cannot prompt for sudo password." >&2
|
||||||
|
echo " Set SUDO_PASS=<password> in the environment and re-run." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
locally_managed_hosts() {
|
||||||
|
for f in "${keydir}"/*_ssh_host_ed25519_key.pub; do
|
||||||
|
[[ -e "$f" ]] || continue
|
||||||
|
basename "$f" _ssh_host_ed25519_key.pub
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- git state check/fix --------------------------------------------------
|
||||||
|
# Hosts rebuild from the remote Gitea flake:
|
||||||
|
# nixos-rebuild switch --flake "git+https://<gitea>/nixos.git#<target>"
|
||||||
|
# so .sops.yaml (updated recipients) and secrets/*.yaml (re-encrypted DEKs)
|
||||||
|
# must be committed and pushed before any rebuild can succeed. This check
|
||||||
|
# catches the common case where --regenerate-all-keys was just run but the
|
||||||
|
# resulting diff hasn't been committed/pushed yet.
|
||||||
|
ensure_remote_current() {
|
||||||
|
[[ "$skip_git_check" -eq 1 ]] && return
|
||||||
|
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
local dirty_unstaged dirty_staged
|
||||||
|
dirty_unstaged="$(git diff --name-only -- .sops.yaml secrets/ 2>/dev/null || true)"
|
||||||
|
dirty_staged="$(git diff --cached --name-only -- .sops.yaml secrets/ 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ -n "$dirty_unstaged" || -n "$dirty_staged" ]]; then
|
||||||
|
echo "Uncommitted changes in sops-managed files:"
|
||||||
|
[[ -n "$dirty_unstaged" ]] && sed 's/^/ (unstaged) /' <<<"$dirty_unstaged"
|
||||||
|
[[ -n "$dirty_staged" ]] && sed 's/^/ (staged) /' <<<"$dirty_staged"
|
||||||
|
echo
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would prompt to commit .sops.yaml/secrets/ before continuing."
|
||||||
|
else
|
||||||
|
read -rp "Commit .sops.yaml + secrets/ now? [y/N]: " ans
|
||||||
|
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
||||||
|
git add -- .sops.yaml secrets/
|
||||||
|
git commit -m "secrets: update recipients and re-encrypt for host key changes"
|
||||||
|
echo "Committed."
|
||||||
|
else
|
||||||
|
echo "Continuing with uncommitted changes -- the remote won't have the"
|
||||||
|
echo "updated recipients until you commit and push."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we're ahead of the remote tracking branch
|
||||||
|
local ahead
|
||||||
|
ahead="$(git rev-list --count '@{upstream}..HEAD' 2>/dev/null || echo "")"
|
||||||
|
if [[ -z "$ahead" ]]; then
|
||||||
|
echo "NOTE: no remote tracking branch found -- skipping push check."
|
||||||
|
echo " Ensure the remote has the current .sops.yaml/secrets/ before"
|
||||||
|
echo " triggering nixos-rebuild on any host."
|
||||||
|
echo
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ahead" -gt 0 ]]; then
|
||||||
|
echo "Local branch is ${ahead} commit(s) ahead of remote."
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would prompt to push before continuing."
|
||||||
|
else
|
||||||
|
read -rp "Push to remote now? [y/N]: " ans
|
||||||
|
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
||||||
|
git push
|
||||||
|
echo "Pushed."
|
||||||
|
else
|
||||||
|
echo "Continuing without pushing -- remember to push before running"
|
||||||
|
echo "nixos-rebuild on any of these hosts."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- key installation (shared) -------------------------------------------
|
||||||
|
_do_push() {
|
||||||
|
local hostname="$1" target="$2"
|
||||||
|
local keyfile="${keydir}/${target}_ssh_host_ed25519_key"
|
||||||
|
local pubfile="${keyfile}.pub"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo " [dry-run] would scp host-keys/${target}_ssh_host_ed25519_key{,.pub} to /tmp/"
|
||||||
|
echo " [dry-run] would: sudo -S install -m 0600/0644 to /etc/ssh/ and rm /tmp copies"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload to /tmp (writable as nixos, no privilege needed)
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
"$keyfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key"
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
"$pubfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key.pub"
|
||||||
|
|
||||||
|
# Install via sudo -S: the password is piped via herestring so no PTY is
|
||||||
|
# needed on either side. -p '' suppresses sudo's own prompt string.
|
||||||
|
ssh -o StrictHostKeyChecking=no "${SSH_USER}@${hostname}" \
|
||||||
|
"sudo -S -p '' bash -c '
|
||||||
|
install -m 0600 /tmp/push_ed25519_key /etc/ssh/ssh_host_ed25519_key
|
||||||
|
install -m 0644 /tmp/push_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
rm -f /tmp/push_ed25519_key /tmp/push_ed25519_key.pub
|
||||||
|
echo \" [ok] host key installed\"
|
||||||
|
'" <<< "$sudo_password"
|
||||||
|
|
||||||
|
# Drop the stale known_hosts entry for this host (public key just changed)
|
||||||
|
ssh-keygen -R "$hostname" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo " Done. Run nixos-rebuild switch on ${hostname} to activate."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- single named target --------------------------------------------------
|
||||||
|
push_target() {
|
||||||
|
local target="$1"
|
||||||
|
local keyfile="${keydir}/${target}_ssh_host_ed25519_key"
|
||||||
|
|
||||||
|
if [[ ! -f "$keyfile" ]]; then
|
||||||
|
echo "ERROR: host-keys/${target}_ssh_host_ed25519_key not found." >&2
|
||||||
|
echo " This target may not be locally managed (e.g. &${target} was" >&2
|
||||||
|
echo " registered from the host's real SSH key, not generated here)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local hostname
|
||||||
|
hostname="$(flake_target_hostname "$repo_root" "$target")"
|
||||||
|
if [[ -z "$hostname" ]]; then
|
||||||
|
echo "ERROR: cannot resolve hostname for '${target}' from the flake." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> ${target} (→ ${hostname})"
|
||||||
|
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" true 2>/dev/null; then
|
||||||
|
echo " SKIP: ${SSH_USER}@${hostname} unreachable."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sanity-check that /etc/flake-target on the host agrees
|
||||||
|
local live_target
|
||||||
|
live_target="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" \
|
||||||
|
"cat /etc/flake-target 2>/dev/null || true")"
|
||||||
|
if [[ -n "$live_target" && "$live_target" != "$target" ]]; then
|
||||||
|
echo " WARN: host reports /etc/flake-target='${live_target}', not '${target}'."
|
||||||
|
echo " Pushing the key you specified (${target}) anyway."
|
||||||
|
fi
|
||||||
|
|
||||||
|
_do_push "$hostname" "$target"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- all managed hosts ----------------------------------------------------
|
||||||
|
# For each unique hostname derived from managed targets, SSHes in and reads
|
||||||
|
# /etc/flake-target to determine which key to push -- handles the case where
|
||||||
|
# multiple targets share a hostname (e.g. lxc-server and proxmox-server both
|
||||||
|
# resolve to "server"; only one is actually running).
|
||||||
|
push_all() {
|
||||||
|
mapfile -t managed < <(locally_managed_hosts)
|
||||||
|
if [[ "${#managed[@]}" -eq 0 ]]; then
|
||||||
|
echo "No managed keys in host-keys/ -- nothing to push."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pushing to all reachable managed hosts..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
declare -A seen_hostnames=()
|
||||||
|
local t hostname
|
||||||
|
for t in "${managed[@]}"; do
|
||||||
|
hostname="$(flake_target_hostname "$repo_root" "$t" 2>/dev/null || true)"
|
||||||
|
[[ -z "$hostname" ]] && continue
|
||||||
|
[[ -n "${seen_hostnames[$hostname]+x}" ]] && continue
|
||||||
|
seen_hostnames["$hostname"]=1
|
||||||
|
|
||||||
|
echo "==> checking ${hostname}"
|
||||||
|
|
||||||
|
if ! ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" true 2>/dev/null; then
|
||||||
|
echo " SKIP: ${SSH_USER}@${hostname} unreachable."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ask the host which flake target it actually is
|
||||||
|
local live_target
|
||||||
|
live_target="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" \
|
||||||
|
"cat /etc/flake-target 2>/dev/null || true")"
|
||||||
|
|
||||||
|
if [[ -z "$live_target" ]]; then
|
||||||
|
echo " SKIP: no /etc/flake-target on host -- can't determine which key to push."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local live_keyfile="${keydir}/${live_target}_ssh_host_ed25519_key"
|
||||||
|
if [[ ! -f "$live_keyfile" ]]; then
|
||||||
|
echo " SKIP: host is '${live_target}' but no host-keys/${live_target}_... (hand-registered key, not managed here)."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " target: ${live_target}"
|
||||||
|
_do_push "$hostname" "$live_target"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- main -----------------------------------------------------------------
|
||||||
|
mode="all"
|
||||||
|
target_arg=""
|
||||||
|
extra_args=()
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) dry_run=1 ;;
|
||||||
|
--skip-git-check) skip_git_check=1 ;;
|
||||||
|
--all) mode="all" ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--*) echo "Unknown option: $arg" >&2; usage >&2; exit 1 ;;
|
||||||
|
*) extra_args+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${#extra_args[@]}" -gt 1 ]]; then
|
||||||
|
echo "ERROR: specify at most one target (or --all)." >&2
|
||||||
|
usage >&2; exit 1
|
||||||
|
elif [[ "${#extra_args[@]}" -eq 1 ]]; then
|
||||||
|
mode="single"
|
||||||
|
target_arg="${extra_args[0]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$dry_run" -eq 1 ]] && { echo "[dry-run] no changes will be made"; echo; }
|
||||||
|
|
||||||
|
nix_extra_opts
|
||||||
|
ensure_remote_current
|
||||||
|
prompt_sudo_password
|
||||||
|
|
||||||
|
if [[ "$mode" == "single" ]]; then
|
||||||
|
push_target "$target_arg"
|
||||||
|
else
|
||||||
|
push_all
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply."
|
||||||
|
else
|
||||||
|
echo "Key push complete. For each updated host, run nixos-rebuild switch to"
|
||||||
|
echo "apply the config and let sops-nix decrypt secrets with the new key."
|
||||||
|
fi
|
||||||
@@ -11,17 +11,16 @@
|
|||||||
# sync-host-keys.sh --regenerate-all-keys Remove and freshly regenerate
|
# sync-host-keys.sh --regenerate-all-keys Remove and freshly regenerate
|
||||||
# every locally-managed key.
|
# every locally-managed key.
|
||||||
#
|
#
|
||||||
# "Generate/register" is idempotent and additive only: an existing
|
# "Generate/register" is idempotent and additive only: an existing clan
|
||||||
# host-keys/ file is never touched, and .sops.yaml only ever gains an
|
# var is never overwritten, and .sops.yaml only ever gains an anchor/alias
|
||||||
# anchor/alias it doesn't already have -- safe to re-run any time, e.g.
|
# it doesn't already have -- safe to re-run any time, e.g. right after
|
||||||
# right after adding a new host to flake.nix.
|
# adding a new host to flake.nix.
|
||||||
#
|
#
|
||||||
# --remove and --regenerate-all-keys only ever operate on anchors that have
|
# --remove and --regenerate-all-keys only ever operate on anchors that
|
||||||
# a corresponding host-keys/<name>_ssh_host_ed25519_key file. Anchors
|
# have a corresponding clan var (vars/per-machine/<name>/openssh/) or
|
||||||
# without one (&admin, and any anchor for an already-deployed host whose
|
# host-keys/ file. Anchors without either (&admin) are never listed,
|
||||||
# real /etc/ssh key was registered by hand, e.g. &docker/&server/&nix-cache
|
# removed, or regenerated -- this tooling only ever touches keys it itself
|
||||||
# today) are never listed, removed, or regenerated -- this tooling only
|
# manages.
|
||||||
# ever touches keys it itself manages.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
@@ -39,6 +38,8 @@ source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
|||||||
source "${repo_root}/scripts/lib/sops-age.sh"
|
source "${repo_root}/scripts/lib/sops-age.sh"
|
||||||
# shellcheck source=../lib/confirm.sh
|
# shellcheck source=../lib/confirm.sh
|
||||||
source "${repo_root}/scripts/lib/confirm.sh"
|
source "${repo_root}/scripts/lib/confirm.sh"
|
||||||
|
# shellcheck source=../lib/clan-vars.sh
|
||||||
|
source "${repo_root}/scripts/lib/clan-vars.sh"
|
||||||
|
|
||||||
mkdir -p "$keydir"
|
mkdir -p "$keydir"
|
||||||
|
|
||||||
@@ -54,13 +55,14 @@ Usage: $0 --all [--dry-run]
|
|||||||
<flake-target> Same, for just one target (e.g. lxc-server).
|
<flake-target> Same, for just one target (e.g. lxc-server).
|
||||||
Reports if it already has one.
|
Reports if it already has one.
|
||||||
--remove Interactively pick one locally-managed key to
|
--remove Interactively pick one locally-managed key to
|
||||||
remove from .sops.yaml and host-keys/.
|
remove from .sops.yaml and vars/per-machine/
|
||||||
|
(or host-keys/ for legacy keys).
|
||||||
--regenerate-all-keys Remove every locally-managed key and generate
|
--regenerate-all-keys Remove every locally-managed key and generate
|
||||||
fresh replacements for every current flake
|
fresh clan-var replacements for every current
|
||||||
target. Destructive -- requires typed
|
flake target. Destructive -- requires typed
|
||||||
confirmation.
|
confirmation.
|
||||||
--dry-run Combine with any of the above: print what would
|
--dry-run Combine with any of the above: print what would
|
||||||
change (host-keys/ files, .sops.yaml anchors and
|
change (clan vars, .sops.yaml anchors and
|
||||||
key_groups, which secrets/*.yaml would be
|
key_groups, which secrets/*.yaml would be
|
||||||
re-encrypted) without touching anything. No keys
|
re-encrypted) without touching anything. No keys
|
||||||
generated, no files written, no sops calls,
|
generated, no files written, no sops calls,
|
||||||
@@ -83,6 +85,10 @@ ensure_admin_decrypt_key() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
|
local key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
|
||||||
|
# Expand a leading ~ that survived variable substitution without tilde
|
||||||
|
# expansion (happens when SOPS_AGE_KEY_FILE or XDG_CONFIG_HOME is set with
|
||||||
|
# a literal ~ in the caller's environment).
|
||||||
|
key_file="${key_file/#~\//$HOME/}"
|
||||||
|
|
||||||
if [[ -s "$key_file" ]]; then
|
if [[ -s "$key_file" ]]; then
|
||||||
echo "Found existing sops age key at ${key_file}."
|
echo "Found existing sops age key at ${key_file}."
|
||||||
@@ -91,36 +97,23 @@ ensure_admin_decrypt_key() {
|
|||||||
|
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
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] 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] Continuing dry run without one -- any 'would re-encrypt' output below"
|
||||||
echo "[dry-run] 'would re-encrypt' output below couldn't actually run for real yet."
|
echo "[dry-run] couldn't actually run for real until a key is present."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "No sops age decryption key found (checked \$SOPS_AGE_KEY, \$SOPS_AGE_KEY_FILE, ${key_file})."
|
cat >&2 <<EOF
|
||||||
echo "Generating a new one at ${key_file}..."
|
No sops age decryption key found (checked \$SOPS_AGE_KEY, \$SOPS_AGE_KEY_FILE, ${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="$(age_pubkey_from_identity_file "$key_file")"
|
|
||||||
|
|
||||||
cat <<EOF
|
Place your admin age private key at ${key_file}, or set SOPS_AGE_KEY (inline
|
||||||
|
key) or SOPS_AGE_KEY_FILE (path to a different key file) and re-run.
|
||||||
|
|
||||||
A brand-new age key was just generated -- it cannot decrypt anything that
|
If the key is truly missing (not just mislocated), this is a manual recovery
|
||||||
already exists in secrets/*.yaml, since nothing was ever encrypted for it.
|
situation -- generating a brand-new admin key won't help, since it cannot
|
||||||
That trust can't be bootstrapped automatically (nobody can decrypt a file
|
decrypt anything already encrypted for the old one. Each secrets/*.yaml is
|
||||||
for a recipient that didn't exist when it was last encrypted).
|
also encrypted for its respective host key(s), so a running deployed host can
|
||||||
|
still decrypt what it needs -- but the admin key is required for re-encryption
|
||||||
To actually use this key:
|
(e.g. adding new recipients via sops updatekeys).
|
||||||
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
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -133,10 +126,17 @@ discover_targets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
locally_managed_hosts() {
|
locally_managed_hosts() {
|
||||||
|
{
|
||||||
for f in "$keydir"/*_ssh_host_ed25519_key.pub; do
|
for f in "$keydir"/*_ssh_host_ed25519_key.pub; do
|
||||||
[[ -e "$f" ]] || continue
|
[[ -e "$f" ]] || continue
|
||||||
basename "$f" _ssh_host_ed25519_key.pub
|
basename "$f" _ssh_host_ed25519_key.pub
|
||||||
done
|
done
|
||||||
|
local d
|
||||||
|
for d in "${repo_root}/vars/per-machine"/*/openssh/ssh_host_ed25519_key/secret; do
|
||||||
|
[[ -f "$d" ]] || continue
|
||||||
|
basename "$(dirname "$(dirname "$(dirname "$d")")")"
|
||||||
|
done
|
||||||
|
} | sort -u
|
||||||
}
|
}
|
||||||
|
|
||||||
add_keys_json="[]"
|
add_keys_json="[]"
|
||||||
@@ -146,13 +146,15 @@ dry_run=0
|
|||||||
queue_host_sync() {
|
queue_host_sync() {
|
||||||
local host="$1"
|
local host="$1"
|
||||||
local keyfile="${keydir}/${host}_ssh_host_ed25519_key"
|
local keyfile="${keydir}/${host}_ssh_host_ed25519_key"
|
||||||
local has_local_key=0 has_anchor=0
|
local has_local_key=0 has_clan_key=0 has_anchor=0
|
||||||
[[ -f "$keyfile" ]] && has_local_key=1
|
[[ -f "$keyfile" ]] && has_local_key=1
|
||||||
|
clan_ssh_key_exists "$host" "$repo_root" && has_clan_key=1
|
||||||
grep -qE "^ - &${host} age1" "$sops_yaml" && has_anchor=1
|
grep -qE "^ - &${host} age1" "$sops_yaml" && has_anchor=1
|
||||||
|
|
||||||
if [[ "$has_local_key" -eq 0 && "$has_anchor" -eq 1 ]]; then
|
if [[ "$has_local_key" -eq 0 && "$has_clan_key" -eq 0 && "$has_anchor" -eq 1 ]]; then
|
||||||
echo "SKIP ${host}: .sops.yaml already has an &${host} anchor, but"
|
echo "SKIP ${host}: .sops.yaml already has an &${host} anchor, but"
|
||||||
echo " host-keys/${host}_ssh_host_ed25519_key is missing locally."
|
echo " neither host-keys/${host}_ssh_host_ed25519_key nor"
|
||||||
|
echo " vars/per-machine/${host}/openssh/ exist locally."
|
||||||
echo " Not generating a replacement -- it wouldn't match whatever's"
|
echo " Not generating a replacement -- it wouldn't match whatever's"
|
||||||
echo " already registered (and possibly deployed). Remove the"
|
echo " already registered (and possibly deployed). Remove the"
|
||||||
echo " &${host} line from .sops.yaml first if you really want a"
|
echo " &${host} line from .sops.yaml first if you really want a"
|
||||||
@@ -160,21 +162,26 @@ queue_host_sync() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$has_local_key" -eq 0 ]]; then
|
if [[ "$has_local_key" -eq 0 && "$has_clan_key" -eq 0 ]]; then
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] ${host}: would generate host key"
|
echo "[dry-run] ${host}: would generate host key via clan vars"
|
||||||
else
|
else
|
||||||
echo "==> ${host}: generating host key"
|
echo "==> ${host}: generating host key via clan vars"
|
||||||
generate_host_ed25519_key "$host" "$keyfile"
|
clan_generate_ssh_key "$host" "$repo_root"
|
||||||
|
has_clan_key=1
|
||||||
fi
|
fi
|
||||||
|
elif [[ "$has_clan_key" -eq 1 ]]; then
|
||||||
|
echo "==> ${host}: clan-managed SSH host key already present"
|
||||||
else
|
else
|
||||||
echo "==> ${host}: host key already present"
|
echo "==> ${host}: host key already present (host-keys/)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$has_anchor" -eq 0 ]]; then
|
if [[ "$has_anchor" -eq 0 ]]; then
|
||||||
local age_pub
|
local age_pub
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
age_pub="dry-run-placeholder-not-a-real-key"
|
age_pub="dry-run-placeholder-not-a-real-key"
|
||||||
|
elif [[ "$has_clan_key" -eq 1 ]]; then
|
||||||
|
age_pub="$(ssh_pubkey_to_age "$(clan_ssh_pubkey_path "$host" "$repo_root")")"
|
||||||
else
|
else
|
||||||
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
|
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
|
||||||
fi
|
fi
|
||||||
@@ -299,7 +306,7 @@ cmd_remove() {
|
|||||||
local hosts
|
local hosts
|
||||||
mapfile -t hosts < <(locally_managed_hosts)
|
mapfile -t hosts < <(locally_managed_hosts)
|
||||||
if [[ "${#hosts[@]}" -eq 0 ]]; then
|
if [[ "${#hosts[@]}" -eq 0 ]]; then
|
||||||
echo "No locally-managed keys in host-keys/ -- nothing to remove."
|
echo "No locally-managed keys found (checked host-keys/ and vars/per-machine/) -- nothing to remove."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -308,7 +315,9 @@ cmd_remove() {
|
|||||||
for host in "${hosts[@]}"; do
|
for host in "${hosts[@]}"; do
|
||||||
local registered="not registered in .sops.yaml"
|
local registered="not registered in .sops.yaml"
|
||||||
grep -qE "^ - &${host} age1" "$sops_yaml" && registered="registered in .sops.yaml"
|
grep -qE "^ - &${host} age1" "$sops_yaml" && registered="registered in .sops.yaml"
|
||||||
printf ' %d) %s (%s)\n' "$i" "$host" "$registered"
|
local where="host-keys/"
|
||||||
|
clan_ssh_key_exists "$host" "$repo_root" && where="clan-vars"
|
||||||
|
printf ' %d) %s [%s, %s]\n' "$i" "$host" "$where" "$registered"
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -325,7 +334,7 @@ cmd_remove() {
|
|||||||
local target="${hosts[$((choice - 1))]}"
|
local target="${hosts[$((choice - 1))]}"
|
||||||
|
|
||||||
if [[ "$dry_run" -ne 1 ]]; then
|
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
|
read -rp "Really remove '${target}'? Its key files will be deleted and it will lose access to every secrets file it can currently decrypt. (y/N): " confirm
|
||||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
echo "Cancelled."
|
echo "Cancelled."
|
||||||
return
|
return
|
||||||
@@ -338,11 +347,13 @@ cmd_remove() {
|
|||||||
apply_edit_plan "$plan"
|
apply_edit_plan "$plan"
|
||||||
|
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] would delete host-keys/${target}_ssh_host_ed25519_key(.pub)."
|
echo "[dry-run] would delete host-keys/${target}_ssh_host_ed25519_key(.pub) if present."
|
||||||
|
echo "[dry-run] would delete vars/per-machine/${target}/openssh/ if present."
|
||||||
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
|
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
|
||||||
else
|
else
|
||||||
rm -f "${keydir}/${target}_ssh_host_ed25519_key" "${keydir}/${target}_ssh_host_ed25519_key.pub"
|
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)."
|
rm -rf "${repo_root}/vars/per-machine/${target}/openssh"
|
||||||
|
echo "Removed key for ${target} (host-keys/ and/or vars/per-machine/ as applicable)."
|
||||||
echo
|
echo
|
||||||
echo "Review the diff, then commit and push."
|
echo "Review the diff, then commit and push."
|
||||||
fi
|
fi
|
||||||
@@ -352,15 +363,18 @@ cmd_regenerate_all() {
|
|||||||
local hosts
|
local hosts
|
||||||
mapfile -t hosts < <(locally_managed_hosts)
|
mapfile -t hosts < <(locally_managed_hosts)
|
||||||
if [[ "${#hosts[@]}" -eq 0 ]]; then
|
if [[ "${#hosts[@]}" -eq 0 ]]; then
|
||||||
echo "No locally-managed keys in host-keys/ -- nothing to regenerate."
|
echo "No locally-managed keys found (checked host-keys/ and vars/per-machine/) -- nothing to regenerate."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "This will remove and freshly regenerate ALL locally-managed keys:"
|
echo "This will remove and freshly regenerate ALL locally-managed keys:"
|
||||||
printf ' %s\n' "${hosts[@]}"
|
printf ' %s\n' "${hosts[@]}"
|
||||||
echo
|
echo
|
||||||
echo "Every host above will need its new key baked into a rebuilt install"
|
echo "After regenerating, each host needs its new key before it can decrypt secrets:"
|
||||||
echo "image/tarball before it can decrypt secrets again."
|
echo " • Already running: push the key before rebuilding:"
|
||||||
|
echo " scripts/secrets/push-host-keys.sh --all"
|
||||||
|
echo " • Not yet deployed: rebuild the install image with the new keys baked in"
|
||||||
|
echo " (see docs/auto-installer.md)."
|
||||||
|
|
||||||
if [[ "$dry_run" -ne 1 ]]; then
|
if [[ "$dry_run" -ne 1 ]]; then
|
||||||
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
|
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
|
||||||
@@ -378,8 +392,8 @@ cmd_regenerate_all() {
|
|||||||
apply_edit_plan "$plan"
|
apply_edit_plan "$plan"
|
||||||
|
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] would delete ${#hosts[@]} host-keys/ file pair(s)."
|
echo "[dry-run] would delete ${#hosts[@]} key pair(s) from host-keys/ and/or vars/per-machine/."
|
||||||
echo "[dry-run] would then generate fresh replacements for the same hosts"
|
echo "[dry-run] would then generate fresh clan vars replacements for the same hosts"
|
||||||
echo "[dry-run] (not simulated further here -- run without --dry-run, or"
|
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 "[dry-run] preview a specific target with: $0 <target> --dry-run)."
|
||||||
echo
|
echo
|
||||||
@@ -391,12 +405,23 @@ cmd_regenerate_all() {
|
|||||||
local host
|
local host
|
||||||
for host in "${hosts[@]}"; do
|
for host in "${hosts[@]}"; do
|
||||||
rm -f "${keydir}/${host}_ssh_host_ed25519_key" "${keydir}/${host}_ssh_host_ed25519_key.pub"
|
rm -f "${keydir}/${host}_ssh_host_ed25519_key" "${keydir}/${host}_ssh_host_ed25519_key.pub"
|
||||||
|
rm -rf "${repo_root}/vars/per-machine/${host}/openssh"
|
||||||
done
|
done
|
||||||
echo "Removed ${#hosts[@]} host-keys/ file pair(s)."
|
echo "Removed ${#hosts[@]} key pair(s)."
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Regenerating fresh keys for every current flake target..."
|
echo "Regenerating fresh keys for every current flake target..."
|
||||||
cmd_all
|
cmd_all
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Commit and push .sops.yaml + secrets/ so the remote flake is current."
|
||||||
|
echo " 2. Push the new host key to each already-running managed host:"
|
||||||
|
echo " scripts/secrets/push-host-keys.sh --all"
|
||||||
|
echo " (this also prompts to commit/push if step 1 wasn't done yet)"
|
||||||
|
echo " 3. Run nixos-rebuild switch on each updated host."
|
||||||
|
echo " 4. For hosts not yet deployed, rebuild the install image (see"
|
||||||
|
echo " docs/auto-installer.md)."
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
|||||||
+168
-87
@@ -1,135 +1,216 @@
|
|||||||
root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str]
|
root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str]
|
||||||
nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,type:str]
|
nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,type:str]
|
||||||
nix-github-token: ENC[AES256_GCM,data:OfNRGJg16Ede6EilWUetCs9za+xk5/Lsa3SpVajsqz8PMdA1xQNeCWdX7ZAMdijHClpBhU6ETFGsXvt41O9aORS951uijeGSW7/NH35/bnPISrKdYeBx/+xEiqwH,iv:QGU3v7xOy89uzRTCb1U9ICyJ8XYIpXrUsDt12aL3g2Y=,tag:Bde2wcWNv8H4WLxSEUAodg==,type:str]
|
nix-github-token: ENC[AES256_GCM,data:k1vYz7SqVhzpWa6jTL6NUD8lKOCpHCgTm+HT4IcnbzbSTUZP/bJUYw==,iv:UqAULZnr/4+VcioUDfTwvOSuwM8K9JgGhiApvYQPyoc=,tag:1LKHXhWAO/AHPDIZFBb04A==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIakJkWDQ2TjVIS1Q1UlRJ
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrMGd3ZVNlNXdmOUZMUzdQ
|
||||||
elVLNmhEVUpMNjROWmN6VC8wTnZzeWhLb0hFCmxmTXp3ZVdQMUhDeGlscThSdFhP
|
TW1acEs0NFA2Q01rM2dkc1h0NHkzQmhiWFZZCjMyK202VWdlaGhsZW04MnVwUVdO
|
||||||
eWNZSkFpeEg0cUUxbGFyQWIrTkJkc0EKLS0tIGRBTHdaZ2d5eHhxd3BtWVBLbFlo
|
alA0Q2FETThsYkhSS0hKdHBaS3VaY28KLS0tIG1Gdk8yalREOUtIZTUyY2p1UHlJ
|
||||||
OVJlYnQ0N09qWFp2TmtXb2E0Wkk2bzQKTxA4rfkF3qlGpDqaZF/J9tgTx1UZ7ZFt
|
eG5iQnJsaTJBY3Y1dkw1c0VEaDQwdDQKfV04fLy32Lp2ZQ2VnvQ0h/Vsf+qdaJiv
|
||||||
W1KevMq/Klnkjb9XDuTEx7zgrjILoViVspe6eGI14myBNYtAP4nLSA==
|
DnLXGZ9hE5yzpKWkQIRgqYGBkF8PkH0YC4OIaVkA53wrtjqS4ZHR9Q==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUWXYyUG5UdHdFa2NDZmtt
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWFNRY2FiK3VkSm1RdHBn
|
||||||
ZG1GSmxVbTg5Qnh4UGxJaitmTmFqK2J1cUV3CmdPSU5GTC9hWkROTkJzMk5Tcmpw
|
cWl1ZStLcTRFZWY5VVI5N0FhODZvR1ltM0FvCnBHZUtTUm9QeHNlbVBoZEx1V3Fa
|
||||||
ODJzdlIvNkp0ZXdZNlRlUHBkOGVoR1kKLS0tIGsvUWxpU2hkanFNaExJWGozMTJL
|
Nk9iMmJKVnhocEpERi9leE1ySUtNMFkKLS0tIDRRYkxnbU90S2RyMHdJNzRJNXBi
|
||||||
bndPNkpQNktmQVNKNEhGOEtHSlRBRXMK9rr6NHf3H91GIqTmckjD1eV94FW1vk9G
|
QjRmZFhVakVic2tYODZHcWtJRmNQTDQK7G8eSJInt11P0DiL9uzNQ/ZHHLVNIYPe
|
||||||
h2KauyebWPyBe8hsCExiPd45ZGqKF0g8pEtWUzonMq9NU/MNVL8YVQ==
|
bvlhuGkEuQ/+j5sVSKOfSI2Y7CvM7TpE3APyKBcLG3ajYg6F/Ev3SA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
recipient: age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXdnpGR2ZnaFRSUUNlOUVy
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1S3dOcFdwV2NoMW1oMnY1
|
||||||
ME9pWkFJRTl6ampuZTR0QlQ2TUVEVHV3dkRFCmxML1NSV2dwanRPK3AySDJiUU9P
|
VGdxeXJVR1lsbzNVZHlGb0NGOVo0SndiakVjCmg0QlNnZDV5RlFja2hCbVRXV1VF
|
||||||
a2VKZ09EaktYU2xvWWpESWtrU3oxalEKLS0tIEt1aWxuNmlaV3l1OGNqS2RtMmhm
|
S1ZtbC9KU0U1ZW9zeVoyR3hxNW1XTFUKLS0tIFUzWTJhTzM4QnpWV3h1OFU0N3BK
|
||||||
Rk9SUEpBaXY0RG9uMk53ejdJdVZ4NDQKfn4paPsHrfU3Ki2AgPBB8aLBbmD2yh1O
|
RnF1N2k0S0lIVitoNDJLUmZqdHRzZVkKUfNg24p8zxb3749v/A1BOKCNw75AUKpf
|
||||||
9rDxv/6xSsDXNTquP11smPOKsRG7mDMDHVByn6GieZrpSxUf9vu3Iw==
|
RUmFCw5DDWF2aNM0mZqcjjVmJ/FRKV2HXwwUGsHPKSOTnKfOUlPNKA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArNHZpMDV5a2JCN1NxY2hF
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzazl5MUpUNEVNMTdLWXBp
|
||||||
MWxtNUNVQXp6K3lWYndTRU85Wjh4YlN3UFFrCnNZbHA2Z2RUSmxITm4yeU9rYnV0
|
c1hkaVhjcTZXTUNsWS96QVVWb0RVQlZ4VlNvCnE3TFRySU5jTFk2WjBONUQyQUhl
|
||||||
TTFkZUczcTZLU2pJRDQ3TVl5ZGErWDgKLS0tIDJGbzZZNjQ2RXUzRmUwKzdiUHlK
|
U1lNTDFTRmZhMUFyZmpVY2xpaUVxRW8KLS0tIE9MQ1M2U2ZXQmRCVll2UGRWL1RG
|
||||||
TEU5VERpZ1p3bU1KcnEvRDRCSVNqT2MKQPfu0lskXaEAYgecmN1a0kPHF+fGEm7R
|
K2tnU0NOKzU5dkpiekF0Vk1VM0ZZZDgKK13aFypGAqrKWPOr3UwtXI1EoXf1+UzS
|
||||||
wiY7TFNLeXM5aJqEnKARtOotJDetI+9VNssTT21X/Qaik6fqgM+b1A==
|
rBqcwnX6WPxSKUwWoins4Aojek4QhbhY4R5ei6rRS0KEQeryGxy8bg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOUlp5MmcvSFRnaVdWZ1g0
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZUpVT1p4TGZyNC9qMm5G
|
||||||
NmwvS3pGa1JCd0RuQnFMeU5wZDZYK0NQR1FJClF0Z0VENThtTzZ1OUtmUjdDenV4
|
UzNHV08rWURjY2lqS3FTWFdoc0FYYTFjb0NBCk1mZ2JzaXk5RmExNE9xWGZ5K0pv
|
||||||
Mko0T2o4UzVubVRCdUlCMERyV2w5WmcKLS0tIGZUallPeUhXUGtVMmNIWWRkaFVT
|
UEtqMkltV0dIWll5eVBUVVRNOUNDWUUKLS0tIGpKNVJudUM1UGNvaGl1UDBOeFA1
|
||||||
TVNVTjhiQzJkdlVQN3p1bnR1aUYyQmcKVnqbCuaSYEA7stk1MyfCzRbqt7EL+E5/
|
RjUyRlZ6a0Y4SXNsL21zSURVRk9KTFEKU1L6BQ6ZlYQQtqx3uF/uM5CQ1ercmvRT
|
||||||
jgxraiFmaZqjDI5mxG/e7eFdXfv53AtKzZm92TlzvV+4bfidj5SvYw==
|
TL3r2/Y07gE7CjRn3pR9z0co8KndGzxV6YR+ubyWptwBS8KQh5stkw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1jy444f9d9stygj4p3w9kh54cqcfr654tvr75tdvee5cxsgtdtc9q3v60ep
|
recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCWTdCWHhTamI1VHpvUzZ3
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsaWxxWS9xQXViY3VnUEx2
|
||||||
M2VnV3NFZTcrc2Jzazc0V3NsMDQ4RmtYZ0ZrCmVhV0pweTVLdzBLRnlJZURwNXVT
|
V09FbTI0WGtNbW0yclhOSGZDbG5NUTNaTkFFCitkcjJ3OE9BSnN4bjFWcE9nYVBk
|
||||||
YzN1aHZyTmRqc0dtYjJWekRaODZoR3MKLS0tIHZPV2ppSTVwb2VHWStRY0pKSTZ5
|
ZzMyVHlJQ2wwdU5JOXdCQm9oNkhNd2MKLS0tIE9tRzFYS05vSkUwWFRkaTdtc0k0
|
||||||
MXVFSE1odzRMeW9LQjVQUytBTWhHUDAK+UUowhIQ3w5O4Y/m9Pq41X2l0DZQfzOT
|
blVoMWV0QklBVkluT0Z4NHYyS1F0blUKO+Uc0of/V77ZUZOsxTzeH8/LmmAOQt+J
|
||||||
itFkXJRnvUmkCxWYRDFJjQ6FpHHaNDqs0BlQZ5QZ+chzcKuUAG+uEg==
|
x/COHxnLCnZ4eWI6q1a0Qn5Br15OJYTxUI2QTV4goTnXBNUDo9wdpQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2MEZxZTI3Y0xWSDdzanRC
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsLzFnZkdVdGQ2dHRwcnRt
|
||||||
RytNV2x0UkY0SnRzVnR0eFNvaDRJeUtpZjBjCko2T09qSTJsQm51aW9pR3JUbkRm
|
Ui8rc1NoQzNKSHVzTk0vYU1vMlRxcjBKZ2hNCmhLYXVGSis4RU9HNGVxZkpvUkd5
|
||||||
NWRXODg0L08xdGNLdCtORXA1VWRteTAKLS0tIC90bkZEd2pOYWFCeGg3UW54TWxH
|
L0xvalhDYTQ4N21OcHRheTlkaUxvTkEKLS0tIFVkRGxtLzhQT0paV2U3ZnNScVdn
|
||||||
Y1liQWVWS0t4WXBROENGNnpDVUpxRVUK57qCQ0l5Gw1ZqM50XSBNwVlXkue3QOT/
|
alZnaVppeGI3OUVscGpONkk3YTRXd3MK61na8x5qX7+dyMHasDz2dj7yeaUlX8me
|
||||||
BPmPFkMNpVIcUDENj+mJAS5GHuEm1MkZcMi/wN9Hp2KE05ZywPR2Zg==
|
N4/SIk1JDBhv9G7mdKLbKhSF1UJrSY7TJqJqx8/dqEc0uG3vptA1ew==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCaGhvbE9EU1dMZitXYlZk
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZEtvOUhMU1FRWWpJQjF4
|
||||||
NVBEYWxHbzAwQ1FUU2JidjJPZDZLVDZLODI4CjYxaG9OSlVOeGQ5SmlGNWEzNG1n
|
NWphNXp5M0dLZXhkZndhT1Y3L09maytHazJVCm1MeEtMWXg1Zjg3bFVnZEorci9J
|
||||||
WU9PRVpVRk9rd1BvWDNnMFcrZFNJZDAKLS0tIEUzV0E3OGE5cWM4MjBwTk9Mb3Np
|
bkNZQU9Ta1dDTFFHaGFWQVBpK3pYRDQKLS0tIEhPVGliRDR3ZTF2aEl3ZnJEYWtR
|
||||||
cDJQZGNqNXlySjRLcWF4R0V0ZS9DVFEKS0CmXOfJ1qpUF24EJT6F5/6xzR7h593O
|
anh0SEpnVW8xdXNkZEZQSjcxU1BHMFEKVRJUA71fi1QawB2TnuTWMYhzQR18u4M2
|
||||||
mLiMdOCeFYWlTtBwrD1dkweAzStiHlzTVEq3w9BFvAsr5x/NbQJUJA==
|
s1V4j4TwYyyKZFoNvt8kOUayjC499c5OBUufYs6G2ciC6gK2A9E0EQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzOUxYa2xOYlVYd1lWclR1
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTV2IzSjBEMjUwZjB0dCtj
|
||||||
bDJFTEg1SjBhNjhFT0IwY0ZxZFRFTzRUVlhZCmpwL1VBMElZNzV3UkFCV0FWb3Jr
|
aTdZVlEvYkVRbTNjUC9ZRVdNZVhPQTJEd1ZZClc3NDJiR1BVYkZkdVVxMVZGc0VN
|
||||||
SlN2dTNmOFRXeGJVTFgwdjA4SmdSQXMKLS0tIHZOdjFzalRkL2N5eU1iSDkzbHYw
|
dWdSSXBFR2xxR0xrV2thRmUwSS96S0UKLS0tIHRRTXVlUi9UYnFRRlhsU21HZVY4
|
||||||
NEU3VWxBNE9NZy9hVFJwM0VnQ2RIbjQKc62J04UtVjqiU7p7GueMicdCDRTvM9zY
|
SDFYd0NwVEtVZXNsWUI1a1ZZU2xNRGMKuQUhOq2FRD+PGn5OkdODZItbxCzRKjne
|
||||||
IPOJSwTCatRMWeuBJIsRNkbyOLeSAesQdfAXL5GoAE8mBtBdhJKaLA==
|
E60UOYtHjanuGjJ1svuR9cYsLZz7lLOwItklecYaQYpMRZEwzzBGCQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1NkppWjNXRDlsVXZ6b1ND
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSjJoUVUyd1JqRm1ZQzZx
|
||||||
OExFRXVOcXVSWmZXemNoK09EcXBHZ0JMYnd3CjlsaEM4MjdJT1lBZGFCZTNISHZV
|
SFJ2cWNUY0E4b3FndVA5Y0hIOEZnZUlVWlhBCm1vY1luOXZBelRUTmF5Y1NMeDBn
|
||||||
MjBRTUlTVkZBQitFc1I3ekRLN0RtYzQKLS0tIC9oelk1TTRzeFNpbWQzL0pObXJq
|
cE1BTDErc041UjJCWTBQbnk0Wk80dkEKLS0tIDVSZzd1UktvZGdyanFUMkVORUtl
|
||||||
eWxHbkF3anVPRFgyQ0kvSlJqQTdLQW8KLfZ+UIzas43ROGO51KKOmy58R2tl3jwh
|
eVB5TnJkMlp6dUpXSTlxRlplZ2NxUlEK0AYOxIbswjM0SUASDfmZ7PqcEU844fgI
|
||||||
M+1WXB8vIXA7DLQ6vMbzMPMhBnJ4kbZgRtxELg7fzSLQStLvkY+O9A==
|
ycFWVSEPodwUZ6UFoYXhHlJzHFcgpLvwUd1PMktLHe1qrZ7GOQJIMA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1fxxzpnfse8nd9wz78ht3m0plrmraacf4cpga0pe8fm2tdnqcgy8q7qsyvp
|
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWVm5uZU43cFFLcC8wVkZO
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPakhQcE92ZU0zYmk2QS9D
|
||||||
NmlOMmlocGpwbXFkUFlCMEl5Tkx1QWJIdVRjCnFBTzdWT3NaM0N2eGlCbDV2NUU3
|
TG0xc1JPZXZCZ0tZOXA3MGNLVnBlZGVtRFRzCmIvMHhQKzFVWCtpMTQrQUhGVGJp
|
||||||
Z3FnYktsZ3NHT3Y0b2hOd0xmRjlLMjgKLS0tIFBQVXVDeHkyeVNNek5CSldSMFlY
|
RU1jbldYckw3TXI2SlNpZVBIZHRsWWcKLS0tIGtJTUtJejFxem5jajFQUDFTQWU1
|
||||||
a0Q5bHVlRHUxMWduMCtLRjdnMUw5RG8KDJX8I21+bUpkJ5wnX+kGhsSa8mKqsR25
|
VnlxYmVlNG04ay9ETi9FRmVYQXVoRkUK9oFNolI7jRjo9RUs1g4ghrx7aYV4U/ce
|
||||||
iNdOtVtb2WRxIAgyjR72yp1tLMrop9NkZN68MGvd/LsNM+6simyosQ==
|
ZTc2tFh57+7aKgrDi+2W3jwhfkjvBsThk//p5mLlqEEgw2lwlnhvPA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHcmIxZVc4aGRmNEhSY3Iv
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoVnh6dFAwTkY4cHJpaEs4
|
||||||
YXhzLzduWFdzVVlKZTR6WExzcDNtSWQza0FjCmlsZDVJUWpqY0pRSktQeGk2SlAy
|
QnZPeXZHK0tXYWZPNytmYXVsdGRWQVI3RlhRClFVb2I5OVZzZFNrRXFaa0JTUkRJ
|
||||||
cjZXL0JCVFRtQXpHNkNIejhyTnVtWjgKLS0tIHBSZ1JyMFBJVWZ3VWFLOUt5OHd6
|
OC9GQ1V5K0JhWlhkUjU1WStCa1lPV1kKLS0tIEhzdnBBZkRnK0NtV1FuTkVsNlgv
|
||||||
YUI4ODhHc1I2UHVRQ0diSnhsSUhDelEK3hBBX1+Uwe/MusLqmt4oAy7Z6jOU96Cd
|
QzVEcEVkQm5NL0Z5dUU1U0ZFaTJITnMKaWE9vlrOpQstr6FGP5ObdilsCYk4kYAj
|
||||||
7zTe8YyLc0/DUDsyuFZ7a68riO9/My+zrFllwtKe/JPmPHLY0+3Pnw==
|
/phboR+Ym7QDTyUF9LZXJCU54YJp6vEWkRnlJFqC75UW/v/lgBhBMQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZSE53Yy9FOHNpeVFtTmF1
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkUHAzWk1KblJxVVZZV3FM
|
||||||
UUExbzJlZktxMGR3Ukora3RpbVN2THlyVjNJCnFpWVEyK2U1N1diOWZQWm5Bb1lD
|
WndzRGRtbkw2azRVWnBuZmhKWElqRFk0RENzCk5DMHlMVWpwbXEwVUhkaFZUbkp2
|
||||||
djV0U3QxamRJY3BRN3hBTmZ4SVRiV0UKLS0tIFU2Q0o1UTRjaEFaSTRVTm5MVW5T
|
QXNlZFV4SjBEdmR6UEw0N1JOUnhNKzAKLS0tIHo5RkNDUk1ESWRHQmV6bzkvSTlP
|
||||||
QXdEbVB0Y3YwM3R5dGp5d1oxeUxHNGcKRj5hNLlXtZoT3IwXHTxaReJLu8k133n/
|
dk1GQ0Y3V0dTRlByb2xUOERVOTVwbVEKY4sAHyAhvGSYJzPuufWUIQD2xZcSt/nX
|
||||||
ZoCtv470LPL1M/kTjdPc/nWMgYOHDKkO90gr3WILfHg8idkVuCLT2Q==
|
t2ZFXu891/QdEzyUXCIzdwAV+Y/LjvroIlCp5Hkbrk0s7N+ghqsB1A==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQazFqSWpaVGtkNFhPU3A5
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBYVkwYXpYSjdmM1FpbDl3
|
||||||
cE1oVXEvSU5RNnY5NU9mbFhGOFQycVFuL1U4CnNwRnVkYmxCNFZzNVZvM2N5eVN0
|
QWpycXoyL1AyOEpZUmtpbjl3MFAwTkJoOWpVCmVDd0FBUWxaQmZCU2VmNkZGMk9o
|
||||||
clNjUWY4RHZxbVNsMFlFSnZKcmI3dm8KLS0tIFBDZTRuRlprdzdVT1ZHejNsNDBJ
|
TUdLNGtac2N4REg2eVF1eVh0WnNaTE0KLS0tIDJvcFErSjRiWmhPMmpadjROOHdt
|
||||||
OUVjclpPSjl0RGtLVlZRSkpFeG02KzAKt0rcJunZppojjijrjbXsztLwRD9pgWRb
|
NXp6Y1JpdHFlSlRoa3JTaEt3emdnalUKjoFfZAiKMPF3noX+K0+vc3+p/XUHnhic
|
||||||
jYsn7dCvWGFZGVgeyxqfL0jfYNz6dW0yecRE/gbP5hIooxbcPcxfag==
|
k888KdUwcZYl2/dAIc8UDSggbMnncJAJgoezoCHLkj97GNNAD7E+gQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ehkswwz2pqaz4svzh7ela5tdnssl8kn6d4vwwxd6zwg8exfpd43syyrrjp
|
recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
||||||
lastmodified: "2026-07-19T02:30:40Z"
|
- enc: |
|
||||||
mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str]
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqYnQyYzF5cktZNG5PcThy
|
||||||
|
SEszVEwrUkQ1VVRsM3pSTlRQaHVLN0VuSHhrCmN1Z3pwNlFsbDN2UGI0KzYyallM
|
||||||
|
SHJ5eklQeEIxSlhiYW5PUlpJcG5KNjQKLS0tIEk0QkpMdlBlRjVYMmJaMzJUbDNm
|
||||||
|
K25pZldwd3JoZi9vdURoa3Myb2RQNG8K6N6bO2YKooPfpKihgsYqilfz/yAYCLZD
|
||||||
|
XJ/THgT4URX2VNvSspvBtN8luOiJUVcchp5WtL2m9jARL5txEcDorA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvY0VpeW1nazVvRWJFaGNU
|
||||||
|
RGVzVjRmWWV4dnJzRHdsaW9ENVZYck5lWGtVClNtd3ppelowRjZpRFFSMC9EK09n
|
||||||
|
b2hqNnkzejdrTnhYNGNKblpteDNLRWcKLS0tIE02bWVjazRWNEVKbURITGlQODlR
|
||||||
|
cTJJVnBVdGIrdzBoSXExelNrVk1XcEUKc77o2EX7PCm/HjUo5GsUiQdm488WB2mg
|
||||||
|
wHd/qDbQhF1W75RrVTuIKgtEtrRjZqpmr8toe+aHJizPofcrToUfzw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSMmZnd1pXRWh5NDVSN2xq
|
||||||
|
MVhTN3N1OFpkcUh0a0tjYzJnWUlINGNVd3k0CmZkTnBac0l1dllxazdLY2l0Rzli
|
||||||
|
bE9sNTBVSkJNaWF1T3c0WktoOHl0NU0KLS0tIHEyeWZTUjdQeUN6U2t0d3JwNTBX
|
||||||
|
ZE1Za0tXb0gwc1FSakVYdU9OTHkyd28KkwmlzSYP8XofB0VGag+S18+S2TyQjLrM
|
||||||
|
qaXtbBtLzJGNDhe9FhAKTPFcjTLWbohlG69vxcImyCyCns+QQ+gvug==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2Q2RMWmZVOEwrOC9VcUxp
|
||||||
|
VVE4R2NqYlZWZlhKSTBHRks4bDNoaUliMWlJCm5FbWdZS05GY0VLc0sxY2x1U21V
|
||||||
|
eExwK29GVVBqYlRPZ0l5RWVXRFhRNlEKLS0tIDl6dVZJQndwVStFVEJnRHRyMW1W
|
||||||
|
NnFqc1F0SGJqT0xmREpaN21EdnlJK3MKRPE5rfFpVnH5wAOkuB5pNMlMd3omcpku
|
||||||
|
do2hFZwyI7t80jxF4+g3J7EolOx8AGjpc9Ba7Gj6IMDjye728q5N+g==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhSG94VE96TVlTNkEyclFE
|
||||||
|
SmdCSkJpeWlVWDFIMDhwUEkxL1RUTDJ5UG5FCjhKRDB6VGtwTVozdUVzbUxGL3BW
|
||||||
|
SnR3cmpSN2RxNnl4QmNvT2lkYmtoVFkKLS0tIHd2V2h2Wk5xOXlISzhjVzBsVkhz
|
||||||
|
VVpRenVnSVpHUWJqV0JHNXNWWXJOdW8Kv7PJSTDbwFOAcl7pynALaJiTXU/87bSF
|
||||||
|
F3HQllYOwOoibGzBCe18H2N+VxyNxoQL9OWe0TvOIR6bgHFIIF0/Dg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsMEZPUExRVHFFb3pSVHNO
|
||||||
|
cGJFN1ZzTDFVNEdneVpMZ253ekFJNjVtYkNnCnRGQjU2Q3dsRGRFV25LQ3pCbTJE
|
||||||
|
OTROaFBiT01xb200S1pUK0NYaTQ3R2sKLS0tIEdiQlZTbi9Vcm0zc2t6bHplZktF
|
||||||
|
ekRySENXcjBuR2psdHZSSUJrR0xUdjgKvBsmnC+cbq5TUDFjXCyImIoPKvh8wsjE
|
||||||
|
7Shk7Act8Jayrhx0lXBDRmfpHRrB4L16rDSmqO0DTE48VhT3TiFyug==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxNDlya3RmYzNhU2p6RkVw
|
||||||
|
cHJVWWY0Yi93cW1uZi9FWkVONmtpRWh5SzFzCmtWYTRIY3BkTWU5R2Jsa0ZJK3kz
|
||||||
|
SnZvZ3YwaGtoMVZ3V2laTlBBK3UyTmMKLS0tIFdpZWtqeGlacjlLbmFySHlSUUlj
|
||||||
|
RmRqQWVHK0FUT3VDbFhLbXQ5WDhLeEEKcDkgV34lUFJRIHRoLB8F2IOvGAM93sM+
|
||||||
|
AkmaM4+WRcGeYWQKMG2x6cYCUKFaT1lDXuWZ9kI8Fd7b9gTSnQMs6w==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjeFM4ajhHeVd4bzlqZmJ1
|
||||||
|
MUxYMUpZVjdjR0NGTzVteVB6WFBQeFI0ZkZrCk55TEt0Zjdwbk51RnhYclNzam1H
|
||||||
|
cExKZXQxWFVLa1pDNFpkcGZzcnl6a0kKLS0tIFJ4ZEdJc3JVaEc5RU1aZk1uYm1l
|
||||||
|
d3RHS3hHSkRKRXFnN21FQmh0TlNtNmcKdc2G/1dhTJen6iT9kUWZM5OzCmDVprgx
|
||||||
|
WN1Bl3JzYhLsNKn794887bVAICVqbXqkdpEZztNIS5n/Rw6geKsNvQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYa0gxNnBwNjNNTTgxKzgv
|
||||||
|
YkQwTjZxb25tQ053Ny9tSW4wNGVYYnlIaFZNCkVvOFNPOFkwQzhYZGxjb0FOZzJ5
|
||||||
|
ZjNXam1ZTWZrS1M4cGhhcHZaT1NjblkKLS0tIEdvL1dDTHpWcWF0S3ZqNkxrQW52
|
||||||
|
VlYwa29sZVloOS9qajJWQWFzY2FKRmsKy074SLdttogXsWycaFX8xso4ek7Cbjph
|
||||||
|
MMEhZd/svmnSiYM81nmeaze7qXEUcsZXuSmZCYATTBEGtx/Srll8aA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||||
|
lastmodified: "2026-07-23T21:15:41Z"
|
||||||
|
mac: ENC[AES256_GCM,data:qFhnPra6IE3wyKQ4WKweON0S0YtD5I0adGZVfA0m6BVilN6bX5oC/1j5NK2oHrsz920hSl0SOF8LrpqOrUyGjSRkPsN4kq8qr9bJcrX4URiktP0oRden5LLt6hf+ZRP7WmRXFqixPkPHJnZIoAvkNnTFce7cDq5NEAHkKUEKG7k=,iv:nyblUDGeu3TUfFivYylOn3C/HITj99qiPI2+mh8AGh4=,tag:FrtRzSCylC4wlIoqZdfx7w==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.1
|
version: 3.13.2
|
||||||
|
|||||||
+35
-17
@@ -3,31 +3,49 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrWFpRSURBR0gvRzVIMElk
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzTFFlZHdGUzk0b1Zva1U5
|
||||||
ZlJGZlJvc3lFOVJIZ29YbFp3akFCNG42TWkwCjhNM0J6cjIwRXBzc3VWNVpZSGdL
|
V1UwREEwS05icWxYNEdKTE8rQ3lJMjM5YXdzCkw4OE4xWVUzVWZMaXg5OFo1UG8z
|
||||||
Q1M1bm90OE1tTG9GbUxvS1dvRkZ0UGsKLS0tIE43L1dnbWhQOUhhYjg5bEIvZkVD
|
WkNwK20yb09rV2VVSENwNWUvTmhJNk0KLS0tIGVPWUhFS2RDcTNjY2JLaWxvcTZt
|
||||||
Zkh1NDhvZjlDc2c0cUZUYVRLdEozS2sKU/r6JnEnUs2WPj/J724B+lgiV84iteZa
|
Z2RscURQdDVCMUdiVng2YWRMSlEvVVEKmyd3re6AaKn4gBjoT0x3e/zJznvJFYKn
|
||||||
uMlhgnwYJFLkH7ZyydQqjYcHL7xEInr0taYJN+M0nZIsZTvzAEi7iw==
|
ugKu3EsUX+gbailPmY1ss9+MVtpJFGZa2FiM0x1wSMKm6UJH0aPhVA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXZmYrVTZxWW1QKzhLWThX
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtK2tud3pWSmd2aC95ZkV4
|
||||||
UFAwSEhId24yMitmVk1kT215OGdqbGp4akFjCjRzN2dkTURXS0wzbzhYL0YxRW41
|
RkVvZXNrK09ZVmZsYWhqZ3JlaXBCd1NRWGhzCk5lelhSN2N6N2VhWEVXUjhLTjZH
|
||||||
TWJ3UExXaHNhSUxaYkVsMFNCekZHZDgKLS0tIGQrSWNzalhCbGJZdkxvT1N5ZWlo
|
V2VlMm5XdUtuK2d0MjIyRi9xeTh2ZXcKLS0tIDh5Rk1UT0dWblBkUXYzU3YzVGkw
|
||||||
a0l0Nk9DY1BKYU9ReWovaHJ0Z2NyWGcKFoIYS1M4EbR6H6QG3Wjv2ZdX3r2W8zKp
|
OFk0bkJ5RXZpWE4rK05QUHlLQktYSmsK/HsVEIhBEIo3qqVWdUJEWnHZiKB3uHVH
|
||||||
S9f578O5ZLh2OWaawcSb0oecZJykT9pgudVcuKRunzZN7NQvFxBRnQ==
|
R+nJGuXa2B/oUoxEwMP2YBHwwjLLiJCTYy+aQtiPdTrVq0YJ0HmC7w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
|
recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlWnpPUk10YW82TVRzbk80
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpVVVxWE1nUDBLTVFUaGJJ
|
||||||
WHJqbXVVSUFrSTVjMEdqUTdSQWwvdG1TcW1nClQ5T0E0akFtSlFGdC9WaU1LbGkw
|
Vm9EUldwRGcvbXhuT3JPd3N0WXo2S0gwRnh3CktabkdaSndaZUNSeUJGRzFKcjlH
|
||||||
allOSjJ2SjQ0MkVMcVNtdzN3cW1hVzgKLS0tIERlQ2l3NitPQndERTYrNVA3ajF0
|
b0x0SFdEQ0VqNWdQcEkxb0drVVJNcVUKLS0tICtyVGRqUmFtRHV1SlpXSVd3N2VN
|
||||||
bCtnYzhHcnZkalRNK1BHbjdIS2JLNmcK5RwFzeaK1KafO4cAtdFh5Tnz1lpZbj1Q
|
elQrVFdhWTJ1R2pJbjdYa2w4NmRmc2sKJHqLYdNQJcna71KNhGF80iS1hIYG1U1w
|
||||||
aeMmTGMkxJYVGzXKS6SRspVT3MvY4Fvay8B0MezjG5Y5HKNIv22Vog==
|
I2kihepJsrmYr76ld9k+u1ZfnuIuJ1ozYsZothE+dr4pV0k8s6wkpg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ehkswwz2pqaz4svzh7ela5tdnssl8kn6d4vwwxd6zwg8exfpd43syyrrjp
|
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSWXJxUEk5Mkp5eTZXamFR
|
||||||
|
SXZ4L1JTSGNaQjFiVXFGaEdvWkV2ZzBlVzJJCmd4NmUzZEdCbi9vdWdkVGZRL2Qv
|
||||||
|
cnVXa05xd2gzaXh1SnlMZmtueGpZMHMKLS0tIFowdnhFVGFheURQU1V2M0ZuM1Y0
|
||||||
|
NTJFWXBEYUxmOU9ROTkxWGhYUmlqMHMK3pexvc16BLKjh2meqtNm3M1zyLQ3eEsz
|
||||||
|
7C5WkdcSpCkW1lPDGtW7pEdAIL15StD4x7ut4MkSk0BjG1S+RpDbzA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkR0o0SFh1L2xjZ1RjSC9K
|
||||||
|
ZnFyb1lJbnlMbENYN0VjSVQyU1F6RDBrL2hjCnRwTUp6Y1hJWldVeFdQZSttZ1Vw
|
||||||
|
L0dCS0ZROFArb0ppVzB5WmV4bWI5alEKLS0tIDh4VDV2TFhhaUp4L09jYm52UCsr
|
||||||
|
NGh5a3VMY2ZMZVBQbmRHeWsrQnZVWDQKR1UeSZ/EzZEXMqyjB1I2SHELv8Ha/tmI
|
||||||
|
kJKs2WT1RtDhAiTrbty3f4oVrXWSYKZr40kNiP/RLbUcH1s65ys/tQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||||
lastmodified: "2026-07-22T01:15:21Z"
|
lastmodified: "2026-07-22T01:15:21Z"
|
||||||
mac: ENC[AES256_GCM,data:dC/oIqMUHkOh3AocOwP7Gc6XGH3L+nTqJfhFNts1DNbRXsopNIxVBtIz2pEhwnWSQrqPisDLmPHFBwRpGVn01u8w8IU1FKbAKC0J2nJXF8ozpInbjzDOmehqPWZG7yaKoq8cwAnp5XOk+IVO4l6tPxLxkExU5fT2ALuMq+sgOko=,iv:jaVyArpf6zMCFa6J9X1aQMGrmFq+W2CPZdWO6vVW68c=,tag:S+qF8/FkgHc4uW0e4ICmSQ==,type:str]
|
mac: ENC[AES256_GCM,data:dC/oIqMUHkOh3AocOwP7Gc6XGH3L+nTqJfhFNts1DNbRXsopNIxVBtIz2pEhwnWSQrqPisDLmPHFBwRpGVn01u8w8IU1FKbAKC0J2nJXF8ozpInbjzDOmehqPWZG7yaKoq8cwAnp5XOk+IVO4l6tPxLxkExU5fT2ALuMq+sgOko=,iv:jaVyArpf6zMCFa6J9X1aQMGrmFq+W2CPZdWO6vVW68c=,tag:S+qF8/FkgHc4uW0e4ICmSQ==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
STUB: run cluster-init.sh to generate, then: sops -e --input-type binary /etc/corosync/authkey > secrets/ha-corosync-authkey
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# STUB — not yet encrypted with sops.
|
||||||
|
# Bootstrap:
|
||||||
|
# bash scripts/secrets/sync-host-keys.sh proxmox-ha-server-1
|
||||||
|
# sops updatekeys secrets/common.yaml (allows ha-server-1 to decrypt shared secrets)
|
||||||
|
# sops secrets/ha-server-1.yaml (create with: beszel-token)
|
||||||
|
beszel-token: REPLACE
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# STUB — not yet encrypted with sops.
|
||||||
|
# Bootstrap:
|
||||||
|
# bash scripts/secrets/sync-host-keys.sh proxmox-ha-server-2
|
||||||
|
# sops updatekeys secrets/common.yaml (allows ha-server-2 to decrypt shared secrets)
|
||||||
|
# sops secrets/ha-server-2.yaml (create with: beszel-token)
|
||||||
|
beszel-token: REPLACE
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:z2bazlADvtWkGcY4an7NgZgIDSDq8KmYpSq8tcBleW/33HlElMNhdmje8m8hd7WxgrVvCQDdlAfodBLj2mjbgSwBptlPX2zbwcmJdm0g0Ope+YyQZucIio7NnBaYa+OK8meWqmxO2WEeOk+hnNfNPbt8MkzF/FMMbP8xv9wSTKP9CDdmO7s8rdg8G1JxVfn9dyaSDUTuA2JGNtbh+osd4CtNCV/bc84MjXDZTgTfT0W/uv8VH3iPNT7BhQbU9FIY4dsFRYBf758VtJ+3mTBiQkR38IevJBvhFB7Wiqn118LSBgjdTKJnBwmGo6lMdo0LRYHEgUnb2ejhWMNie19x+LEAErDki0RBmjc2BbnOF908kUlW2fUPo5fz/hlfqlhDLS1uOxESKhfnKt7s5qzWiMX/nohpknVfyMbmu9j/M571z9sgFnJAylpW3NLp53lX8N9mINwRGXsyL/ijbNE=,iv:M/MNquAGN+lIIyVVvFRgKR6DKAhLclc/OY1HV5Xwoog=,tag:eEuURMErSqsoh+l9ZaHY9Q==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqZlUxRG0zR3IzMlpDL3Vl\nNmRWQXUvc1NDek5wbWtKVEFTcTlxL3ZoWldNCmJVMkFtbDkxN1FGQ2VLTlhBUW5Q\nZ3VzL0UydUQ2YnlUMmtqa1Z0d0FSVFUKLS0tIGJFelc4Rm84T3ZHSGZDdnNUbUFM\nRXZHV3N3aVlSOXhUZk04UFRMWU5TbFUKhPU80PVYuDFUCxu1CA8+W8bqkr+Ne2fh\n+nBUPJbGxfN9TyD9tUC77AMbcL1R2L5x+SSAh0bEgSWE24/RjsuVKw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3M1o3S2FibHV6N2tIOFEx\nRytJRDFIUU05azNDbWlpaDdFQXFRWlBUNnlFCnBwNVB0SEVwL3NWNS9aeG4xSUI1\nV3NOcUFxUUhaODYyWXdFMEhWeFpvancKLS0tIHY4NHFQRGJwUURQbWZ2cGlSdm9s\nOFJodGZkekk3UnBRRUwxb2YrQ1ZPcU0K0tCapb1hfVHFSNpmESXexYa5k9OE9Tha\n51QpU4mZHKGrnWPK/kyj7rHiz95TLsmwUdA8Q2hOiYNSxEaVBjYDQw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkTmNrbUFoVFVGcytzQkk1\nMUxDdmJGL0xDcUNZaDFRQnUwNXoyMU90SEVvCi9EaTlVOGlMeUZtQzRJS1J2TTJY\neWdkMGNFMWZzQXVKOU5KcXVvalk1ekkKLS0tIDN2MlRQcG5aQnN3bUMrSDdvc09X\nM29hQ0pJRzk1amRHaS9mRlhrb2FMZG8KEWkSSP+MqGRU75qo7ctOCL7qHhyCM3l4\nL+ga1XOKxRsQkKQPozCi5Bm+k28W9hIDaC34Rw9difxICM9Z1rAbOA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1TUtxVjJ2STFpU3RNdWpJ\nVjcwUHVORVN6L1pKVC9TeWt4cTNoR2pPWVRBCkR4QVJ1bHFUa2Z5aWw1bG0vQWVy\ncjNGRDl5REZzbzdZWTFMRkU1eHFTc1kKLS0tIHNLVDFiRkRKTHhrSEx2S0J1R3cx\nSkdEQWJya1ptTTRqUjJZT3FFaFhkd0kKgecHrnzW9+Eb+b7c0z1yR+Y0Czsr9Kjh\nx1UUOfleHntJUCs8oYcOogKknEnhBJJoJ5oe/gWruRSwle1fs3cBqQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-27T21:22:58Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:VG2ygV4X6yMxGlJgq3sN4GAzgVqiD06noyMxB2yb/FCmiFVYZAH/9LqF//G7MTInfIjFrTDyoHAny+m/ztUrJkHXI1Fzg0U8R/zYyQ6wj/GguL0gyhA70uriQhvHsRHefHPa7Km61Blo9cME6CBJWejtRvh1IMw9itAioVrSIZE=,iv:zjxNG6IYxkcNqLk08NADoDiIbS6at86y136SBzadW1U=,tag:sj4ZRR7ixU8h7aOUJmTCYg==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-17
@@ -4,31 +4,40 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPWE1HTUhiSUp5ZEUwWEpI
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwMklZTFJuR3NYbkFvV0l0
|
||||||
bGpkZlBIMUo5ZlYrQ09SN3Q1a0ZkQ0ZnOEhVCnJPNEZQenVWWGZiODlzQzNEc1Zq
|
eUhMWU4vMHpnN1NKVThuVVdiOFpkZW9rTjBVCjJabWkvOFpOSm1hdEdlZTYxc3BP
|
||||||
c3l4OWZJTElJc2Y2UE15OGtEUzhyY1EKLS0tIHNJUStyWnlQWjZBbEZjQ3UwdUpz
|
WkRURDFEQzMvRFlka1VnaU9zRjhiSEkKLS0tIHZXRG9GaE5iVjg3M3I0SzhtN0JP
|
||||||
ZndoUDR6bisrNGJCUHk3TGI4bTZaMFUK87fFsm9ne9s+PK2pcwtrDjqyGBss2r2E
|
UlJVUzRzN3NEZWxXZHJ6RW1oYWwxS2MKonnhq7YDg4v93PZtoaLANDy8mdRCenjo
|
||||||
8lhqoeiKZ2j96z8kP/7ChzovwTCmqdcmAQuyNQD+ZAFijseipSvfbQ==
|
FjRUzozMLpgWBll4DwRWikejsrRofRBwlcsiIdrBr90f8Lr9pHdQ/Q==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUYi9SRFFGV3Z6cFd2Znk5
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsWDJQYzM5Z1JRT3FlVkJy
|
||||||
b2FLbWtzTllJMDBUaGk0NTViOTNBa2hQclVZClZHKzNhbGVjQUJhWkFWdTFBMG5a
|
TkVaSXdzanYwbnpOM1d2RFh0c2NaSUFmdW1ZCit4SVFxSS9HNXhVM2gveERGVS81
|
||||||
cUFJdUdyVG5HQXJRRnJId3hqRTN2cXMKLS0tIEFMRjh3WE1ON0U2TTNTZ3hxMTR4
|
NVFQaUV2RGR5Q3kyUTQ5eEpDVXJVMWcKLS0tIHdHSy9WM0o3Q0o1THhXZW11K2Vp
|
||||||
ZGRlemlIbDZKeExmVHROc3Eyak5DdzQKaLwIVDi6BN4cxpVxJoqTYvJETPOp4thc
|
NTNtSGM2UDFuWDBEdS9tbTY1ZWt6UlUK8z5qoi0kGn0ES3m9khummuU51rkR0Sb9
|
||||||
l9uVMvIGuEsEZgDsvShw1dYLljd+uGy/A+dXbcxIUCP/mmPkwmd1Pw==
|
TWT92+BWvPdNrAsDFjv0fgpUKyTMzN72EzHZKAJCIM3crUG9I0tX2g==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArOWovSW9DeFpxL0VDUDQ3
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpY3o4SEpwZEdFQmVnOTVV
|
||||||
SHUwTzJVZUtPV01ZRkdCUXZGL2lTRCtCNFNnCjBSNExqRW5mTEN5SFVucHJHSzZt
|
d2NqL0VudHM4VjdDK1N4dWdlS0lGR1V6a0c0CmJUTHlsbEMwaEU0Kzdaa21lRFRm
|
||||||
cDlNc3BjY3M1c1k1Z2tkVEg4R1pacGsKLS0tIEFWbHNKZW0vbVh1Y2VhQW93OUwx
|
RTRnTUlGUG9GQVB4U2pzTHdRY09udkEKLS0tIGlFQW9Wd0N5WFg2WUpCQzhWUm5v
|
||||||
MWV0eW9sOXdQd0l2ZjlWOEVVc1dwcTgK2s4p9xoNkawH2OkGsl80bNIo3ad5vn4W
|
aG1VVWV5ajBmc2o4ckgyQWpWaFVxVmcKisAw40bGQBRH+u6uNygfYpfb7iEgfEHj
|
||||||
Z2w+jwppSoUmbQnD3WFbLmSSxmuobmU8HILwElv6SZu+KE3aspF6XA==
|
E+g9n2WeVH8kUzD2O1o6VAu4m/SVuI4+IQ77j9GEWmlI/wgp8wKXgQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1elpNZXVJbmJWSU85aUV6
|
||||||
|
NG9YTWVBeWxiRHUveStTQnl4NC8rT2VNdHpNCldxZVJYNVhUR2V3Vk41VnJxenVT
|
||||||
|
WS9rRWlKcGpVdVJPakkwUTY2a2xQTlkKLS0tIFhFVTRucFNuS0pMK0FNRk1ndnFO
|
||||||
|
SlVvakczaktUa2VLY3RLYUdVRzFyamcKIhctg0mbYL7OE08dRwj5wMu2x+O8/BMu
|
||||||
|
IqA477+noQ/Rjrszb2hEvxID7keogcDUWMWQzQvdMc22+3mvAr9qIg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||||
lastmodified: "2026-07-19T23:30:21Z"
|
lastmodified: "2026-07-19T23:30:21Z"
|
||||||
mac: ENC[AES256_GCM,data:kLGE2xawQT7mx+sfw68hmGk5nCEGiEjZrqTEl9B1dtQmTrMwmoVr/1RISi4LfJrwxy31mDgff4lcIL4wIJuM373uk3X8j4RNyYQNTfKEkORT6r8NHeepNs267O77pKGd7OmcM4MT/BqOnB8ELS7Wlf2ect7CAlvUUVyc8icxgZE=,iv:EYLDsHYHZ1XOQXafOTqHHWpk/OBNq/R6IJnOBYV33E4=,tag:thxrCPC5oGvDjhK7Dz87YA==,type:str]
|
mac: ENC[AES256_GCM,data:kLGE2xawQT7mx+sfw68hmGk5nCEGiEjZrqTEl9B1dtQmTrMwmoVr/1RISi4LfJrwxy31mDgff4lcIL4wIJuM373uk3X8j4RNyYQNTfKEkORT6r8NHeepNs267O77pKGd7OmcM4MT/BqOnB8ELS7Wlf2ect7CAlvUUVyc8icxgZE=,iv:EYLDsHYHZ1XOQXafOTqHHWpk/OBNq/R6IJnOBYV33E4=,tag:thxrCPC5oGvDjhK7Dz87YA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
+23
-23
@@ -3,40 +3,40 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaYU9HR0lETDhkYXR5NWlj
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZeDQyNFh3dm1YZ2FTMjdK
|
||||||
L0diSE5JQU9KWjRoU3hrVjdqZ2tPNUtOdFZvCjdUVGNFbDVYa3pVSC9ZWVNORER4
|
cDBWcDE2a1c1S0s1enhWRnVuMlVJc0gyTXk0CmxWNkRJMDhkeGpRTGltaitnVkZS
|
||||||
QWozUlhoSEtjTk9IRlM3VkZoYlc1RTAKLS0tIDlHS01WVWlOMWFEQU5GTVRLZVhV
|
a3Q5TW4zYm5Ja2FETEhJcGF0N2ZKbmcKLS0tIDN0a1FqRGNOY3Y1UWxvUU8zWU1m
|
||||||
VTcwekhrRHB3SlVYT2MzOW5GbE52dkkKKCWehPhpdGapdyzpll20NJUcZwvW/7X8
|
bE9DVzZESG1HTEhVWUdJOTF0bDhRVGcKP6OoyDAGLB9jQ69jpFyho5eaeK9XtZgN
|
||||||
KQ1EqAgI2fewnbwuIDYCleN0b0SLJNUeSV/tFKDDoTMnHWCdeD4ECg==
|
RlSJpBm2Jo19h/crpH9AWXUAIG0BWueyr8mwBu12cQdFIU3IyZT6gg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4YndybFBTQ2p4SGZ4SDJs
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwRVFQcHpkQkNlSDhhM1Zp
|
||||||
a2p0eFRQOVVWcGd2a25ESW9ESGx1RWw0Zno4CnhpVVh3cGI5UjY0YmFINFFPMTh4
|
bUZyOFduQXg1cDd6ZlNObFgzL2hmUmJhM1JFCnprMldPQXJNVW10dVRqQTdWcGlv
|
||||||
b1B5SjJ3NTNvUE1QUmJjVFozY1dYS2MKLS0tIC9HZGNpOFZhZGNFZGt5blJuZXVV
|
RnBYWWFsaVNrMkJpS0pkOGlQQzlJVVUKLS0tIFNsMEEwZTREZ1lwWFJGdE5YSVVU
|
||||||
SXpkRzV4d2ppV3ZQZSt1dmxYNGVFMUEKmSe9dkrmkND81Hw2/ATAmFvcmhk1tUC1
|
ZEZ1bVpFMEQ5N0g0L2RacUpLMWQrVDQKxPzq6f960purgAmUJw6IZnZSnhkzNE8r
|
||||||
LxxBw54IVHUqwYKgRYUYRNu+pykDT5OnFDPiskd49Xso99LY87PyiA==
|
CSrFDowKTZI2KRdCtQ5fGhEoWO0ZPgVNxYV0KH7JBttylcpRLm6r5w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLd1FSSHVTSGdHWm8wMVQz
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0czVoT3ZyYTZhOEQ3RWtR
|
||||||
d0dlOEsxeGwxdHU0eUlFSUxka0ZmcVpnOTM0CndYNUVjVy92QVhNY2orQmpSQjYy
|
bVl1MW5vNERxUEpmNXV0MGVBRE9ySnBjT3pzCkM2aUVpZjg0SkNVTnRRMlhyMTN6
|
||||||
VS9KM0NUTXhuM0lCSDBZMWtISEdtWTgKLS0tIExTL2wvS3FEdVViUmRYZEFsR3R0
|
NlFrZDVKV09Yc0tuKzFzR0ZtQ2t6WkkKLS0tIG1mbUNFdHBycS9UOGc2cjNpeHVm
|
||||||
YTJFM200RjF6MjNxOFA4eHRpWVhtRkUKOqBIT445HnPXrrH/qV6FIuAhAuJmSL6V
|
NUd1NThRQlZXeG1WbmR5Y3pTYXRKc3MKwSnE+0bGmxOAQUje6jHxuzIIyD6ZAwVz
|
||||||
+PQopM/m3PAnK5m5Mu3cfjYfDiB8+GWTABhljfT+GbcoK7CqWLehrw==
|
b5AAYwbGRagKj6fimsHBUmi4ohyG1huIGGOU8HiUYpu4PGJgOscztg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ukpqxzl44mnjpy5r96sfuc5sqzm47u4k8ujjh5qdgy6jvl9uqgpspymqfk
|
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvVXF4a2NmaW15R2VXS3FP
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlVGRDMWNDZUR6c1VpUjRK
|
||||||
SDVPUGpUWTlIWjl2N2N2SXR1UlRkdllWNVRJClZDbVo4dUhrZytqZkMrYWtpOEZx
|
WDY3L2lNcWFFcm1UV3RPMjlqYnBGVEJLcFFzCjFxck4wdlp0Wmtzc1RKNS82MXpK
|
||||||
c0dSZGViN04zQ1B1WEZEWm1QM1lsejQKLS0tIE0wa0k5Rm1xZmw3OHFESkVXc25j
|
ZHBzOGhkc3ZuZUE2UmpUSTgycWdLSGMKLS0tIFUyYjczeUFWU2FyMlBTdzAxMTBE
|
||||||
NlFpYTJSckQ4MVlZQ01reDlkaWY3TkEKsmQlreRhRAjVZ/q5x52FNATDF2sLhbHo
|
VzhaVzlSL05nZzNmR0ZjNEFPTXYycHcKfiJ0KjdxtLWsXxsWKzAL+H3hYYjHrYO9
|
||||||
djOZigZx2rs6shqQ6It/XRJ8CiPaXZBPOt529Gwmplu+hWlAU1+l2Q==
|
BjKknq1ZQJM0sB/Tid+GLqDwKi966MQK+AwHF5MqbsHW7eE5bO1nwg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age15kh7akxlx7zn00tey79rq2g8lgs4j5y77rcnyfxrxap8ckfu0a9sqvtdhh
|
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||||
lastmodified: "2026-07-19T02:30:40Z"
|
lastmodified: "2026-07-19T02:30:40Z"
|
||||||
mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str]
|
mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:+/1AAVha+86abQIX8tebqPJ6BIcCCiTJgPe0OfFOwA2Mcx0NJdiPtQgyeo3G8fAolIQOFkk5reL+GUhlOz4iEbPOCWwFCckdH0tXiCiVcD35qQNMRurY8HmpM5FWFnyzBkKuyNZ8okPuo2+fA3NQh3gs94rpm2kBsfS18xvrlv9b8u1JvAxlH6GRMN1uUgFGmOXlpGAVjDUFiKUHN8tRSZpsxG0aKKPBZ82JdKgYfCiCQifS1366gIFrsjGriUC+P8wkadRnD1JE2ZAGSL7wJLaRmzA4LAMGXFGbitOsFxKxX3q8VWEXaaL+9h4rTe0WCrvmgDM9NUeVHpfRiJrLvQgrFZMUxuZF66vQVsLybjIWpYYJcjC6nfR0U3lMa9gjzOTMPxm8HdGKC53FhLM1HiPoe1a2Nno283fV0uaByMPmo257O6l1CXqb6KoZOGFccm7fKR/VnFAS1AInF+szI7/r+UcaTW6LwYeV3OwLE4a8b4OiqTLgCu6v,iv:kUlVXF4Yl5HGvoLwu9loiuuDVtz0kARRiUU3K+BPL2A=,tag:pVaGzQAX9Etoqc5XMGmNeg==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6OUVUdkoxM1M5THlYZ1ky\nYWJ5RjhtRGoxTldwRkI2T1p6dHR4Y3gwR1ZrCjRDS1hTM01lZ0FSalBYQ1B0bENs\nUytXNmlrUkV4TmJkMUM1QW1ZaExIcGMKLS0tIHdHKzdEcUxvM0ZYUEp0a2d6SHp5\nYXQydy9uNG15V2FhUUd4NEFTMTNNMEUKkIzKEoYzoVs+nhnpkHFgDkQqrWykatND\ntsNxcr1SXSKeEW1m/QpXZnn/aW3zSQR09PqOHf7PYU47/AdkwrUaAg==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByZENrck42dWJjR2hYUVBl\ncERtRjR6dTdING5nWXlBc3o3eXFhaSs5NkFRCk43QXFUeTBJR0U5dG9YTDRmQ0Yx\naEo2blUvUXNZY3dpbEZRTUx1elNzdkkKLS0tIHJwNjRNM3V6WktWZ1BlUUtLRTNI\nMTVEbCtYbllIbTdxTGozWUluS3pIRXMKZCruDIkD/JofdAHWgPuaaKTDsz408ZkY\n77mhO8J+kd03qwt6qhFC5KF1lyjhwEnqrOE195+R/8Yl7hA/DsL2ZQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaC95RjYybzVlU2tYdHdU\naGJ4M2IvNkJwVlNIU0VCZEFUZk5VaksrVnpFCmJXS3luTG9WejR0Rk9DbHA3dngy\nRElFWjU1N2xuc3JaKzQ0L0U2Yktib1kKLS0tIFlrK3F0ZFh5Ync3ejVWMzRKTUx2\nZDhxdEMwa3B4TFZUcWdTdktDeGt1QUEKfgYnK2lW3cZuJGaw+bAKDipLuC4S5vK2\nxK2eJB5TP/xXrp0F3lx9sc2b1FOY9Vt9IQ7zVlBqJFkzJrcw8zYJJw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwOGFkSXNxTUIzLzR1Skk5\nMGZveHFDN3YwVmpwT3VTYkppWEU1aGVpdkhRCjZtRC92bVphd0ZFbzFxVzRUcGk4\nMUd1aWdEbFRaM01FT25JSVRoMTRsNUUKLS0tIDAybmcrYjFVWkFYaGY5TFZjcjFO\nYlZoQjJjN2lFNzd4RlJuSHFlRlNCOFEKgUIPnL2/OJgz9oMYt86/llHa7adTkhs6\n8yGYGV1wtU9aYtUMIR907SfYyZ6M4z8jH2wwzpQLbwQQMLgkejl1jg==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-27T23:14:51Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:CYhzR0Y29GXvWUBbb13s16v9hDQ4k4Xmd4FUpcyNtHe0L/IrDGkx1WHzAohNGZjGMNB9W3BgiEH85OoOo8r1F82El4/8s2prEJi8AARvQU3+2cmRjjhNCCYQWmJkoF/cZNpw6nVyWzZdfUpPvHjbuf6utlI3rtR0qiCpSo/32S0=,iv:XLKTU2Ute4jMkfRAbXiVGxqP9+fjSs6HwRv1JfTTe7w=,tag:oIqS/1HWQZ7mLWaO7GwLFA==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
beszel-token: ENC[AES256_GCM,data:qg2eb7Eq/w7vIuiiDeK/s6h9nmHWuooyrd93GvdLD2hGOvka,iv:FWRBQTIY9PzV2V8J7z5ftEb5dvjN/S0hP0H9JV+Uuq8=,tag:WP7DHOu4elfqdLoW7vo9qg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZmozK3VjcENGb3l2MDNj
|
||||||
|
QVE3VVI3Z3R0MjduSlJGL3BFWkg5NHZmZms0CkNBTWZKZFJVNkJMZ0tpNXhXWW5q
|
||||||
|
WndvbmhuOVh5ZHhJMmJwenh0M2dEVlUKLS0tIGpXQ3dhRXVWUDZzdGg4RHN4V3p0
|
||||||
|
WDZnK1lMUHVIS09hRUEzVHRHQ0EwcWsKEd5NtstHSPH8okXcFLW7BId5xPGmgyC3
|
||||||
|
dAodIGkLBlJlaL1bZuRy6Vaac+mJZL5KJYD/o1kYSXRiHJfNpaulTg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXWWhYOFU3VHZ3S1VuRTF2
|
||||||
|
QlZvMkJUNDZmT1RDWWZsMkxnZjJldjdrRG44CkU0NEJTR0NhQUpjMEY4RXBLSVBJ
|
||||||
|
bDNtZ1UrRDNUbzA0NDJ1cWJKdlBBMWMKLS0tIE9vTmEzTUxpODF4SG1MQ1czM1N5
|
||||||
|
dVgrMERMRXJwVmhtMldTZUlHNUJVTEkKzUf9cETbo0K9bCPkrlbUkgkhRIPvjCHR
|
||||||
|
GFVwgb2fcaOzZHcDwlLDLaP6HbRPtzJfVbLpQ1XU0N/xijSr9ENB6g==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlOVdWN0tHNWZmaWlWQ2hJ
|
||||||
|
MVFYd0pMNmJTdTlJQ3h1c0dzbDE3U2VqdzNzCitCcFdhbjRPYnl3Q214WnRxa2Jq
|
||||||
|
eTFnc2VBcXlGcTVYdWpWTFZiWXBMUDAKLS0tIC9YdVkrTU5EeXNlNzliLzBlYUR2
|
||||||
|
Qk1ITW9tMElzUHF4MVJyWS9uRGs1QncKj8OgxWYabf8MccWD6zYLxl+FvYPAvLCW
|
||||||
|
p/FJ8uMa9YWp0RjICvWIwyO0gK7oaxTh/JzfBjABngEZnZazNYP7Uw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsbzFKckFQVDg0VEpJMFlv
|
||||||
|
WSs1ODVoYlZZK3YrSXlMU1hIQjdLTUhNdHhJCkRnbHY5Z2NGQVMvOHV4R2hTd0xu
|
||||||
|
LzdheXdZVVBsTjhVOU8zZGFKN0lPWjQKLS0tIFdaaG5SeElDN3YvMjFxdHVLWlNX
|
||||||
|
M1BQQmtJTUdsK2x5TEo5TmNrMEZidkEKhi4jNKhrkBAPhJoeYNg1d60LQTqmJt+G
|
||||||
|
Z/2X5uWzh12zF5keL8cNHH6sD0z0MOChtd4UF37yr+lb/LFJkia6LQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||||
|
lastmodified: "2026-07-26T01:12:11Z"
|
||||||
|
mac: ENC[AES256_GCM,data:0r68y6XmdiW7p/h9QmTR5h37TxKp+0wF44LP8QfG6cGP0tvaasuvv+V9EC3msnREYjwJhlnHjRd8hJ31HGVkOHbYWI4V5cF4ZnaiGgrZJhsGcJvOJSroAvQwev+vIlXZpAPgZz3Z35FDvOpgb71l5c0so0l4EJhc70JOh2FxhvQ=,iv:Or6l/v/E01+9wtZwyl+pCttiuReR6V0kmBzE9KhsfgQ=,tag:cAEUgj/9tKDToKFr00g5UQ==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
beszel-token: ENC[AES256_GCM,data:gjbT3uROiVKQOJaUeafTxjVknQO1Tvbyx/Pl2bTad7DezByX,iv:3ikf7OaT2omO8yd6G6UwYbaRBSzyvbn+NghxAe5bcgI=,tag:Zuc2EP8rUtdDhr5CzSW2Pw==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJL25EUUZack1FMzlnMmdk
|
||||||
|
Z1hnejZRNnMwVHpmWkFNdGcyeHVsNnRsSGtZCk9OdDhhcnR1WW9ZMEZ1OUVYbm1n
|
||||||
|
RmZRVy8wb1J3emJBK3Rrd1d4U1dYUDAKLS0tIGJaaElvSk1sOTBOM0lKck16OUtu
|
||||||
|
NnRZb3U0ZndmaHBZTm8zczhWdE1oaEUKkf6fLomAHoKPhuM4e9q96YmmH+h4VrEj
|
||||||
|
2x0rnwBwOoRzYWutB2MVtlsphAZmZ/PK0tEecT2MM0XXayVG/33qdg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpeEJvaHl4akc1TFdEVGV2
|
||||||
|
czg3OVprU3p4ejNpTktXZEpneDgrOEhrZGlZCmJwT1dhSkZneHE5UmR0WTd5UENq
|
||||||
|
VHJEWG1EekJLY2pRZldtVGtxTHlGaGMKLS0tIEYrWHE0WTgyUlIwdktmNzNIS3FW
|
||||||
|
ZVRvT1dHa1Vzc2RSakVISzdMTlpnVGsKeT+edn4+LUkVtpRUNd/gKX3H1HG2bvNo
|
||||||
|
c8iI6qr/l6oxfP85OrKYFDU9IGvDMxSSdbixHtojPEb5OKVurV0WPQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||||
|
lastmodified: "2026-07-23T23:32:57Z"
|
||||||
|
mac: ENC[AES256_GCM,data:l9a/yNRoxY1hvSkLuR4N7deeKue/1JPlSvZvJfCSNbQ21p1qR433BbSDYvfW+kXQXS8GVcfgXSd9ywNzgVvkA5lR1++uYsZBLbYxJ+s3TKWs6/yECAZ0eM1KBA0BEm7cLSsHTOwd+2WspvmCYir++FDO9XRuS3guiMnQBglDf/E=,iv:eE6GVqexQNSiLYfmTTUdUx5AO//wyjSIsr96xAX1pcI=,tag:CNsoyDZYLUt5Seu7W5wJrw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.2
|
||||||
+76
-10
@@ -4,8 +4,20 @@
|
|||||||
homeDomain = "sweet.home"; # base LAN domain for service subdomains (pve., docker.)
|
homeDomain = "sweet.home"; # base LAN domain for service subdomains (pve., docker.)
|
||||||
tailnetDomain = "tail13f623.ts.net"; # Tailscale MagicDNS suffix
|
tailnetDomain = "tail13f623.ts.net"; # Tailscale MagicDNS suffix
|
||||||
lanCidr = "192.168.2.0/24"; # LAN subnet
|
lanCidr = "192.168.2.0/24"; # LAN subnet
|
||||||
pxeServerIp = "192.168.2.247"; # pxe-boot host's LAN IP
|
lanGateway = "192.168.2.254"; # LAN default gateway (router)
|
||||||
pbsIp = "192.168.2.108"; # Proxmox Backup Server LAN IP
|
lanPrefixLength = 24; # LAN subnet prefix length (/24 = 255.255.255.0)
|
||||||
|
lxcLanInterface = "eth0"; # LAN NIC name in LXC containers (set by Proxmox --net0 name=eth0)
|
||||||
|
vmLanInterface = "ens18"; # LAN NIC name in Proxmox VMs (virtio, first NIC)
|
||||||
|
vmStorageInterface = "ens19"; # storage NIC name in HA server VMs (virtio, second NIC on vmbr1)
|
||||||
|
pxeServerIp = "192.168.2.223"; # pxe-boot LXC container LAN IP
|
||||||
|
nixCacheIp = "192.168.2.224"; # nix-cache LXC container LAN IP
|
||||||
|
tailscaleRouterIp = "192.168.2.222"; # tailscale-router LXC container LAN IP
|
||||||
|
torRelayIp = "192.168.2.221"; # tor-relay LXC container LAN IP
|
||||||
|
serverIp = "192.168.2.226"; # server (NFS/ZFS) Proxmox VM LAN IP
|
||||||
|
dockerIp = "192.168.2.225"; # docker Proxmox VM LAN IP
|
||||||
|
pbsIp = "192.168.2.244"; # Proxmox Backup Server LAN IP (not NixOS-managed)
|
||||||
|
domainControllerIp = "192.168.2.253"; # FreeIPA domain controller / primary DNS (not NixOS-managed)
|
||||||
|
ipaServer = "domain-controller.sweet.home"; # FreeIPA server hostname (used by security.ipa and Kerberos; must be a resolvable FQDN, not an IP)
|
||||||
|
|
||||||
# Cross-host references (LAN hostnames/users other hosts reach over the network)
|
# Cross-host references (LAN hostnames/users other hosts reach over the network)
|
||||||
nixCacheHost = "nix-cache"; # substituter/remote-builder hostname
|
nixCacheHost = "nix-cache"; # substituter/remote-builder hostname
|
||||||
@@ -26,7 +38,7 @@
|
|||||||
# fresh client that has never manually ssh'd to nix-cache before. Update
|
# 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
|
# this if nix-cache's host key is ever rotated or the host is rebuilt
|
||||||
# from scratch.
|
# from scratch.
|
||||||
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPeWgMsdaiz4axT/deFc1+0B5bN+GX/NOeW9bbQ0c/IT lxc-nix-cache";
|
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuHUxGNH6ei3BZD+EfZs3l4X8uJNcjQiOsM/G4yo4O/ 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
|
||||||
@@ -45,12 +57,20 @@
|
|||||||
# the installer image's nixos/root users.
|
# the installer image's nixos/root users.
|
||||||
adminSshKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream";
|
adminSshKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream";
|
||||||
|
|
||||||
# Prestaged wifi credentials for the gui host's NetworkManager profile
|
# Prestaged wifi SSID for the gui host's NetworkManager profile
|
||||||
# (modules/networking/wifi.nix). Leave blank until the bare-metal
|
# (modules/networking/wifi.nix). The password is not here -- it's
|
||||||
# hardware profile is wired up — an empty ssid disables the profile
|
# sops-encrypted in secrets/gui.yaml (wifi-password) instead, since this
|
||||||
# rather than creating a broken empty one.
|
# file isn't a secret store.
|
||||||
wifiSsid = "nbn-fttp-net-5G";
|
wifiSsid = "nbn-fttp-net-5G";
|
||||||
|
|
||||||
|
# Bare-metal gui host's two disks for a ZFS RAID0 (striped) root pool
|
||||||
|
# (modules/disko/baremetal.nix). Only used transiently at disko-format
|
||||||
|
# time (partitioning); the resulting fileSystems/zpool import reference
|
||||||
|
# by-partlabel/by-id paths afterward regardless, same as
|
||||||
|
# modules/disko/proxmox.nix's own plain "/dev/sda".
|
||||||
|
guiRootDisk1 = "/dev/sda";
|
||||||
|
guiRootDisk2 = "/dev/sdb";
|
||||||
|
|
||||||
# System
|
# System
|
||||||
timeZone = "Australia/Brisbane";
|
timeZone = "Australia/Brisbane";
|
||||||
|
|
||||||
@@ -60,6 +80,24 @@
|
|||||||
# one-line change.
|
# one-line change.
|
||||||
primaryUser = "nixos";
|
primaryUser = "nixos";
|
||||||
|
|
||||||
|
# HA file server cluster
|
||||||
|
# LAN IPs (vmbr0 / ens18) — client-facing: iSCSI initiators, NFS, management.
|
||||||
|
# Storage IPs (vmbr1 / ens19) — isolated internal bridge, used for DRBD
|
||||||
|
# replication and Corosync heartbeat only; never leaves pve1.
|
||||||
|
# haServerVip: floating virtual IP managed by Pacemaker's IPaddr2 resource;
|
||||||
|
# NFS and iSCSI clients connect here regardless of which node is Active.
|
||||||
|
haServer1Host = "ha-server-1";
|
||||||
|
haServer2Host = "ha-server-2";
|
||||||
|
haServer1Ip = "192.168.2.228"; # LAN IP, node 1
|
||||||
|
haServer2Ip = "192.168.2.227"; # LAN IP, node 2
|
||||||
|
haServerVip = "192.168.2.229"; # floating VIP (Pacemaker IPaddr2)
|
||||||
|
haServer1StorageIp = "192.168.4.228"; # storage-net IP, node 1 (vmbr1 / ens19)
|
||||||
|
haServer2StorageIp = "192.168.4.227"; # storage-net IP, node 2 (vmbr1 / ens19)
|
||||||
|
haStorageCidr = "192.168.4.0/29"; # storage subnet — internal to pve1 only
|
||||||
|
haStoragePrefixLength = 29; # storage subnet prefix length (/29)
|
||||||
|
haStorageRoot = "/srv/ha-data"; # XFS-over-DRBD mount point on the Active node
|
||||||
|
haIscsiIqn = "iqn.2026-01.home.sweet:ha-storage";
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
storageRoot = "/tank"; # ZFS pool root on `server`
|
storageRoot = "/tank"; # ZFS pool root on `server`
|
||||||
|
|
||||||
@@ -75,6 +113,7 @@
|
|||||||
# dataset or moving where it's mounted only needs changing it here — the
|
# dataset or moving where it's mounted only needs changing it here — the
|
||||||
# export and every client reference follow automatically.
|
# export and every client reference follow automatically.
|
||||||
nfsShares = {
|
nfsShares = {
|
||||||
|
options = "(rw,sync,no_subtree_check,no_root_squash)";
|
||||||
dockerConfig = {
|
dockerConfig = {
|
||||||
subpath = "docker/config";
|
subpath = "docker/config";
|
||||||
mountpoint = "/mnt/docker/config";
|
mountpoint = "/mnt/docker/config";
|
||||||
@@ -95,6 +134,18 @@
|
|||||||
subpath = "raspi/volumes";
|
subpath = "raspi/volumes";
|
||||||
mountpoint = "/mnt/raspi-backup";
|
mountpoint = "/mnt/raspi-backup";
|
||||||
};
|
};
|
||||||
|
proxmoxIsos = {
|
||||||
|
subpath = "proxmox/iso";
|
||||||
|
mountpoint = "/mnt/iso";
|
||||||
|
};
|
||||||
|
proxmoxLxcImages = {
|
||||||
|
subpath = "proxmox/lxc";
|
||||||
|
mountpoint = "/mnt/lxc";
|
||||||
|
};
|
||||||
|
pxebootImages = {
|
||||||
|
subpath = "pxe-boot/images";
|
||||||
|
mountpoint = "/mnt/pxe-images";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# The Raspberry Pi's own NFS export — not under storageRoot/nfsServerHost,
|
# The Raspberry Pi's own NFS export — not under storageRoot/nfsServerHost,
|
||||||
@@ -121,10 +172,25 @@
|
|||||||
# (modules/build-types/pxe-boot.nix).
|
# (modules/build-types/pxe-boot.nix).
|
||||||
pxeBootTftp = 69;
|
pxeBootTftp = 69;
|
||||||
|
|
||||||
# `server`'s NFS exports need both the portmapper (rpcbind) and the
|
# `server`'s NFS exports: portmapper (rpcbind), NFS data, and the
|
||||||
# NFS data port itself opened (modules/build-types/server.nix).
|
# mountd RPC service (used by showmount/NFSv3 mount protocol).
|
||||||
|
# Mountd listens on a fixed port so the firewall can whitelist it
|
||||||
|
# explicitly rather than opening all of rpcbind's dynamic range.
|
||||||
|
# All three need both TCP and UDP (modules/build-types/server.nix and
|
||||||
|
# modules/build-types/ha-server.nix).
|
||||||
nfsRpcbind = 111;
|
nfsRpcbind = 111;
|
||||||
nfsd = 2049;
|
nfsd = 2049;
|
||||||
|
nfsMountd = 20048;
|
||||||
|
|
||||||
|
# HA cluster ports opened on ha-server-1 and ha-server-2
|
||||||
|
# (modules/build-types/ha-server.nix / modules/ha/cluster-config.nix).
|
||||||
|
haServerDrbd = 7789; # DRBD replication (TCP)
|
||||||
|
haServerIscsi = 3260; # iSCSI target (TCP)
|
||||||
|
haServerCorosync1 = 5404; # Corosync totem ring (UDP)
|
||||||
|
haServerCorosync2 = 5405; # Corosync totem ring (UDP)
|
||||||
|
haServerCorosyncCrypto = 5407; # Corosync crypto sync (UDP)
|
||||||
|
haServerPacemakerRemoted = 3121; # pacemaker-remoted (TCP)
|
||||||
|
haServerPcsd = 2224; # pcsd cluster daemon (TCP)
|
||||||
|
|
||||||
# Opened on the docker host's firewall for the Traefik-fronted
|
# Opened on the docker host's firewall for the Traefik-fronted
|
||||||
# container stack (docker-compose config lives in the separate
|
# container stack (docker-compose config lives in the separate
|
||||||
@@ -158,7 +224,7 @@
|
|||||||
# build (modules/disko/proxmox.nix, config.system.build.diskoImagesScript
|
# build (modules/disko/proxmox.nix, config.system.build.diskoImagesScript
|
||||||
# — see docs/proxmox-images.md). Root fills whatever's left after the ESP
|
# — see docs/proxmox-images.md). Root fills whatever's left after the ESP
|
||||||
# and swap partitions within this total.
|
# and swap partitions within this total.
|
||||||
proxmoxImageSize = "20G";
|
proxmoxImageSize = "50G";
|
||||||
|
|
||||||
# nix-cache's Nix store garbage collection retention
|
# nix-cache's Nix store garbage collection retention
|
||||||
# (modules/nix-cache/server.nix).
|
# (modules/nix-cache/server.nix).
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPwUaVHP4MHZXicMAYGOe2ME1tp+CJShr8WocevlGWMh baremetal-gui
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:iVu2256Uzr2ThOGmFvCe48vWxCTJFflKUZ5vFSlFn+NomD69+y7RXf0ZICYOIH9t8pDAT3ZQDoMDw/kbPHxIj2R2U4AAt2H5QmKddTK3YCR8GgPde6Jp//v8NxLv+ZkAfj4W532rdaJ4CYrga9m8rOZi/6FubldUjBAVLVBiHxX86nCMbP5z2YTdHi2/IfEVK4Agy63oPZ0yD0T7QcXEX5F5eMcd6ceIDjJoROsQw5yVwUnjREo/Sgv5mQy9wBaf4ZtLw+eY5mJECIXjxhSkz417N31GB0D57A9N+WrEm8BEeuIbd+x7zdTSnZRsCZXLI3HRmkFH8bK75G9kPpXrpTQ5TT58LwDNuAGUK9K6dS/h8WAD8JRhtLhnEdCZObpxwoxOT2cCredwYZSdoVVirhuQI5EsGiTU1SGP78guAqpXvwvfbgIo2Qvic/e9OPQCHWLO4YrWonc6VUHhh/HmPOmaSrMcCFHE027fGXP8uTUEzOyE5hzmcM0d0WOX26xJ7PttbdKcLF3dZDnJQphn,iv:vpaX2prOsN/4Ke2HvoNHHp86DQpcPC6EtfgWvlgRGlQ=,tag:2Fv0oy99E88owAnB1fkvDw==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsU2pBT3M1TGJUYVlWUjNp\nN3JudVdVamN0TnF6cjlXN3BJRW5HbDFIZTBZCmhyRlQyNkl4bjBjOEhMZVhmQWxK\nVEJvaEpnTDREOXlKV3VxYzdkcW5tUEkKLS0tIHROY2RZeXFKQXBUeTZLODEzTGxZ\nSHl4WFhmOWVVR2NEcUxQSCtObU8xWkUKa/qmGyWYuEjf+BCoag5H9cA4ovW+ro8V\nhGsi3GqPEFfrI+qx/e6JqxazYpwfwIEaMZljhfgoFzg0I1U7OsnpSQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:21Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:7xbxAir+/3FbbJU4L4qRpZIYr/gqyoHjGxJvYYVLZ5yZLs8k/UiTG/qlu2zVkN2yfQaUzAu4/bQco5vOin6pwFodRH8BK4fbVIOheExzN1fk2uayejt/wWRiWj8w+qH+HaWLejlvrmbNced9HTiGViAHa+EZv3OUphlZKAdk/3Q=,iv:ZBuC8htjmKKc84kCBRHj7TdRdiPaEGeVMl+8ydOfIMc=,tag:uO4evh0xShWmG8kY/AkOWQ==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO+Ub4qK5Asqgi3A17zEy/+VOQ4ozL85ZBvlJnyhLZHD linode-docker
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:KpK5pPE33YtraIHSylq1MmlDiRvpDdwz9QTHsZCVrGuB3M19y5EXCBnARXreEHQyRgXWgeL4XZUKhR10xzeq/TqqZdKROtGn+LNwDM2f0OxLLxwPoqAqGQ2yEH2WbQdui28zEPcG1qKxzSVH2P2LIdLLghVTVeR1MM+sWgcJC5aoQGjjHt1tgGdQDv/jfGz9gDB9KRlJ6BVCdOdbkR4Ui5QFJX3edwvHRXsZUvlDNoVNrnQnGXUtw9FI2Ma+4mZ06x9Jv53LgAGsVf+Bu5USAn2VUKZDZu6Tfcdg8gb9iDKQrb+1jDq/dIb4/rh8fJwynMyG+pdrLtAoHs1D2K/3itglifs4sX8XDxSotMJWBB/KZUqdI2TIjv3fnBgkENbMFiv2rboWULRKA8z+viP2phS7kwm+mp7bGqRn36Bs90O5iVgifu/PYVykdnKggDbCyTbuKqdoq/C/s2oO7WbQISmjj+HjRrQ4wQd74iUvYjV2364AhwDQ+1L/OCR7HYr/l/NRpBcrjI5Kxaa5Usef,iv:GfqfGPjKxpOhdLJvJWTduYe2FbIKrMlKevErvnxfOa0=,tag:khM375z1e04y/JebLz/VgA==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSbS96Rmc4eDJkNGNUV2c3\nS3FCYnY1TCtOWXh1MzBPbnRnL09VbzNxblZBCkp5NkpxUXlJbUFjay9KbjVZeGc3\nTzgxRk0yKzU0SFBGelBDZnVHVzNaNlUKLS0tIFZRWXFUVW56a2owWk9TYXZPTE9r\nbmZHcG5ZN1dla3pXa3ZhOUdjR2dyTVkKaqAlfNkc2wzjB2//7DzW7JWg2BZd0Vqe\nO9YttWf4ikU6vfM+M/yiWGpWJ4U8p/3PBmT3ZLQBRGRtcyPby5DxZQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:23Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:+arWx20i4iAlFhbzH8/R9jggzJEP2PlEzyBnbAiDYZhp+2vfbsn+28WBzmjhi4AXz8CbBrhhFS6IFmYQZSFyYFuT943qoo82d/8E6p1DoyEEBL856SmkuVKpCq6IpWEJmkj8j7z3yyrpXva0aagReP+8+agWI+wWdNmG9M2tBRQ=,iv:Edqregq0OXRM4AWJ9EOtR/dZCObRepHnP4s8SYYkKHo=,tag:bBSiY7EVtli41w9s6JbQVw==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOFHRs2HkjhOL/Ilii8PiwxGkQNsstmGn/Bs2Vx/OWsI linode-gui
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:SRq6nUjsMjpM5CXHpAMN2j20/2LbdvHfwqOOoWMTDkW+oC8tLhQfKHpXzKFcKVfqiqjZqaS8J1pmb59buDuYvShXm21i+nRTfhzOHAvdQJNYghBKVLt10RuIDdgPuhpi7r9y5hPZrF+jFSyk4wLcoE+F9FpktTPZwrGnjd5kT1btehuwbjQFIgl23L9E7BtJMBOecUNc3xTLymaEi26kWga8zFXQMLiyLLP4ZPSrjhSeZpXx6zGj2iIHKnPSsaYTFhfoWbiOIzCZrSpV6GsDIsH4MDEwNjwk8fPL7VuGFoMJev0vM0IGeoKB1Jn7HIe6an1t8DoX1TZHk9NA0L2IJR7nglP5I9CHjDJzQZ4hOZvmy7PZd0M8P4be5sQ72Yl2ZgsX9y4ZhANJ+IvYMI7VM1GamcQqbuYrzYCc+zqVeq75FJYUW/fkcKpQihWjQeSO/PAzevdMH4H1k810MEQJSbQxeBB6xc7bArwip2vMvrmDAvBeJCkkJlDvLW5acnbX5MMiP+FiDOsTrggOSL6B,iv:g87PWSi9TyN4GwZdYAxUbRpJvtoYSon6tIoz4SC5bWg=,tag:hDem9tgzD0/lg44g3Us0vg==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByQWUxUHpyZkk5aFNFMTVY\nc09vcHdDUFIwUHA1TVhwblVPMWRiS0dTaHlBCmtHWk9YTVRTcGJndjQ3bmhIWEJL\nMkFVdzNmU3RRUGN6clhZNGRtVnRTQjgKLS0tIFoxWnBsa2Y2MTM2SjBjNjhrUDNx\nU1dyZkhpZHZkb2toeUFpS2tLN2Fic1EKUxQ1J57TJytHqIuLowiSyoS/nJPxSoZI\ni35CHWWE0+Y344m/DmdJPspvYZSI3rY1JbgaqjIKZrshDggZVTka2A==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:25Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:ARpnwhj3Y88r6FsqPFBUK/Nth/R6Kfel5cVtlKqjkAdK1FB5BMmdxNbbOn2AH7+zlzdH7qRKvkH49CS6P+bfLaIJBkdLanzF0lSzxq8+88fgqFhJi+mroAZGH6OJaVcHVdTytaE2b8xAMeoz07e1Smp07F6Sp5mwHemAB9EHi38=,iv:JrYUaIGeJYLcgKj845irmKPH05tD9z6Rabv1B5tVmZw=,tag:gK5X/dBJvrWF8LTpdhytAg==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPp8ZFWz97BFJJxHE+3w0/RpIXpRV64XE0eNBYl3jpSz linode-minimal
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:b9t5ALXBMo3Y1HJZkIeCsrevIe8ZCmthgvKOU21MKJqV6UZF6JxUwh/ZVZG2aTBDU7yiocGHHoxGOcTqC21RgpEPQTRkklQbO33fvb3bh35pQTxPH8J5ZVgjLgThXDDE7pYcSWClJwxznfJQXiefLyJwxqowBIL2FAdLiWc6Em0lThbcbdlwLhxsmUR375tjRyp41gsYYUZOAdAwlNOVXi3EuMK3l6lEVY14cUJj4JdDGBErPy6l46R/jOkwTeDrsxRWIsOhR+Dq0MVo5TllYoTKpTvQEQQV4pG8v0XrTsjhrPPvbQ0y3QKI6KkU1f50d0eHPKeCrJOe4puQ9K2VAxPnzwWO1UTw3wY4SsE8DXE0wXM2auAdQbDXRpPidOXwyYTR+Rr7DQ7avHUqRDkV9ZW27EadnNQWSCiomdwkbTg0i89+cyDRf6Wh4viYtBH59WFt1qkGX4nFnvSyx84SzH1/uSVjJ2CE7NJQ7/8NWZHVjqDQcKDrONYvP3resCFGKTu3miAArTin1+NQ8HvzfH41FV41BRsMQDXP,iv:OUUUl4lyZPCTQcw+RELE7W6fWoCHnJmKzN2ELJwXp9Q=,tag:jS7TtEsoUipHiMgT/rQUBQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArSGJ0VFAyazlrNGRENGhT\nWDhnd1BreHJ1Ukg0a3B1d1hDcWdRWUdUVDNrCkkwQUtNaEZjdnJXNmx3SjhWVUpV\nVGNlQnh6U1dwY0JCY3NMdXppT1oyc00KLS0tIHlMeitMT21JQkE3UFg4V3FIL2pk\ndVVFV3dpU1RMWGEydHl6TjYyRlJQMGsKrGJc51sxBuSFcVj07otUJoZnaPZhwVPp\nSOat4/BsH/NgWN2W1b+x+qMZQhlwwUy/f4oIC0y2XSvDfif5hUNwJw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:28Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:3C/etclYVQ/T3AhF+BfvYxTSCemmhPAHPUUsfLLB/B/VVMYQUMcy8RGpe1jWdtlRfszuJ7UyRsFOgWrpbUQiRTFSDIkTRb6hlPBuK85MVDuEbGCQb8e2ztnYgGBm0fHqTf4c5+QEKoVFZE2QMnAdjGSn1tJmaOW6zcDRFQOHLKE=,iv:0sE1bbWSQabtcMa+mi9ym7/MXEaEKhTFlkDNWruDQ+k=,tag:dRzgBJ//7O1x51M1deCn2Q==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOBJ/vcsXr8yc/dGQNkEJcBwtnApunrrK8BdNJZPk/D3 linode-nix-cache
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:+VpjDTycBuzaUzZuWfHPrXEPXAOip17PxYNbNTufr52EQxfME/VbvMlhvHeh7eysPFduH4IUHVdjulpymlSvPt5+BpE+o29WmwvdRThUEWKArXS+dYBdVtcZppHG0DcBvg6ogWWpKpnn/nyC1nqSlOPXbLvH8A/9iF4JrB8L9wvi8CnvhetEbzbUy9T7PnJuG7nv0GUZdqcIr+srxG+imdGPonpce+HZVAWbAUNo2duGMm51SnJKZUvN17wVqgjbFcK/D7GETyU/XOavHCoDrkum7EUcO0i7/9AvWLMdcEqTXekMSNE9v6ZCoFjO6lbWDORwxgaqiNgu9rFh9PtZ7Ji1wyvk0FQ2MXrRCMjRnxrrm3RQNXbYnZpKjEcXT+Y4jCCEL00SwQAwqcR4GvOAkyA2fog+kvT00Kpu2lr3ZV0flxDxmbDOCHc9FWcf34FxWQxKTpTKYAbh3t+9EHF/fZzrpaB/Lwvt4+9grwkq0HtdV409asKMhPObz/b41U1fw9u5OJ/drsKbDBD89vnvp4+Lj7cSbJVkObVk,iv:h5xMpGCXUd/Jo2OfolPGDO9Rcd4Q9pV4bGnfFMhPhwk=,tag:ABE3c9TNH83Bu2ogNj1Zxw==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5eUQyV1Y5dUdpR0RVUm1F\ndEZseWFRbEhQTFJxQ2hvVlJZVjBBQVRsaUZjCnJBNVFibG5tVmwvNTFYV0IyQ3BR\ncTlZN0U5R2U4RmpVOFU4MTNKOHZqSDAKLS0tIDZNeXF0NDRQM2pNckYwUlFzRVFm\nMkM5MHJJcThqeTh3OWxleWZNblBabFkK1RISRRs4CZn07ounWKO6tZo3OGLY5K52\nW+PAxOpvREtGsQiPq8dQFTxjfg5YyfHkK6o976eKD3Y2VM7pDo8kew==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:30Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:ZWtdVL9q67Zo10eiCNCe+bZ8pUqPjUcKDD8M7N6+N4l1jjngkOajJ4wpft4UQOmAmzB4RSWyajpXxwOg324H5KylYAFxfc6QH22bTPY2gaa5hD/NPnpM9kxZlcI3WsElI2xZBGo6n0nD9q5JV254QMBPLlg5EkAs7KXPY1hXFTc=,iv:LDXaZAmxIWQzSBK8KdNJ2nrFOwr3+vuiZDRW54usGj8=,tag:dwXewWjVBtc5qDaLyFz4qg==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPW/X9Mdrqs0wLR7XbEDTihk7TEkNZ3LcCeXoa2ITSDA linode-server
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:hzI2cJ81+Psv35MMZO06UHpC8F9FLVkfUzn0ReuR+WoUds8D5zhDFbQHeR6ByeHTuK8Hlj712okhoXh6Vm7l3WmhLJZkr8IGLsV9W+P2PYityjuOtBPrphrKUeSqxJfjQ31/EhZLOqw/508XUmQDNcs1/n2g0TtQ2UQSTAuO8r1OlRfRSPizvVdj7lu+Vqg3dDBRETSOJAYIh8XQXoQWl3M4dS7jOYUvgc4EsYOWrvPPPS+8xXzctBPeToasY0IyWtLzoeNajCs5EpH5u7S5K9S40/vFPa45Ic2IxEkb1HHClyN6lXWbnc5QlJ5HPrq+2YBU2y9dJHd9DQiwEbcEBIvR3/Lo9puVDppXx9kez7i4XR1UHU7WVu39nQZuP50BQQazj+eM0/HJapXfjoqtaaq0qAGN4onnmRd0vEmrZZl4WMXmx8m9HhxdNJoZ4C2Wb2skdHZok88tPzjd4r7xKzVhAbB+wqS4LML6FmCNk+m0USQXa25USXs0JDV7lgiTFQJPE/67JGa820WFicGs,iv:YmOeK2Ha3yBXumVO9strgLgqNmPOcnqwUDJv5QNR1WM=,tag:Z7kZvQLDOKx3uX4dRXJqeQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOSWl5ajRSVndlMHJJQTFp\nYzRRU3RQMEVzazB0Sm5FdWZmMzBRaUl5RUFvCjgwRUZWdUpvUjkzWHFVdThIZ1ZV\nM2RMNy9pUWF4VVBCVHBGWHgwakhKQ2cKLS0tIERIOTVVZm55QWkvL25SRGZkZmtI\nT2o3ZEJNQ2hmTDNoRUx2Z3UxcmlyekUKLOajmvRfLdCJL74PKSgBtIXDuAVd8NwM\nh4BtDs0hONOz82JaBqFw8Uz28hVFG/gcS80br1o2klqPd2gN62PCVw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:33Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:JJC+12gTCsDVvMWRtL5cj50kf1n6Xn2j1hNBnwvZXUA9Pdd96SXt61U/Q80h8GZ7Ycs/slsV7h5f3g6+8tV0PcdBM/vy0vPM5qX1zMySyMv2p+dkJb9MwQpPg2xAQ9jjYM9237p5n9nysgu74h4V7ccBqmBzp764bL9wx6hEzS4=,iv:U6u1GFvoxaqxmHv1zCht24nW0ZMJR2b4pZqCG3bGNzs=,tag:MocwywOBg/1rUuoSADuJVA==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJEPWLRc9oCcn9tqY+a+yMJPGaqDx67vNpzC6mpyXqFp linode-tailscale-subnet-router
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:OeRskwkEPIKfgzK1409bt/5Raw4HNgUvkfB1nBUlFZghjTn9nuZqWoIjclgaV+qcZWqi86YHL2QYU20fAOOINM8pppknh4iH2WLgkde8LCxFdijWq6hn1TWaLOBVOn5UxNNj/rSzyXqjImaKOXbxrOHJ4/snDaVJccIhqo7BfQfJlXf1eHyuqdAl33y/2NRYxd/6+cqntlALT61yNFEpX1iyoO7NyCsniYYvfOU3hzlW4Y7OLQT2wiVuns5Tt/ty1IRk2LU3AnpyklMnSalwFSBs4CWjzow36AOmmDRL/fUiQ2I4JJjsYmXlatvl1zYoG5Cr0utnclr6juUkpb7W4Pa4ZtA4xQIpbIBpo2b6RHedM9i48RA+JhrxaFnyM8Qhq0HJAd7usVRPtXLWEP7uHs89e8emSoam84gW27Ayyv6zWEZGungAzghNLcCEvc0JlQXYfkVla/gxCIKTTUNtch+aGAn/hSNNPXQH4E2Jcs8/zdXvyHl67Y6okrmcytn9ZdV7nSJ2HbjzOyLZOomiix0uHSeU5/t9BlhfBKPeHpapt0xv+ZAujKAuXalkSapP,iv:3doK6Rx3p8dLHBmqa6x/gi2qdQul0m5Er9fEbYaHa54=,tag:k+bzF7uBu/gt+Qt43vPQkw==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlUmxGdGhpQmZXQmtURXBp\nVHdhMlpHWjh3R0lvSzNJcFZ2RFhoMFBYZTNrCitpQXFpRlU5RlpOSnJkQ3BVb0I4\nWWt5aWtPbDVHRUlPOEpLRGZTQzI1U3MKLS0tIG5semJrN3RIMjcwdW42NGgxcWh3\nQjlKL0N6Ly9ZWEUyazVINC9YL0dLeXcKh46uyhNdyOYketjOHBP71Ad5Yz3TRb4u\nVZrcMuXT3+fbTumtvySu7sCMh2/UsKJxVXGnW20Rac7MrJEk4xX38g==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:35Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:UeBDJaEaiA/6FvmTDnyKqXqNH079RG/W2iRY/92i5DRw94+DkXckcQbv0Cw30maRAJnu04JUhPSwdCsrDM8m/ajqZYTK/VuWKVt7CMFxeA3quHYD4IlPpvh9lLuBLZ5/7sfvRMxRUN83Acr5JqFUiX4wUGcgMTUdnwU+t96hDyw=,iv:LoErePhZTtvxvxNwyllTafup/Q79xgpH306jx0xwUjI=,tag:qvTLlr33yi2eVwsQqAX8Ag==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWVWRVrhvVLI8e65/nQXUXoM4PctQDC2HQpA3VHMFKE lxc-docker
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:4ZcerznWIChXPeVnf/LxMC7XMt9icvoTz9U7ysgRkWdrpdiV1NFQ8k9QD8ORcoTNEhFKMHTmr5HCCsJmnq9RIxCXrWPSPVWGXq9PvdjzlUv8BaYRH6EUCoSsbjqMgi/YQKr5kBV8WcqFBaHw3txm71Np8r++bb4p+Kx8DctupC0EVybttLMMSmhH+2a2+54gZ2qDIQSCApnqaal7JeuVMLkitkFOeekpuotyt4ZGBpM6v/CggSEQhQ/SuuxMRQEpLDS6ccRbws5Jo0Jz7ZuHfkufm4JC96X89pPFSSTK72u1xRiHqNzW0s+oFm8Bbjj5p7jrx89itgodlr9m1bb4RWxScEtGTrVY5Um7sVRIIF+/acez4GwAodeE5WlzXqGsrwPRJk5iYvZHTWjBAZCQC04lZst1fHhC5PAOsslwSfWLxXI1QD8jCki/hFXfZpxqfRTZ0CFqKVcZYrgPKH4dpkuUsDLe0zAmzraONdT55QbbyVV9PbB4+0ZJlwYIrRSa0oMaiWkT8imj8abGKXNd,iv:PCDcSAN0GkuLcDkqapMjUqQBKVYJte/RI4j8cx0X0RY=,tag:Zpb0GP9OxyC3FRi4BwDwvQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2aHBnVGJybDQ5c2hmOHNO\nRlZSMW80WkRJbjJPdkN1RmNiWnVhTlRpSlF3CmVDelNrUm1tRVJrWXNXOWdkNDhi\nN3puamJEc2RNLzFJYnMrSTE3VDEwTWcKLS0tIEIyYXJLMFd6YWFYNGlDbWM4SnJ5\nUGFxck4rYjduSCttU0JRWVI3Q2k3a28KYIgRQ/qXgBlk1XjA34keP8d26EvPpLAI\nCtAjAI3YiX/titHYUPA4YYcrqnVDdgEfS6Z402AXUzY2yePdzMF8Mw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:37Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:ZQlaXZFog31nzrGqM2Sj3B9FjkdNhEMSZUNC7QLXcG74ny4XSkqciOdruXtQ6Ay5i+1Z6voTooObSJmKfFNZbU/6G165jXzmuvQ3X6DL2SI8HI6czt8bbmIiUCyEeikqeIf9UF64yuEYj0PjayEez69rHGbzUuAsp2t+72inoq8=,iv:+MBEx7EeSmExE8of9Xtfzas8fIff0E3SppfsDgopggs=,tag:ggBB/OEIeAmKt7veKoPHBQ==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAChD7dJfSEYgyOulaehMua36lWswcAsVusQWmJ1susx lxc-gui
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:VEW/6Sml7VnLGbI9lrdeaqrvVaIBtfOliAc5FgX/hzOa8xoXIjscoTn6CbFTN12lDS2/PSQ4hIBgGqIIAhhfOwYDnZWejNDdYhDBqGO1QeTnOOMAFDEdVQeAcSsfYzv3LA1RFjcKJcP3feTE0MNdTYAEhqxQ4oQ84hmM2IQ83Np5Il9vLspF4nZ0YmzCRKfMksQ8kPBsdpG1K5EDMVlH9ZYICcleMB5nkFIVfkbXgrhvtKoP2/EURVOH0k8u1TntGp3ip8kf63qrYP0HjCUi7GpU3fTSD+dFjFMkLtYIftSqScvJmnF/8Fq5yPMxnq7Y/Tdfped5PK/bNyMgUYV/C84CsN+RJey5wv0/E3Yf1hjMEVVZza9zw+MfINejyPnbY46T1uTtmRXPN/XW7UZVOkEFbqVGr8WFoKw0odCgCgXmAgzcbTg2OIb5DMiKo3+ZWxm6xg8wmTEFoY70v6VEa5rRofKAiteI05V2OkHRpQWcT02cPPjKXUkt0KuRFYI+oHsYRTac13G9C9DRofNR,iv:4cEUM9xBLGGzyVFgqjp651n5gMwUOzaXbMe+8/3Vw8s=,tag:v4t9YOfFGg+CDJibQXhSyQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWVVV4cTdGSFo2L2k3Sy9r\nanVQUHBoaTFyUkxOS0swTjJZRDA2UGZNemxjCmRVQndTZWl6c0hXRWUybXRNd2cw\nSGE0aTlGWXBKdmdRc2FqOUxoeUZUVlkKLS0tIHRUV3YxeWlrSFFvWmZubnRZN1Ro\nemR3S0RSLzE1cDlLaVI1TTVSeXRWWXMKgDgm6pvONdl5J5jfOR/t+Z4CDF+bIX47\nm344Ba+3ucI2Ecktr6+PWfcJ3Xp3afZQhOgjlg+A+XR9eN2CIwZw+A==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:48:12Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:P3VpYl7m7AL0bl71P4EqCEz+2IAjpDykzq0nkoPQGq/uTO75OcZJXowgdeUy6iat1IkX0vsuZ6kSl+54hQUs9ZOVCFku2QavpqWUjXgyxi15GJJtC8/4OKJWSliPHyObGRaAq70KB00LAZEP+QsSLVXGraz7qzidXh6HVuldIeA=,iv:+p9mckbRciUG2JoYEj6/WaWpqyL1rqlZ2aIh4aZH6AY=,tag:rrziwm7l0jNNmWFnI2GbEA==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII6hAokaz4VQ1MHBQ80fhlT7WFshzZph2GDt7jf6Gaj+ lxc-minimal
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:c4zQurvcJbIbjxewdE0oldS4iFOaawY83OJ9NbyH9Zri7Rw3++dsHyAbleN3PEKy/u75FVW+oWTmAx6dp/buUNfmxD0OYpUs8+ZBons+kI0rpOwXfag1Ns8CMGzyk1Wr0tRwmjLSMN7h1x+JyRu3eMe1YOgJOvNg3+kITars1DGCe80H0YTBZsU/XzzWDhW5g6ENpLsl4Ds633ohyHNzwdmSaXPuKNiv3eL3VihGDD1tFz4eq/SZM0ikJ2e1r+iyPUts0JW05BKIUnVqu6fNGMolOGtEiWnN0GStzo4wxPdv+jMK2gyFZNeNV6ei4qA5hXUe5kEEGTqmQvhqxz4uFzXZrVNkOT92ZBq2azZv8wyxPlea3yLBLicu+OR5CHDpVcxioV8Y/orFQ+XRjJhPVif0xCXoNXkBejvVLAQfH/7KpezdzLENiCqruY/WnW9Fs/+A5Uv5VxaExhrI9xWZ/ojMDdCJJVEV/o4X+uCg4/HIiows/dMOnVnvbRFGHAuKp/pCGGNmLQ3Ys0TJXDFF,iv:SePxW87sw+YdRbc7MPwBbh26efQrBd7S2FDrjaK/5Yc=,tag:8iH8gFRDTMegsbEdgd2boA==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKanMxRk43RGhvL2Z3MnpD\nVTZvV2lRNFgvaDZEYWNDbHN6akhHdjJHY2g0ClArMWkvWTFNeUxMd01lVFF4QnYv\nWTF3MGtWU1VFZkJQTnZPVlBHL3hsQ0EKLS0tIFBWMllSNGZXNGpPNythc0MxN1hE\nMFNodUJyV0xDc0JrVFQxSGF3RnZXVk0KyskQhrNomMc/0LKfuPoVwdUWVPMh5NH5\ntf+3izlcaDmS/BDFRHwAZRZ2mVFAj1g6JtPZI/ZSQQt2KbaLdEr7yw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:40Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:de0rtksPfBeVSAEUkyQ6xez5FRZ2I36JjwiOW3QrGwFFa0uZ9TPe9uaYh+l2xj6fei6HS3KLD4WDNq26GTDh8mjKlKIINOjEyBBqq9R56+UP6lQ1XDApiOTK/w22Y9zmEIBAQ7qi0WpzmYl14VeOlK/DxpPvog3SCMQP4zHW+nw=,iv:qO+2QH52v0oQ2z/VnZUqSc3AzJElS/pO+AalEudmFY4=,tag:fZboSJ7Ae68eDqf6u4Gqcw==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuHUxGNH6ei3BZD+EfZs3l4X8uJNcjQiOsM/G4yo4O/ lxc-nix-cache
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:nm1TQswDfqLgzwOZTpllM96/nqW1xyOoUmbmxL3kEbygnU9kKruuijhoLMSigti8Cgb4onyI3OFkOaNnBShGaXsrFHwIH4rwd4CRv4mHLFTqs/hTXxMxo98gKdhJYuCLXOOdhWrOfOO0GofmxLEX12AUhjS/cxtJWXqINIbmOzPm0rHVVx56r6kj1MojGssV3y68gbioSF1StGG+OIQIXL1LRBWknyTJ+NK3aViXn7oBglHzL9D74Lz5v8iFQQroZ4sBPi+ov0CKAFaghRxl7T7jGA9pO354ZbLdiGmpqtYFazCGq21hwjpWyobAog/Iidngm9Mz0wleB1pEpkCaEpUsjVYQkAoknITasCGhwpmFH0F9v8Du87rJ4jdWT3W8qh3uPlCHw7e5QwQYE/I9v1HU6Au+liQJWeAbbwt9wRC5NMETCqH1PDDpRjt19Le2ECXjstBRR4b9wQQkeDefgcFibQK156/i2twhAqIgz+RqIZ05aQYfy8Kfj3c7G34PEdOdz1P3bsAgISVpgMQO,iv:Cth8/Wlf7vJo050aDNt1XyFUDiihyuMxv6WH3yEZGXM=,tag:qM1/MVU3J2Kso8w0fzEvIQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqWFF4ekdNRG05bysxMGRz\nbkt0QmRKT2hsUzZWVXRxY0ZjenRXMXhrT1JNClE4WlRmU0grRG5BSjM5NWZwc1o0\nSFJ3dGRsR0pwYlJhRkV4MjJqNVFWL2MKLS0tIFBSVTU5aFVZNm5qZGtIU05XbDd1\neTd5Y1puYjBkKzlVRDVvaW9pL1B0R28KZbutmxm33H8qOE0jSDr+mrx6nFESmKLg\nTfjZBp1W0SDRVJfUbRQJvcp1ECMJIS5cWzXDoXmLnaeLFzawBPbIeQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:10:21Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:IQ/XcH2boEt97exSgTUxuZwH+IwFHA0ota3Y15T9DP3tpN/Tvp4zWOqHYtiPfRf/Ea8Si/LDHwlVnuLIhFYtrMmTzJjKf8Xa+eN2bACzn1BAGKKkY1uQH2tIJ1HIxC08x0L7YhkGPVNMFcu2FsNB0bk57+iwUb/w5p4m5O4AZ6Q=,iv:/BkRQEGvUImCewxsfrpV+EYZVnfRlIChASpBXBAtppA=,tag:DAkApHsRza9DXOnKhhVNNA==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGY9pDiJDumHVu+IVu/11qEPFmEbwoYbJqUvfwc2LOdo lxc-pxe-boot
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:bqh2QqW6YynFbDvr50/akjZN3XL0NehZK0+bNd2c5Mk/GzPUi+HzoXPUbMEvs1WXWWXTcIHmLuaCO8x/FbZ2+T4g7GSTK2E2qSt3CwO9XAE4ODoFc6KM8Yg6sK05Zj1NotUKZ8lfjLqEhaIL9g4/Ge+pEWlPSno+jNtzPE9QJxY+H7kCbjf3Qn+49PXl5P2Y/3Q1+rpbJ7kvdDx592o8lKsftWgZQJc3gDWIhIPFGfyAZr5nM5j9cioALbICJ8epVfHJsFgNMuxBi+vONltjoxYN7VG4Zqy1U6ch6nDY15FIhx+3w0xAo/q9dakFYSR0Q+MHIWTddQUmmWYUtI4TAHDtLC4qAKmP1PtF88hDHmO4NjpM8LuRt1giun+naJiIWmu6jsifcbMB9kmai2KjCU8+29/r9yuTySFA5ttIWTM6D8UA7wQGDuMT1df1fJUu9JQtVCMPxjy3Nulu+KQOwat5BxhG4yFuZgd4VRIjOWRpxFtIqnjyXZvOdRWu2Fwj4pGSnyJH5W69vo5seTnL,iv:3jnfp5x0Sm/bA5okh3LmDw2LaVFPD3C1W3rrkf3lNSQ=,tag:5CJ5ocs6DJDXNtwexbZ+Rg==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuVk5MejJRR1RVQ2hPbTRE\nbnZQaXpudjZ0WHZ5RU0rWDBCamphVWJIaDEwCkMrSkowSkdwaEZJT3FDMi9FZE5D\nbEZQOFg0M29jajc5Lzl0TUI0YTNCZ2sKLS0tIDgzVy9nNE9ZSnZucTVwbDZpN1Ja\nWnVDaEJUZjNlUm84ODZvdVZ0OFJuaDQKeMz9nFIYwVR2DTkqcko+CkkDOBNAUqYy\nfWTR1bYB6TEIDncp4T3cTH/EOf2xJuLxYli4QuJJqxWHjyho5+csbQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:42Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:A9Y/BtPGkaafNDiED1d95xepHdgmu/Y9a0jU1+7KqJLGJhxQKCExW31PYgJ8dDB75BNGE/3ZgwU1bWbyqjM5liZVNmMWjRhJTJEvC2dWIupaO3RcReoiRMXIeVh9IfNeFZ6ytare/kqpUgWyYbfCAni4/a5C7W2F0SsdugOCrKA=,iv:CQOn9b0Ld0gbKGIycZA7duBq/jfN66l0H58csTq9XXM=,tag:OSGV8CKZ7SkesEwuPIf8cg==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILVRdEddGd+AMNe4kXbmA9UXK8JRsPEuxPx1vhNT9ZG8 lxc-server
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:JTmbN++hNMddBdMKIdzqZ6hLD1So9hyAzwbce/QvqWBsa1At85cMRUx+P1Yi4Sq2aXCg1Tf2g8XC9DXsVfMusUGEHrHbJApwkia/xHNMcFOohscUkPltXnm/lRilm4hwJUv6ay6UyLvIqMQAzzC8YWebHkKo00rxRxQWQ77wjmUG4dJeujlK5wMH0zTXUkrf7tIdJ20Zzg1BrAU6s4FYLvRvcUSW2ROu+sF2Sxgy1qVNJfDrRnY4REh9013tTSB5IlgJzUSQLs1vHr7EpIfmA5MCCSkZjplGCKvxkcNHB3aaOkrxkGa18JeWoklRen31UPU9zMUhTjAZ6VbbIagxzaFGKVP27cOiHQPNpEIMXwaRzoprjBX2PJ/Bs6pxe6hBpmiKOb40XdtxFQx2rofPcbQwklTpM5SFwvK+/bgawy1m8O3NHMhcw1qvd+6KjUdqy++/ivFC2Nyy+VfUDyGu0JLK3X+YqU6JXXMcUWOq79pVQIL3q1ofbhVHQ8p17+V3wQQUB3kPo4EEcKikI7Z9,iv:TVYVDOiTsgXaIcuJdnd3djPWXCMDDGjpefAW0MR+7Es=,tag:jIYO1oatbvInp77d4fC8mQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5QlNzNEFqSkhKTVhWVisr\nS21zdVBZSnpITDdrb243c0pUdDR5WXl1bFVrCmkxaWR6U01sLzhLL0MwRGh1VG5D\nNEFiU1FtNXRBdGtPellFTXYraitCUTgKLS0tIFdwWGFnMWFCN1ZvK2VBMWR2VTJZ\nRlQ4dEpmczA4bEdvUkNhaC93OXhDTkEK0QBkaLV1mbTMlSnjmN4x+qljGipHM/DQ\nUlmBYyi3nEOrI36I/Mm8yoBZai/qWqdg6IG+sxDE49ZOLp9PhpAWww==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-25T11:45:44Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:/l6pa3LE7+kFYqH2pv2RIcYBycLrZfpb92Al3SIU2tMeFUUvh+C8q8P9CgmAaiQuQ8S2dfYIJVx67zc7cRqI/UL/dFvDSv0YaMTGQ8Wn4fIXSh01EL0f/QVaIfb+uuvyEsdjy2ScWTWcCf2ICnC/zaMmp+xP+MmR1DRBM8KPIkY=,iv:xsmaYZ6dyHLU3BVfT3jxbfWWeBvYKMT+D9MtRxF9jlo=,tag:VzOiutHQJGHqb5UCI+cxSA==,type:str]",
|
||||||
|
"version": "3.13.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMrWIWzBAi0oQv1S9vEhQCRXgadRcjC85VBgMLnS+iB9 lxc-tailscale-subnet-router
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user