Archived
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a1d6842d7 | ||
|
|
f5ef3194d4 | ||
|
|
90e3397b42 | ||
|
|
be5812d5bb | ||
|
|
84f7e038cb | ||
|
|
91d8f8fab1 | ||
|
|
723212a81f | ||
|
|
a5990ccf7d | ||
|
|
75d09d57e3 | ||
|
|
6847a7a6f4 | ||
|
|
17dd00bee1 | ||
|
|
b3c81453e4 | ||
|
|
656dd975f0 | ||
|
|
2661f6d271 | ||
|
|
0532c3a282 | ||
|
|
ac8c9a20e3 | ||
|
|
0e66cdabc9 | ||
|
|
9c892ce1c2 | ||
|
|
bfeea90597 | ||
|
|
cafeb8853b | ||
|
|
2c2d464503 | ||
|
|
5ec7033439 | ||
|
|
9133afd444 | ||
|
|
eeec9ce302 | ||
|
|
a62c4fc023 | ||
|
|
7e51168d1b | ||
|
|
97ede62f6d | ||
|
|
ab5206b1c7 | ||
|
|
2041557ab3 | ||
|
|
2fd483697b | ||
|
|
89186b0dee | ||
|
|
8e3606cbd3 | ||
|
|
a18dfb0127 | ||
|
|
75f1342339 | ||
|
|
36ba99c9a1 | ||
|
|
0cd8f15b48 | ||
|
|
8613b93fa8 | ||
|
|
c939454983 |
Submodule
+1
Submodule .claude/worktrees/proxmox-remote-build added at a5990ccf7d
Submodule
+1
Submodule .claude/worktrees/scripts-dedup added at e578443914
+4
-2
@@ -1,12 +1,13 @@
|
|||||||
keys:
|
keys:
|
||||||
- &admin age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
- &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||||
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||||
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||||
- &lxc-minimal age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
- &lxc-minimal age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||||
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||||
- &lxc-nix-cache age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
|
||||||
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||||
|
- &lxc-nix-cache age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
||||||
|
- &lxc-docker age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
||||||
|
|
||||||
creation_rules:
|
creation_rules:
|
||||||
# Shared across every currently-deployed host: root/nixos password hash,
|
# Shared across every currently-deployed host: root/nixos password hash,
|
||||||
@@ -24,6 +25,7 @@ creation_rules:
|
|||||||
- *nix-minimal
|
- *nix-minimal
|
||||||
- *lxc-nix-cache
|
- *lxc-nix-cache
|
||||||
- *proxmox-minimal
|
- *proxmox-minimal
|
||||||
|
- *lxc-docker
|
||||||
|
|
||||||
- path_regex: secrets/nix-cache\.yaml$
|
- path_regex: secrets/nix-cache\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ servers and workstation.
|
|||||||
|
|
||||||
The flake exposes NixOS configurations named `<platform>-<buildtype>`
|
The flake exposes NixOS configurations named `<platform>-<buildtype>`
|
||||||
(platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`,
|
(platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`,
|
||||||
`server`, `docker`, `gui`, `pxe-boot`), generated from `modules/platforms/*`
|
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`), generated from `modules/platforms/*`
|
||||||
and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
|
and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
|
||||||
every combination is built — `pxe-boot` has no `linode` variant. See
|
every combination is built — `pxe-boot` has no `linode` variant. See
|
||||||
`README.md` for the full current target list; treat `flake.nix` as the
|
`README.md` for the full current target list; treat `flake.nix` as the
|
||||||
|
|||||||
@@ -62,8 +62,9 @@ There is no test suite — "correctness" here means the flake evaluates and
|
|||||||
sweeps: after editing one or two hosts/modules, evaluate just the
|
sweeps: after editing one or two hosts/modules, evaluate just the
|
||||||
`nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball`
|
`nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball`
|
||||||
/`diskoImagesScript`/package output affected) rather than looping over every
|
/`diskoImagesScript`/package output affected) rather than looping over every
|
||||||
host — `codex-maintenance.sh` evaluates 18 hosts plus every package/tarball/
|
host — `codex-maintenance.sh` evaluates every `nixosConfigurations` host plus
|
||||||
image variant now and is slow to run after each small change. Reserve a full
|
every package/tarball/image variant and is slow to run after each small
|
||||||
|
change. Reserve a full
|
||||||
`codex-maintenance.sh` run for changes that plausibly affect every host
|
`codex-maintenance.sh` run for changes that plausibly affect every host
|
||||||
(`modules/common/*`, `flake.nix`, `variables.nix`) or as a final check before
|
(`modules/common/*`, `flake.nix`, `variables.nix`) or as a final check before
|
||||||
committing. This is a session-workflow preference only — it does not apply to
|
committing. This is a session-workflow preference only — it does not apply to
|
||||||
@@ -93,9 +94,10 @@ Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` also has:
|
|||||||
(`--force-rebuild` to skip that and always rebuild), and probes
|
(`--force-rebuild` to skip that and always rebuild), and probes
|
||||||
nix-cache's substituter/remote-builder reachability once up front rather
|
nix-cache's substituter/remote-builder reachability once up front rather
|
||||||
than letting every `nix build` call retry against it individually.
|
than letting every `nix build` call retry against it individually.
|
||||||
Refuses to create a target whose host identity already has a real
|
Refuses to create a target whose host identity already exists live on
|
||||||
deployment elsewhere (`variables.nix`'s `deployedTargets`) unless
|
the node (checked directly via `qm`/`pct`, not any file in this repo)
|
||||||
`--allow-duplicate-host` is passed. `--dry-run` throughout both modes.
|
unless `--allow-duplicate-host` is passed. `--dry-run` throughout both
|
||||||
|
modes.
|
||||||
- `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) sourced by `create-proxmox-resource.sh`. Add new
|
||||||
cross-script config here instead of duplicating it per-script.
|
cross-script config here instead of duplicating it per-script.
|
||||||
@@ -104,13 +106,38 @@ Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` also has:
|
|||||||
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
||||||
errors on it), so this is the closest equivalent to a single source of
|
errors on it), so this is the closest equivalent to a single source of
|
||||||
truth for the tracked release.
|
truth for the tracked release.
|
||||||
|
- `scripts/rotate-admin-key.sh <backup-admin-key> [--new-key-file <path>]
|
||||||
|
[--dry-run]` — rotates `.sops.yaml`'s `&admin` age key: decrypts with a
|
||||||
|
backed-up copy of the key currently trusted as `&admin` (verified by
|
||||||
|
deriving its public key and comparing, not taken on faith), replaces the
|
||||||
|
`&admin` line with a new key already present in the environment
|
||||||
|
(defaults to wherever sops/age itself would look), and runs
|
||||||
|
`sops updatekeys` on every `secrets/*.yaml`. One-way: the old key can no
|
||||||
|
longer decrypt anything re-encrypted this way. This is the automation
|
||||||
|
for the manual steps `sync-host-keys.sh`/`create-proxmox-resource.sh`
|
||||||
|
print when they bootstrap a brand-new, not-yet-trusted key on a machine
|
||||||
|
with no prior admin access.
|
||||||
|
- `scripts/backup-admin-key.sh <dest-path> [--key-file <path>] [--force]
|
||||||
|
[--dry-run]` — copies the local sops age key (source resolution matches
|
||||||
|
sops/age itself: `$SOPS_AGE_KEY` inline, then `--key-file`, then
|
||||||
|
`$SOPS_AGE_KEY_FILE`, then the XDG default) to an arbitrary destination
|
||||||
|
path with `0600` permissions, validating it's a real age identity and
|
||||||
|
round-tripping the public key before and after the write. Refuses to
|
||||||
|
overwrite an existing `<dest-path>` without `--force`. Purely a local
|
||||||
|
filesystem copy — never touches `.sops.yaml`/`secrets/*.yaml` or the
|
||||||
|
repo at all. The resulting file is exactly what `rotate-admin-key.sh`
|
||||||
|
expects as its backup-key argument.
|
||||||
|
|
||||||
`sync-host-keys.sh` and `create-proxmox-resource.sh` genuinely mutate real
|
`sync-host-keys.sh`, `create-proxmox-resource.sh`, and
|
||||||
state when run for real (not `--dry-run`): real `secrets/*.yaml`
|
`rotate-admin-key.sh` genuinely mutate real state when run for real (not
|
||||||
recipients, real Proxmox VMs/containers. They require the operator's own
|
`--dry-run`): real `secrets/*.yaml` recipients, real Proxmox VMs/
|
||||||
SSH/sops access, which an agent session doesn't have — but don't suggest
|
containers, real revocation of decrypt access. They require the
|
||||||
running either non-dry-run without the operator's explicit go-ahead even
|
operator's own SSH/sops access, which an agent session doesn't have — but
|
||||||
if it becomes technically reachable.
|
don't suggest running any of them non-dry-run without the operator's
|
||||||
|
explicit go-ahead even if it becomes technically reachable.
|
||||||
|
`backup-admin-key.sh` only writes a key copy to a path the operator gives
|
||||||
|
it — lower-stakes than the others, but it still handles a real private
|
||||||
|
key, so treat its destination path choice as the operator's call too.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -133,9 +160,10 @@ nixosSystem {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
|
Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
|
||||||
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`. Not every
|
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`. Not
|
||||||
combination is built — e.g. `pxe-boot` has no `linode` variant (PXE/DHCP/TFTP
|
every combination is built — e.g. `pxe-boot` has no `linode` variant
|
||||||
need LAN L2 adjacency a Linode VPS doesn't have). Treat `flake.nix`'s
|
(PXE/DHCP/TFTP need LAN L2 adjacency a Linode VPS doesn't have), and
|
||||||
|
`tor-relay` currently only exists as `lxc-tor-relay`. Treat `flake.nix`'s
|
||||||
`generatedTargets` as the source
|
`generatedTargets` as the source
|
||||||
of truth for which hosts exist — `README.md`, `AGENTS.md`,
|
of truth for which hosts exist — `README.md`, `AGENTS.md`,
|
||||||
`docs/flake-lock-automation.md`, and the CI eval workflows
|
`docs/flake-lock-automation.md`, and the CI eval workflows
|
||||||
@@ -163,7 +191,7 @@ removing a host.
|
|||||||
`vzdump` backup-archive metadata this doesn't have), no install step —
|
`vzdump` backup-archive metadata this doesn't have), no install step —
|
||||||
see `docs/auto-installer.md`.
|
see `docs/auto-installer.md`.
|
||||||
- `modules/build-types/*.nix` — what a system is for:
|
- `modules/build-types/*.nix` — what a system is for:
|
||||||
minimal/server/docker/gui/pxe-boot/nix-cache.
|
minimal/server/docker/gui/pxe-boot/nix-cache/tailscale-exit-node/tor-relay.
|
||||||
- `modules/common/configuration.nix` — base NixOS config imported by every
|
- `modules/common/configuration.nix` — base NixOS config imported by every
|
||||||
host: locale, users, nix settings, git.
|
host: locale, users, nix settings, git.
|
||||||
- `modules/common/home.nix` / `hosts/nixos/home.nix` — Home Manager config for
|
- `modules/common/home.nix` / `hosts/nixos/home.nix` — Home Manager config for
|
||||||
@@ -198,7 +226,8 @@ removing a host.
|
|||||||
and `environmentFile`; used by `hosts/server/host.nix` and
|
and `environmentFile`; used by `hosts/server/host.nix` and
|
||||||
`hosts/nix-cache/host.nix` to avoid duplicating that boilerplate.
|
`hosts/nix-cache/host.nix` to avoid duplicating that boilerplate.
|
||||||
- `modules/tailscale/`, `modules/docker/`, `modules/networking/`,
|
- `modules/tailscale/`, `modules/docker/`, `modules/networking/`,
|
||||||
`modules/traefik/`, `modules/services/*` — single-purpose, single-host
|
`modules/traefik/`, `modules/tor/`, `modules/services/*` — single-purpose,
|
||||||
|
single-host
|
||||||
feature modules (e.g. `docker/enable-service.nix`,
|
feature modules (e.g. `docker/enable-service.nix`,
|
||||||
`services/zfs/enable-service.nix`). Grep `modules/build-types/*.nix` for
|
`services/zfs/enable-service.nix`). Grep `modules/build-types/*.nix` for
|
||||||
each build type's `imports` list to see which modules apply where.
|
each build type's `imports` list to see which modules apply where.
|
||||||
|
|||||||
@@ -10,28 +10,32 @@ pieces composed in `flake.nix`:
|
|||||||
|
|
||||||
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`
|
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`
|
||||||
- **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`,
|
- **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`,
|
||||||
`gui`, `pxe-boot`, `tailscale-exit-node`
|
`gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
|
||||||
|
|
||||||
Not every combination exists — `pxe-boot` has no `linode` variant, since
|
Not every combination exists — `pxe-boot` has no `linode` variant, since
|
||||||
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have. The full
|
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have, and
|
||||||
list:
|
`tor-relay` currently only exists as `lxc-tor-relay`. The full list:
|
||||||
|
|
||||||
| Target | Purpose |
|
| Target | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS (real, deployed) |
|
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS |
|
||||||
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox (real, deployed — previously the flat `nix-minimal` target) |
|
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox — previously the flat `nix-minimal` target |
|
||||||
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
|
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
|
||||||
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder (`proxmox-nix-cache` is the real, deployed one — previously the flat `nix-cache` target) |
|
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder — previously the flat `nix-cache` target |
|
||||||
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host (`proxmox-server` is the real, deployed one — previously the flat `server` target) |
|
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host — previously the flat `server` target |
|
||||||
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack (`proxmox-docker` is the real, deployed one — previously the flat `docker` target) |
|
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack — previously the flat `docker` target |
|
||||||
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation (`proxmox-gui` is the real, deployed one — previously the flat `nixos` target) |
|
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation — previously the flat `nixos` target |
|
||||||
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host (`proxmox-pxe-boot` is the real, deployed one — previously the flat `pxe-boot` target) |
|
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host — previously the flat `pxe-boot` target |
|
||||||
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node (no deployed target yet; `lxc-tailscale-exit-node` is the one planned for actual use) |
|
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node |
|
||||||
|
| `lxc-tor-relay` | Tor middle relay |
|
||||||
|
|
||||||
The "(real, deployed)" targets above are also tracked machine-readably in
|
Which variant of a given buildtype is actually deployed isn't tracked
|
||||||
`variables.nix`'s `deployedTargets` — keep both in sync when a deployment
|
anywhere in this repo — that's live infrastructure state, not something a
|
||||||
changes. `scripts/create-proxmox-resource.sh` reads that list to refuse
|
committed file can keep accurate, and it changes independently of the code.
|
||||||
creating a same-identity duplicate of an already-deployed host by accident.
|
Check the Proxmox node itself, or `/etc/flake-target` on a running host (see
|
||||||
|
below), if you need to know what's really out there right now.
|
||||||
|
`scripts/create-proxmox-resource.sh`'s duplicate-host guard works the same
|
||||||
|
way: it checks the Proxmox node directly rather than any file here.
|
||||||
|
|
||||||
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
|
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
|
||||||
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
|
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
|
||||||
@@ -111,9 +115,10 @@ Three different paths depending on target, none of them involving a manual
|
|||||||
`docs/proxmox-images.md`.
|
`docs/proxmox-images.md`.
|
||||||
|
|
||||||
`scripts/create-proxmox-resource.sh --type lxc|vm --host <name>` automates
|
`scripts/create-proxmox-resource.sh --type lxc|vm --host <name>` automates
|
||||||
either of the last two end to end (build, host-key registration, upload,
|
either of the last two end to end (host-key registration, building the
|
||||||
`pct create`/`qm create`), with `--dry-run` and a guard against duplicating
|
image directly on the Proxmox node itself, `pct create`/`qm create`), with
|
||||||
an already-deployed host's identity. See its `--help`.
|
`--dry-run` and a guard against duplicating an already-deployed host's
|
||||||
|
identity. See its `--help`.
|
||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ see "LXC hosts" immediately below for why those are different.**
|
|||||||
## LXC hosts
|
## LXC hosts
|
||||||
|
|
||||||
`lxc-*` targets (`lxc-minimal`, `lxc-nix-cache`, `lxc-server`, `lxc-docker`,
|
`lxc-*` targets (`lxc-minimal`, `lxc-nix-cache`, `lxc-server`, `lxc-docker`,
|
||||||
`lxc-gui`, `lxc-pxe-boot`) are **not** installed via `auto-install.sh` — the
|
`lxc-gui`, `lxc-pxe-boot`, `lxc-tailscale-exit-node`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the
|
||||||
interactive menu deliberately excludes them. Don't try to select one there;
|
interactive menu deliberately excludes them. Don't try to select one there;
|
||||||
`nixos-install` would bind-mount `/` onto `/mnt` (LXC containers have no raw
|
`nixos-install` would bind-mount `/` onto `/mnt` (LXC containers have no raw
|
||||||
disk to partition) and then refuse to touch the filesystem it's currently
|
disk to partition) and then refuse to touch the filesystem it's currently
|
||||||
@@ -74,8 +74,8 @@ booting one:
|
|||||||
First boot runs `boot.postBootCommands` (registers the Nix store DB and
|
First boot runs `boot.postBootCommands` (registers the Nix store DB and
|
||||||
system profile) — there's no separate activation step to run yourself.
|
system profile) — there's no separate activation step to run yourself.
|
||||||
`scripts/create-proxmox-resource.sh --type lxc --host <name>` automates all
|
`scripts/create-proxmox-resource.sh --type lxc --host <name>` automates all
|
||||||
of this (build, host-key handling, upload, `pct create` with the flags
|
of this (host-key handling, building the tarball directly on the Proxmox
|
||||||
above) — see its `--help`.
|
node itself, `pct create` with the flags above) — see its `--help`.
|
||||||
|
|
||||||
Host keys still need pre-seeding the same way as any other host — the
|
Host keys still need pre-seeding the same way as any other host — the
|
||||||
sops-nix activation-vs-first-boot race is identical regardless of how the
|
sops-nix activation-vs-first-boot race is identical regardless of how the
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ On `nix-cache`, install the matching public key used by `nixremote` authorized k
|
|||||||
The committed `nixremote` authorized keys are public SSH keys only. Keep the
|
The committed `nixremote` authorized keys are public SSH keys only. Keep the
|
||||||
matching private keys on client hosts and out of the repository.
|
matching private keys on client hosts and out of the repository.
|
||||||
|
|
||||||
|
nix-cache's own SSH *host* key is trusted declaratively via
|
||||||
|
`programs.ssh.knownHosts` in `modules/nix-cache/remote-builder-client.nix`,
|
||||||
|
sourced from `vars.nixCacheHostKey` (`variables.nix`) — every client rebuild
|
||||||
|
picks it up automatically, so distributed builds don't fail with "Host key
|
||||||
|
verification failed" on a client that has never manually SSH'd to nix-cache
|
||||||
|
before. If nix-cache's host key is ever rotated or the host rebuilt from
|
||||||
|
scratch, update `vars.nixCacheHostKey` to match its new
|
||||||
|
`/etc/ssh/ssh_host_ed25519_key.pub`.
|
||||||
|
|
||||||
## Manual verification
|
## Manual verification
|
||||||
|
|
||||||
After deployment:
|
After deployment:
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ install, so there's nothing host-specific to write; it's available for every
|
|||||||
|
|
||||||
`scripts/create-proxmox-resource.sh --type vm --host <name>` automates the
|
`scripts/create-proxmox-resource.sh --type vm --host <name>` automates the
|
||||||
whole walkthrough below (and the equivalent LXC one) end to end, including
|
whole walkthrough below (and the equivalent LXC one) end to end, including
|
||||||
host-key handling and upload — see its `--help`. The steps here are what it
|
host-key handling and building the image directly on the Proxmox node
|
||||||
runs under the hood, useful for doing any of it by hand or understanding
|
itself (no local build, no image transfer) — see its `--help`. The steps
|
||||||
what it does before you trust it against real infrastructure.
|
here are what it runs under the hood, useful for doing any of it by hand
|
||||||
|
or understanding what it does before you trust it against real
|
||||||
|
infrastructure.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
# Spec: Refactor Flake Targets into Platform × Build-Type Matrix
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The flake at `~/nixos` currently defines these output targets (flat, ad-hoc naming):
|
|
||||||
|
|
||||||
- `docker`
|
|
||||||
- `linode-minimal`
|
|
||||||
- `nix-cache`
|
|
||||||
- `nix-minimal`
|
|
||||||
- `nixos`
|
|
||||||
- `server`
|
|
||||||
- `pxe-boot`
|
|
||||||
|
|
||||||
Some already follow a `platform-buildtype` convention (`linode-minimal`), most don't.
|
|
||||||
`~/nix-auto-installer` is a related repo and should be checked for any coupling to
|
|
||||||
these target names (scripts, docs, CI, or install automation that reference them by
|
|
||||||
name) before renaming anything.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Restructure the flake so targets are generated from two orthogonal concepts:
|
|
||||||
|
|
||||||
**Build types** (what the system is for):
|
|
||||||
- `minimal`
|
|
||||||
- `nix-cache`
|
|
||||||
- `server`
|
|
||||||
- `docker`
|
|
||||||
- `pxe-boot`
|
|
||||||
- `gui`
|
|
||||||
|
|
||||||
**Platforms** (what it's deployed on):
|
|
||||||
- `linode` (Linode VM)
|
|
||||||
- `proxmox` (Proxmox VM)
|
|
||||||
- `lxc` (Proxmox LXC container)
|
|
||||||
|
|
||||||
Final targets should be named consistently as `<platform>-<buildtype>`, e.g.:
|
|
||||||
|
|
||||||
```
|
|
||||||
linode-minimal proxmox-minimal lxc-minimal
|
|
||||||
linode-nix-cache proxmox-nix-cache lxc-nix-cache
|
|
||||||
linode-server proxmox-server lxc-server
|
|
||||||
linode-docker proxmox-docker lxc-docker
|
|
||||||
linode-pxe-boot proxmox-pxe-boot lxc-pxe-boot
|
|
||||||
linode-gui proxmox-gui lxc-gui
|
|
||||||
```
|
|
||||||
|
|
||||||
That's the full matrix (18 targets) if every build type applies to every platform.
|
|
||||||
See **Open Questions** below — some combinations may not make sense and should be
|
|
||||||
confirmed with me before being built out, not silently included or dropped.
|
|
||||||
|
|
||||||
## Migration mapping (old → new)
|
|
||||||
|
|
||||||
| Old target | New target | Notes |
|
|
||||||
|--------------------|------------------------------------------------------|-------|
|
|
||||||
| `linode-minimal` | `linode-minimal` | Already correct, keep as-is |
|
|
||||||
| `nix-minimal` | likely `proxmox-minimal` or a platform-less base module | Ambiguous — see Open Questions |
|
|
||||||
| `nix-cache` | base module consumed by `linode-nix-cache`, `proxmox-nix-cache`, `lxc-nix-cache` | Currently platform-less; needs to become a build-type module, not a standalone target |
|
|
||||||
| `server` | base module consumed by `linode-server`, `proxmox-server`, `lxc-server` | Same as above |
|
|
||||||
| `docker` | base module consumed by `linode-docker`, `proxmox-docker`, `lxc-docker` | Confirm docker actually makes sense as an LXC/VM guest build vs. a standalone container image — see Open Questions |
|
|
||||||
| `pxe-boot` | TBD — may stay a single target rather than a per-platform one | See Open Questions |
|
|
||||||
| `nixos` | TBD — unclear what this maps to in the new scheme | See Open Questions |
|
|
||||||
|
|
||||||
## Open Questions (Claude Code: raise these with me before implementing, don't guess)
|
|
||||||
|
|
||||||
1. **`nixos` target** — what is this currently used for (bare metal install, dev
|
|
||||||
shell, template)? It doesn't obviously map to any of the six build types.
|
|
||||||
2. **`nix-minimal` vs `linode-minimal`** — are these two different things, or is
|
|
||||||
`nix-minimal` a leftover/duplicate?
|
|
||||||
3. **`pxe-boot` and `gui` across all three platforms** — does PXE boot make sense
|
|
||||||
for an LXC container or a cloud VM (Linode), or is it inherently bare-metal/
|
|
||||||
network-boot only and should remain a single non-platform target? Does `gui`
|
|
||||||
make sense inside an LXC container?
|
|
||||||
4. **`docker` as a build type** — is this "a NixOS host configured to run Docker"
|
|
||||||
(which would sensibly have linode/proxmox/lxc variants), or "a Docker container
|
|
||||||
image built by the flake" (which wouldn't take a platform prefix at all, since
|
|
||||||
it doesn't run on Linode/Proxmox/LXC as a guest OS)? These are structurally
|
|
||||||
different and change how it should be wired in.
|
|
||||||
5. Confirm whether all 18 combinations should actually exist, or whether this is
|
|
||||||
meant to produce only the combinations that are genuinely useful (e.g. maybe no
|
|
||||||
one needs `lxc-pxe-boot`).
|
|
||||||
|
|
||||||
## Implementation approach
|
|
||||||
|
|
||||||
1. **Inventory first.** Read the current `flake.nix` and any `nixosConfigurations`/
|
|
||||||
`modules` structure. Map every existing target to what module(s) it actually
|
|
||||||
pulls in. Don't assume — confirm against the real file contents.
|
|
||||||
2. **Separate build-type and platform into their own module directories**, e.g.:
|
|
||||||
```
|
|
||||||
modules/build-types/minimal.nix
|
|
||||||
modules/build-types/nix-cache.nix
|
|
||||||
modules/build-types/server.nix
|
|
||||||
modules/build-types/docker.nix
|
|
||||||
modules/build-types/pxe-boot.nix
|
|
||||||
modules/build-types/gui.nix
|
|
||||||
|
|
||||||
modules/platforms/linode.nix
|
|
||||||
modules/platforms/proxmox.nix
|
|
||||||
modules/platforms/lxc.nix
|
|
||||||
```
|
|
||||||
Build-type modules should contain only what makes a system "minimal" vs
|
|
||||||
"server" vs "gui", etc. Platform modules should contain only what's specific
|
|
||||||
to running as a Linode VM vs Proxmox VM vs LXC container (virtualisation
|
|
||||||
guest tools, boot method, filesystem/image format, LXC-specific constraints
|
|
||||||
like no kernel modules, etc).
|
|
||||||
3. **Generate the target matrix programmatically** in `flake.nix` rather than
|
|
||||||
hand-writing 18 near-identical `nixosConfigurations` entries — e.g. a small
|
|
||||||
function that takes a platform name and build-type name, composes the two
|
|
||||||
modules plus any shared base module, and produces the named output. This
|
|
||||||
keeps future build types/platforms a one-line addition rather than a copy-paste
|
|
||||||
job.
|
|
||||||
4. **Only build combinations we've confirmed make sense** (see Open Questions) —
|
|
||||||
don't emit all 18 by default if some are structurally invalid.
|
|
||||||
5. **Preserve existing working configs during the transition.** Don't delete the
|
|
||||||
old target names until their replacements build successfully — rename/alias
|
|
||||||
at the end, not the start, so there's no window where the flake is broken.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
For every new target produced:
|
|
||||||
```bash
|
|
||||||
nix flake check
|
|
||||||
nix build .#nixosConfigurations.<target>.config.system.build.toplevel
|
|
||||||
```
|
|
||||||
Confirm each builds without evaluation errors before considering it done. If a
|
|
||||||
target fails to build, report which one and why rather than silently skipping it.
|
|
||||||
|
|
||||||
## Deliverables
|
|
||||||
|
|
||||||
- Refactored `flake.nix` using the composed module + generated-matrix approach.
|
|
||||||
- New `modules/build-types/*.nix` and `modules/platforms/*.nix` files.
|
|
||||||
- Old flat target names removed only after their replacements are verified.
|
|
||||||
- A short `README.md` (or section in existing docs) listing the final target
|
|
||||||
names and what each one is for.
|
|
||||||
- A summary at the end of what changed, what was removed, and any of the Open
|
|
||||||
Questions above that got resolved differently than expected.
|
|
||||||
|
|
||||||
## Out of scope
|
|
||||||
|
|
||||||
- Don't touch `~/nix-auto-installer` contents beyond checking it for references
|
|
||||||
to the old target names — if changes there are needed, flag them, don't make
|
|
||||||
them without confirming.
|
|
||||||
- Don't add new build types or platforms beyond the ones listed here.
|
|
||||||
@@ -98,6 +98,8 @@
|
|||||||
linode-tailscale-exit-node = mkTarget { platform = "linode"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
linode-tailscale-exit-node = mkTarget { platform = "linode"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
||||||
proxmox-tailscale-exit-node = mkTarget { platform = "proxmox"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
proxmox-tailscale-exit-node = mkTarget { platform = "proxmox"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
||||||
lxc-tailscale-exit-node = mkTarget { platform = "lxc"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
lxc-tailscale-exit-node = mkTarget { platform = "lxc"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
|
||||||
|
|
||||||
|
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
|
||||||
};
|
};
|
||||||
|
|
||||||
# Auto-install environments (migrated from the former nix-auto-installer
|
# Auto-install environments (migrated from the former nix-auto-installer
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
_:
|
||||||
|
|
||||||
|
{
|
||||||
|
networking.hostName = "tor-relay";
|
||||||
|
|
||||||
|
# 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,7 @@
|
|||||||
|
{ ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../tor/enable-relay.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -17,6 +17,26 @@ let
|
|||||||
--refresh \
|
--refresh \
|
||||||
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# lxc-* hosts pre-seed their SSH host key at build time (see
|
||||||
|
# modules/platforms/lxc.nix) so sops-nix's .sops.yaml recipient matches on
|
||||||
|
# first boot -- without it, secrets permanently fail to decrypt (see that
|
||||||
|
# file's comment for the confirmed failure). That requires --impure plus
|
||||||
|
# NIXOS_HOST_KEYS_DIR pointing at the repo's host-keys/ dir, same pattern
|
||||||
|
# docs/auto-installer.md uses for the installer ISO. A function, not a
|
||||||
|
# shellAlias, since the target name has to interpolate into the middle of
|
||||||
|
# the flake attribute path, not just append after it. Must be run from the
|
||||||
|
# repo root, same as every other host-keys/ command in this repo.
|
||||||
|
buildImageFn = ''
|
||||||
|
buildImage() {
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "usage: buildImage <flake-target> (e.g. lxc-docker)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||||
|
".#nixosConfigurations.$1.config.system.build.tarball"
|
||||||
|
}
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
programs.bash = {
|
programs.bash = {
|
||||||
@@ -25,5 +45,6 @@ in
|
|||||||
"Switch-nix" = mySwitchCmd;
|
"Switch-nix" = mySwitchCmd;
|
||||||
"Test-nix" = myTestCmd;
|
"Test-nix" = myTestCmd;
|
||||||
};
|
};
|
||||||
|
initExtra = buildImageFn;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
{ config, lib, pkgs, vars, ... }:
|
{ config, lib, pkgs, vars, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# `x-systemd.automount` never works inside a Linux container (LXC
|
||||||
|
# included, regardless of privilege) -- confirmed live on lxc-docker:
|
||||||
|
# systemd logs "Starting of <unit>.automount unsupported" for every
|
||||||
|
# share and never mounts them. Mount eagerly there instead, with
|
||||||
|
# `nofail` so a boot with the NFS server unreachable doesn't hang
|
||||||
|
# (the VM platforms rely on automount itself to get that same
|
||||||
|
# non-blocking behavior, so they don't need `nofail` too).
|
||||||
|
automountOpts = if config.boot.isContainer then [ "nofail" ] else [ "x-systemd.automount" ];
|
||||||
|
in
|
||||||
{
|
{
|
||||||
fileSystems = {
|
fileSystems = {
|
||||||
${vars.nfsShares.dockerConfig.mountpoint} = {
|
${vars.nfsShares.dockerConfig.mountpoint} = {
|
||||||
@@ -9,9 +19,8 @@
|
|||||||
options = [
|
options = [
|
||||||
"nfsvers=4.2"
|
"nfsvers=4.2"
|
||||||
"_netdev"
|
"_netdev"
|
||||||
"x-systemd.automount"
|
|
||||||
"noatime"
|
"noatime"
|
||||||
];
|
] ++ automountOpts;
|
||||||
};
|
};
|
||||||
|
|
||||||
${vars.nfsShares.dockerDatabases.mountpoint} = {
|
${vars.nfsShares.dockerDatabases.mountpoint} = {
|
||||||
@@ -21,9 +30,8 @@
|
|||||||
options = [
|
options = [
|
||||||
"nfsvers=4.2"
|
"nfsvers=4.2"
|
||||||
"_netdev"
|
"_netdev"
|
||||||
"x-systemd.automount"
|
|
||||||
"noatime"
|
"noatime"
|
||||||
];
|
] ++ automountOpts;
|
||||||
};
|
};
|
||||||
|
|
||||||
${vars.nfsShares.dockerVolumes.mountpoint} = {
|
${vars.nfsShares.dockerVolumes.mountpoint} = {
|
||||||
@@ -33,9 +41,8 @@
|
|||||||
options = [
|
options = [
|
||||||
"nfsvers=4.2"
|
"nfsvers=4.2"
|
||||||
"_netdev"
|
"_netdev"
|
||||||
"x-systemd.automount"
|
|
||||||
"noatime"
|
"noatime"
|
||||||
];
|
] ++ automountOpts;
|
||||||
};
|
};
|
||||||
|
|
||||||
${vars.nfsShares.nextcloudData.mountpoint} = {
|
${vars.nfsShares.nextcloudData.mountpoint} = {
|
||||||
@@ -45,9 +52,8 @@
|
|||||||
options = [
|
options = [
|
||||||
"nfsvers=4.2"
|
"nfsvers=4.2"
|
||||||
"_netdev"
|
"_netdev"
|
||||||
"x-systemd.automount"
|
|
||||||
"noatime"
|
"noatime"
|
||||||
];
|
] ++ automountOpts;
|
||||||
};
|
};
|
||||||
|
|
||||||
${vars.nfsShares.raspiVolumes.mountpoint} = {
|
${vars.nfsShares.raspiVolumes.mountpoint} = {
|
||||||
@@ -57,9 +63,8 @@
|
|||||||
options = [
|
options = [
|
||||||
"nfsvers=4.2"
|
"nfsvers=4.2"
|
||||||
"_netdev"
|
"_netdev"
|
||||||
"x-systemd.automount"
|
|
||||||
"noatime"
|
"noatime"
|
||||||
];
|
] ++ automountOpts;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
# sudo install -d -m 0700 /root/.ssh
|
# sudo install -d -m 0700 /root/.ssh
|
||||||
# sudo install -m 0600 ./nixremote /root/.ssh/nixremote
|
# sudo install -m 0600 ./nixremote /root/.ssh/nixremote
|
||||||
# sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
# sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
||||||
|
# Trust nix-cache's SSH host key declaratively so the nix-daemon (root)
|
||||||
|
# can connect the first time without a manual ssh-keyscan/known_hosts
|
||||||
|
# step on every new client.
|
||||||
|
programs.ssh.knownHosts.${vars.nixCacheHost} = {
|
||||||
|
hostNames = [ vars.nixCacheHost ];
|
||||||
|
publicKey = vars.nixCacheHostKey;
|
||||||
|
};
|
||||||
|
|
||||||
nix = {
|
nix = {
|
||||||
distributedBuilds = true;
|
distributedBuilds = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{ vars, ... }:
|
{ config, lib, vars, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
fileSystems.${vars.raspiMountpoint} = {
|
fileSystems.${vars.raspiMountpoint} = {
|
||||||
@@ -9,6 +9,15 @@
|
|||||||
"_netdev"
|
"_netdev"
|
||||||
"noatime"
|
"noatime"
|
||||||
|
|
||||||
|
# Explicitly use NFSv4.2 if supported
|
||||||
|
"nfsvers=4.2"
|
||||||
|
] ++ lib.optionals (!config.boot.isContainer) [
|
||||||
|
# `x-systemd.automount` never works inside a Linux container (LXC
|
||||||
|
# included) -- confirmed live on lxc-docker: systemd logs "Starting
|
||||||
|
# of <unit>.automount unsupported" and never mounts it. `nofail`
|
||||||
|
# above already keeps boot non-blocking there, so plain eager
|
||||||
|
# mounting is fine.
|
||||||
|
|
||||||
# Don't mount until first access
|
# Don't mount until first access
|
||||||
"x-systemd.automount"
|
"x-systemd.automount"
|
||||||
|
|
||||||
@@ -17,9 +26,6 @@
|
|||||||
|
|
||||||
# Give the Pi/Tailscale a little time to appear
|
# Give the Pi/Tailscale a little time to appear
|
||||||
"x-systemd.device-timeout=10s"
|
"x-systemd.device-timeout=10s"
|
||||||
|
|
||||||
# Explicitly use NFSv4.2 if supported
|
|
||||||
"nfsvers=4.2"
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
_:
|
_:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
imports = [ ./enable-service.nix ];
|
||||||
|
|
||||||
services.tailscale = {
|
services.tailscale = {
|
||||||
enable = true;
|
# 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
|
# extraSetFlags (tailscale set, via the always-on tailscaled-set
|
||||||
# service), not extraUpFlags -- extraUpFlags is only ever applied by
|
# service), not extraUpFlags -- extraUpFlags is only ever applied by
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{ pkgs, vars, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
services.tor = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
# Opens settings.ORPort (and DirPort, unset here) in the firewall —
|
||||||
|
# see the nixpkgs tor module's own networking.firewall.mkIf block.
|
||||||
|
openFirewall = true;
|
||||||
|
|
||||||
|
relay = {
|
||||||
|
enable = true;
|
||||||
|
# Plain middle/guard relay, not "exit" — relays onion traffic between
|
||||||
|
# other Tor nodes without ever making requests to the public internet
|
||||||
|
# on a user's behalf, avoiding the abuse complaints and legal exposure
|
||||||
|
# an exit node invites.
|
||||||
|
role = "relay";
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.ORPort = vars.ports.torRelayOrPort;
|
||||||
|
|
||||||
|
# Unix control socket at /run/tor/control (GroupWritable, group "tor")
|
||||||
|
# -- what nyx below actually monitors the relay through. Nyx's own
|
||||||
|
# default control-socket path (/var/run/tor/control) resolves to the
|
||||||
|
# same place, so no extra nyx config is needed.
|
||||||
|
controlSocket.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Lets the primary user's shell session read/write the control socket
|
||||||
|
# above without being root -- otherwise nyx fails to authenticate against
|
||||||
|
# it at all.
|
||||||
|
users.users.${vars.primaryUser}.extraGroups = [ "tor" ];
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.nyx ];
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
# Spec: Remove Sensitive Information from NixOS Flake
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Every secret currently readable in plaintext anywhere in this repo (working tree *and* git history) gets removed, replaced with `sops-nix`-managed encrypted references, and rotated. When this is done, the repo should be safe to make public without exposing anything about the systems it configures.
|
|
||||||
|
|
||||||
Treat this as three sequential milestones. Do not start git history rewriting (Milestone 3) until Milestones 1 and 2 are fully verified and the flake still builds. This should be its own branch (`refactor/secrets`) until fully verified, then merged.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 1 — Audit
|
|
||||||
|
|
||||||
Before touching anything, produce a complete inventory. Do not guess at scope — grep the whole tree and the whole history.
|
|
||||||
|
|
||||||
1. Run a secret scanner across the working tree and full history. Use both, since they catch different things:
|
|
||||||
- `gitleaks detect --source . -v --log-opts="--all"` (scans history too)
|
|
||||||
- `trufflehog git file://. --since-commit=$(git rev-list --max-parents=0 HEAD) --only-verified=false`
|
|
||||||
If neither is installed, add them via a temporary `nix-shell -p gitleaks trufflehog` — don't install anything globally on the host.
|
|
||||||
|
|
||||||
2. Manually grep for the categories below, since scanners miss config-specific patterns:
|
|
||||||
- `hashedPassword`, `password`, `initialPassword`, `initialHashedPassword` in any `users.users.*` block
|
|
||||||
- `age.secrets`, `sops.secrets` (if any partial secrets work already exists — check for it)
|
|
||||||
- PSK / `preSharedKey`, `privateKeyFile` inline values (vs. file references) for WireGuard
|
|
||||||
- `authKey`, `apiToken`, `api_key`, `token =`, `secret =` in service modules (Tailscale, Cloudflare, backup tools, etc.)
|
|
||||||
- SSH private key material: search for `BEGIN OPENSSH PRIVATE KEY` / `BEGIN RSA PRIVATE KEY` literals
|
|
||||||
- TLS cert/key pairs committed under e.g. `secrets/`, `certs/`, `pki/`
|
|
||||||
- Real name, personal email, home address, or anything in comments/hostnames that maps a machine to your physical identity or network layout (e.g. hostnames like `wayne-desktop`, static LAN IPs, ISP-identifying info)
|
|
||||||
- `.env` files, `secrets.nix`, `secrets.yaml`, or any file that looks like it was meant to be gitignored but wasn't
|
|
||||||
|
|
||||||
3. Produce `secrets-inventory.md` (temporary, delete before finishing) listing: file path, line, secret type, and which host/service it belongs to. This becomes the checklist for Milestone 2 — every row must be either migrated to sops or deleted, with nothing left unaccounted for.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 2 — Migrate to sops-nix
|
|
||||||
|
|
||||||
### 2.1 Set up sops-nix
|
|
||||||
|
|
||||||
1. Add the flake input:
|
|
||||||
```nix
|
|
||||||
sops-nix.url = "github:Mic92/sops-nix";
|
|
||||||
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
```
|
|
||||||
2. Import `sops-nix.nixosModules.sops` into each host's module list (or into a shared `common.nix` if all hosts use it).
|
|
||||||
3. Generate an age keypair **per host** (not one shared key for everything — a compromised host shouldn't decrypt every other host's secrets):
|
|
||||||
```
|
|
||||||
nix-shell -p age --run "age-keygen -o /var/lib/sops-nix/key.txt"
|
|
||||||
```
|
|
||||||
Print the public key (`age-keygen -y`) for each host — you'll need it for `.sops.yaml`.
|
|
||||||
4. Also generate one age key for yourself (your admin workstation) so you can edit secrets without needing to SSH into a host: store it at `~/.config/sops/age/keys.txt`, back it up somewhere outside this repo (password manager, offline). **If this key is lost, every secret encrypted with it is unrecoverable — losing the age key is equivalent to losing the secrets.**
|
|
||||||
5. Create `.sops.yaml` at the repo root defining creation rules: which age public keys can decrypt which secrets files, keyed by path regex, so e.g. `secrets/hostA.yaml` is decryptable by your admin key + hostA's key, `secrets/hostB.yaml` by your admin key + hostB's key.
|
|
||||||
|
|
||||||
### 2.2 Migrate each secret category from the inventory
|
|
||||||
|
|
||||||
For each row in `secrets-inventory.md`:
|
|
||||||
|
|
||||||
- **Password hashes**: generate hash with `mkpasswd -m sha-512` (or `bcrypt` if your setup wants that), store under `sops.secrets."<name>/hashedPassword"`, reference via `users.users.<name>.hashedPasswordFile = config.sops.secrets."<name>/hashedPassword".path;`. Do not put the *plaintext* password anywhere, only the hash, and only the hash goes into the encrypted sops file.
|
|
||||||
- **API tokens / auth keys**: move the raw value into the per-host sops YAML, reference in the module via `config.sops.secrets."<service>/token".path` — most NixOS service modules that take a token also accept a `*File` variant (e.g. `environmentFile`, `tokenFile`); use that instead of passing the value directly.
|
|
||||||
- **Private keys / certs**: move the PEM/key content wholesale into a sops secret, output as a file with appropriate `sops.secrets.<name>.path`, `owner`, `mode`, `restartUnits` so the depending service (sshd, wireguard, nginx) reloads when the secret changes.
|
|
||||||
- **Personal/identifying info**: this doesn't belong in sops (it's not "secret," it's just information you don't want public). Replace real names/emails with placeholders or move to a small untracked `local.nix` that's `.gitignore`'d and imported conditionally, with a documented template (`local.nix.example`) committed instead.
|
|
||||||
|
|
||||||
### 2.3 Verify before moving on
|
|
||||||
|
|
||||||
- `nixos-rebuild dry-build --flake .#<host>` succeeds for every host.
|
|
||||||
- `sudo nixos-rebuild switch --flake .#<host>` on at least one real machine (or a VM) confirms secrets decrypt and services start.
|
|
||||||
- Confirm decrypted secrets land under `/run/secrets/` (not the Nix store — anything placed in `/nix/store` is world-readable by design, so sops-nix's runtime-only placement is the whole point; double check no module accidentally pulls a secret path into a store-built config file).
|
|
||||||
- Re-run the grep/scanner sweep from Milestone 1 against the *working tree only* (not history yet) — it should now come back clean.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 3 — Scrub git history
|
|
||||||
|
|
||||||
Do this only after Milestone 2 is merged to your main branch and confirmed working, since it rewrites every commit SHA from the point of the earliest offending commit onward.
|
|
||||||
|
|
||||||
**This is destructive and irreversible on your local clone. Back up first:**
|
|
||||||
```
|
|
||||||
cp -r /path/to/nixos-repo /path/to/nixos-repo-backup-$(date +%F)
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Install `git-filter-repo` (not the older `git filter-branch` / BFG — filter-repo is the currently maintained, faster, safer tool):
|
|
||||||
```
|
|
||||||
nix-shell -p git-filter-repo
|
|
||||||
```
|
|
||||||
2. Use the `secrets-inventory.md` list to build a list of literal strings/paths to strip. Two approaches, use both:
|
|
||||||
- Path-based: if whole files were secret (e.g. `secrets.nix`, a `.env`, a private key file), remove them entirely from history:
|
|
||||||
```
|
|
||||||
git filter-repo --path secrets.nix --path .env --invert-paths
|
|
||||||
```
|
|
||||||
- Value-based: for secrets embedded inline in files you're keeping (not deleting the whole file), use `--replace-text` with a file listing each literal secret string to replace with `***REMOVED***`:
|
|
||||||
```
|
|
||||||
git filter-repo --replace-text expressions.txt
|
|
||||||
```
|
|
||||||
3. After filtering, verify: run the Milestone 1 scanners again against full history (`--log-opts="--all"`). They must come back clean.
|
|
||||||
4. Force-push the rewritten history:
|
|
||||||
```
|
|
||||||
git push origin --force --all
|
|
||||||
git push origin --force --tags
|
|
||||||
```
|
|
||||||
5. **Every other clone of this repo (other machines, WSL instances, CI) must be deleted and re-cloned fresh** — a `git pull` against rewritten history will not work cleanly and risks resurrecting the old commits. Don't try to reconcile old clones; throw them away and re-clone.
|
|
||||||
6. If this repo has ever been pushed to a public host (GitHub, etc.) or a fork/mirror exists, treat every secret that was ever in history as **permanently compromised regardless of the rewrite** — caches, forks, and Wayback-style archives can retain old commits indefinitely. History scrubbing prevents *future* exposure via `git clone`; it does not undo past exposure.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 4 — Rotate everything
|
|
||||||
|
|
||||||
Because the secrets were exposed in history (even briefly, even in a private repo), the migration is not complete until every credential in the inventory has been **rotated**, not just re-encrypted. Re-encrypting an already-leaked value protects it going forward but doesn't undo the leak.
|
|
||||||
|
|
||||||
For each row in the original inventory:
|
|
||||||
- Password hashes → change the actual account password, regenerate the hash, update the sops file.
|
|
||||||
- API tokens/auth keys → revoke the old token in the issuing service's dashboard (Cloudflare, Tailscale, backup provider, etc.) and generate a new one.
|
|
||||||
- SSH/WireGuard private keys → generate new keypairs, update the corresponding public key wherever it's trusted (authorized_keys, peer configs, etc.), retire the old ones.
|
|
||||||
- TLS certs → reissue if the private key was exposed.
|
|
||||||
|
|
||||||
Keep `secrets-inventory.md` open during this step and check off each row as rotated. Delete the file only once every row is checked off — it should not be committed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ongoing prevention
|
|
||||||
|
|
||||||
Add a pre-commit hook (or a `nix flake check` step) running `gitleaks protect --staged` so a secret can't be committed again by accident. Document in the repo README (briefly) that new secrets go through `sops <file>` to edit, never as plaintext in a tracked file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Definition of done
|
|
||||||
|
|
||||||
- [ ] Milestone 1 inventory complete and reviewed
|
|
||||||
- [ ] All hosts have per-host age keys; admin key backed up outside the repo
|
|
||||||
- [ ] Every inventoried secret migrated to sops-nix, referenced via `*File`/`sops.secrets.*.path`, nothing plaintext in the working tree
|
|
||||||
- [ ] `nixos-rebuild dry-build` and at least one real `switch` verified per host
|
|
||||||
- [ ] Working-tree scanner sweep clean
|
|
||||||
- [ ] History rewritten with `git-filter-repo`, force-pushed, full-history scanner sweep clean
|
|
||||||
- [ ] All other clones deleted and re-cloned from the rewritten history
|
|
||||||
- [ ] Every credential in the original inventory rotated (not just re-encrypted)
|
|
||||||
- [ ] Pre-commit secret scanning hook added
|
|
||||||
- [ ] `secrets-inventory.md` deleted from the working directory (never committed)
|
|
||||||
Executable
+148
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Backs up the local sops age key (the private key that decrypts
|
||||||
|
# secrets/*.yaml -- normally the one trusted as &admin) to an arbitrary
|
||||||
|
# destination path, e.g. a USB drive or other offline storage, so it can
|
||||||
|
# later be restored and handed to rotate-admin-key.sh if this machine's
|
||||||
|
# copy is ever lost, or to run either script from a different machine.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/backup-admin-key.sh <dest-path> [--key-file <path>] [--force] [--dry-run]
|
||||||
|
#
|
||||||
|
# Source key resolution matches sops/age's own default order:
|
||||||
|
# $SOPS_AGE_KEY (inline identity text) if set, else
|
||||||
|
# --key-file if given, else
|
||||||
|
# $SOPS_AGE_KEY_FILE if set, else
|
||||||
|
# ${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
sops_yaml="${repo_root}/.sops.yaml"
|
||||||
|
|
||||||
|
# shellcheck source=env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
|
||||||
|
# Pin cwd for the same reason rotate-admin-key.sh does: age/sops calls
|
||||||
|
# below should never depend on wherever the caller's shell happened to be.
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 <dest-path> [--key-file <path>] [--force] [--dry-run]
|
||||||
|
|
||||||
|
<dest-path> Where to write the backup. Parent directories are
|
||||||
|
created as needed. Written with 0600 permissions.
|
||||||
|
--key-file <path> Read the key from here instead of the default
|
||||||
|
sops/age resolution (\$SOPS_AGE_KEY_FILE, then
|
||||||
|
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt).
|
||||||
|
Ignored if \$SOPS_AGE_KEY is set (that always wins,
|
||||||
|
same precedence sops/age itself uses).
|
||||||
|
--force Overwrite <dest-path> if it already exists.
|
||||||
|
--dry-run Print what would happen; write nothing.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
force=0
|
||||||
|
key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
||||||
|
args=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
dry_run=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--force)
|
||||||
|
force=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--key-file)
|
||||||
|
key_file="${2:?--key-file requires a path}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
args+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${#args[@]}" -ne 1 ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
dest="${args[0]}"
|
||||||
|
|
||||||
|
nix_extra_opts
|
||||||
|
|
||||||
|
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
|
||||||
|
echo "==> Source: \$SOPS_AGE_KEY (inline identity from the environment)."
|
||||||
|
src_content="$SOPS_AGE_KEY"
|
||||||
|
else
|
||||||
|
[[ -s "$key_file" ]] || {
|
||||||
|
echo "ERROR: no key found. \$SOPS_AGE_KEY is unset and ${key_file} doesn't exist or is empty." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "==> Source: ${key_file}"
|
||||||
|
src_content="$(cat "$key_file")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Round-trip through a private scratch file (rather than trusting the
|
||||||
|
# source string as-is) so age-keygen -y validates it's a real identity
|
||||||
|
# before anything is written to <dest-path>.
|
||||||
|
scratch="$(mktemp)"
|
||||||
|
trap 'rm -f "$scratch"' EXIT
|
||||||
|
( umask 077; printf '%s\n' "$src_content" > "$scratch" )
|
||||||
|
|
||||||
|
src_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$scratch'")" || {
|
||||||
|
echo "ERROR: source doesn't look like a valid age identity (age-keygen -y failed)." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " public key: ${src_pub}"
|
||||||
|
|
||||||
|
current_admin_pub="$(grep -E '^ - &admin age1' "$sops_yaml" 2>/dev/null | awk '{print $NF}' || true)"
|
||||||
|
if [[ -n "$current_admin_pub" && "$current_admin_pub" != "$src_pub" ]]; then
|
||||||
|
echo "NOTE: this key does not match .sops.yaml's current &admin entry (${current_admin_pub})."
|
||||||
|
echo " Backing it up anyway -- this script doesn't require it to be the admin key."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "$dest" && "$force" -ne 1 ]]; then
|
||||||
|
echo "ERROR: ${dest} already exists. Pass --force to overwrite." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
echo "[dry-run] would write $(wc -c <"$scratch" | tr -d ' ') bytes to ${dest} (mode 0600)"
|
||||||
|
[[ -e "$dest" ]] && echo "[dry-run] would overwrite existing file (--force given)"
|
||||||
|
echo "[dry-run] Nothing was written. Re-run without --dry-run to apply this."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$dest")"
|
||||||
|
install -m 600 "$scratch" "$dest"
|
||||||
|
|
||||||
|
dest_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$dest'")"
|
||||||
|
if [[ "$dest_pub" != "$src_pub" ]]; then
|
||||||
|
echo "ERROR: ${dest} was written but its public key doesn't match the source -- investigate before relying on this backup." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done. Backed up to: ${dest}
|
||||||
|
public key: ${dest_pub}
|
||||||
|
|
||||||
|
This is a private key -- store it somewhere offline/secure, not in this
|
||||||
|
repo or anywhere it'd get committed. Restore it with:
|
||||||
|
scripts/rotate-admin-key.sh ${dest}
|
||||||
|
EOF
|
||||||
@@ -1,22 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
export NIX_CONFIG="${NIX_CONFIG:-}
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
experimental-features = nix-command flakes
|
# shellcheck source=lib/nix-bootstrap.sh
|
||||||
accept-flake-config = false
|
source "${script_dir}/lib/nix-bootstrap.sh"
|
||||||
warn-dirty = false
|
# shellcheck source=lib/nix-eval.sh
|
||||||
"
|
source "${script_dir}/lib/nix-eval.sh"
|
||||||
|
|
||||||
MODE="${1:-validate}"
|
MODE="${1:-validate}"
|
||||||
|
|
||||||
ensure_nix_profile() {
|
|
||||||
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
|
||||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
|
||||||
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
|
||||||
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_nix_profile
|
ensure_nix_profile
|
||||||
|
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
@@ -24,8 +16,7 @@ if ! command -v nix >/dev/null 2>&1; then
|
|||||||
exit 127
|
exit 127
|
||||||
fi
|
fi
|
||||||
|
|
||||||
hosts_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames)"
|
hosts="$(list_flake_targets .)"
|
||||||
hosts="$(echo "$hosts_json" | jq -r '.[]')"
|
|
||||||
|
|
||||||
echo "Hosts:"
|
echo "Hosts:"
|
||||||
echo "$hosts"
|
echo "$hosts"
|
||||||
@@ -44,17 +35,17 @@ fi
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Checking Nix formatting with nixpkgs-fmt..."
|
echo "Checking Nix formatting with nixpkgs-fmt..."
|
||||||
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
|
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Running statix lint..."
|
echo "Running statix lint..."
|
||||||
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#statix -- check .
|
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check .
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Evaluating host toplevel derivations..."
|
echo "Evaluating host toplevel derivations..."
|
||||||
for host in $hosts; do
|
for host in $hosts; do
|
||||||
echo "==> $host"
|
echo "==> $host"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
||||||
|
|
||||||
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of
|
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of
|
||||||
# nixos-install (see docs/auto-installer.md); proxmox-* hosts can
|
# nixos-install (see docs/auto-installer.md); proxmox-* hosts can
|
||||||
@@ -64,22 +55,21 @@ for host in $hosts; do
|
|||||||
case "$host" in
|
case "$host" in
|
||||||
lxc-*)
|
lxc-*)
|
||||||
echo "==> $host (tarball)"
|
echo "==> $host (tarball)"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
|
||||||
;;
|
;;
|
||||||
proxmox-*)
|
proxmox-*)
|
||||||
echo "==> $host (diskoImagesScript)"
|
echo "==> $host (diskoImagesScript)"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Evaluating buildable packages..."
|
echo "Evaluating buildable packages..."
|
||||||
packages_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#packages.x86_64-linux --apply builtins.attrNames)"
|
packages="$(nix eval --json "${NIX_EVAL_FLAGS[@]}" .#packages.x86_64-linux --apply builtins.attrNames | jq -r '.[]')"
|
||||||
packages="$(echo "$packages_json" | jq -r '.[]')"
|
|
||||||
for pkg in $packages; do
|
for pkg in $packages; do
|
||||||
echo "==> packages.x86_64-linux.${pkg}"
|
echo "==> packages.x86_64-linux.${pkg}"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "$MODE" == "dry-run" ]]; then
|
if [[ "$MODE" == "dry-run" ]]; then
|
||||||
@@ -87,16 +77,16 @@ if [[ "$MODE" == "dry-run" ]]; then
|
|||||||
echo "Running dry-run builds for all hosts. This will not create result symlinks."
|
echo "Running dry-run builds for all hosts. This will not create result symlinks."
|
||||||
for host in $hosts; do
|
for host in $hosts; do
|
||||||
echo "==> Dry-run build: $host"
|
echo "==> Dry-run build: $host"
|
||||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel"
|
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel"
|
||||||
|
|
||||||
case "$host" in
|
case "$host" in
|
||||||
lxc-*)
|
lxc-*)
|
||||||
echo "==> Dry-run build: $host (tarball)"
|
echo "==> Dry-run build: $host (tarball)"
|
||||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball"
|
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball"
|
||||||
;;
|
;;
|
||||||
proxmox-*)
|
proxmox-*)
|
||||||
echo "==> Dry-run build: $host (diskoImagesScript)"
|
echo "==> Dry-run build: $host (diskoImagesScript)"
|
||||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
|
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -105,7 +95,7 @@ if [[ "$MODE" == "dry-run" ]]; then
|
|||||||
echo "Running dry-run builds for all packages."
|
echo "Running dry-run builds for all packages."
|
||||||
for pkg in $packages; do
|
for pkg in $packages; do
|
||||||
echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
|
echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
|
||||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}"
|
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+18
-15
@@ -1,19 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
export NIX_CONFIG="${NIX_CONFIG:-}
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
experimental-features = nix-command flakes
|
# shellcheck source=lib/nix-bootstrap.sh
|
||||||
accept-flake-config = false
|
source "${script_dir}/lib/nix-bootstrap.sh"
|
||||||
warn-dirty = false
|
# shellcheck source=lib/nix-eval.sh
|
||||||
"
|
source "${script_dir}/lib/nix-eval.sh"
|
||||||
|
|
||||||
ensure_nix_profile() {
|
|
||||||
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
|
||||||
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
|
||||||
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
|
||||||
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_nix_if_missing() {
|
install_nix_if_missing() {
|
||||||
if command -v nix >/dev/null 2>&1; then
|
if command -v nix >/dev/null 2>&1; then
|
||||||
@@ -49,6 +41,17 @@ warn-dirty = false
|
|||||||
build-users-group = nixbld
|
build-users-group = nixbld
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# The official installer's single-user root path still shells out to
|
||||||
|
# `sudo` to create /nix even though it already knows it's running as
|
||||||
|
# root -- confirmed live against a sudo-less minimal Debian/Proxmox
|
||||||
|
# node, where it fails with "sudo: not found" and prints this exact
|
||||||
|
# mkdir/chown as the manual fix. Pre-create it so that branch of the
|
||||||
|
# installer is skipped entirely.
|
||||||
|
if [ ! -d /nix ]; then
|
||||||
|
mkdir -m 0755 /nix
|
||||||
|
chown root /nix
|
||||||
|
fi
|
||||||
|
|
||||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||||
else
|
else
|
||||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||||
@@ -79,13 +82,13 @@ if ! command -v jq >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Available NixOS hosts:"
|
echo "Available NixOS hosts:"
|
||||||
hosts="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')"
|
hosts="$(list_flake_targets .)"
|
||||||
echo "$hosts"
|
echo "$hosts"
|
||||||
|
|
||||||
echo "Evaluating all host toplevel derivations..."
|
echo "Evaluating all host toplevel derivations..."
|
||||||
for host in $hosts; do
|
for host in $hosts; do
|
||||||
echo "==> Evaluating $host"
|
echo "==> Evaluating $host"
|
||||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Codex setup complete."
|
echo "Codex setup complete."
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
# existing ones -- the manual workflows in docs/proxmox-images.md (VM) and
|
# existing ones -- the manual workflows in docs/proxmox-images.md (VM) and
|
||||||
# docs/auto-installer.md's "LXC hosts" section (container), automated.
|
# docs/auto-installer.md's "LXC hosts" section (container), automated.
|
||||||
#
|
#
|
||||||
|
# Images are built directly on the Proxmox node (PROXMOX_REMOTE_REPO_DIR /
|
||||||
|
# --remote-repo-dir in scripts/env.sh), not on whatever machine runs this
|
||||||
|
# script -- there's no multi-gigabyte image to transfer afterward. The first
|
||||||
|
# time a node doesn't have that repo path yet, it's bootstrapped: cloned from
|
||||||
|
# this checkout's own `origin` remote, then scripts/codex-setup.sh installs
|
||||||
|
# the build tooling (Nix, etc.). Every run after that just `git pull`s it and
|
||||||
|
# copies over the locally-managed host-keys/ (gitignored, so a git pull
|
||||||
|
# alone wouldn't carry it) before building.
|
||||||
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# scripts/create-proxmox-resource.sh --type lxc|vm --host <name> [options]
|
# scripts/create-proxmox-resource.sh --type lxc|vm --host <name> [options]
|
||||||
# scripts/create-proxmox-resource.sh --type lxc|vm --list
|
# scripts/create-proxmox-resource.sh --type lxc|vm --list
|
||||||
@@ -10,13 +19,24 @@
|
|||||||
#
|
#
|
||||||
# SAFETY:
|
# SAFETY:
|
||||||
# - The default (create) mode only ever creates a NEW resource -- it
|
# - The default (create) mode only ever creates a NEW resource -- it
|
||||||
# refuses to run if the target VMID already exists on the node.
|
# refuses to run if the target VMID already exists on the node, or if
|
||||||
|
# a VM/CT identified as --host already exists under any other VMID
|
||||||
|
# (checked live against the node; --allow-duplicate-host overrides).
|
||||||
|
# - --allow-duplicate-host distinguishes an exact match (same --type
|
||||||
|
# *and* --host, e.g. re-running --type lxc --host docker while an
|
||||||
|
# lxc-docker container already exists -- almost always a redeploy of
|
||||||
|
# the same target to pick up a rebuilt image) from a cross-type match
|
||||||
|
# (a different platform sharing the same host identity, e.g. a
|
||||||
|
# proxmox-docker VM coexisting with lxc-docker). Only the exact match
|
||||||
|
# is destroyed and replaced, after typing the hostname back to
|
||||||
|
# confirm (outside --dry-run) -- a cross-type match is always left
|
||||||
|
# untouched, matching-or-not.
|
||||||
# - --modify only ever touches a resource you name explicitly via
|
# - --modify only ever touches a resource you name explicitly via
|
||||||
# --vmid, shows exactly what will change first, and (outside
|
# --vmid, shows exactly what will change first, and (outside
|
||||||
# --dry-run) always requires typing that VMID back to confirm before
|
# --dry-run) always requires typing that VMID back to confirm before
|
||||||
# anything is sent to the node. There is no bulk/implicit modify.
|
# anything is sent to the node. There is no bulk/implicit modify.
|
||||||
# - Neither mode can start/stop/delete a resource. Not implemented on
|
# - Outside of --allow-duplicate-host's exact-match replace above,
|
||||||
# purpose -- ask before adding it.
|
# neither mode can start/stop/delete a resource.
|
||||||
#
|
#
|
||||||
# See --help for the full option list.
|
# See --help for the full option list.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -24,6 +44,8 @@ set -euo pipefail
|
|||||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
# shellcheck source=env.sh
|
# shellcheck source=env.sh
|
||||||
source "${repo_root}/scripts/env.sh"
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=lib/nix-eval.sh
|
||||||
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
|
|
||||||
sync_keys="${repo_root}/scripts/sync-host-keys.sh"
|
sync_keys="${repo_root}/scripts/sync-host-keys.sh"
|
||||||
|
|
||||||
@@ -51,15 +73,26 @@ Create mode (default):
|
|||||||
Refuses to run if this ID already exists.
|
Refuses to run if this ID already exists.
|
||||||
--disk-size <GB> lxc only: rootfs size for \`pct create\`
|
--disk-size <GB> lxc only: rootfs size for \`pct create\`
|
||||||
(default: \$PROXMOX_DEFAULT_LXC_DISK_GB, ${PROXMOX_DEFAULT_LXC_DISK_GB}).
|
(default: \$PROXMOX_DEFAULT_LXC_DISK_GB, ${PROXMOX_DEFAULT_LXC_DISK_GB}).
|
||||||
--image <path> Use this local image/tarball instead of
|
--image <path> Use this local image/tarball (uploaded to the
|
||||||
checking the node / building one from the flake.
|
node via scp) instead of checking the node /
|
||||||
|
building one there from the flake.
|
||||||
--force-rebuild Skip the "does the node already have this
|
--force-rebuild Skip the "does the node already have this
|
||||||
image" check -- always build fresh and
|
image" check -- always build fresh and
|
||||||
overwrite what's there.
|
overwrite what's there.
|
||||||
--allow-duplicate-host Required if --host already has a real
|
--remote-repo-dir <path> Where this flake repo lives (or gets
|
||||||
deployment elsewhere (variables.nix's
|
cloned) on the node, and is built from
|
||||||
deployedTargets) -- otherwise refused, since
|
(default: \$PROXMOX_REMOTE_REPO_DIR, ${PROXMOX_REMOTE_REPO_DIR}).
|
||||||
it'd share that host's hostName/hostId.
|
--allow-duplicate-host Required if a VM/CT identified as --host
|
||||||
|
already exists on the node (checked live via
|
||||||
|
qm/pct, not any file in this repo) --
|
||||||
|
otherwise refused, since it'd share that
|
||||||
|
host's hostName/hostId. An existing resource
|
||||||
|
of this *same* --type (e.g. re-running --type
|
||||||
|
lxc --host docker over an existing lxc-docker)
|
||||||
|
is destroyed and replaced, after confirming --
|
||||||
|
a different --type sharing the same --host
|
||||||
|
(e.g. a proxmox-docker VM) is always left
|
||||||
|
untouched.
|
||||||
|
|
||||||
Modify mode (reconfigure an EXISTING resource -- requires --modify):
|
Modify mode (reconfigure an EXISTING resource -- requires --modify):
|
||||||
--modify Switch to modify mode.
|
--modify Switch to modify mode.
|
||||||
@@ -111,6 +144,7 @@ storage="$PROXMOX_STORAGE"
|
|||||||
iso_storage="$PROXMOX_ISO_STORAGE"
|
iso_storage="$PROXMOX_ISO_STORAGE"
|
||||||
bridge="$PROXMOX_BRIDGE"
|
bridge="$PROXMOX_BRIDGE"
|
||||||
node="$PROXMOX_HOST"
|
node="$PROXMOX_HOST"
|
||||||
|
remote_repo_dir="$PROXMOX_REMOTE_REPO_DIR"
|
||||||
do_list=0
|
do_list=0
|
||||||
allow_duplicate_host=0
|
allow_duplicate_host=0
|
||||||
force_rebuild=0
|
force_rebuild=0
|
||||||
@@ -131,6 +165,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--iso-storage) iso_storage="$2"; shift 2 ;;
|
--iso-storage) iso_storage="$2"; shift 2 ;;
|
||||||
--bridge) bridge="$2"; shift 2 ;;
|
--bridge) bridge="$2"; shift 2 ;;
|
||||||
--node) node="$2"; shift 2 ;;
|
--node) node="$2"; shift 2 ;;
|
||||||
|
--remote-repo-dir) remote_repo_dir="$2"; shift 2 ;;
|
||||||
--list) do_list=1; shift ;;
|
--list) do_list=1; shift ;;
|
||||||
--allow-duplicate-host) allow_duplicate_host=1; shift ;;
|
--allow-duplicate-host) allow_duplicate_host=1; shift ;;
|
||||||
--force-rebuild) force_rebuild=1; shift ;;
|
--force-rebuild) force_rebuild=1; shift ;;
|
||||||
@@ -237,15 +272,21 @@ platform_prefix="lxc"
|
|||||||
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
|
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
|
||||||
|
|
||||||
# --- discover / resolve the flake target from --host --------------------
|
# --- discover / resolve the flake target from --host --------------------
|
||||||
|
# Emits "<target>\t<hostName>" pairs for every ${platform_prefix}-* flake
|
||||||
|
# target -- the one source both --list and the --host lookup below read
|
||||||
|
# from, so they can never see a different set of targets from each other.
|
||||||
|
targets_for_platform() {
|
||||||
|
local target
|
||||||
|
for target in $(list_flake_targets "$repo_root" 2>/dev/null | grep -- "^${platform_prefix}-"); do
|
||||||
|
printf '%s\t%s\n' "$target" "$(flake_target_hostname "$repo_root" "$target")"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
list_hosts() {
|
list_hosts() {
|
||||||
local target hostname
|
local target hostname
|
||||||
for target in $(nix eval --json --no-use-registries --no-accept-flake-config \
|
while IFS=$'\t' read -r target hostname; do
|
||||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames 2>/dev/null \
|
|
||||||
| jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do
|
|
||||||
hostname="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
|
||||||
"${repo_root}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null)"
|
|
||||||
printf ' %-12s -> %s\n' "$hostname" "$target"
|
printf ' %-12s -> %s\n' "$hostname" "$target"
|
||||||
done
|
done < <(targets_for_platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "$do_list" -eq 1 ]]; then
|
if [[ "$do_list" -eq 1 ]]; then
|
||||||
@@ -260,16 +301,12 @@ if [[ -z "$host" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
flake_target=""
|
flake_target=""
|
||||||
for target in $(nix eval --json --no-use-registries --no-accept-flake-config \
|
while IFS=$'\t' read -r target hostname; do
|
||||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames \
|
if [[ "$hostname" == "$host" ]]; then
|
||||||
| jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do
|
|
||||||
hn="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
|
||||||
"${repo_root}#nixosConfigurations.${target}.config.networking.hostName")"
|
|
||||||
if [[ "$hn" == "$host" ]]; then
|
|
||||||
flake_target="$target"
|
flake_target="$target"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done < <(targets_for_platform)
|
||||||
|
|
||||||
if [[ -z "$flake_target" ]]; then
|
if [[ -z "$flake_target" ]]; then
|
||||||
echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2
|
echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2
|
||||||
@@ -286,25 +323,132 @@ 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"
|
||||||
|
|
||||||
# --- refuse to duplicate a host that's already really deployed ----------
|
# --- refuse to duplicate a host that's already live on the node ---------
|
||||||
# Checked by hostName, not exact flake target: proxmox-server being
|
# Queries the node itself (qm/pct's own name/hostname config), not any
|
||||||
# deployed also blocks --type lxc --host server, since both would carry
|
# static list in this repo -- a file can't track whether a resource still
|
||||||
# the same hosts/server/host.nix identity (hostName, hostId).
|
# actually exists, and this used to be checked against variables.nix's
|
||||||
if [[ "$allow_duplicate_host" -eq 0 ]]; then
|
# deployedTargets, which drifted stale (it kept naming a VM as "the real
|
||||||
deployed_targets_json="$(nix eval --json --no-use-registries --no-accept-flake-config \
|
# deployment" well after that VM had been destroyed, blocking its own
|
||||||
--file "${repo_root}/variables.nix" deployedTargets)"
|
# redeploy) until that list was dropped in favour of this live check. This
|
||||||
for dt in $(echo "$deployed_targets_json" | jq -r '.[]'); do
|
# only catches guests identified with the default --name (== --host, what
|
||||||
dt_hostname="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
# this script itself always uses unless --name is overridden) -- a guest
|
||||||
"${repo_root}#nixosConfigurations.${dt}.config.networking.hostName" 2>/dev/null || true)"
|
# manually renamed on the node afterwards wouldn't match, but nothing here
|
||||||
if [[ "$dt_hostname" == "$host" ]]; then
|
# creates guests that way.
|
||||||
echo "ERROR: '${host}' already has a real deployment (${dt}, per variables.nix's" >&2
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "deployedTargets). Creating ${flake_target} would share its hostName/hostId --" >&2
|
echo
|
||||||
echo "refusing by default. Pass --allow-duplicate-host if you really mean to spin" >&2
|
echo "[dry-run] would check ${node} for an existing VM/CT identified as '${host}'"
|
||||||
echo "up a separate test instance of this host (it'll still get its own distinct" >&2
|
if [[ "$allow_duplicate_host" -eq 1 ]]; then
|
||||||
echo "sops key and VMID, never touching ${dt})." >&2
|
echo "[dry-run] --allow-duplicate-host: an existing ${type} named '${host}' would be" \
|
||||||
|
"destroyed and replaced; a different-type match would be left untouched"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
|
||||||
|
ssh_check_status=0
|
||||||
|
existing="$(ssh "$ssh_target" bash -s -- "$host" <<'REMOTE_SCRIPT'
|
||||||
|
target="$1"
|
||||||
|
for id in $(qm list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||||
|
n="$(qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
|
||||||
|
[[ "$n" == "$target" ]] && echo "vm ${id} ${n}"
|
||||||
|
done
|
||||||
|
for id in $(pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||||
|
n="$(pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
||||||
|
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
REMOTE_SCRIPT
|
||||||
|
)" || ssh_check_status=$?
|
||||||
|
if [[ "$ssh_check_status" -ne 0 ]]; then
|
||||||
|
echo "ERROR: couldn't reach ${node} (ssh exited ${ssh_check_status}) to check for an" >&2
|
||||||
|
echo "existing '${host}' resource -- refusing to guess. Fix connectivity and retry," >&2
|
||||||
|
echo "or pass --allow-duplicate-host if you're sure none exists (this skips the" >&2
|
||||||
|
echo "check entirely)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Split into "exact" (same resource kind as --type -- i.e. literally this
|
||||||
|
# same host+platform combo already exists, almost always a redeploy of
|
||||||
|
# the same target to test a rebuilt image) vs "cross-type" (a different
|
||||||
|
# platform sharing this host identity, e.g. a stopped proxmox-docker VM
|
||||||
|
# coexisting with an lxc-docker container -- a deliberate, valid setup
|
||||||
|
# this script has never managed and still won't). Read via a herestring
|
||||||
|
# (not a pipe) so the appends below survive outside the loop.
|
||||||
|
this_kind="$type"
|
||||||
|
exact_matches=""
|
||||||
|
cross_matches=""
|
||||||
|
if [[ -n "$existing" ]]; then
|
||||||
|
while read -r kind id n; do
|
||||||
|
[[ -z "$kind" ]] && continue
|
||||||
|
if [[ "$kind" == "$this_kind" ]]; then
|
||||||
|
exact_matches+="${kind} ${id} ${n}"$'\n'
|
||||||
|
else
|
||||||
|
cross_matches+="${kind} ${id} ${n}"$'\n'
|
||||||
|
fi
|
||||||
|
done <<<"$existing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$exact_matches" && "$allow_duplicate_host" -ne 1 ]]; then
|
||||||
|
echo "ERROR: '${host}' already exists on ${node} as this same resource type:" >&2
|
||||||
|
echo "$exact_matches" | while read -r kind id n; do
|
||||||
|
[[ -z "$kind" ]] && continue
|
||||||
|
echo " - ${kind} VMID ${id} (${n})" >&2
|
||||||
|
done
|
||||||
|
echo "Refusing to create a second ${this_kind} sharing this identity. Pass" >&2
|
||||||
|
echo "--allow-duplicate-host to destroy it and create a fresh one in its place" >&2
|
||||||
|
echo "(after confirming), or use --modify to reconfigure the existing one instead." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$cross_matches" && "$allow_duplicate_host" -ne 1 ]]; then
|
||||||
|
echo "ERROR: '${host}' already exists on ${node} as a different resource type:" >&2
|
||||||
|
echo "$cross_matches" | while read -r kind id n; do
|
||||||
|
[[ -z "$kind" ]] && continue
|
||||||
|
echo " - ${kind} VMID ${id} (${n})" >&2
|
||||||
|
done
|
||||||
|
echo "Refusing to create a second resource sharing this identity. Pass" >&2
|
||||||
|
echo "--allow-duplicate-host to create one anyway (it gets its own distinct" >&2
|
||||||
|
echo "sops key and VMID -- the existing resource above is left untouched)," >&2
|
||||||
|
echo "or use --modify to reconfigure the existing one instead." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$cross_matches" ]]; then
|
||||||
|
echo "--allow-duplicate-host: '${host}' also exists on ${node} as a different resource" \
|
||||||
|
"type -- leaving it untouched:"
|
||||||
|
echo "$cross_matches" | while read -r kind id n; do
|
||||||
|
[[ -z "$kind" ]] && continue
|
||||||
|
echo " - ${kind} VMID ${id} (${n})"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$exact_matches" ]]; then
|
||||||
|
echo "--allow-duplicate-host: '${host}' already exists on ${node} as this same resource" \
|
||||||
|
"type -- it will be destroyed and replaced:"
|
||||||
|
echo "$exact_matches" | while read -r kind id n; do
|
||||||
|
[[ -z "$kind" ]] && continue
|
||||||
|
echo " - ${kind} VMID ${id} (${n})"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
read -rp "Type the hostname (${host}) to confirm destroying the above and replacing it: " confirm
|
||||||
|
if [[ "$confirm" != "$host" ]]; then
|
||||||
|
echo "Cancelled -- input didn't match ${host}." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
echo "$exact_matches" | while read -r kind id n; do
|
||||||
|
[[ -z "$kind" ]] && continue
|
||||||
|
echo "==> Destroying ${kind} VMID ${id} (${n})..."
|
||||||
|
if [[ "$kind" == "vm" ]]; then
|
||||||
|
# qm destroy has no --force to stop-then-destroy in one call (pct's
|
||||||
|
# does) -- stop explicitly first if it's running.
|
||||||
|
if ssh "$ssh_target" "qm status ${id}" 2>/dev/null | grep -q running; then
|
||||||
|
ssh "$ssh_target" "qm stop ${id}"
|
||||||
|
fi
|
||||||
|
ssh "$ssh_target" "qm destroy ${id} --purge 1"
|
||||||
|
else
|
||||||
|
ssh "$ssh_target" "pct destroy ${id} --force 1 --purge 1"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'"
|
echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'"
|
||||||
@@ -369,6 +513,84 @@ if [[ "$type" == "lxc" ]]; then
|
|||||||
fi
|
fi
|
||||||
remote_path="${remote_dir}/${remote_filename}"
|
remote_path="${remote_dir}/${remote_filename}"
|
||||||
|
|
||||||
|
# --- ensure the flake repo (+ tooling) exists on the node, and is current --
|
||||||
|
# Bootstraps once (git clone from this checkout's own `origin`, then
|
||||||
|
# scripts/codex-setup.sh installs Nix + friends) if ${remote_repo_dir}
|
||||||
|
# doesn't exist yet on the node; otherwise just `git pull`s it, so the image
|
||||||
|
# built there reflects what's actually committed and pushed. Only called
|
||||||
|
# right before an actual remote build below -- reusing an image already on
|
||||||
|
# the node, or an explicit --image, never touch the node's checkout at all.
|
||||||
|
ensure_remote_repo() {
|
||||||
|
echo
|
||||||
|
echo "==> Ensuring ${remote_repo_dir} exists and is current on ${node}..."
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would ensure ${remote_repo_dir} exists on ${node} (clone if missing, git pull if present), and would verify/bootstrap build tooling there (scripts/codex-setup.sh) if \`nix\` isn't already on PATH"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ssh "$ssh_target" "test -d '${remote_repo_dir}/.git'"; then
|
||||||
|
echo "Repo present -- pulling latest..."
|
||||||
|
ssh "$ssh_target" "cd '${remote_repo_dir}' && git pull --ff-only"
|
||||||
|
else
|
||||||
|
local origin_url
|
||||||
|
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
|
||||||
|
if [[ -z "$origin_url" ]]; then
|
||||||
|
echo "ERROR: ${remote_repo_dir} doesn't exist on ${node}, and this checkout has no" >&2
|
||||||
|
echo "'origin' remote to clone from. Set one (git remote add origin <url>) or create" >&2
|
||||||
|
echo "${remote_repo_dir} on ${node} yourself (e.g. git clone), then re-run." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Not present -- cloning from ${origin_url}..."
|
||||||
|
ssh "$ssh_target" "git clone '${origin_url}' '${remote_repo_dir}'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Trivial check, run every time (not just right after a fresh clone) --
|
||||||
|
# confirmed live: a first bootstrap can clone the repo successfully and
|
||||||
|
# still leave the node without a working `nix` (e.g. the node had no
|
||||||
|
# `sudo`, which the Nix installer's root path depends on -- see the fix
|
||||||
|
# in scripts/codex-setup.sh), and a later run with the repo already
|
||||||
|
# present would otherwise never retry it. Sources
|
||||||
|
# scripts/lib/nix-bootstrap.sh's ensure_nix_profile first -- a
|
||||||
|
# single-user Nix install typically only gets sourced into login shells,
|
||||||
|
# and ssh's non-interactive command execution is neither, so a
|
||||||
|
# freshly-installed `nix` still wouldn't be on PATH here without it.
|
||||||
|
#
|
||||||
|
# Just `nix` today -- the only thing the remote build commands below
|
||||||
|
# actually invoke -- but a list (not a single hardcoded check) so a
|
||||||
|
# future remote step needing another tool can add itself here instead of
|
||||||
|
# growing a parallel check.
|
||||||
|
local remote_required_cmds=(nix)
|
||||||
|
local tooling_check_cmd="cd '${remote_repo_dir}' && . scripts/lib/nix-bootstrap.sh && ensure_nix_profile"
|
||||||
|
local cmd
|
||||||
|
for cmd in "${remote_required_cmds[@]}"; do
|
||||||
|
tooling_check_cmd="${tooling_check_cmd} && command -v ${cmd}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if ssh "$ssh_target" "$tooling_check_cmd" >/dev/null 2>&1; then
|
||||||
|
echo "Build tooling already present on ${node}."
|
||||||
|
else
|
||||||
|
echo "==> Bootstrapping build tooling on ${node} (scripts/codex-setup.sh)..."
|
||||||
|
ssh "$ssh_target" "cd '${remote_repo_dir}' && bash scripts/codex-setup.sh"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- sync locally-managed host-keys/ to the node ---------------------------
|
||||||
|
# Gitignored (see .gitignore), so `git pull` above never carries it -- both
|
||||||
|
# build paths need it present as NIXOS_HOST_KEYS_DIR / --pre-format-files
|
||||||
|
# input on the node itself now that the build runs there. scp (not rsync,
|
||||||
|
# not already a dependency anywhere else in this repo) mirrors how this
|
||||||
|
# script already transfers the --image case below.
|
||||||
|
sync_remote_host_keys() {
|
||||||
|
echo
|
||||||
|
echo "==> Syncing host-keys/ to ${node}..."
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] would copy ${repo_root}/host-keys/ to ${ssh_target}:${remote_repo_dir}/host-keys/"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'"
|
||||||
|
scp -pr "${repo_root}/host-keys/." "${ssh_target}:${remote_repo_dir}/host-keys/"
|
||||||
|
}
|
||||||
|
|
||||||
# --- build (or reuse an image already on the node) ------------------------
|
# --- build (or reuse an image already on the node) ------------------------
|
||||||
echo
|
echo
|
||||||
local_image=""
|
local_image=""
|
||||||
@@ -385,7 +607,7 @@ else
|
|||||||
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 ${remote_path}"
|
||||||
elif ssh "$ssh_target" "test -f '${remote_path}'" 2>/dev/null; then
|
elif ssh "$ssh_target" "test -f '${remote_path}'" 2>/dev/null; then
|
||||||
echo "Found it -- reusing, skipping build and upload (use --force-rebuild to override)."
|
echo "Found it -- reusing, skipping build (use --force-rebuild to override)."
|
||||||
image_already_remote=1
|
image_already_remote=1
|
||||||
else
|
else
|
||||||
echo "Not found -- will build."
|
echo "Not found -- will build."
|
||||||
@@ -393,69 +615,131 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
|
if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
|
||||||
# Mirrors the real build commands' "${NIX_OPTS[@]}" below -- nix_extra_opts
|
ensure_remote_repo
|
||||||
# (called earlier, once) has already decided whether nix-cache is in play,
|
sync_remote_host_keys
|
||||||
# and the dry-run preview needs to reflect that decision instead of always
|
|
||||||
# printing the same command regardless of outcome.
|
# Relayed into the remote build below exactly as decided by the local
|
||||||
|
# nix_extra_opts call earlier in this script -- that decision (whether
|
||||||
|
# nix-cache is reachable) is made once, locally, same as it always has
|
||||||
|
# been; only *where* the resulting "${NIX_OPTS[@]}" gets used as a `nix
|
||||||
|
# build` flag moves to the node. NIX_EXTRA_OPTS is already a %q-quoted
|
||||||
|
# string built for exactly this eval-based reconstruction (see env.sh).
|
||||||
nix_opts_display=""
|
nix_opts_display=""
|
||||||
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
|
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
|
||||||
printf -v nix_opts_display '%q ' "${NIX_OPTS[@]}"
|
printf -v nix_opts_display '%q ' "${NIX_OPTS[@]}"
|
||||||
nix_opts_display=" ${nix_opts_display% }"
|
nix_opts_display=" ${nix_opts_display% }"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$type" == "lxc" ]]; then
|
if [[ "$type" == "lxc" ]]; then
|
||||||
if [[ "$dry_run" -eq 1 ]]; then
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
echo "[dry-run] would build: NIXOS_HOST_KEYS_DIR=${repo_root}/host-keys nix build --impure \\"
|
echo "[dry-run] would build on ${node}: NIXOS_HOST_KEYS_DIR=\$(pwd)/host-keys nix build --impure \\"
|
||||||
echo "[dry-run] --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
echo "[dry-run] --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||||
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.tarball"
|
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.tarball"
|
||||||
|
echo "[dry-run] would stage the result at ${remote_path}"
|
||||||
local_image="<built-tarball>"
|
local_image="<built-tarball>"
|
||||||
else
|
else
|
||||||
echo "==> Building LXC tarball for ${flake_target}..."
|
echo "==> Building LXC tarball for ${flake_target} on ${node}..."
|
||||||
NIXOS_HOST_KEYS_DIR="${repo_root}/host-keys" nix build --impure \
|
# Built as a single already-%q-quoted command string, not separate ssh
|
||||||
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
# argv elements -- ssh joins remote command args with plain spaces and
|
||||||
".#nixosConfigurations.${flake_target}.config.system.build.tarball" \
|
# hands the result to the remote shell to re-split, which would
|
||||||
--out-link "${repo_root}/result-${flake_target}"
|
# otherwise scatter NIX_EXTRA_OPTS (itself several space-separated,
|
||||||
local_image="$(find "${repo_root}/result-${flake_target}/tarball" -maxdepth 1 -type f | head -1)"
|
# %q-quoted tokens) across the wrong positional parameters below.
|
||||||
echo "Built: ${local_image}"
|
printf -v remote_cmd 'bash -s -- %q %q %q %q %q' \
|
||||||
|
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS"
|
||||||
|
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
||||||
|
set -euo pipefail
|
||||||
|
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"
|
||||||
|
declare -a NIX_OPTS=()
|
||||||
|
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||||
|
cd "$repo_dir"
|
||||||
|
# A single-user Nix install only gets sourced into login shells; this ssh
|
||||||
|
# session is neither, so `nix` wouldn't otherwise be on PATH here even
|
||||||
|
# right after a successful install.
|
||||||
|
. scripts/lib/nix-bootstrap.sh
|
||||||
|
ensure_nix_profile
|
||||||
|
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||||
|
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||||
|
".#nixosConfigurations.${target}.config.system.build.tarball" \
|
||||||
|
--out-link "result-${target}"
|
||||||
|
built="$(find "result-${target}/tarball" -maxdepth 1 -type f | head -1)"
|
||||||
|
if [[ -z "$built" ]]; then
|
||||||
|
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$dest_dir"
|
||||||
|
cp "$built" "${dest_dir}/${dest_name}"
|
||||||
|
echo "Built and staged: ${dest_dir}/${dest_name}"
|
||||||
|
REMOTE_SCRIPT
|
||||||
|
local_image="$remote_path"
|
||||||
|
echo "Built on ${node}: ${remote_path}"
|
||||||
fi
|
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: nix build --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
echo "[dry-run] would build on ${node}: nix build --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
|
||||||
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
|
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
|
||||||
echo "[dry-run] would run: sudo ./result-${flake_target} \\"
|
echo "[dry-run] would run: ${sudo_display}./result-${flake_target} \\"
|
||||||
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \\"
|
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \\"
|
||||||
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \\"
|
echo "[dry-run] --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] --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 script for ${flake_target}..."
|
echo "==> Building Disko image for ${flake_target} on ${node}..."
|
||||||
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
# See the LXC branch above for why this is one %q-quoted command
|
||||||
".#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript" \
|
# string rather than separate ssh argv elements.
|
||||||
--out-link "${repo_root}/result-${flake_target}"
|
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q' \
|
||||||
echo "==> Running it (builds the .raw image in a temporary QEMU VM, needs sudo)..."
|
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
|
||||||
( cd "$repo_root" && sudo "./result-${flake_target}" \
|
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
|
||||||
--pre-format-files "host-keys/${flake_target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
set -euo pipefail
|
||||||
--pre-format-files "host-keys/${flake_target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_prefix="$6"
|
||||||
--build-memory 2048 )
|
declare -a NIX_OPTS=()
|
||||||
local_image="$(find "$repo_root" -maxdepth 1 -name "*.raw" -newer "${repo_root}/result-${flake_target}" | head -1)"
|
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
|
||||||
if [[ -z "$local_image" ]]; then
|
cd "$repo_dir"
|
||||||
echo "ERROR: expected a .raw image after the build but didn't find one in ${repo_root}." >&2
|
. scripts/lib/nix-bootstrap.sh
|
||||||
exit 1
|
ensure_nix_profile
|
||||||
fi
|
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||||
echo "Built: ${local_image}"
|
".#nixosConfigurations.${target}.config.system.build.diskoImagesScript" \
|
||||||
|
--out-link "result-${target}"
|
||||||
|
$sudo_prefix "./result-${target}" \
|
||||||
|
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
||||||
|
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
||||||
|
--build-memory 2048
|
||||||
|
built="$(find . -maxdepth 1 -name '*.raw' -newer "result-${target}" | head -1)"
|
||||||
|
if [[ -z "$built" ]]; then
|
||||||
|
echo "ERROR: no .raw image found in ${repo_dir} after build." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$dest_dir"
|
||||||
|
mv "$built" "${dest_dir}/${dest_name}"
|
||||||
|
echo "Built and staged: ${dest_dir}/${dest_name}"
|
||||||
|
REMOTE_SCRIPT
|
||||||
|
local_image="$remote_path"
|
||||||
|
echo "Built on ${node}: ${remote_path}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- upload (skip entirely if reusing an image already on the node) ------
|
# --- upload -- only for an explicit --image; a build above stages its
|
||||||
|
# result directly at ${remote_path} on the node already, and reusing an
|
||||||
|
# image already on the node needs nothing transferred either. ------------
|
||||||
echo
|
echo
|
||||||
if [[ "$image_already_remote" -eq 1 ]]; then
|
if [[ -n "$image" ]]; then
|
||||||
: # nothing to upload
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
elif [[ "$dry_run" -eq 1 ]]; then
|
echo "[dry-run] would upload: scp ${local_image} ${ssh_target}:${remote_path}"
|
||||||
echo "[dry-run] would upload: scp ${local_image} ${ssh_target}:${remote_path}"
|
else
|
||||||
else
|
echo "==> Uploading to ${node}:${remote_path}..."
|
||||||
echo "==> Uploading to ${node}:${remote_path}..."
|
ssh "$ssh_target" "mkdir -p ${remote_dir}"
|
||||||
ssh "$ssh_target" "mkdir -p ${remote_dir}"
|
scp "$local_image" "${ssh_target}:${remote_path}"
|
||||||
scp "$local_image" "${ssh_target}:${remote_path}"
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- create -----------------------------------------------------------------
|
# --- create -----------------------------------------------------------------
|
||||||
@@ -481,7 +765,17 @@ if [[ "$type" == "lxc" ]]; then
|
|||||||
# systemd routinely uses (even plain getty units), and every getty
|
# systemd routinely uses (even plain getty units), and every getty
|
||||||
# crash-loops on a denied mount every ~3s (visible as garbage on the
|
# crash-loops on a denied mount every ~3s (visible as garbage on the
|
||||||
# console) while core services like nsncd fail the same way.
|
# console) while core services like nsncd fail the same way.
|
||||||
create_cmd="pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged 1 --features ${PROXMOX_DEFAULT_LXC_FEATURES} --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
|
#
|
||||||
|
# ...,mount=nfs;nfs4: without it AppArmor blanket-denies the `nfs`/
|
||||||
|
# `rpc_pipefs` mount syscalls any NFS client share needs -- confirmed
|
||||||
|
# live on lxc-docker: `mount: /var/lib/nfs/rpc_pipefs: permission
|
||||||
|
# denied`. The value's `;` (Proxmox's own multi-fstype separator for
|
||||||
|
# this one feature, per PVE::LXC's use of PVE::ParseUtils::split_list)
|
||||||
|
# must stay single-quoted here: create_cmd is sent to `remote()`, which
|
||||||
|
# hands the whole string to `ssh` as a single command for the *remote*
|
||||||
|
# shell to parse -- unquoted, that `;` would be read as a remote
|
||||||
|
# command separator and silently truncate this into two commands.
|
||||||
|
create_cmd="pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged 1 --features '${PROXMOX_DEFAULT_LXC_FEATURES}' --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
|
||||||
remote "$create_cmd"
|
remote "$create_cmd"
|
||||||
remote "pct start ${vmid}"
|
remote "pct start ${vmid}"
|
||||||
else
|
else
|
||||||
|
|||||||
+19
-2
@@ -14,6 +14,14 @@
|
|||||||
: "${PROXMOX_HOST:=pve.sweet.home}"
|
: "${PROXMOX_HOST:=pve.sweet.home}"
|
||||||
: "${PROXMOX_SSH_USER:=root}"
|
: "${PROXMOX_SSH_USER:=root}"
|
||||||
|
|
||||||
|
# Where this flake repo lives on the Proxmox node itself.
|
||||||
|
# scripts/create-proxmox-resource.sh builds images directly on the node
|
||||||
|
# instead of transferring them over the network -- it clones the repo here
|
||||||
|
# (from this checkout's own `origin` remote) the first time it doesn't
|
||||||
|
# find it, installing build tooling via scripts/codex-setup.sh, then
|
||||||
|
# `git pull`s it before every subsequent build.
|
||||||
|
: "${PROXMOX_REMOTE_REPO_DIR:=/root/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.).
|
||||||
# Verify with `pvesm status` on the node and correct these if wrong.
|
# Verify with `pvesm status` on the node and correct these if wrong.
|
||||||
@@ -45,11 +53,20 @@
|
|||||||
# crash-loops on a denied `/run/credentials/*` mount every ~3s (visible
|
# crash-loops on a denied `/run/credentials/*` mount every ~3s (visible
|
||||||
# as garbage on the console) and core services like nsncd fail the same
|
# as garbage on the console) and core services like nsncd fail the same
|
||||||
# way on userns_create; system.build.tarball never finishes activating.
|
# way on userns_create; system.build.tarball never finishes activating.
|
||||||
: "${PROXMOX_DEFAULT_LXC_FEATURES:=nesting=1,keyctl=1}"
|
#
|
||||||
|
# mount=nfs;nfs4: without it, AppArmor blanket-denies the `nfs`/
|
||||||
|
# `rpc_pipefs` mount syscalls any NFS client share needs -- confirmed
|
||||||
|
# live on lxc-docker (which mounts several, see modules/docker/mount-data.nix
|
||||||
|
# and modules/raspi/mount-data.nix): `mount: /var/lib/nfs/rpc_pipefs:
|
||||||
|
# permission denied`. Harmless to grant on lxc targets that don't mount
|
||||||
|
# NFS at all -- it only widens what the container is *allowed* to mount,
|
||||||
|
# nothing here forces a mount to happen.
|
||||||
|
: "${PROXMOX_DEFAULT_LXC_FEATURES:=nesting=1,keyctl=1,mount=nfs;nfs4}"
|
||||||
|
|
||||||
export PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE PROXMOX_ISO_STORAGE \
|
export PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE PROXMOX_ISO_STORAGE \
|
||||||
PROXMOX_BRIDGE PROXMOX_DEFAULT_CORES PROXMOX_DEFAULT_MEMORY_MB \
|
PROXMOX_BRIDGE PROXMOX_DEFAULT_CORES PROXMOX_DEFAULT_MEMORY_MB \
|
||||||
PROXMOX_DEFAULT_LXC_DISK_GB PROXMOX_DEFAULT_LXC_FEATURES
|
PROXMOX_DEFAULT_LXC_DISK_GB PROXMOX_DEFAULT_LXC_FEATURES \
|
||||||
|
PROXMOX_REMOTE_REPO_DIR
|
||||||
|
|
||||||
# Matches variables.nix's nixCacheHost -- update both if it ever changes.
|
# Matches variables.nix's nixCacheHost -- update both if it ever changes.
|
||||||
: "${NIX_CACHE_HOST:=nix-cache}"
|
: "${NIX_CACHE_HOST:=nix-cache}"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared Nix bootstrap for scripts/codex-setup.sh and
|
||||||
|
# scripts/codex-maintenance.sh: the nix.conf settings both need in effect
|
||||||
|
# before a single `nix` command runs (flakes enabled, never honor a flake
|
||||||
|
# input's own nixConfig, no "dirty tree" warning spam), plus a helper to
|
||||||
|
# pull an already-installed Nix's daemon/profile script onto PATH if it
|
||||||
|
# isn't there yet. Source this instead of copying it -- see CLAUDE.md.
|
||||||
|
export NIX_CONFIG="${NIX_CONFIG:-}
|
||||||
|
experimental-features = nix-command flakes
|
||||||
|
accept-flake-config = false
|
||||||
|
warn-dirty = false
|
||||||
|
"
|
||||||
|
|
||||||
|
ensure_nix_profile() {
|
||||||
|
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
|
||||||
|
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||||
|
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
||||||
|
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared flake-introspection helpers for scripts/*.sh. Source alongside
|
||||||
|
# env.sh:
|
||||||
|
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-eval.sh"
|
||||||
|
#
|
||||||
|
# NIX_EVAL_FLAGS: --no-use-registries so a call here never resolves through
|
||||||
|
# the user's global flake registry (every call targets this repo's own
|
||||||
|
# flake, or an explicit github: ref, not a registry alias); --no-accept-flake-config
|
||||||
|
# so a flake input's own nixConfig (e.g. a dependency's substituters) is
|
||||||
|
# never honored -- matches accept-flake-config = false already set repo-wide
|
||||||
|
# (see lib/nix-bootstrap.sh / CLAUDE.md). Reuse this array rather than
|
||||||
|
# retyping the two flags at each call site.
|
||||||
|
declare -a NIX_EVAL_FLAGS=(--no-use-registries --no-accept-flake-config)
|
||||||
|
|
||||||
|
# list_flake_targets <flake_ref>
|
||||||
|
# Prints the attribute names under <flake_ref>#nixosConfigurations, one per
|
||||||
|
# line, e.g.:
|
||||||
|
# list_flake_targets . # from inside the repo
|
||||||
|
# list_flake_targets "$repo_root" # from anywhere
|
||||||
|
list_flake_targets() {
|
||||||
|
local flake_ref="$1"
|
||||||
|
nix eval --json "${NIX_EVAL_FLAGS[@]}" \
|
||||||
|
"${flake_ref}#nixosConfigurations" --apply builtins.attrNames \
|
||||||
|
| jq -r '.[]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# flake_target_hostname <flake_ref> <target>
|
||||||
|
# Prints one nixosConfigurations target's config.networking.hostName.
|
||||||
|
# Empty (not an error under set -e) if the target doesn't exist or the
|
||||||
|
# eval otherwise fails -- callers that need to distinguish "empty" from
|
||||||
|
# "eval failed" should check $? themselves instead of relying on this.
|
||||||
|
flake_target_hostname() {
|
||||||
|
local flake_ref="$1" target="$2"
|
||||||
|
nix eval --raw "${NIX_EVAL_FLAGS[@]}" \
|
||||||
|
"${flake_ref}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared SSH-host-key / age-conversion helpers for scripts/sync-host-keys.sh
|
||||||
|
# and scripts/prepare-host-key.sh -- both generate the same kind of key
|
||||||
|
# (ed25519, no passphrase, the sops-nix age-derivation input) and convert it
|
||||||
|
# to an age recipient the same way; kept in one place so the two can't
|
||||||
|
# drift apart.
|
||||||
|
#
|
||||||
|
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
|
||||||
|
# nix_extra_opts) if the caller has already set it, so a decision to avoid
|
||||||
|
# an unreachable nix-cache is reused here instead of probed again. Falls
|
||||||
|
# back to no extra options if the caller never sourced env.sh.
|
||||||
|
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
|
||||||
|
declare -a NIX_OPTS=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
# generate_host_ed25519_key <hostname> <keyfile>
|
||||||
|
# Writes <keyfile> and <keyfile>.pub. Caller is responsible for refusing to
|
||||||
|
# overwrite an existing keyfile -- this always runs ssh-keygen fresh.
|
||||||
|
generate_host_ed25519_key() {
|
||||||
|
local hostname="$1" keyfile="$2"
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
|
||||||
|
"ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ssh_pubkey_to_age <pubkeyfile>
|
||||||
|
# Prints the age public key derived from an ed25519 SSH public key file.
|
||||||
|
ssh_pubkey_to_age() {
|
||||||
|
local pubkeyfile="$1"
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${pubkeyfile}'"
|
||||||
|
}
|
||||||
@@ -23,6 +23,10 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
# shellcheck source=env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=lib/ssh-host-keys.sh
|
||||||
|
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
||||||
|
|
||||||
hostname="${1:?usage: scripts/prepare-host-key.sh <hostname>}"
|
hostname="${1:?usage: scripts/prepare-host-key.sh <hostname>}"
|
||||||
sops_yaml="${repo_root}/.sops.yaml"
|
sops_yaml="${repo_root}/.sops.yaml"
|
||||||
@@ -41,9 +45,10 @@ if [[ -f "$keyfile" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
nix-shell -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
|
nix_extra_opts
|
||||||
|
generate_host_ed25519_key "$hostname" "$keyfile"
|
||||||
|
|
||||||
age_pub="$(nix-shell -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")"
|
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
|
|||||||
Executable
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rotates the &admin sops age key: decrypts with a backed-up copy of the
|
||||||
|
# key CURRENTLY trusted as &admin, replaces .sops.yaml's &admin entry with
|
||||||
|
# a new key already present in this environment, and re-encrypts every
|
||||||
|
# secrets/*.yaml for the new recipient set. After this runs, the old key
|
||||||
|
# can no longer decrypt anything -- this is a real, one-way handoff of
|
||||||
|
# trust, not a preview.
|
||||||
|
#
|
||||||
|
# This is the automation for the manual steps create-proxmox-resource.sh /
|
||||||
|
# sync-host-keys.sh print when they bootstrap a brand-new, not-yet-trusted
|
||||||
|
# age key on a machine that's never had admin access before:
|
||||||
|
#
|
||||||
|
# scripts/rotate-admin-key.sh /path/to/backed-up/admin/keys.txt
|
||||||
|
#
|
||||||
|
# The backup key's *public* key must match .sops.yaml's current &admin
|
||||||
|
# entry -- this script verifies that by deriving it, it doesn't just trust
|
||||||
|
# the filename or take it on faith. The new key defaults to wherever sops
|
||||||
|
# itself would already look ($SOPS_AGE_KEY_FILE, then the XDG default), so
|
||||||
|
# the common case is just pointing this at the restored backup.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
sops_yaml="${repo_root}/.sops.yaml"
|
||||||
|
|
||||||
|
# shellcheck source=env.sh
|
||||||
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
|
||||||
|
# sops resolves .sops.yaml by walking up from the process's cwd, not from
|
||||||
|
# the target file's own path -- if this script were invoked from somewhere
|
||||||
|
# other than the repo root (or from inside another checkout/worktree that
|
||||||
|
# happens to have its own .sops.yaml), `sops updatekeys` would silently
|
||||||
|
# re-encrypt against the WRONG config's recipient list instead of this
|
||||||
|
# repo's. Pin cwd here so every sops/age call below is unambiguous
|
||||||
|
# regardless of where the caller's shell started out.
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 <path-to-backed-up-admin-key> [--new-key-file <path>] [--dry-run]
|
||||||
|
|
||||||
|
<path-to-backed-up-admin-key> age identity file for the key CURRENTLY
|
||||||
|
trusted as &admin. Only ever read -- never
|
||||||
|
copied or modified.
|
||||||
|
--new-key-file <path> age identity file for the key to promote
|
||||||
|
to &admin. Defaults to \$SOPS_AGE_KEY_FILE,
|
||||||
|
then
|
||||||
|
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt
|
||||||
|
(sops/age's own default resolution order).
|
||||||
|
--dry-run Print what would change; touches nothing
|
||||||
|
(.sops.yaml untouched, no sops updatekeys
|
||||||
|
calls).
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
new_key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
||||||
|
args=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
dry_run=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--new-key-file)
|
||||||
|
new_key_file="${2:?--new-key-file requires a path}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
args+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${#args[@]}" -ne 1 ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
backup_key="${args[0]}"
|
||||||
|
|
||||||
|
[[ -s "$backup_key" ]] || { echo "ERROR: backup key file not found or empty: ${backup_key}" >&2; exit 1; }
|
||||||
|
[[ -s "$new_key_file" ]] || { echo "ERROR: new key file not found or empty: ${new_key_file}" >&2; exit 1; }
|
||||||
|
|
||||||
|
nix_extra_opts
|
||||||
|
|
||||||
|
age_pub() {
|
||||||
|
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$1'"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "==> Deriving public keys..."
|
||||||
|
old_pub="$(age_pub "$backup_key")"
|
||||||
|
new_pub="$(age_pub "$new_key_file")"
|
||||||
|
echo " backup (old admin) key: ${old_pub}"
|
||||||
|
echo " new admin key: ${new_pub}"
|
||||||
|
|
||||||
|
if [[ "$old_pub" == "$new_pub" ]]; then
|
||||||
|
echo "ERROR: backup key and new key are identical -- nothing to rotate." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_admin_line="$(grep -E '^ - &admin age1' "$sops_yaml" || true)"
|
||||||
|
if [[ -z "$current_admin_line" ]]; then
|
||||||
|
echo "ERROR: couldn't find a '&admin age1...' line in ${sops_yaml}." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
current_admin_pub="$(awk '{print $NF}' <<<"$current_admin_line")"
|
||||||
|
|
||||||
|
if [[ "$current_admin_pub" != "$old_pub" ]]; then
|
||||||
|
echo "ERROR: ${backup_key} doesn't match the current &admin key in .sops.yaml." >&2
|
||||||
|
echo " .sops.yaml &admin: ${current_admin_pub}" >&2
|
||||||
|
echo " backup key pubkey: ${old_pub}" >&2
|
||||||
|
echo "Wrong backup file, or .sops.yaml has already moved on -- not touching anything." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t secrets_files < <(find "${repo_root}/secrets" -maxdepth 1 -name '*.yaml' | sort)
|
||||||
|
if [[ "${#secrets_files[@]}" -eq 0 ]]; then
|
||||||
|
echo "ERROR: no secrets/*.yaml files found under ${repo_root}/secrets." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Confirming the backup key can actually decrypt..."
|
||||||
|
if ! SOPS_AGE_KEY_FILE="$backup_key" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||||
|
"sops -d '${secrets_files[0]}'" >/dev/null; then
|
||||||
|
echo "ERROR: backup key failed to decrypt $(basename "${secrets_files[0]}") -- aborting." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " OK: decrypted $(basename "${secrets_files[0]}")"
|
||||||
|
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
echo "[dry-run] would replace .sops.yaml's &admin line:"
|
||||||
|
echo "[dry-run] - ${current_admin_pub}"
|
||||||
|
echo "[dry-run] + ${new_pub}"
|
||||||
|
echo "[dry-run] would then re-encrypt (sops updatekeys --yes) for the new recipient set:"
|
||||||
|
for f in "${secrets_files[@]}"; do
|
||||||
|
echo "[dry-run] secrets/$(basename "$f")"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Rotating .sops.yaml's &admin key..."
|
||||||
|
sed -i "s|^ - &admin age1[a-z0-9]*| - \&admin ${new_pub}|" "$sops_yaml"
|
||||||
|
grep -qF "$new_pub" "$sops_yaml" || {
|
||||||
|
echo "ERROR: sed edit didn't take -- .sops.yaml left unchanged, check it by hand." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " Updated."
|
||||||
|
|
||||||
|
echo "==> Re-encrypting secrets/*.yaml for the new recipient set..."
|
||||||
|
for f in "${secrets_files[@]}"; do
|
||||||
|
echo "==> $(basename "$f")"
|
||||||
|
SOPS_AGE_KEY_FILE="$backup_key" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||||
|
"sops updatekeys --yes '${f}'"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Verifying the new key can decrypt everything..."
|
||||||
|
for f in "${secrets_files[@]}"; do
|
||||||
|
if ! SOPS_AGE_KEY_FILE="$new_key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||||
|
"sops -d '${f}'" >/dev/null; then
|
||||||
|
echo "ERROR: new key failed to decrypt $(basename "$f") after rotation -- investigate before committing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " OK: $(basename "$f")"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done. .sops.yaml's &admin key is now:
|
||||||
|
${new_pub}
|
||||||
|
|
||||||
|
The old key (${old_pub}) can no longer decrypt any secrets/*.yaml
|
||||||
|
re-encrypted above.
|
||||||
|
|
||||||
|
Review the diff, then commit:
|
||||||
|
git add .sops.yaml secrets/*.yaml
|
||||||
|
git commit -m "Rotate sops admin age key"
|
||||||
|
EOF
|
||||||
@@ -31,6 +31,10 @@ editor="${repo_root}/scripts/lib/sync-host-keys-edit-sops.py"
|
|||||||
|
|
||||||
# shellcheck source=env.sh
|
# shellcheck source=env.sh
|
||||||
source "${repo_root}/scripts/env.sh"
|
source "${repo_root}/scripts/env.sh"
|
||||||
|
# shellcheck source=lib/nix-eval.sh
|
||||||
|
source "${repo_root}/scripts/lib/nix-eval.sh"
|
||||||
|
# shellcheck source=lib/ssh-host-keys.sh
|
||||||
|
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
|
||||||
|
|
||||||
mkdir -p "$keydir"
|
mkdir -p "$keydir"
|
||||||
|
|
||||||
@@ -118,12 +122,10 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
discover_targets() {
|
discover_targets() {
|
||||||
nix eval --json --no-use-registries --no-accept-flake-config \
|
|
||||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames \
|
|
||||||
| jq -r '.[] | select(. != "installer")'
|
|
||||||
# installer is the one nixosConfigurations target that doesn't import
|
# installer is the one nixosConfigurations target that doesn't import
|
||||||
# sops-nix at all (see CLAUDE.md's "Security Notes" -- hardcoded login
|
# sops-nix at all (see CLAUDE.md's "Security Notes" -- hardcoded login
|
||||||
# password instead) -- config.sops.secrets doesn't exist for it.
|
# password instead) -- config.sops.secrets doesn't exist for it.
|
||||||
|
list_flake_targets "$repo_root" | grep -v '^installer$'
|
||||||
}
|
}
|
||||||
|
|
||||||
locally_managed_hosts() {
|
locally_managed_hosts() {
|
||||||
@@ -159,7 +161,7 @@ queue_host_sync() {
|
|||||||
echo "[dry-run] ${host}: would generate host key"
|
echo "[dry-run] ${host}: would generate host key"
|
||||||
else
|
else
|
||||||
echo "==> ${host}: generating host key"
|
echo "==> ${host}: generating host key"
|
||||||
nix-shell "${NIX_OPTS[@]}" -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${host}' -f '${keyfile}'" >/dev/null
|
generate_host_ed25519_key "$host" "$keyfile"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "==> ${host}: host key already present"
|
echo "==> ${host}: host key already present"
|
||||||
@@ -170,7 +172,7 @@ queue_host_sync() {
|
|||||||
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"
|
||||||
else
|
else
|
||||||
age_pub="$(nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")"
|
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
|
||||||
fi
|
fi
|
||||||
add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \
|
add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \
|
||||||
'. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")"
|
'. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")"
|
||||||
|
|||||||
+51
-42
@@ -5,76 +5,85 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpWjNSdEdZbUUzamswa08w
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaMUpxYmUzaVY4d1pRY3g3
|
||||||
aCtzSHB0bFVZMnYxTkpuM1psdVYzWW55SzMwCkliMWVOUlBqRG5wOGZjQVg4MkFz
|
empaVUJSN0JaSlNQQlJtZENwc0VMYVhJSkFvCmJQMkpSd3RoYzlKQjBSZWErQzR2
|
||||||
NEVkMXdkTjhWRlZmVGlzZElid2pUMXMKLS0tIGtHUmRCNXNhVmloUHYzQnE5YlBS
|
aG5SVndOQWpSRTBDSWJVQkg0c0hiNFUKLS0tIHhiaEdpY3gwZkpCcHl2TW5CNThn
|
||||||
YnVSQjJlT3JnQ1RNMm9xV2xKOGRZUDAKc4VTl9NEI9Rv8+4J3JTeHTt2h8Dr2IJv
|
aVVUQy9Qd0trb0RNdUpVTXdrdGlrTmcK1uphQAyDV+Gk5+K1YOqw1Z8ynGP5sAPF
|
||||||
tfvoNJQM/w6RAJWNTkaDmzZa9OnUW+grDlBQKlDuAnr6fZmuNTH2hQ==
|
q5icujja/SGexX18hPYXbkyUtOrBYjW62gCuGJinSBPROoFUJbiP7g==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOR25UN1c0aE5SYWphbU0y
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKOWZxcFlhRHhKelhDS29M
|
||||||
SHRTZ1B0WC9NU3Z5VHpzTXpLSUxHTDI1ZFE0CnFpYUN1eGZQejJMblZPd1ROeUth
|
NGRlTEhJUGUzcFV6QVFCdTNkdEN3MlljWnl3CnI3LzVxSHBwSi9TYlRUcFdyN2py
|
||||||
dklZYVVNa1ZNZ1d4dW9vMCsvQWp1RkEKLS0tIERxakx5L0JrQitib1EyNDRMbDQ5
|
cVZBV0Q3Z3FhRXA0T1NFQzd2R00yeEUKLS0tIHkxL2dHV3RkVFYxZTJNTVZvVFIr
|
||||||
Z3hDWUFEazdxczVhaHJYK3VZeEJSSDgKkw9T4ZuT+VHIF4WopqRHt8vW30kOysJ3
|
TXpJZzdnYlpJaXBmcjdWWUtxNkc4dWMKVsImJiavzUzSFn78pciNJPHaS3KWqJer
|
||||||
vOq6EZ3Fqkgmoxm69Zp2gFnuE9GZIBy3VPQVLU2k6dZGJ3IvmLYeBA==
|
VkxF6kF3tl2HmW46eVXtsQowdu+zVR9HS35i/HvQ1r1TyP6qT5ofcQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0THJzMFBTTCtDMWRmZ25M
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyWjdSanRRaEI4VHFZVkdw
|
||||||
dU55OVhBb0trWWRUNlArTnEzRjhiYngvRENNCjF4d0M5NlYyQW50TTdMRXpuUjRr
|
NkxMODRuakFJTFNoTHZIRHptZFNxaGZ3K3lnCjJGR1FJalRsYmtLcHFxZW8yNU5W
|
||||||
M1NwV05JOHV6T2cxT2FheVpuZ0w2T0kKLS0tIFBxdlVpVEoxOUpSWjk1ejRsK1NM
|
MzBjWmltbzI3MTByUjB6djlEdllHNXMKLS0tIEhEOVhLVjZkRi9vUEtDWUhxT0NX
|
||||||
V1UwTU1scG91L2FIemtwSW5JbFlmeG8K/1WIlaIidy3x3ptoRpS/DG88064LQ6Mq
|
eUZ3bGdEaHdGbXZIYkowYUZuTWJ3d2MKMwgxOqlMH7GfLlseD4J277Dcg0KCD3d5
|
||||||
GbfB0jfq5PILDQMMuZu5oIBY31SxwnhZ02Ns7gA67kgNIRSCmk9WyQ==
|
jwJDb82kRWoBMicTJZXoq/5oe5blJNa7dWYoqgkYxMA2O+0igT+I6g==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmRURQVGw1a1E1MkIzV2pa
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsZzdRMm9zdlJHckFoREVs
|
||||||
RG5HSkM0c0huUGVWcnFZOGlacGlGOHFMTVFrCm5OS09HRFc3TGVUYmtzTStxL0Q5
|
UkVXL3dCeWRuN2NqcnVsOWVNT1FLUHY3andrCmNFY0ZCby9SdnUyYlV6R05YNDB0
|
||||||
N2hRMEcxUE9MTmZXL0wvME5EZXF1Z00KLS0tIHdHMVVHcTZzMmdXU0s4QlVqSS9Y
|
em43SjRCNVhYbmpRZUVWT0cxQlhGdmsKLS0tIDZHazNyck5VNHBuNVM5bmRZUlpR
|
||||||
ZUVmcWhPaURIUFJGR0V4bUZwKzM1bm8KlvGMNEClbLlfvJqNQHhd0dI4ihShLChF
|
QzBSNy84VDdLVkZZbnNlUFYydXlreEEKYZaR2b7tyRAhPdP+ytpP0veUTi7pY9Nw
|
||||||
GI/fydgrBruw3Otv6KLZu3CBC7iNcKlvZxz+YGD2qbicmyQ5hAhDSQ==
|
pK0h4hcegLNYJL1AfOYwFQoW7vb256GdmwdcuuBl1YBGXWGraaBnZg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5bWpFenBlQldna3RhSFpr
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFSnZiOGpsL1BESWVEdUxx
|
||||||
bEJBaHJDMzM3OHlqTmozcWU0VDM1bTFUWFZzCnZtNHZjZ1U1RzNkUlBHZFozWXdt
|
dXc5LzBHTlNodGJ1S3hOeHd5Wmgxb3BPSHl3CnBEWDNTc3NDdzN3RHQxUlNLOEs5
|
||||||
VVkxQjMvTDJtbFZnclpkUEd4TEVmNTAKLS0tIFlhV2ZSSzJLRVNoMmFyVktDOElR
|
RU1SS2tVT29XbDJCWVliWnVkekN4L1kKLS0tIGFqY2pNK2h2S2ZWMndrRVN6eUdN
|
||||||
YUxqZUFoY1ZWeGlldGplMjVQa1A5aUUKWelY6yO7Mr6dRvj4MVMbq/Z9JgrAnahz
|
M0NLY2EvTTVIYlZvdk9XY0NwNE14ZlEKcOwgCK3g56kId/4tEt+2iCcylisn+Fca
|
||||||
BDhHqObzJrOCtfDCTWiYuP+0yvIFWItMWhGSMw9MwwivvwnrEa+ZuQ==
|
5VWamsXdbwxbxmCEEUbgN3aHrdVz3mV4+8FZqA34yXz8pc45/PUcFg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiK2hKb2VaVnoyaEVYUHZZ
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJU1cydFFuRnpCUU5mbjFu
|
||||||
OWVUSGtONGs5dEljTjJQdlhEcjNjdjViT2pVCkpaOHVZMlpXOFRveVlMeXZqWmoz
|
TTIxREkxZ1hMalVtNjNjcHVaUXpPNWdib2tzCkRScWRTWHhxN3lhckI4UFpMSFRa
|
||||||
ZHJRQTR2dmJQSEozeTRGMEdUdFlmZ2MKLS0tIERWS1RVdW1jQytBZzlkb3puNjhH
|
RGxWcldlWnd5SVJFNWxQWjB3R0pMb3MKLS0tIGdjLzlhTHgyT2hjTmRTK1RiZmxS
|
||||||
ZjdlZmtzNXVOQ25DeCthUzhRRm1MT2cKaxc7zGm57iJFSeYc2IPqF4Eaxa44nR37
|
OWRWYklBWkoyUG8raFNzd3JlRUtEUEUKw+NdAp1Mz0dEOUGPbqCV8y7029I3Kye0
|
||||||
pWZw+erG4F9AAZ2F047q+oLKe0B8FLSF54IbcXdQhitgGNR7B2HVeA==
|
keU2T29JGCN1D6x//1NcMUYSaFZKgv0ZVSjVUCl1EnmlJ0nBdbaDXA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1ekhUTG5VOFErL3pFeWZM
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnNFlzYVBqaGFDVE9xbmt5
|
||||||
RG9NVnN6NFl3bzlFeTQzdHFtZmhwem04alQ0ClpJRStObERMZ0w2V0NhR1FSeW96
|
OVd4ZlZRdEVJd0JFdkRIYVA5ZUEwYjJjWVJjCnZzTExNbDlYbGN6c1dYU2ZlNEVs
|
||||||
M1d2V2NjUkUrLzN2ZVNSbGY4bll5WmsKLS0tIC94dVFQcXJ6d3pLU0VHNEFGR0ls
|
ZUo4MXdpc0tzbDI0MHl5eXc3d09VRDQKLS0tIGs4SGMya1RZQ0hpa2NtQk9Wa29C
|
||||||
a1Q2UmNuSjVMNG5XZGZKV1VmNHFPQXcKQJrZGw/9fPnXeFZ4omrkEgrzwplhwvRW
|
bmxIYWk1VThsaFhxRDlOOGhGQ2tYamsKs+PcOiaeNFujCWwZBr+nq1MzrGohl+ch
|
||||||
i0FXuepoU353sR7enyL34qPoOdm05ivowuPKNzkq8D4i5AF6vGv+YA==
|
TASI7eNsiHmSVRzSMHv4mX+8yXDHIRgHbFuUty2gdgGFLRjYXe4gDA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoZ2lHM2RBQ3lUK216dkcr
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkcmVQYXV1NzRrQ2IxTElt
|
||||||
Y3RrcXR3QTdFVjJRWlhpQWVFQ1cyZWNabkRFClAwRi9PSHF5ZWFSUzJuRXF3bU1R
|
QUJRMkhiUUJqRDdwQW03K0lCTEtkSTU5UDJrCjh6SldpQkF1NzIybTJTdnBlOHdQ
|
||||||
b0U5TFRZaFdmR1NMS3RRT3E3M2hUdE0KLS0tIEtrUy8ydkNyVHBiOVR6WEdjV2VN
|
VlREMy9hM0ZaL21Pa0VPUzcvb0pQQjQKLS0tIFcrK0I3Z3Nhem45eHpZYmdWeit6
|
||||||
NVJHUVgwRkhxcmlwcFkrRlFwTEF6YVkKzXyJk0UnmUsvb+NzNVcf/gf7OEEt3P/K
|
eXFtRHVxODhaNTVDdVR1a2tOb1N1U1EKZBdLHq8PRMKX2ndFr3AxFVAZRyvhFa1u
|
||||||
OIGxDrGfs/zNQgeKXNbQlQ4p4jOaybG8aCmX+A4qTk6/I8yY8LTJWg==
|
72R5tordo4IR3HSxG3Z06rokOITd+KKhaQ8NEWEC8qioAAMxEC9QWw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0WjJPbXhXNTNyQUZzMmxS
|
||||||
|
Rkd3Yk5Ga3pPRXFtMFpXcmpBZ3RJYnJlaHhjClRDbFBHRVZ0SDdrNFdVWmUwalpT
|
||||||
|
WUt5a2RUTnQ4VFBGMlowR3JycDM3aXMKLS0tIERSeFd4cGdSbzRpSmpSQnB1Yng5
|
||||||
|
OHBma3lYeWEwQ2RvelpzZHRkN1JPWFEKLXkJfEkH3lo5Z4mj8PZXTfAfZn6B46To
|
||||||
|
U0G54tUh0U1EeMnI5ZPRGwDxi5K0eD68HjeE9MBvFBysOChP/ANDPQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
|
||||||
lastmodified: "2026-07-19T02:30:40Z"
|
lastmodified: "2026-07-19T02:30:40Z"
|
||||||
mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str]
|
mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
|
|||||||
+16
-16
@@ -4,29 +4,29 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbHd1Y1dpa2ZiQ2E1bXRP
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmOHlPcGZvN3o5aEZCcFdz
|
||||||
SnkrWHI0V2U1eGFKY0RZamZQQjluS0hGeENZCmduUFQyd1Q5RkZGMGFoenJhNmZh
|
WlYydFdoQ0ZHbElMdTRkZWljMmoxTHZqNjJZCnE3cmgzKzZya3FROEczbHVveDND
|
||||||
VmZ2OStNaFpJejFxQ2JUZXFpNThaMncKLS0tIEhJdGdQeXEzb25Nbk5YZktCWVUz
|
VnEzRW12cnRKdzhkZm5uTXpkTEtrNUEKLS0tIHFDbkc4Mk4wVlM2R29zZXYwR2Ri
|
||||||
NUJDUWRoTkd5R2pFblZjS0NSbVR5dkkKDYeW+zRpha04/CasFM91K6v1PpkNGHRu
|
ODBML1p4eUZiZldYUERQTUhTU1llV0UKxjvH6zbW6wKghzR1o34CyKPEa2FqZmo0
|
||||||
qAoKs9KSg9VxS7ya8RuLmylKRdpPkupm/8SXIJvQuCXp5LWmNJ4zkA==
|
PxgqyuXkIwas9soXVAkScx7ElaV09Fjaj+mDrKwi4a+DwdoSP7czyA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4UktiUUdmd092bnZMdFMz
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0VDBXYUZNbnpEZHlqV05C
|
||||||
Q05xVlhnNzhPZUY4TjF5bjRCbklacTdJd1RBCnJ4bEVMaWRLZExIUFVEUDBXTnlV
|
WWxQZjVYcWlZeXlGc0RiaUpERzR0T21CWWtBCkM0dW5kYlFNK1RGSzRudFZ2Z0t3
|
||||||
MDU4WUk5VlJjdlhVUVpzZXlnWHhqTjQKLS0tIE9iclhGVElPcU9OLzF0bEVjMjVp
|
aVBreElGM1BIT0RzQkxTVTA3S2NhQncKLS0tIERyTU8zWWRTRm95SmRZQ1BhalVV
|
||||||
YzlQTGFYZEhLdTk3N3ZramVjVGRXREUKjOjqO/jX5iEKN43WiJ8qS/1mDfxdj8uO
|
SVdBajN1V1BuM2s4K216S3c0VHczNlkKM5jvsSEfCBA5uZRjBJNbM91lLRQkj+jK
|
||||||
K7Yyc/Yj0qMvGwLNzphjmbT29P7dXk4Ht4TedqCd+8DfhpOgLVIaSA==
|
rM5uSfGLTvSjPgXIMIq03OXxH1CE7GoKxAPwFrJdAFMMQcutIethhw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjaXpvQU9yRUc3ektPMmU5
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvbk9tRE9jYkpxNThQeElH
|
||||||
Q0xLWU9mYXVOeTVBNGNsZzJPa1lkVlRUZHlvClhoSVV6YjB1Q3pTTzVvNFlNYkU4
|
dWpKYThDb1ZkUWtMWlRxc2tYbjhBbEFEcWtVCkNiK0NBRko2TkJQdUVtdGtHNGcv
|
||||||
Y2RrbFk5SUFKSDVjeFU0Wlc2aGFUMFUKLS0tIDlVeXM4WWRUakg4Zk5mcjdJL3VC
|
andSaUlKSFA4THRyZXNTYmI0Yk5WbmsKLS0tIHlQTUtpWDdPeTVZL0M1RElRdFNk
|
||||||
S1k5eFBpVnREQlZwYllpeTNhWW5GS1EKi2sMwyJJ0D8acjCZmxlcwdU1sglBuxR7
|
QWlGdFo5NkZWSmY0YXdpNzNUQnlsK3MKtzC0bM7Ek+K73nMranOA1Mc98RUnYnq1
|
||||||
2SEMsctdGC+5E3ilPXvPpZ5RONZHbXxn6kQRBlBv6AJERpGDzsfgfA==
|
hAt0QEFKWK4QVKubaN/rG3AzE0U7qPKWHTzoxgnAiL3WyV9teLW+iA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||||
lastmodified: "2026-07-19T23:30:21Z"
|
lastmodified: "2026-07-19T23:30:21Z"
|
||||||
|
|||||||
+11
-11
@@ -3,20 +3,20 @@ sops:
|
|||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaFZURjAxMGRJZEJ5MW1x
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURmMzN3hrSlNrUkkvVWNl
|
||||||
ZjVwWEQrQlkwNmRibVNiL2RpTTFLeUVDQ3hRCkVIamVnZkM1MnlueloxMHVFQnBF
|
L3M1dEhWeW14N0RFNVRPci9QK1YyTFdqRVVJCk5WaWswT2NicldkYzZjbVhYU2xu
|
||||||
RjV2bnUrZUo4WGZJTmR4Y0xITkxRUkUKLS0tIGlJdVQ5MFBubVhxRUVMWW0wSGpP
|
MGFsNmUzeTN2TS9wOEdvRURpVUVYZXMKLS0tIDZ6MEdPTVhCaTQ2UXFWTUFtc0pm
|
||||||
UGdKNUNPYW9nek1UZ0tWbXd3QVNUNDgKIHOiKelITQdH5R4Nc3WF7mzz15D1f9on
|
MFlJb0c2WXJtMGRLZEZYY0pZWWpFWm8K/mlYZIe8UC0QU+1mq3NtrtTF5b2m5hCK
|
||||||
VaTdr5qkf8LNNvPI0fxsXA9is5cqeg+KbDRHtUumEhNp6Zrf8zWBkw==
|
+K0QiZLTKmmDcr4bRhZ32VE7R7GRwtMNnOP/mElZvPAyWyHHhRiOHg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N0hvV01naytDSWVwK1B1
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURHlUL0RNMUtNallEcG5p
|
||||||
MXM1ZkdpaVc2Q3FPV2VBcC83WmcvSjdGUGgwClJGSXZ6YW5JeFlValNJbjVhK29u
|
eG4xby8yVzdQUTBaNkl4ano0YjBMcDd0Wm5NCkZvaXNPZm9wemJkMmNSdGdOaTI4
|
||||||
bUFqN2dRQTI4ZkwyeXNWYk5JeWVJRXcKLS0tIDlMMkNBUnNUSTJwVVFmc2dlcEZS
|
Z1RwUnhiRUpCMWZaeWtlSVBmNW5KOXMKLS0tIDk0R0k3ZHczTFNCWUZxSWF0M0FJ
|
||||||
VGQ1VHR2dXB0M3RsalppVWxiUUROM0UKZM/4QDTam3LDTzjnDs41Ije50R7Q7GC4
|
MGlZMmtuSFYrcG1meDNMWDNqSjFxcE0KDu2dAc0gqmmPkpbpBe4YohM7rYmUwEkI
|
||||||
IZbUZjs72rBzY8IkJDbN9JidadEc4NAtMOJwXiJbpZGiCBNfc8+SXw==
|
V2FUQwjlvh50svtjCVdYbx2xuq4sQLnKelk/q1onLw60FwsVfzD8sQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||||
lastmodified: "2026-07-19T02:30:40Z"
|
lastmodified: "2026-07-19T02:30:40Z"
|
||||||
|
|||||||
+17
-16
@@ -19,6 +19,15 @@
|
|||||||
|
|
||||||
remoteBuilderUser = "nixremote"; # remote builder SSH user
|
remoteBuilderUser = "nixremote"; # remote builder SSH user
|
||||||
|
|
||||||
|
# nix-cache's own SSH host public key (not a secret — the private half
|
||||||
|
# never leaves the host). Wired into every client's
|
||||||
|
# programs.ssh.knownHosts by modules/nix-cache/remote-builder-client.nix
|
||||||
|
# so distributed builds don't hit "Host key verification failed" on a
|
||||||
|
# fresh client that has never manually ssh'd to nix-cache before. Update
|
||||||
|
# this if nix-cache's host key is ever rotated or the host is rebuilt
|
||||||
|
# from scratch.
|
||||||
|
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHrMKZlIGUd3pH9G3AqbsruqUGjxIXMAZw52u9MwiBCn lxc-nix-cache";
|
||||||
|
|
||||||
# Public keys authorized to SSH in as remoteBuilderUser on the nix-cache
|
# Public keys authorized to SSH in as remoteBuilderUser on the nix-cache
|
||||||
# host (modules/nix-cache/server.nix) — one per client host that's allowed
|
# host (modules/nix-cache/server.nix) — one per client host that's allowed
|
||||||
# to use it as a distributed builder.
|
# to use it as a distributed builder.
|
||||||
@@ -128,6 +137,14 @@
|
|||||||
# shortcuts on the gui build type (hosts/nixos/home.nix).
|
# shortcuts on the gui build type (hosts/nixos/home.nix).
|
||||||
pveWeb = 8006;
|
pveWeb = 8006;
|
||||||
pbsWeb = 8007;
|
pbsWeb = 8007;
|
||||||
|
|
||||||
|
# Tor relay's ORPort — the port other Tor relays connect to for onion
|
||||||
|
# routing traffic (modules/tor/enable-relay.nix). Tor's own conventional
|
||||||
|
# default; opened via services.tor.openFirewall rather than
|
||||||
|
# networking.firewall.allowedTCPPorts directly, but kept here anyway so
|
||||||
|
# it's not a bare literal duplicated between the relay's settings and
|
||||||
|
# anything else that ever needs to reference it.
|
||||||
|
torRelayOrPort = 9001;
|
||||||
};
|
};
|
||||||
|
|
||||||
# .raw disk image size for every proxmox-* host's standalone Disko image
|
# .raw disk image size for every proxmox-* host's standalone Disko image
|
||||||
@@ -147,20 +164,4 @@
|
|||||||
keep = 20; # number of rotated logs to retain before deleting the oldest
|
keep = 20; # number of rotated logs to retain before deleting the oldest
|
||||||
};
|
};
|
||||||
|
|
||||||
# Flake targets with a real, currently-running deployment somewhere —
|
|
||||||
# matches README.md's Hosts table "(real, deployed)" annotations; update
|
|
||||||
# both together. Not consumed by any NixOS module (nothing in the actual
|
|
||||||
# system config should behave differently because of this) — it's read
|
|
||||||
# by scripts/create-proxmox-resource.sh to refuse creating a same-identity
|
|
||||||
# duplicate of an already-deployed host (shared hostName/hostId) unless
|
|
||||||
# you explicitly pass --allow-duplicate-host.
|
|
||||||
deployedTargets = [
|
|
||||||
"linode-minimal"
|
|
||||||
"proxmox-minimal"
|
|
||||||
"proxmox-nix-cache"
|
|
||||||
"proxmox-server"
|
|
||||||
"proxmox-docker"
|
|
||||||
"proxmox-gui"
|
|
||||||
"proxmox-pxe-boot"
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user