Compare commits

..
Author SHA1 Message Date
beatzaplentyandClaude Sonnet 5 9ef2efad88 Fix LXC deployment path and clean up remaining eval warnings
Check NixOS configurations / eval-hosts (push) Failing after 10m52s
LXC hosts (device busy fix):

modules/platforms/lxc.nix now imports nixpkgs' own
virtualisation/proxmox-lxc.nix, giving every lxc-* host a real
config.system.build.tarball output — a directly `pct restore`-able
Proxmox container image. This is the actual bug fix behind the
"cannot remove real root directory: device busy or in use" error:
lxc-* targets were only reachable through nixos-install, which
bind-mounts / onto /mnt for containers (no raw disk to partition)
and then correctly refuses to modify the filesystem it's currently
running on. auto-install.sh's menu now excludes lxc-* targets
entirely (they deploy via nix build + pct restore instead, see
docs/auto-installer.md) — and, on the same reasoning, also excludes
`installer`/`proxmox-lxc`, which are the installer image's own flake
targets, not deployable hosts.

manageHostName = true keeps host.nix's declared hostnames (upstream's
default would let Proxmox's ambient container config win instead);
privileged = false matches how these containers are actually created.

Eval warnings, now zero across all 19 nixosConfigurations + 4 packages:

- Multiple password options (root/nixos in the installer): nixpkgs'
  own installer profile sets initialHashedPassword = "" for
  passwordless login, conflicting with our explicit hashedPassword.
  Force-nulled the upstream option rather than adopting passwordless
  login, since this image now also boots over LAN PXE with
  PasswordAuthentication enabled.
- boot.zfs.forceImportRoot default value: set explicitly to false
  (matching the two places that already did) in
  modules/common/configuration.nix and modules/installer/common.nix,
  covering every host and the installer alike.
- Deprecated pkgs.system in modules/build-types/gui.nix: switched to
  pkgs.stdenv.hostPlatform.system.

All confirmed non-behavioral where it matters: unrelated hosts'
drvPaths are byte-identical to their pre-existing baselines throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 05:58:56 +10:00
beatzaplentyandClaude Sonnet 5 f55e33b807 Migrate host-key tooling from nix-auto-installer, bake keys into images
Check NixOS configurations / eval-hosts (push) Failing after 11m18s
Finishes the nix-auto-installer migration: scripts/prepare-host-key.sh
and the local host-keys/ directory (gitignored, private key material,
never committed — moved as plain files, not through git history)
weren't carried over in the initial migration.

Also implements automatic key staging, replacing the manual
scp-after-boot step:

- modules/installer/host-keys.nix reads host-keys/ via
  builtins.getEnv, which Nix silently returns as "" under normal
  (non---impure) evaluation — the module is a no-op by default, safe
  for CI, until explicitly opted into:

    NIXOS_HOST_KEYS_DIR=$(pwd)/host-keys nix build .#iso --impure

  When built this way every key present gets baked into the image at
  /etc/host-keys/, and auto-install.sh installs whichever one matches
  the flake target selected at install time — no manual per-host scp.

- This deliberately includes the PXE netboot variant, even though
  pxe-boot serves it unauthenticated over LAN HTTP: accepted
  explicitly as a reasonable trade-off for a network that sits behind
  LAN-only infrastructure, not the open internet. auto-install.sh
  still falls back to /root/host-keys (manual scp) if a key isn't
  baked in, so images built without --impure keep working exactly as
  before.

- docs/auto-installer.md replaces nix-auto-installer's README,
  updated for in-repo paths and the new build flow.

Verified: normal `nix eval` (no --impure) evaluates identically across
all 19 nixosConfigurations + 4 packages with zero host-keys/* entries
(CI-unaffected); with --impure + the env var set, all three installer
variants (installer/ISO, proxmox-lxc, pxe) correctly embed every key
in host-keys/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 04:41:34 +10:00
beatzaplentyandClaude Sonnet 5 f0fae441ca Authorize the WSL workstation as a nix-cache remote-builder client
Check NixOS configurations / eval-hosts (push) Failing after 11m55s
Adds debian@surface's existing SSH key (already used as its admin
key elsewhere in this repo) to vars.remoteBuilderAuthorizedKeys so
nix-cache will accept it as a distributed-build client once deployed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 04:08:19 +10:00
beatzaplentyandClaude Sonnet 5 9bfd804f7a Resolve all statix and nixpkgs-fmt warnings repo-wide
Check NixOS configurations / eval-hosts (push) Failing after 11m17s
Zero W20 (repeated attribute keys), W10 (empty { ... }: variadic
pattern, use _: instead), and W04 (a = x.a instead of inherit)
warnings remain anywhere in the tree, and nixpkgs-fmt --check is
clean on all 46 .nix files.

Repeated-key merges go as deep as statix actually flags per file
(e.g. boot.loader.* nested under boot.loader = { ... } once the
outer boot.* merge exposed it as its own repeat) — every merge is a
pure attribute-path restructuring with no value changes, verified by
comparing config.system.build.toplevel.drvPath before/after for a
representative host per changed module plus a full 19-host + 4-package
eval sweep.

One indentation slip caught and fixed during this pass: nesting
modules/installer/common.nix's environment.etc."auto-install.sh".text
under an environment = { ... } block initially normalized the
script's shebang/set line indentation, which actually changes the
rendered file (Nix's '' string dedent treats it as real content, not
cosmetic whitespace) — reproduced the original's exact indentation
and reverified the rendered script is byte-identical to before.

modules/services/zfs/auto-mount-volumes.nix picked up formatting too;
worth noting it isn't imported by anything in this flake at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 03:47:42 +10:00
beatzaplentyandClaude Sonnet 5 0f7ab6fe7f Migrate nix-auto-installer into this flake
Folds the separate nix-auto-installer repo's build outputs into this
flake so it can build every auto-installer artifact it used to,
negating the need for that repo:

- modules/installer/{common,iso,proxmox-lxc}.nix — migrated from
  nix-auto-installer's common.nix/installer.nix/proxmox-lxc.nix.
- flake.nix gains nixosConfigurations.{installer,proxmox-lxc} and
  packages.x86_64-linux.{iso,lxc,pxe,all}, matching the original
  repo's interface (nix build .#iso / .#lxc / .#pxe / .#all).
- Dropped the live Gitea PAT baked into every installer image via
  environment.etc."git-credentials" — gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git
  allows anonymous read, so the installer (which only ever reads the
  flake, never pushes) doesn't need a credential at all.
- installer_old.nix wasn't migrated — dead code, unreferenced by the
  source repo's flake.nix, and carried a second stale leaked token.
- The installer environment's own hardcoded login password hash is
  preserved as-is: sops-nix has no stable per-boot host key to derive
  an age key from on ephemeral installer media, so it can't reuse the
  same per-host secret mechanism the rest of this repo uses without
  separate design work.
- vars.adminSshKey / vars.dockerHost-style dedup: the installer's SSH
  authorized key and its FLAKE_BASE_URL domain were exact duplicates
  of values already in variables.nix / modules/common/configuration.nix,
  so both now reference the single source of truth instead.

Verified eval-equivalent for every existing host (drvPath-identical)
and confirmed the migrated auto-install.sh script renders byte-for-byte
identical to the source repo's output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 03:47:19 +10:00
beatzaplentyandClaude Sonnet 5 75aef6ba4c Refresh stale architecture docs
Check NixOS configurations / eval-hosts (push) Failing after 11m16s
CLAUDE.md's "Composition pattern" section still described the
pre-refactor layout (hosts/<host>/configuration.nix as a thin imports
list, hardware-configuration wired in from flake.nix) from before the
platform x build-type matrix landed. Rewrite it to match the current
mkTarget/host.nix architecture and the module moves from the prior
commit. Also fixes docs/nix-cache.md, which referenced a
modules/nix/ path that never existed in this repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 02:58:25 +10:00
beatzaplentyandClaude Sonnet 5 bd65e71413 Reorganize loose modules into themed directories
Several single-purpose modules sat at modules/ root or in the
services/ catch-all despite the repo's established pattern of one
directory per concern (tailscale/, beszel/, docker/, nix-cache/):

- remote-builder-client.nix -> nix-cache/ (always co-included with
  nix-cache/client.nix in flake.nix's mkTarget, same buildType guard)
- set-locale.nix -> common/ (unconditionally imported by
  common/configuration.nix already)
- enable-ip-forwarding.nix -> networking/
- rotate-traefik-logs.nix -> traefik/rotate-logs.nix
- services/docker-health-to-gotify.nix and services/nextcloud-cron-job.nix
  -> docker/ (both only ever imported by the docker build type, same
  as the rest of modules/docker/*)

Pure path moves plus import-path updates in flake.nix,
common/configuration.nix, and build-types/docker.nix — verified
eval-equivalent (drvPath-identical) across representative hosts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 02:58:09 +10:00
beatzaplentyandClaude Sonnet 5 3a821158ca Add parameterized beszel host-token helper module
hosts/server/host.nix and hosts/nix-cache/host.nix each hand-rolled the
same sops secret/template/environmentFile wiring for the beszel agent
token, differing only in the sops file path and template name. Factor
it into modules/beszel/host-token.nix ({ name, sopsFile }) so a third
host can adopt it without copy-pasting the boilerplate again. Also
drops two dead, stale commented-out HUB_URL lines left over from
before variables.nix grew a homeDomain var.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 02:57:55 +10:00
beatzaplentyandClaude Sonnet 5 94bc27bcbb Extract ports, docker host, and remote-builder keys into variables.nix
Pulls the beszel hub / PVE / PBS ports, the docker-compose host's LAN
name, and the remote-builder client SSH keys out of scattered inline
literals across modules/hosts and into variables.nix as the single
source of truth, matching the existing pattern for other cross-host
references (nixCacheHost, nfsServerHost).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 02:57:38 +10:00
beatzaplentyandClaude Sonnet 5 326e764f51 Switch LXC targets from NetworkManager to systemd-networkd
Check NixOS configurations / eval-hosts (push) Failing after 11m15s
boot.isContainer disables services.udev, which NetworkManager depends on
to enumerate devices — this left NM unable to reliably manage the
container veth, breaking DHCP-hostname registration in Pi-hole. It also
defaulted networking.useHostResolvConf to true, which assumes a
systemd-nspawn-style resolv.conf bind-mount that real Proxmox LXC doesn't
provide (nixpkgs' own proxmox-lxc.nix module forces this false for the
same reason). Also guard the networkmanager extraGroups membership in the
minimal/gui build-types, since that group only exists when NM is enabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La55Nsss8jZ7ZuzUV9mfot
2026-07-20 01:16:29 +10:00
beatzaplenty bbc6124dc0 added lxc-nix-cache key 2026-07-19 17:22:20 +10:00
beatzaplenty b64ef613c3 updated sops keys
Check NixOS configurations / eval-hosts (push) Failing after 12m16s
2026-07-19 16:59:44 +10:00
beatzaplentyandClaude Sonnet 5 29d7059f70 Consolidate minimal-buildtype hosts onto a single nix-minimal identity
Check NixOS configurations / eval-hosts (push) Failing after 11m45s
linode-minimal, proxmox-minimal, and lxc-minimal now all share
hosts/nix-minimal/host.nix instead of three separate per-platform
host files with different hostnames — every minimal-buildtype host is
named nix-minimal regardless of which platform it runs on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 16:30:02 +10:00
beatzaplentyandClaude Sonnet 5 e667215ea6 Enable boot.loader.initScript for LXC targets
LXC containers share the host kernel — Proxmox starts them by exec'ing
/sbin/init directly in the container's rootfs, no bootloader or initrd
involved at all. Without boot.loader.initScript.enable, that file
isn't wired to launch the current generation, so even a correctly
installed system (see the nix-auto-installer bind-mount fix, same
underlying issue) could still fail to come up after reboot. This is
exactly what nixpkgs' own virtualisation/proxmox-lxc.nix module sets
for the same reason.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 16:11:17 +10:00
beatzaplenty 9c0b74d731 updated sops keys
Check NixOS configurations / eval-hosts (push) Failing after 11m56s
2026-07-19 15:06:59 +10:00
beatzaplentyandClaude Sonnet 5 6dcbae5659 Centralize shared values into variables.nix
One file (variables.nix) holding every value that was previously
hardcoded and repeated across modules: LAN domain/CIDR, home/tailnet
domains, cross-host references (nix-cache substituter hostname, NFS
server hostname, remote-builder user), PXE/PBS IPs, timezone, and the
primary username.

Wired in via flake.nix's specialArgs (and home-manager's
extraSpecialArgs for the two home.nix files), so any module picks it
up by just adding `vars` to its function arguments — no explicit
import needed. Two hosts (nix-cache, server) now derive their own
networking.hostName from the same variable other hosts use to reach
them, so there's exactly one place to change either identifier.

Purely mechanical: every substituted value matches what was already
there, confirmed by identical toplevel .drv paths for all 17 targets
before and after.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 14:51:37 +10:00
beatzaplentyandClaude Sonnet 5 2f7831aea8 Migrate live secrets to sops-nix (Milestone 2)
Check NixOS configurations / eval-hosts (push) Failing after 11m5s
Audited the working tree and full git history for committed secrets
(gitleaks + trufflehog + manual grep, see secrets-inventory.md, kept
local/gitignored per the spec). Found: a password hash shared by root
and the nixos user across every host, two live Beszel monitoring
tokens, and a GitHub fine-grained PAT embedded in a home-manager
nix.conf.

Migrates all of them to sops-nix:
- .sops.yaml + secrets/*.yaml, encrypted for admin + the age keys
  derived (via ssh-to-age) from each live host's existing SSH host
  key — no new key material transferred to any machine.
- users.users.{root,nixos}.hashedPasswordFile replaces the inline
  hashedPassword shared by every target.
- The GitHub PAT moves from a home-manager-managed, store-visible
  nix.conf to a sops.templates-rendered file included via nix.conf's
  native !include, system-wide instead of per-user.
- Beszel TOKEN moves from `environment` (store-visible) to
  `environmentFile` (runtime-only via sops.templates); the dead
  commented-out docker token is removed from the tree entirely.

Added a tracked pre-commit hook (gitleaks protect --staged, wired via
core.hooksPath) so a secret can't be committed by accident again, and
documented the sops workflow in README.md.

Structural verification only: all 17 flake targets evaluate, and
`nix build --dry-run --no-link` succeeds for the three currently
deployed hosts. Per CLAUDE.md, actual `nixos-rebuild switch` — the
step that confirms secrets decrypt and services start on a real
machine — is left for manual verification.

Git history still contains the original plaintext secrets; scrubbing
history (Milestone 3) and rotating every credential (Milestone 4) are
separate, deliberately gated steps per remove-sensetive-info-refactor.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 12:46:39 +10:00
beatzaplentyandClaude Sonnet 5 c05e3a3821 Refactor flake targets into platform x build-type matrix
Check NixOS configurations / eval-hosts (push) Failing after 12m2s
Generates all nixosConfigurations from mkTarget(platform, buildType,
hostPath) instead of hand-written per-host blocks, so adding a new
platform or build type is a one-line addition. Per-machine identity
(hostname, hostId, secrets, stateVersion) moves into hosts/<name>/host.nix;
platform-specific config (hardware, boot, networking) into
modules/platforms/*.nix; build-type config (minimal/server/docker/gui/
nix-cache/pxe-boot) into modules/build-types/*.nix.

Old flat targets (nixos, docker, server, nix-cache, nix-minimal, pxe-boot)
are replaced by the 17-target <platform>-<buildtype> matrix; each new
target was verified to evaluate before its old counterpart was removed.
CI workflows and docs/aliases now discover hosts dynamically via
nixosConfigurations attrNames and /etc/flake-target instead of hardcoded
lists, so they can't drift from flake.nix again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 12:03:33 +10:00
beatzaplentyandClaude Sonnet 5 0b090cab07 Sync host lists with flake.nix and add CLAUDE.md
Check NixOS configurations / eval-hosts (push) Successful in 34m44s
Remove the nonexistent kuma host and add the missing linode-minimal
host across README, AGENTS.md, docs, and CI eval workflows so they
match flake.nix's nixosConfigurations. Also add CLAUDE.md with
architecture/safety guidance for future Claude Code sessions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 09:48:05 +10:00
beatzaplenty 14accceb02 updated to efi boot and resolved module paths
Check NixOS configurations / eval-hosts (push) Failing after 25m24s
2026-07-19 06:27:22 +10:00
beatzaplenty 90baa168d1 remove swap label
Check NixOS configurations / eval-hosts (push) Failing after 14m52s
2026-07-18 08:14:39 +10:00
beatzaplenty d3ae41a031 updated to efi boot
Check NixOS configurations / eval-hosts (push) Failing after 14m57s
2026-07-18 07:52:30 +10:00
beatzaplenty a3230128b2 updated proxmox disko configuration
Check NixOS configurations / eval-hosts (push) Failing after 17m51s
2026-07-18 05:54:32 +10:00
beatzaplenty 1395fdaccc removed fileSystems definition for disko config
Check NixOS configurations / eval-hosts (push) Failing after 23m22s
2026-07-17 11:42:29 +10:00
beatzaplenty 8259e57191 update configuration
Check NixOS configurations / eval-hosts (push) Failing after 14m28s
2026-07-17 09:18:24 +10:00
beatzaplenty 9d42450c65 update grub config
Check NixOS configurations / eval-hosts (push) Failing after 14m19s
2026-07-15 13:23:09 +10:00
beatzaplenty add9a29908 disabled disko module
Check NixOS configurations / eval-hosts (push) Failing after 14m41s
2026-07-15 13:10:09 +10:00
beatzaplenty 13b0c028a1 updated mounts
Check NixOS configurations / eval-hosts (push) Failing after 14m18s
2026-07-15 12:59:08 +10:00
beatzaplenty fdc4ea4596 updated mounts
Check NixOS configurations / eval-hosts (push) Failing after 14m26s
2026-07-15 12:57:45 +10:00
beatzaplenty c2c86f0854 disabled disko in linode-minimal
Check NixOS configurations / eval-hosts (push) Failing after 14m31s
2026-07-15 12:47:35 +10:00
beatzaplenty 40b68c2e16 updated bios partition definition
Check NixOS configurations / eval-hosts (push) Failing after 14m33s
2026-07-15 11:44:03 +10:00
beatzaplenty 2ea39994f8 remove grub mount point
Check NixOS configurations / eval-hosts (push) Failing after 14m54s
2026-07-15 11:29:52 +10:00
beatzaplenty 331246a1cb remove bios type
Check NixOS configurations / eval-hosts (push) Failing after 18m43s
2026-07-15 11:25:25 +10:00
beatzaplenty 442e7986f2 added bios partition
Check NixOS configurations / eval-hosts (push) Failing after 14m28s
2026-07-15 11:22:21 +10:00
beatzaplenty 6064c69d3b removed duplicate boot entry
Check NixOS configurations / eval-hosts (push) Failing after 15m0s
2026-07-15 11:08:50 +10:00
beatzaplenty 56a2f3ff51 add boot loader path
Check NixOS configurations / eval-hosts (push) Failing after 15m4s
2026-07-15 11:05:12 +10:00
beatzaplenty a29efce29e remove file systems. they are defined in disko
Check NixOS configurations / eval-hosts (push) Failing after 14m22s
2026-07-15 10:29:10 +10:00
beatzaplenty 8e8183e371 updated disko config
Check NixOS configurations / eval-hosts (push) Failing after 14m40s
2026-07-15 09:59:37 +10:00
beatzaplenty 4c53cce35c update linode host folder name to linode-minimal
Check NixOS configurations / eval-hosts (push) Failing after 14m21s
2026-07-15 09:38:12 +10:00
beatzaplenty dea6bfec25 add linode disko config 2026-07-15 09:36:04 +10:00
beatzaplenty 04de84d03b updated flake
Check NixOS configurations / eval-hosts (push) Failing after 14m52s
2026-07-15 09:28:33 +10:00
beatzaplenty 7bfe8cb0b2 added disko configuration to remove disk formatting from auto installer
Check NixOS configurations / eval-hosts (push) Failing after 14m31s
2026-07-15 07:30:32 +10:00
beatzaplenty 8460eff2de Merge branch 'main' of https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos
Check NixOS configurations / eval-hosts (push) Failing after 14m21s
2026-07-14 20:52:05 +10:00
beatzaplenty bcfeb08b11 update hardware config 2026-07-14 20:50:21 +10:00
beatzaplenty 783bda00c1 Update hosts/nix-minimal/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 15m52s
2026-07-13 14:07:57 +00:00
beatzaplenty e3ac63aefa update module path
Check NixOS configurations / eval-hosts (push) Failing after 14m47s
2026-07-13 19:49:45 +10:00
beatzaplenty 4f1aff9a16 updated flake.lock
Check NixOS configurations / eval-hosts (push) Failing after 14m51s
2026-07-13 19:44:35 +10:00
beatzaplenty befab7f3c6 revert
Check NixOS configurations / eval-hosts (push) Failing after 15m29s
2026-07-13 19:22:10 +10:00
beatzaplenty b9610dbc9d update module path
Check NixOS configurations / eval-hosts (push) Failing after 15m0s
2026-07-13 19:19:39 +10:00
beatzaplenty b605d19870 update disko path
Check NixOS configurations / eval-hosts (push) Failing after 15m18s
2026-07-13 19:17:43 +10:00
beatzaplenty 679dfd80c0 added disko configuration
Check NixOS configurations / eval-hosts (push) Failing after 14m45s
2026-07-13 19:10:21 +10:00
beatzaplenty 298f615929 create linode flake
Check NixOS configurations / eval-hosts (push) Failing after 15m13s
2026-07-13 17:46:12 +10:00
beatzaplenty 5e17baba72 update common home path
Check NixOS configurations / eval-hosts (push) Failing after 15m15s
2026-07-13 16:07:17 +10:00
beatzaplenty 345b5ca657 moved common into modules
Check NixOS configurations / eval-hosts (push) Failing after 18m43s
2026-07-13 16:03:48 +10:00
beatzaplenty 2c4fe8af2b remove hardware configuration
Check NixOS configurations / eval-hosts (push) Failing after 15m18s
2026-07-13 15:50:24 +10:00
beatzaplenty c128bff2bf change path
Check NixOS configurations / eval-hosts (push) Failing after 18m45s
2026-07-13 14:29:29 +10:00
beatzaplenty 8aff9fff29 fix syntax
Check NixOS configurations / eval-hosts (push) Failing after 16m50s
2026-07-13 14:10:38 +10:00
beatzaplenty 8f468d5703 enable beszel agent debugging
Check NixOS configurations / eval-hosts (push) Failing after 14m58s
2026-07-13 14:09:33 +10:00
beatzaplenty 690ecb1eb0 update path
Check NixOS configurations / eval-hosts (push) Failing after 14m32s
2026-07-13 14:04:36 +10:00
beatzaplenty 1ed1b388f5 change paths
Check NixOS configurations / eval-hosts (push) Failing after 14m25s
2026-07-13 13:57:37 +10:00
beatzaplenty f338843166 update extra filesystems
Check NixOS configurations / eval-hosts (push) Failing after 15m4s
2026-07-13 13:49:58 +10:00
beatzaplenty 1c1f65560c update path
Check NixOS configurations / eval-hosts (push) Failing after 14m40s
2026-07-13 13:44:50 +10:00
beatzaplenty 258ebfe1bf fix syntax
Check NixOS configurations / eval-hosts (push) Failing after 14m51s
2026-07-13 13:37:59 +10:00
beatzaplenty 45cb9c38e8 added extra filesystems
Check NixOS configurations / eval-hosts (push) Failing after 14m56s
2026-07-13 13:36:36 +10:00
beatzaplenty b481324c7d remove commented lines
Check NixOS configurations / eval-hosts (push) Failing after 14m36s
2026-07-13 13:23:42 +10:00
beatzaplenty d5467e9a5b remove raspi docker data mount
Check NixOS configurations / eval-hosts (push) Failing after 6m18s
2026-07-13 12:48:14 +10:00
beatzaplenty c3c467bb41 permit root login
Check NixOS configurations / eval-hosts (push) Failing after 14m40s
2026-07-13 12:39:58 +10:00
beatzaplenty 80eff2c050 fix path
Check NixOS configurations / eval-hosts (push) Failing after 15m4s
2026-07-13 12:15:00 +10:00
beatzaplenty 1cff38be43 add docker backup share
Check NixOS configurations / eval-hosts (push) Failing after 18m8s
2026-07-13 12:12:58 +10:00
beatzaplenty 960548c2ac added raspi-backup share
Check NixOS configurations / eval-hosts (push) Failing after 11m30s
2026-07-13 11:44:45 +10:00
beatzaplenty 229cd06f73 updated server configuration
Check NixOS configurations / eval-hosts (push) Failing after 14m23s
2026-07-13 05:55:40 +10:00
beatzaplenty 6e332a6bbd add boot include to zfs mount module
Check NixOS configurations / eval-hosts (push) Failing after 14m47s
2026-07-13 03:33:09 +10:00
beatzaplenty fe3041c06c fix path typo
Check NixOS configurations / eval-hosts (push) Failing after 14m21s
2026-07-13 02:56:20 +10:00
beatzaplenty 591731e3b8 modularized locale settings
Check NixOS configurations / eval-hosts (push) Failing after 14m33s
2026-07-13 02:43:08 +10:00
beatzaplenty 5da5401e91 remove commented lines 2026-07-13 02:26:46 +10:00
beatzaplenty aca46255f8 add pkgs
Check NixOS configurations / eval-hosts (push) Failing after 14m27s
2026-07-13 02:16:05 +10:00
beatzaplenty f85559bb18 fix syntax
Check NixOS configurations / eval-hosts (push) Failing after 14m34s
2026-07-13 02:14:49 +10:00
beatzaplenty 07534e64fc fix syntax 2026-07-13 02:14:21 +10:00
beatzaplenty 54cefce90a removed port from firewall
Check NixOS configurations / eval-hosts (push) Failing after 13m55s
2026-07-13 02:05:13 +10:00
beatzaplenty fcb8f5e01e modularized rpcbind enable and added to server config
Check NixOS configurations / eval-hosts (push) Failing after 13m58s
2026-07-13 02:00:55 +10:00
beatzaplenty 985d4e4bfa updated module paths
Check NixOS configurations / eval-hosts (push) Failing after 13m53s
2026-07-13 01:53:32 +10:00
beatzaplenty 9462d80ccc updated flake.lock
Check NixOS configurations / eval-hosts (push) Failing after 14m11s
2026-07-13 01:45:44 +10:00
beatzaplenty 14deedc12d fix typo
Check NixOS configurations / eval-hosts (push) Failing after 14m27s
2026-07-13 01:44:05 +10:00
beatzaplenty 893a7c997c clean up commented lines
Check NixOS configurations / eval-hosts (push) Failing after 14m12s
2026-07-13 01:42:28 +10:00
beatzaplenty d068195fbe removed let statement
Check NixOS configurations / eval-hosts (push) Failing after 23m55s
2026-07-13 01:34:19 +10:00
beatzaplenty 51fba54d94 modularized docker health to gotify service
Check NixOS configurations / eval-hosts (push) Failing after 22m52s
2026-07-13 01:29:45 +10:00
beatzaplenty f541688973 modularized nextcloud cron job
Check NixOS configurations / eval-hosts (push) Failing after 1h15m54s
2026-07-13 01:25:16 +10:00
beatzaplenty 3bb2b83370 modularized raspi mount
Check NixOS configurations / eval-hosts (push) Failing after 47m52s
2026-07-13 01:12:43 +10:00
beatzaplenty 8ff22175e5 update log path
Check NixOS configurations / eval-hosts (push) Failing after 47m59s
2026-07-13 01:09:34 +10:00
beatzaplenty bc719afe39 modularized traefik log rotatation config
Check NixOS configurations / eval-hosts (push) Failing after 53m51s
2026-07-13 01:05:44 +10:00
beatzaplenty 20dc28069c update path
Check NixOS configurations / eval-hosts (push) Failing after 14m8s
2026-07-13 01:00:03 +10:00
beatzaplenty 28ae709100 updated module directory structure
Check NixOS configurations / eval-hosts (push) Failing after 22m19s
2026-07-13 00:56:19 +10:00
beatzaplenty 3de625309e enable tailscale module
Check NixOS configurations / eval-hosts (push) Failing after 17m2s
2026-07-13 00:46:07 +10:00
beatzaplenty f2744bf3a7 modularized tailscale service
Check NixOS configurations / eval-hosts (push) Failing after 40m6s
2026-07-13 00:44:29 +10:00
beatzaplenty 5b118b05da move docker system packages to docker-service module
Check NixOS configurations / eval-hosts (push) Successful in 39m34s
2026-07-13 00:36:52 +10:00
beatzaplenty f139333980 added pkgs include
Check NixOS configurations / eval-hosts (push) Successful in 38m27s
2026-07-13 00:32:06 +10:00
beatzaplenty 5d5428014a modularized docker service
Check NixOS configurations / eval-hosts (push) Failing after 23m17s
2026-07-13 00:30:10 +10:00
beatzaplenty 529a518cd1 removed promotheus exporter
Check NixOS configurations / eval-hosts (push) Successful in 33m35s
2026-07-12 19:09:46 +10:00
beatzaplenty dc714b41c1 added beszel agent
Check NixOS configurations / eval-hosts (push) Successful in 33m19s
2026-07-12 18:21:03 +10:00
beatzaplenty c470737316 added beszel agent
Check NixOS configurations / eval-hosts (push) Failing after 6m31s
2026-07-12 18:08:44 +10:00
beatzaplenty 8266f43980 added beszel port to firewall
Check NixOS configurations / eval-hosts (push) Failing after 27m37s
2026-07-12 17:07:42 +10:00
beatzaplenty 474d243649 remove beszel agent from docker and create module
Check NixOS configurations / eval-hosts (push) Failing after 2m12s
2026-07-12 15:48:18 +10:00
beatzaplenty 019afb5609 add semi colons
Check NixOS configurations / eval-hosts (push) Failing after 6m36s
2026-07-10 08:01:58 +10:00
beatzaplenty 63062cdda5 fix nix syntax
Check NixOS configurations / eval-hosts (push) Failing after 6m50s
2026-07-10 08:00:37 +10:00
beatzaplenty 8f5599a1c6 syntax change
Check NixOS configurations / eval-hosts (push) Failing after 6m55s
2026-07-10 07:56:16 +10:00
beatzaplenty 416b7196c2 updated beszel syntax
Check NixOS configurations / eval-hosts (push) Failing after 6m43s
2026-07-10 07:51:54 +10:00
beatzaplenty 0bdadcc772 added beszel agent
Check NixOS configurations / eval-hosts (push) Failing after 6m56s
2026-07-10 07:40:26 +10:00
beatzaplenty aa08bf60b9 removed tailscale
Check NixOS configurations / eval-hosts (push) Failing after 6m52s
2026-07-08 13:24:43 +10:00
beatzaplenty 99ad7501bb added new public key for server
Check NixOS configurations / eval-hosts (push) Failing after 6m54s
2026-07-08 13:09:28 +10:00
beatzaplenty 6c973e2300 typo
Check NixOS configurations / eval-hosts (push) Failing after 6m58s
2026-07-08 13:00:26 +10:00
beatzaplenty eb2d3e3260 added tailscale exit node module and enabled on server
Check NixOS configurations / eval-hosts (push) Failing after 6m55s
2026-07-08 12:48:52 +10:00
beatzaplenty f9c0f01aba updated /mnt/docker permissions
Check NixOS configurations / eval-hosts (push) Successful in 42m36s
2026-07-07 03:31:19 +10:00
beatzaplenty 83481b5291 updated NFS shares and deleted individual share modules
Check NixOS configurations / eval-hosts (push) Failing after 14m29s
2026-07-07 03:09:56 +10:00
beatzaplenty a553958bb9 remove prune task 2026-07-07 00:28:15 +10:00
beatzaplenty d16b22d2b5 removed --force from docker prune service command 2026-07-07 00:20:10 +10:00
beatzaplenty 3900d617fa added test share
Check NixOS configurations / eval-hosts (push) Failing after 3h2m9s
2026-07-07 00:03:22 +10:00
gitea-actions c15e88de35 chore: update flake.lock 2026-07-06 06:14:46 +00:00
beatzaplenty ff9fbc36e9 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 30m3s
added systemd job to prune docker data
2026-07-04 09:02:18 +00:00
beatzaplenty fe322c13c6 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 11m12s
2026-07-02 11:35:55 +00:00
beatzaplenty 4d8b00609b Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 23m19s
updated sym links to config shared storage
2026-07-02 09:34:00 +00:00
beatzaplenty 6b7c011480 Update modules/nix/docker-data.nix
Check NixOS configurations / eval-hosts (push) Failing after 1h14m42s
2026-07-02 01:37:55 +00:00
beatzaplenty a488ff507e Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6s
2026-07-01 22:28:36 +00:00
beatzaplenty 9685233553 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 1s
2026-07-01 22:17:33 +00:00
beatzaplenty 837096ca28 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 3s
2026-07-01 21:58:20 +00:00
beatzaplenty bd550b9c2e Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 2s
2026-07-01 21:53:01 +00:00
beatzaplenty 5e8037d02b Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 4s
2026-07-01 21:50:42 +00:00
beatzaplenty 1af8079a8e Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 3s
2026-07-01 21:32:24 +00:00
beatzaplenty 030d344ea2 moved docker data mounts into one module
Check NixOS configurations / eval-hosts (push) Failing after 14m16s
2026-07-01 20:58:19 +10:00
beatzaplenty 6a266449d5 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 13m8s
2026-07-01 10:29:30 +00:00
beatzaplenty 895c9d7003 Add modules/nix/mount-docker-databases.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m43s
2026-07-01 10:28:34 +00:00
beatzaplenty 0edb1a431e Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 7m1s
2026-07-01 10:27:05 +00:00
beatzaplenty 8f32ccdd0f Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 44m18s
add ,crossmnt option to volumes NFS export
2026-07-01 09:47:53 +00:00
beatzaplenty 44e828320f Update common/home.nix
Check NixOS configurations / eval-hosts (push) Successful in 40m14s
2026-07-01 08:59:21 +00:00
beatzaplenty b3c3760c3e Update modules/nix/mount-docker-volumes.nix
Check NixOS configurations / eval-hosts (push) Successful in 38m28s
2026-07-01 08:46:38 +00:00
beatzaplenty b9ff8002bd Update modules/nix/mount-docker-config.nix
Check NixOS configurations / eval-hosts (push) Successful in 36m26s
updated mount path
2026-07-01 08:46:12 +00:00
git ae4c3dba24 updated config for new ZFS/NFS shares
Check NixOS configurations / eval-hosts (push) Successful in 39m58s
2026-07-01 16:59:55 +10:00
beatzaplenty f008c16ea7 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 47m4s
2026-07-01 05:57:13 +00:00
beatzaplenty 4d4a10aaae Add modules/nix/mount-docker-config.nix
Check NixOS configurations / eval-hosts (push) Failing after 47m12s
2026-07-01 05:56:26 +00:00
beatzaplenty 3e298b8c6e Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 47m20s
2026-07-01 05:42:19 +00:00
beatzaplenty f7baaf88bb Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 1h21m30s
2026-07-01 05:29:17 +00:00
git 0606b93570 updated lock file
Check NixOS configurations / eval-hosts (push) Failing after 6m55s
2026-07-01 13:47:33 +10:00
beatzaplenty dd80145e4f Update flake.nix
Check NixOS configurations / eval-hosts (push) Successful in 47m42s
2026-06-30 23:06:26 +00:00
beatzaplenty c25c5d4fbc Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m57s
2026-06-30 21:59:48 +00:00
beatzaplenty 29b1b86286 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m55s
2026-06-30 21:55:42 +00:00
beatzaplenty 33be42e3ca Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m45s
2026-06-30 21:51:37 +00:00
beatzaplenty 28c875e8cb Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m48s
2026-06-30 21:48:02 +00:00
beatzaplenty 09a50f8ff2 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 7m9s
2026-06-30 21:40:38 +00:00
beatzaplenty 161888b037 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 7m37s
2026-06-30 21:34:55 +00:00
beatzaplenty 3578e66a15 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m52s
2026-06-30 21:22:20 +00:00
beatzaplenty f759f98f97 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 7m12s
2026-06-30 21:15:41 +00:00
beatzaplenty 36a5287b7b Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m47s
2026-06-30 21:01:31 +00:00
beatzaplenty 04692c33f2 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m59s
2026-06-30 20:55:53 +00:00
beatzaplenty da3f4b16c9 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 25m35s
2026-06-30 20:49:46 +00:00
beatzaplenty f00364bd14 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 25m29s
2026-06-30 20:39:52 +00:00
beatzaplenty c25eb536c7 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 26m15s
2026-06-30 20:35:01 +00:00
beatzaplenty 34ff056ca9 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m56s
2026-06-30 20:23:40 +00:00
beatzaplenty 4187e7dc79 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 28m5s
2026-06-30 20:09:51 +00:00
beatzaplenty de9549b466 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 25m54s
2026-06-28 08:42:37 +00:00
beatzaplenty 140dea3993 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m42s
2026-06-28 08:39:41 +00:00
beatzaplenty 545775fc26 clean up docker version config
Check NixOS configurations / eval-hosts (push) Failing after 7m1s
2026-06-28 17:48:04 +10:00
beatzaplenty 514fa3068a add overlay for docker version for flake.nix
Check NixOS configurations / eval-hosts (push) Failing after 7m8s
2026-06-28 17:31:02 +10:00
beatzaplenty ded8b7dd55 revert 8444fc370e
Check NixOS configurations / eval-hosts (push) Failing after 8m38s
revert Update flake.nix
2026-06-28 07:27:53 +00:00
beatzaplenty 8444fc370e Update flake.nix
Check NixOS configurations / eval-hosts (push) Failing after 17m57s
2026-06-28 07:26:51 +00:00
beatzaplenty 2c5fca84bd updated configuration
Check NixOS configurations / eval-hosts (push) Failing after 23m13s
2026-06-28 16:55:40 +10:00
beatzaplenty 7e1768c2e8 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 24m45s
2026-06-28 06:40:12 +00:00
beatzaplenty 39332d0b93 update docker version
Check NixOS configurations / eval-hosts (push) Failing after 25m4s
2026-06-28 16:08:46 +10:00
beatzaplenty fc193d0932 allowed old docker version
Check NixOS configurations / eval-hosts (push) Failing after 6m46s
2026-06-28 15:52:38 +10:00
beatzaplenty 0154648aa2 updated flake.lock
Check NixOS configurations / eval-hosts (push) Failing after 23m40s
2026-06-28 15:32:14 +10:00
git 74b3c8cd3d update lock file
Check NixOS configurations / eval-hosts (push) Failing after 22m36s
2026-06-27 13:41:28 +10:00
beatzaplenty e24f9314be Update flake.lock
Check NixOS configurations / eval-hosts (push) Failing after 22m51s
2026-06-27 03:38:55 +00:00
beatzaplenty 8211ff8cb1 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 23m11s
2026-06-27 03:33:49 +00:00
beatzaplenty 88dedeed43 Update flake.lock.old
Check NixOS configurations / eval-hosts (push) Failing after 24m56s
2026-06-27 03:12:17 +00:00
beatzaplenty 997e384973 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 25m37s
2026-06-27 03:02:37 +00:00
beatzaplenty 31371189f5 Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 26m13s
2026-06-27 02:50:33 +00:00
beatzaplenty 70ea6c2e7c Update hosts/docker/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 12m7s
2026-06-24 02:00:32 +00:00
gitea-actions 03adfdd0de chore: update flake.lock 2026-06-22 06:13:04 +00:00
beatzaplenty 84569d13e6 Update common/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 6m39s
2026-06-14 16:32:20 +00:00
beatzaplenty 519d7b8d3c updated pxe config
Check NixOS configurations / eval-hosts (push) Successful in 32m50s
2026-06-02 19:00:30 +10:00
beatzaplenty 5f036c8f54 updated server root folders
Check NixOS configurations / eval-hosts (push) Successful in 34m57s
2026-06-02 18:33:13 +10:00
beatzaplenty 83abba4e25 updated ipxe configuration
Check NixOS configurations / eval-hosts (push) Failing after 6m29s
2026-06-02 18:05:39 +10:00
beatzaplenty c718010a69 added tftp server
Check NixOS configurations / eval-hosts (push) Failing after 6m30s
2026-06-02 12:03:00 +10:00
beatzaplenty 2f985536c4 updated documentation
Check NixOS configurations / eval-hosts (push) Failing after 30m36s
2026-06-01 18:22:05 +10:00
beatzaplenty bf0445ebd6 created pxe-boot configuration
Check NixOS configurations / eval-hosts (push) Successful in 32m25s
2026-06-01 17:34:51 +10:00
gitea-actions 7b945ea4fe chore: update flake.lock 2026-06-01 06:13:00 +00:00
beatzaplenty 5fbc8ba5df Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 32m54s
2026-06-01 01:42:25 +00:00
beatzaplenty d474f1b1b1 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 34m48s
2026-06-01 01:24:04 +00:00
beatzaplenty b36431f210 Update hosts/server/configuration.nix
Check NixOS configurations / eval-hosts (push) Failing after 8m58s
2026-06-01 00:09:59 +00:00
gitea-actions d4771720b9 chore: update flake.lock 2026-05-25 06:13:47 +00:00
gitea-actions 2e2298d66e chore: update flake.lock 2026-05-21 19:58:06 +00:00
beatzaplenty 51a50faf69 updated authorized keys
Check NixOS configurations / eval-hosts (push) Has been cancelled
2026-05-12 13:28:33 +10:00
beatz174-bit 193c10d4b7 Merge pull request #4 from beatz174-bit/chore/update-flake-lock
Check NixOS configurations / eval-hosts (push) Has been cancelled
chore: update flake.lock
2026-05-12 13:02:58 +10:00
beatz174-bit def6be08e7 chore: update flake.lock 2026-05-12 02:58:23 +00:00
beatz174-bit 0633c60b9d Merge pull request #3 from beatz174-bit/codex/implement-flake.lock-automation-in-nixos-26sknf
Check NixOS configurations / eval-hosts (push) Has been cancelled
Add scheduled flake.lock updater and NixOS evaluation workflows (GitHub + Gitea) and docs
2026-05-12 12:40:56 +10:00
beatz174-bit 7cbdcdf29e Merge branch 'main' into codex/implement-flake.lock-automation-in-nixos-26sknf 2026-05-12 12:40:49 +10:00
beatz174-bit cf1a4eefda fix(ci): quote PR title fields in update workflow 2026-05-12 12:38:28 +10:00
beatz174-bit 5256b661a3 Merge pull request #2 from beatz174-bit/codex/implement-flake.lock-automation-in-nixos
ci: add scheduled flake.lock updates and NixOS evaluation checks
2026-05-12 12:31:10 +10:00
beatz174-bit bcccf523bf ci: automate flake lock updates and host evaluation 2026-05-12 12:28:37 +10:00
beatzaplenty 1a4a0c47ef updated docker container monitoring script path 2026-05-12 12:07:59 +10:00
beatzaplenty 286d7347a1 updated authorized keys 2026-05-12 11:55:56 +10:00
beatzaplenty 5a93bdeb28 updated authorized keys 2026-05-12 11:53:43 +10:00
beatzaplenty 770cbaf098 updated authorized keys 2026-05-12 11:24:54 +10:00
beatz174-bit dec3e25472 Merge pull request #1 from beatz174-bit/codex/refactor-nix-cache-for-binary-cache-and-remote-builder
Add Nix cache / remote-builder modules, wire hosts, and harden maintenance scripts
2026-05-12 11:09:50 +10:00
beatz174-bit 8b919d2d5a Fix pure eval and harden nix script bootstrap 2026-05-12 11:09:23 +10:00
beatzaplenty d52e892559 update setup script 2026-05-12 07:15:12 +10:00
beatzaplenty df4161265b Add codex environment scripts, README and AGENTS 2026-05-12 07:02:43 +10:00
beatzaplenty 80bffb58c9 Update hosts/docker/configuration.nix 2026-04-27 12:09:31 +00:00
beatzaplenty f8acce2614 Update hosts/docker/configuration.nix 2026-04-27 12:05:04 +00:00
beatzaplenty daadd97f84 Update hosts/docker/configuration.nix 2026-04-27 12:00:36 +00:00
beatzaplenty a3be101ce3 Update hosts/docker/configuration.nix 2026-04-27 11:54:57 +00:00
beatzaplenty 4bf32f23ab Update hosts/docker/configuration.nix 2026-04-14 23:29:01 +00:00
beatzaplenty f14aa9755b Update hosts/docker/configuration.nix 2026-04-14 23:15:55 +00:00
beatzaplenty 8f5f975d83 Update hosts/docker/configuration.nix 2026-04-14 23:05:01 +00:00
beatzaplenty 7f7b03384f Update hosts/docker/configuration.nix 2026-04-13 21:58:10 +00:00
beatzaplenty 6e138c6f7f Update hosts/docker/configuration.nix 2026-04-02 00:07:32 +00:00
beatzaplenty e6a72813e4 Update hosts/docker/configuration.nix 2026-04-01 23:38:07 +00:00
beatzaplenty 350a2d2bfb Update common/configuration.nix 2026-03-31 21:16:59 +00:00
beatzaplenty cb1ddc2109 Update common/configuration.nix 2026-03-31 21:11:00 +00:00
beatzaplenty 376d53de40 Update hosts/server/configuration.nix 2026-03-27 19:52:58 +00:00
beatzaplenty fd592ba5de Update hosts/server/configuration.nix 2026-03-27 19:48:50 +00:00
beatzaplenty 4b37d36212 Update hosts/docker/configuration.nix 2026-02-25 00:20:50 +00:00
beatzaplenty 0a10efefa6 Update hosts/docker/configuration.nix 2026-02-25 00:17:39 +00:00
beatzaplenty 5e559efc6f Update hosts/docker/configuration.nix 2026-02-25 00:16:37 +00:00
beatzaplenty 149f56ce10 Update common/home.nix 2026-02-17 16:00:33 +00:00
beatzaplenty c6f6441907 Update flake.nix 2026-02-17 15:59:35 +00:00
beatzaplenty cc956d3038 Update hosts/docker/configuration.nix 2026-02-17 15:57:26 +00:00
beatzaplenty d409b5b718 Update hosts/docker/configuration.nix 2026-02-17 15:56:47 +00:00
beatzaplenty 6ef87a3226 Update hosts/docker/configuration.nix 2026-02-17 15:53:06 +00:00
beatzaplenty 9a190a28d6 Update hosts/docker/configuration.nix 2026-02-17 15:52:05 +00:00
beatzaplenty e7a215cd15 Update hosts/docker/configuration.nix 2026-02-17 15:50:06 +00:00
beatzaplenty 651f4e61c5 Update hosts/docker/configuration.nix 2026-02-17 15:40:04 +00:00
beatzaplenty ffad8bd6b8 Update hosts/docker/configuration.nix 2026-02-17 11:44:58 +00:00
beatzaplenty 01aaf10e2d Update hosts/docker/configuration.nix 2026-02-16 09:22:16 +00:00
beatzaplenty 8db2a2db86 Update hosts/docker/configuration.nix 2026-02-16 09:14:46 +00:00
beatzaplenty 3389c9549a Update flake.nix 2026-02-09 10:12:06 +00:00
beatzaplenty 7ab8cf15ca Update hosts/nix-cache/configuration.nix 2026-02-09 10:05:23 +00:00
beatzaplenty 56a25ab5d7 Update hosts/server/configuration.nix 2026-02-09 10:00:53 +00:00
beatzaplenty 76649ad698 modified: hosts/docker/configuration.nix 2025-12-20 13:06:57 +10:00
beatzaplenty 487d8bc474 modified: hosts/server/configuration.nix 2025-12-18 12:24:04 +10:00
beatzaplenty 529535cffd modified: hosts/server/configuration.nix 2025-12-18 12:22:59 +10:00
beatzaplenty aff4cf1c16 modified: hosts/server/configuration.nix 2025-12-18 12:16:06 +10:00
beatzaplenty 3db1205a8d modified: hosts/server/configuration.nix 2025-12-18 12:06:39 +10:00
beatzaplenty bc5be541c3 modified: hosts/server/configuration.nix 2025-12-18 10:51:11 +10:00
beatzaplenty 38c55c3de2 modified: hosts/kuma/configuration.nix 2025-12-17 14:57:34 +10:00
beatzaplenty 25b8ebcad7 modified: flake.nix
new file:   hosts/kuma/configuration.nix
2025-12-17 14:34:23 +10:00
beatzaplenty d08951d46a modified: flake.nix 2025-12-17 14:31:09 +10:00
beatzaplenty 9eba317de0 modified: flake.nix 2025-12-17 14:30:15 +10:00
beatzaplenty a9bb49a7e7 Update hosts/docker/configuration.nix 2025-12-16 12:01:55 +00:00
beatzaplenty 46c71eb619 Update hosts/docker/configuration.nix 2025-12-16 11:56:53 +00:00
beatzaplenty 8918af8d6e Update hosts/docker/configuration.nix 2025-12-16 11:53:51 +00:00
beatzaplenty 48447f7a47 Update hosts/docker/configuration.nix 2025-12-16 11:52:41 +00:00
beatzaplenty 943c5324ca Update flake.nix 2025-12-16 11:42:35 +00:00
beatzaplenty a8f417b4f9 Update flake.nix 2025-12-16 11:41:38 +00:00
beatzaplenty ff75079327 Update flake.lock 2025-12-16 11:34:53 +00:00
beatzaplenty 6f79daefc4 Update flake.nix 2025-12-16 10:56:47 +00:00
beatzaplenty 66b6bc0c2a Add flake.lock 2025-12-16 10:51:45 +00:00
beatzaplenty b952075a1e Update flake.nix 2025-12-16 10:25:19 +00:00
beatzaplenty 49c72848e6 modified: hosts/docker/configuration.nix 2025-07-23 18:23:25 +10:00
beatzaplenty c19919a868 modified: hosts/docker/configuration.nix 2025-07-23 18:09:12 +10:00
beatzaplenty 4c4ed6416d modified: hosts/nixos/home.nix 2025-07-22 17:21:30 +10:00
beatzaplenty a457d04a0a modified: hosts/nixos/home.nix 2025-07-22 17:19:13 +10:00
beatzaplenty 019ab8c2cc modified: hosts/nixos/home.nix 2025-07-22 17:16:42 +10:00
beatzaplenty cb0efa12ce modified: hosts/nixos/home.nix 2025-07-22 17:15:33 +10:00
beatzaplenty bb454d5a97 modified: hosts/nixos/home.nix 2025-07-22 17:13:40 +10:00
beatzaplenty af4021b29f modified: hosts/nixos/home.nix 2025-07-22 17:11:46 +10:00
beatzaplenty a7a6951115 modified: hosts/nixos/home.nix 2025-07-22 17:07:16 +10:00
beatzaplenty 00bab4b69a modified: hosts/nixos/home.nix 2025-07-22 16:55:51 +10:00
beatzaplenty 6c304304f0 modified: hosts/nixos/home.nix 2025-07-22 16:47:21 +10:00
beatzaplenty 60942af93f modified: hosts/nixos/home.nix 2025-07-22 16:44:34 +10:00
beatzaplenty f4dd65c66e modified: hosts/nixos/home.nix 2025-07-22 16:37:38 +10:00
beatzaplenty 3fbc290fd5 modified: hosts/nixos/home.nix 2025-07-22 16:29:29 +10:00
beatzaplenty 1b6779f89c modified: hosts/nixos/home.nix 2025-07-22 16:22:14 +10:00
beatzaplenty dc933fc69b modified: hosts/nixos/home.nix 2025-07-22 16:12:08 +10:00
beatzaplenty 4d322a94ac modified: hosts/nixos/home.nix 2025-07-22 16:10:58 +10:00
beatzaplenty 17ecbc617d modified: hosts/nixos/home.nix 2025-07-22 16:09:06 +10:00
beatzaplenty c892c70860 modified: hosts/nixos/home.nix 2025-07-22 16:04:27 +10:00
beatzaplenty 376f0b98dd modified: hosts/nixos/home.nix 2025-07-22 15:57:39 +10:00
beatzaplenty 728129a4b2 modified: hosts/nixos/home.nix 2025-07-22 15:55:05 +10:00
beatzaplenty fe8649b09a modified: hosts/nixos/home.nix 2025-07-22 15:50:48 +10:00
beatzaplenty 64183b0ab9 modified: hosts/nixos/home.nix 2025-07-22 15:33:23 +10:00
beatzaplenty 8c7a7815a7 modified: hosts/nixos/home.nix 2025-07-22 15:18:27 +10:00
beatzaplenty 0dc6c5099f modified: flake.nix
modified:   hosts/nixos/configuration.nix
2025-07-22 14:29:45 +10:00
beatzaplenty 236385e480 modified: hosts/nixos/configuration.nix 2025-07-22 14:22:49 +10:00
beatzaplenty ddf0133da5 modified: hosts/nixos/home.nix 2025-07-22 14:21:34 +10:00
beatzaplenty f4a849bbac modified: flake.nix
modified:   hosts/nixos/home.nix
2025-07-22 14:15:15 +10:00
beatzaplenty 0c2344098b modified: flake.nix 2025-07-22 14:10:44 +10:00
beatzaplenty ee44a2dafd modified: flake.nix 2025-07-22 14:09:08 +10:00
beatzaplenty ff014f6658 modified: flake.nix 2025-07-22 14:05:43 +10:00
beatzaplenty c8dc59750b modified: hosts/nixos/home.nix 2025-07-22 14:02:14 +10:00
beatzaplenty 35f2c457dd modified: flake.nix
modified:   hosts/nixos/home.nix
2025-07-22 13:59:42 +10:00
beatzaplenty 5a60d574d8 modified: flake.nix 2025-07-22 13:47:56 +10:00
beatzaplenty 08f6dd7347 modified: flake.nix 2025-07-22 13:37:24 +10:00
beatzaplenty 0228bdf429 modified: flake.nix 2025-07-22 13:33:45 +10:00
beatzaplenty 1d99b54374 modified: flake.nix 2025-07-22 13:31:05 +10:00
beatzaplenty dd32d23249 modified: common/home.nix
modified:   hosts/nix-minimal/configuration.nix
2025-07-21 16:16:47 +10:00
beatzaplenty 47927a91f1 deleted: auto-installer/flake.nix
deleted:    auto-installer/installer.nix
	modified:   hosts/nix-minimal/configuration.nix
	modified:   prepare.sh
2025-07-21 13:39:03 +10:00
beatzaplenty 954d89f101 Added minimal build for iso testing and building
modified:   flake.nix
	new file:   hosts/nix-minimal/configuration.nix
2025-07-21 12:13:07 +10:00
beatzaplenty 5bb1b0c6a1 modified: auto-installer/installer.nix 2025-07-21 12:08:08 +10:00
beatzaplenty 706e74c46d modified: auto-installer/installer.nix 2025-07-21 10:33:21 +10:00
beatzaplenty 094b6c34f0 modified: auto-installer/installer.nix 2025-07-21 10:18:29 +10:00
beatzaplenty aeb2cde784 modified: auto-installer/installer.nix 2025-07-21 10:11:02 +10:00
beatzaplenty f1520fbd75 modified: auto-installer/installer.nix 2025-07-21 09:58:34 +10:00
beatzaplenty 0321f764aa modified: auto-installer/installer.nix 2025-07-21 09:54:23 +10:00
beatzaplenty e0a1d1b5dc modified: auto-installer/installer.nix 2025-07-21 09:36:26 +10:00
beatzaplenty cfea52cf7d modified: auto-installer/installer.nix 2025-07-21 09:27:04 +10:00
beatzaplenty 94736a53ef modified: auto-installer/installer.nix 2025-07-21 09:19:07 +10:00
beatzaplenty 1960c4cd8c modified: auto-installer/installer.nix 2025-07-21 08:51:03 +10:00
beatzaplenty b66f5dd800 modified: auto-installer/installer.nix 2025-07-21 08:38:46 +10:00
beatzaplenty 3d2938d896 modified: auto-installer/installer.nix 2025-07-21 02:11:34 +10:00
beatzaplenty 903ac787b1 Merge branch 'main' of https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos 2025-07-21 01:59:28 +10:00
beatzaplenty 34cf3a330a modified: auto-installer/installer.nix 2025-07-21 01:58:06 +10:00
beatzaplenty c8c5d21e5a Merge branch 'main' of https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos 2025-07-21 01:48:57 +10:00
beatzaplenty ec82189d47 modified: .gitignore 2025-07-21 01:47:56 +10:00
beatzaplenty ebf8173845 modified: auto-installer/installer.nix 2025-07-21 01:47:12 +10:00
beatzaplenty 360d0d4f46 modified: auto-installer/installer.nix 2025-07-21 01:32:06 +10:00
beatzaplenty fb73fe70cd modified: .gitignore 2025-07-21 00:53:34 +10:00
beatzaplenty a0699349c7 modified: auto-installer/installer.nix 2025-07-21 00:52:51 +10:00
beatzaplenty 2e6f19e597 modified: auto-installer/installer.nix 2025-07-21 00:27:38 +10:00
beatzaplenty d5f6a154ca modified: auto-installer/installer.nix 2025-07-21 00:26:47 +10:00
beatzaplenty afda3b0a5d modified: auto-installer/installer.nix 2025-07-21 00:25:39 +10:00
beatzaplenty 4c4b3d47f3 modified: auto-installer/installer.nix 2025-07-21 00:24:06 +10:00
beatzaplenty 7413340c75 modified: auto-installer/installer.nix 2025-07-21 00:21:09 +10:00
beatzaplenty b0dd745a03 modified: .gitignore 2025-07-21 00:14:59 +10:00
beatzaplenty be3012a65a modified: .gitignore 2025-07-21 00:13:55 +10:00
beatzaplenty 55dd99e155 new file: auto-installer/flake.nix
new file:   auto-installer/installer.nix
2025-07-21 00:10:48 +10:00
beatzaplenty 845ab268ce modified: hosts/nixos/configuration.nix 2025-07-20 22:54:08 +10:00
beatzaplenty 1df53af9bf modified: hosts/nixos/configuration.nix 2025-07-20 22:53:23 +10:00
beatzaplenty 427155bcb0 modified: flake.nix 2025-07-20 22:52:44 +10:00
beatzaplenty e8ee5981ac modified: flake.nix 2025-07-20 22:45:37 +10:00
beatzaplenty 6d701af3c7 modified: flake.nix 2025-07-20 22:44:31 +10:00
beatzaplenty d6f9fe97f5 added winapps flake to nixos pc
modified:   flake.nix
2025-07-20 22:40:14 +10:00
beatzaplenty d450750818 modified: hosts/nixos/configuration.nix 2025-07-20 22:27:41 +10:00
beatzaplenty 1c5dac74ca modified: hosts/nixos/configuration.nix 2025-07-20 22:25:46 +10:00
beatzaplenty d4f5e0b22d added software
modified:   hosts/nixos/configuration.nix
2025-07-20 22:24:44 +10:00
beatzaplenty b0964fcb80 modified: hosts/nixos/configuration.nix 2025-07-20 22:11:59 +10:00
beatzaplenty a285756040 modified: hosts/nixos/configuration.nix 2025-07-20 22:07:25 +10:00
beatzaplenty 20d38c571c modified: hosts/nixos/configuration.nix 2025-07-20 21:59:56 +10:00
beatzaplenty e22dcd6d02 modified: hosts/nixos/configuration.nix 2025-07-20 21:55:22 +10:00
beatzaplenty a2bc222fc8 modified: hosts/nixos/configuration.nix 2025-07-20 21:50:57 +10:00
beatzaplenty 198ba7a198 modified: hosts/nixos/configuration.nix 2025-07-20 21:50:10 +10:00
beatzaplenty cc6704830e modified: hosts/nixos/configuration.nix 2025-07-20 21:48:47 +10:00
beatzaplenty 072f1690e6 enabled keyring
modified:   hosts/nixos/configuration.nix
2025-07-20 21:47:47 +10:00
beatzaplenty 5ca3254788 move vscode to config from home manager
modified:   hosts/nixos/configuration.nix
	modified:   hosts/nixos/home.nix
2025-07-20 21:40:22 +10:00
beatzaplenty 398dc5161a enable allow unfree software
modified:   hosts/nixos/configuration.nix
2025-07-20 21:27:27 +10:00
beatzaplenty 798c3c10d5 added vscode
modified:   hosts/nixos/home.nix
2025-07-20 21:25:36 +10:00
beatzaplenty af2f4263cc moved aliases to common file and imported to all home.nix variants
new file:   common/aliases.nix
	modified:   common/home.nix
	modified:   hosts/nixos/home.nix
2025-07-20 21:15:31 +10:00
beatzaplenty fa76fb0f13 modified: common/home.nix 2025-07-16 21:11:07 +10:00
beatzaplenty d6c1e48667 modified: hosts/nix-cache/configuration.nix 2025-07-16 20:34:45 +10:00
beatzaplenty aa065f21e3 modified: common/configuration.nix 2025-07-16 20:31:41 +10:00
beatzaplenty 61f5059492 modified: common/configuration.nix
modified:   hosts/nix-cache/configuration.nix
2025-07-16 20:24:23 +10:00
beatzaplenty bd6f0ab825 modified: common/home.nix 2025-07-16 20:06:55 +10:00
beatzaplenty 0ab5a56493 modified: flake.nix 2025-07-16 20:03:18 +10:00
beatzaplenty c9ab34040d modified: flake.nix 2025-07-16 19:57:42 +10:00
85 changed files with 906 additions and 6506 deletions

No files matched your search

Submodule .claude/worktrees/scripts-dedup deleted from e578443914.
+10 -10
View File
@@ -13,17 +13,17 @@ jobs:
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix - name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19 uses: DeterminateSystems/nix-installer-action@v19
# Scoped to files changed since the PR base / previous push -- see - name: Evaluate all NixOS hosts
# scripts/codex-maintenance.sh. CI never passes --full-check: that run: |
# full sweep is for local/manual use, since it's slow enough to time set -euo pipefail
# out this runner. hosts="$(nix --extra-experimental-features 'nix-command flakes' eval --json \
- name: Run maintenance checks (secrets, fmt, lint, eval -- changed files only) .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')"
env: for host in $hosts; do
MAINT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} echo "Evaluating ${host}"
run: bash scripts/codex-maintenance.sh nix --extra-experimental-features 'nix-command flakes' eval \
".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath" --raw
done
+10 -10
View File
@@ -13,17 +13,17 @@ jobs:
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix - name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19 uses: DeterminateSystems/nix-installer-action@v19
# Scoped to files changed since the PR base / previous push -- see - name: Evaluate all NixOS hosts
# scripts/codex-maintenance.sh. CI never passes --full-check: that run: |
# full sweep is for local/manual use, since it's slow enough to time set -euo pipefail
# out this runner. hosts="$(nix --extra-experimental-features 'nix-command flakes' eval --json \
- name: Run maintenance checks (secrets, fmt, lint, eval -- changed files only) .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')"
env: for host in $hosts; do
MAINT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} echo "Evaluating ${host}"
run: bash scripts/codex-maintenance.sh nix --extra-experimental-features 'nix-command flakes' eval \
".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath" --raw
done
-10
View File
@@ -3,19 +3,9 @@
result result
result-* result-*
# Disko's proxmox-* image-builder writes the finished .raw disk image
# directly into the current directory, not into a result-* symlink (see
# docs/proxmox-images.md, scripts/create-proxmox-resource.sh) — several GB
# each, never meant to be committed.
*.raw
# Ignore automatically generated direnv output # Ignore automatically generated direnv output
.direnv .direnv
# Python bytecode cache (scripts/lib/*.py)
__pycache__/
*.pyc
# Locally-generated SSH host keys staged for transfer to a new machine # Locally-generated SSH host keys staged for transfer to a new machine
# during install (see scripts/prepare-host-key.sh) — never commit these. # during install (see scripts/prepare-host-key.sh) — never commit these.
host-keys/ host-keys/
+4 -70
View File
@@ -1,31 +1,11 @@
keys: keys:
- &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad - &admin age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e - &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf - &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu - &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- &lxc-minimal age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu - &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg - &lxc-nix-cache age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
- &lxc-gui age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
- &baremetal-gui age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
- &linode-docker age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
- &linode-gui age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
- &linode-minimal age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
- &linode-nix-cache age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
- &linode-server age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
- &linode-tailscale-router age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
- &lxc-docker age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
- &lxc-minimal age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
- &lxc-nix-cache age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
- &lxc-pxe-boot age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
- &lxc-server age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
- &lxc-tailscale-router age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
- &lxc-tor-relay age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
- &proxmox-docker age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
- &proxmox-gui age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
- &proxmox-nix-cache age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
- &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
- &proxmox-server age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
creation_rules: creation_rules:
# Shared across every currently-deployed host: root/nixos password hash, # Shared across every currently-deployed host: root/nixos password hash,
@@ -39,71 +19,25 @@ creation_rules:
- *docker - *docker
- *server - *server
- *nix-cache - *nix-cache
- *nix-minimal
- *proxmox-minimal
- *lxc-gui
- *baremetal-gui
- *linode-docker
- *linode-gui
- *linode-minimal
- *linode-nix-cache
- *linode-server
- *linode-tailscale-router
- *lxc-docker
- *lxc-minimal - *lxc-minimal
- *nix-minimal
- *lxc-nix-cache - *lxc-nix-cache
- *lxc-pxe-boot
- *lxc-server
- *lxc-tailscale-router
- *lxc-tor-relay
- *proxmox-docker
- *proxmox-gui
- *proxmox-nix-cache
- *proxmox-pxe-boot
- *proxmox-server
- *proxmox-tailscale-router
- path_regex: secrets/nix-cache\.yaml$ - path_regex: secrets/nix-cache\.yaml$
key_groups: key_groups:
- age: - age:
- *admin - *admin
- *nix-cache - *nix-cache
- *linode-nix-cache
- *lxc-nix-cache - *lxc-nix-cache
- *proxmox-nix-cache
- path_regex: secrets/server\.yaml$ - path_regex: secrets/server\.yaml$
key_groups: key_groups:
- age: - age:
- *admin - *admin
- *server - *server
- *linode-server
- *lxc-server
- *proxmox-server
- path_regex: secrets/docker\.yaml$ - path_regex: secrets/docker\.yaml$
key_groups: key_groups:
- age: - age:
- *admin - *admin
- *docker - *docker
- path_regex: secrets/tor-relay\.yaml$
key_groups:
- age:
- *admin
- *lxc-tor-relay
# gui-host-specific secrets (currently: wifi-password, see
# modules/networking/wifi.nix). Only *lxc-gui has a registered key today
# -- proxmox-gui/linode-gui/baremetal-gui haven't been provisioned via
# scripts/secrets/sync-host-keys.sh yet, so whichever variant is actually
# deployed next needs its recipient added here (and `sops updatekeys` rerun)
# before it can decrypt this.
- path_regex: secrets/gui\.yaml$
key_groups:
- age:
- *admin
- *lxc-gui
- *baremetal-gui
- *linode-gui
- *proxmox-gui
+2 -7
View File
@@ -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`, `tailscale-exit-node`, `tor-relay`), generated from `modules/platforms/*` `server`, `docker`, `gui`, `pxe-boot`), 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
@@ -35,14 +35,9 @@ Use these commands when validating changes:
```bash ```bash
bash scripts/codex-setup.sh bash scripts/codex-setup.sh
bash scripts/codex-maintenance.sh bash scripts/codex-maintenance.sh
bash scripts/codex-maintenance.sh dry-run
``` ```
With no flags, `codex-maintenance.sh` scopes fmt-check/statix/eval to files
changed against a base ref — this is what CI runs on every push/PR. For the
full sweep (every host, every package — slow; CI never runs this), use
`bash scripts/codex-maintenance.sh --full-check` (add `--dry-run` for build
planning on top of whichever scope is active).
Host evaluation is safe when limited to drvPath checks: Host evaluation is safe when limited to drvPath checks:
```bash ```bash
-150
View File
@@ -1,150 +0,0 @@
# Flake End-to-End Audit Report
**Date:** 2026-07-21
**Scope:** Full static lint/eval sweep + live build/deploy/interrogate/destroy testing of every `lxc-*` and `proxmox-*` flake target against `pve.sweet.home`, plus an audit of the operator's ability to manage the flake/secrets tooling.
**Branch:** `worktree-flake-e2e-audit` (this session's isolated worktree)
## Executive Summary
The flake itself is in good shape: `nixpkgs-fmt`, `statix`, and a full eval + dry-run build of every host and package are all clean. Every `lxc-*`/`proxmox-*` target's NixOS configuration builds successfully — no target has a broken derivation graph.
The issues found are **operational, not code-level**:
1. **pve.sweet.home is critically low on disk space** (91-95% full during this session) and cannot currently build the two largest closures (`gui`, `pxe-boot`) to completion — this actively blocks deploying/redeploying those hosts via the documented workflow.
2. **A real, reproducible secrets-decryption failure** was caught live: a stale cached container image (built before a same-day sops-key fix) boots with sshd never starting and every secret failing to decrypt. This is a **general hazard in `create-proxmox-resource.sh`'s "reuse the cached image if present" default**, not a one-off.
3. **sops key/anchor drift**: `proxmox-minimal` has a `.sops.yaml` recipient anchor with no corresponding private key anywhere in this environment; several `lxc-*`/`proxmox-*` targets have no sops registration at all yet.
4. One concrete script bug was found and **fixed in this session**: `create-proxmox-resource.sh` never enabled the QEMU guest agent channel on VMs it creates, despite the guest OS already running it.
5. A management-surface audit (of the operator's ability to run this repo day to day) found 5 process gaps, detailed below.
Nothing here required or received a `nixos-rebuild switch/boot/test`, `nixos-install`, or any disk-formatting command — all validation was `nix build`/`nix eval`, plus disposable `pct`/`qm` create-then-destroy cycles via the repo's own `create-proxmox-resource.sh`.
---
## 1. Static Analysis Results — all clean
`bash scripts/codex-maintenance.sh --full-check --dry-run` (whole-tree sweep, not just changed files):
| Check | Result |
|---|---|
| Secret grep | Clean — only the documented exceptions (installer's own hashed passwords, `access-tokens` comment references) |
| `nixpkgs-fmt --check` | 0/53 files would be reformatted |
| `statix` | No lint warnings |
| nix-cache host key drift check | Up to date |
| Full eval of every host's `system.build.toplevel` | All 19 `nixosConfigurations` targets evaluate cleanly |
| Dry-run build of every host + package | All succeed, no derivation errors |
No drift, no formatting issues, no lint findings anywhere in the tree.
---
## 2. Per-Target Test Results
Legend: **LIVE** = built on pve, `pct`/`qm` create → interrogated → destroyed. **BUILD-ONLY** = `nix build` validated the config (mostly `.config.system.build.toplevel`, occasionally `.tarball`), no resource created on pve.
| Target | Test type | Result | Notes |
|---|---|---|---|
| `lxc-docker` | BUILD-ONLY | ✅ PASS | Live redeploy skipped — CT105 is already running this identity in production; `--allow-duplicate-host` would have destroyed it. |
| `lxc-minimal` | **LIVE** | ✅ PASS (after retry) | First attempt reused a stale cached tarball predating a same-day sops-key commit → activation failed, sshd never started (see Finding #2). Redeployed with `--force-rebuild`: clean boot, `systemctl is-system-running` = `running`, secrets decrypted, sshd listening, users correct. |
| `lxc-nix-cache` | BUILD-ONLY | ✅ PASS (after retry) | Live redeploy skipped — CT101 is already running this identity. First local build attempt appeared to hang on a remote-builder handoff to nix-cache; killed and retried with `--builders ""` (local-only), succeeded. |
| `lxc-gui` | **LIVE (attempted)** | ⚠️ BLOCKED by pve disk space | Registered a fresh sops key (no prior registration existed), built successfully through the full NixOS system closure, then **failed packaging the tarball**: `No space left on device` on pve's root filesystem. Not a flake defect. |
| `lxc-pxe-boot` | **LIVE (attempted)** | ⚠️ BLOCKED by pve disk space | Same failure as `lxc-gui` — this target additionally builds a full nested installer/netboot image (`stage-installer-artifacts.nix`), making it similarly large. Failed with the same `No space left on device` error, immediately after the gui attempt had already consumed pve's remaining headroom. |
| `lxc-server` | BUILD-ONLY | ✅ PASS | No sops key registered yet; live deploy also would have hit `boot.zfs.extraPools` trying to import a real ZFS pool that doesn't exist in an isolated test container — an expected limitation of testing this build type outside its real hardware, not a bug. |
| `lxc-tailscale-exit-node` | BUILD-ONLY | ✅ PASS | No sops key registered yet. |
| `lxc-tor-relay` | BUILD-ONLY | ✅ PASS | Live redeploy skipped — CT106 already holds this identity in production. |
| `proxmox-docker` | BUILD-ONLY | ✅ PASS (after retry) | Live redeploy skipped — both CT105 *and* VM103 already hold `docker` identities. Combined `toplevel` + `diskoImagesScript` build crashed with a **Nix-internal assertion failure** (`worker.cc:360`) under this session's memory pressure (see Finding #6) — not a flake bug. Retried with `toplevel` alone: clean. |
| `proxmox-minimal` | **LIVE (attempted)** | ⚠️ BLOCKED by key drift → BUILD-ONLY | `.sops.yaml` has a registered `&proxmox-minimal` anchor but **no corresponding private key exists anywhere in this environment** — the script correctly refused to generate a mismatched replacement. Fell back to `toplevel` build: ✅ PASS. |
| `proxmox-nix-cache` | BUILD-ONLY | ✅ PASS | No sops key registered yet. |
| `proxmox-gui` | BUILD-ONLY | ⚠️ Killed after ~40min (resource-limited) | This session's local build machine has only 2GB RAM; swap filled completely (2.0/2.0GB) and the build stalled, so it was killed rather than risk destabilizing the session further. **Not a flake defect** — the equivalent `gui` NixOS configuration already proved fully buildable during the `lxc-gui` live attempt above (it built the entire system closure successfully and only failed at the pve-side tarball-packaging step due to disk space, not the config). |
| `proxmox-pxe-boot` | BUILD-ONLY | ⚠️ Killed after ~35min (resource-limited) | Was deep into building the nested installer's kernel initrd (this build type bundles a full netboot installer image via `stage-installer-artifacts.nix`) when killed to keep the audit moving. **Not a flake defect** — this target's own module logic was already effectively validated via the earlier *live* pve deploy attempt (`lxc-pxe-boot` above), which built the complete image and only failed at the final tarball-packaging step due to pve's disk space (Finding 1). |
| `proxmox-server` | BUILD-ONLY | ✅ PASS | No sops key registered yet; same ZFS-pool caveat as `lxc-server` would apply to a live deploy. |
| `proxmox-tailscale-exit-node` | BUILD-ONLY | ✅ PASS | No sops key registered yet. |
**Not tested at all:** `linode-*` targets (not deployable to Proxmox) and `installer` (not a normal host) — both were still covered by the static eval/dry-run-build sweep above.
---
## 3. Findings, Ranked by Severity
### Finding 1 — pve.sweet.home is critically low on disk space (blocks real deployments)
At session start: `/dev/mapper/pve-root` was **95% full, 5.3GB free** (of 94GB). After two failed large builds it recovered slightly to **91% full, 8.2GB free** (nix cleans up its own failed-build scratch space). `/nix/store` alone is 26GB; `nix-store --gc --print-dead` reports **zero** reclaimable garbage — everything currently in the store is a live GC root, so `nix-collect-garbage` won't help without first removing old roots.
**Why it matters:** `create-proxmox-resource.sh` builds every VM/CT image **directly on pve**, not on a build machine and transferred over. With <10GB headroom, any closure approaching a few GB (the `gui` build type: full Cinnamon desktop + Firefox + LibreOffice + GIMP + VS Code + xrdp; the `pxe-boot` build type: nginx/atftpd *plus* an entire nested installer/netboot image) cannot currently be built there at all. Both `lxc-gui` and `lxc-pxe-boot` failed live with `No space left on device` during this audit.
**Recommended action:** Expand `pve-root`'s LV, or free space by pruning old container templates in `/var/lib/vz/template/cache` (1.5GB) / old backups in `/var/lib/vz/dump` (306MB) / auditing what's pinning 26GB of `/nix/store` as live GC roots (likely `result-*` symlinks — see below). This is real production disk state; **not something this session touched or fixed** — it needs the operator's judgment on what's safe to remove.
**Secondary, smaller finding:** every `create-proxmox-resource.sh` run leaves a `result-<target>` symlink in the node's repo checkout as a permanent GC root (`ls /root/nixos/result-*` on pve showed 3 from this session alone: `lxc-docker`, `lxc-minimal`, `lxc-nix-cache`). These accumulate forever and pin their entire closures in the store. Consider having the script clean up its own `result-*` link after staging the built artifact (or use a temp `--out-link` under `/tmp`), so `nix-collect-garbage` can actually reclaim old build outputs.
### Finding 2 — Stale cached images can silently ship broken secrets (reproduced live)
`create-proxmox-resource.sh`'s default behavior is: if the node already has `<target>.tar.xz`/`.raw` staged, **reuse it** — only `--force-rebuild` forces a fresh build. This session hit exactly the failure mode `docs/auto-installer.md` already warns about: `lxc-minimal`'s cached tarball (built 2026-07-20T15:57Z) predated a same-day sops-key fix commit (2026-07-20T17:49Z, "clean up in ailse 3"). The deployed container booted with:
```
sops-install-secrets: failed to decrypt '.../common.yaml': Error getting data key: 0 successful groups required, got 0
Activation script snippet 'setupSecrets' failed (1)
```
— every secret permanently failed to decrypt, `sshd` never started (though the container otherwise looked "running"). This was **not a code bug**: the currently-committed `secrets/common.yaml` decrypts fine for that host's key when checked independently; the *cached artifact on pve* simply reflected an older commit's ciphertext. Redeploying with `--force-rebuild` fixed it immediately.
**Why it matters:** this is silent and easy to trigger by accident — any operator who redeploys a host without remembering `--force-rebuild` after a secrets change gets a container that looks like it started (`pct start` succeeds, `pct status` = running) but is completely inaccessible.
**Recommended action:** Have `create-proxmox-resource.sh` compare the cached image's build timestamp (or embed the source commit hash in the staged filename) against current HEAD, and warn (or refuse without `--force-rebuild`) if they differ — rather than silently trusting presence alone.
### Finding 3 — sops key/anchor drift
Two concrete instances hit live during this session:
- **`proxmox-minimal`**: `.sops.yaml` already has a registered `&proxmox-minimal` age recipient, but this environment's `host-keys/` directory has no corresponding private key file. `sync-host-keys.sh` correctly refused to generate a replacement (it would silently mismatch whatever's already registered/deployed) — but this means **no environment currently has this host's private key**, unless it exists on some other machine that was never backed up here.
- **`lxc-gui`**, and by the same logic `lxc-server`/`lxc-tailscale-exit-node`/most `proxmox-*` targets, have **no sops registration at all yet** — expected for undeployed hosts per `docs/auto-installer.md`, but this session's live-testing needed to register `lxc-gui`'s key on the fly, which immediately hit **Finding 3b**: registering a key locally does nothing for pve's build until it's pushed to `origin/main` (pve builds via `git pull`, not from this uncommitted worktree). This is exactly gap #4 the management-surface audit (below) already flagged in the abstract — this session hit it concretely.
**Recommended action:** for `proxmox-minimal`, decide whether to regenerate its key (destroying old-key decrypt access, if anything still holds it) or track down wherever the original private key lives and back it up here. For the general pattern, see the management-surface audit's recommendation to pre-flight-check key registration before building.
### Finding 4 — QEMU guest agent never wired up (found and fixed this session)
`modules/common/configuration.nix:44` sets `services.qemuGuest.enable = true` on every host — the guest-side agent daemon is correctly enabled everywhere. But `scripts/proxmox/create-proxmox-resource.sh`'s `qm create` call never passed `--agent 1`, so **Proxmox never created the virtio-serial channel** the agent needs. Every `proxmox-*` VM this script ever created was silently missing `qm guest exec`/IP-address reporting in the Proxmox UI, despite the guest daemon actually running.
**Status: fixed in this session's worktree** (`scripts/proxmox/create-proxmox-resource.sh`, `qm create` now includes `--agent enabled=1`) — see the diff, included in the PR from this session.
### Finding 5 — Orphaned container on pve (CT102)
`pve.sweet.home` has a stopped LXC container, **VMID 102**, with an essentially empty config (`lock: create` and nothing else — no hostname, no rootfs, no network) — the leftover of a `pct create` that started and never finished. It predates this session (not created by any of this audit's activity) and wasn't touched. **Recommend the operator confirm it's abandoned and remove it** (`pct destroy 102 --purge 1`) — left as-is it may be someone's genuine in-progress work, so it wasn't assumed safe to delete autonomously.
### Finding 6 — Nix-internal crash under memory pressure (tooling, not flake)
Building `proxmox-docker`'s `toplevel` and `diskoImagesScript` together crashed with a Nix-internal assertion failure (`Assertion '!awake.empty()' failed ... worker.cc:360`, a known class of bug in Nix's multi-goal build scheduler) while this session's 2GB-RAM build container was under heavy swap pressure (1.8-2.0/2GB swap in use) from a separate concurrent build. Retrying the same target alone (no concurrency) succeeded cleanly. **Not a flake defect** — purely an artifact of this session's constrained build environment; noted for completeness since it looked alarming in isolation.
### Finding 7 — Management-surface audit: 5 operability gaps
A focused audit of "can the operator actually run this repo day to day" (flake, home-manager, sops, related scripts) found:
1. **No documented recovery path if the `&admin` sops age key is lost without a backup.** `scripts/secrets/backup-admin-key.sh` exists and works but is referenced nowhere in `README.md`/`docs/` — no forcing function ensures a backup was ever taken. `rotate-admin-key.sh` requires the *old* key to re-key; there's no bootstrap-from-nothing path documented (the real fallback — deriving an age identity from any still-live host's own SSH key — isn't written down anywhere).
2. **home-manager has no standalone iteration path.** It's wired only inside `nixosConfigurations` (`flake.nix`) — no `homeConfigurations` output. The fastest real shortcut (`nix build .#nixosConfigurations.<target>.config.home-manager.users.nixos.home.activationPackage`) isn't documented anywhere, so the practical workflow is a full host rebuild to test one HM tweak.
3. **Gitea's flake-lock-update workflow pushes straight to `main` with no pre-merge validation.** `.gitea/workflows/update-flake-lock.yml` commits and pushes `nix flake update`'s result directly; `codex-maintenance.sh` only runs *after*, on the resulting push — a genuinely broken lockfile bump lands on `main` before anything catches it. (The GitHub-side workflow is safer — PR-based — but has the opposite gap: nothing alerts if the PR sits unmerged.)
4. **No pre-flight check that a build target has a registered sops key before building it.** `docs/auto-installer.md` documents the failure mode (silent, total secrets-decrypt failure) but nothing in `create-proxmox-resource.sh` refuses to proceed when it's about to build a target with no `.sops.yaml` anchor — it's on the operator to remember. This session's `lxc-gui` test hit close to this exact gap (needed the key added on the fly, mid-session).
5. **`vars.remoteBuilderAuthorizedKeys` has the same drift risk as `vars.nixCacheHostKey`, but no checker script.** `sync-nix-cache-host-key.sh --check` guards the latter; the former (and `vars.pxeServerIp`/`vars.pbsIp`) has no equivalent — a rotated/revoked client key just silently stops working with no diagnostic pointing back here.
---
## 4. Action Plan (priority order)
1. **Free up disk space on pve.sweet.home** (or expand `pve-root`). Blocking: `lxc-gui`, `proxmox-gui`, `lxc-pxe-boot`, `proxmox-pxe-boot` cannot currently be built/redeployed on this node at all.
2. **Decide on `proxmox-minimal`'s orphaned sops key**: locate the original private key and back it up here, or accept regenerating it (breaks decrypt access for whoever/whatever currently holds the old one).
3. **Merge this session's PR** (see below) to get the `--agent 1` fix and `lxc-gui`'s new sops registration onto `main` — required before `lxc-gui` can be live-redeployed with working secrets.
4. **Add a staleness guard to `create-proxmox-resource.sh`'s cache-reuse path** (Finding 2) — highest-leverage fix, since it silently produces a broken-but-"running" host.
5. **Add a pre-flight sops-anchor check to `create-proxmox-resource.sh`** (management-surface gap #4) — same root cause class as #4 above, catch it before building instead of at first boot.
6. Investigate/clean up **CT102** on pve (Finding 5) — confirm abandoned, then remove.
7. Document `backup-admin-key.sh` in `README.md`'s Security Notes and add the live-host-key bootstrap-recovery procedure to `docs/` (management-surface gap #1).
8. Add pre-push validation to the Gitea flake-lock-update workflow (management-surface gap #3).
9. Lower-priority: document the home-manager `activationPackage` shortcut (gap #2); extend `sync-nix-cache-host-key.sh`'s drift-check pattern to `remoteBuilderAuthorizedKeys` (gap #5).
10. Follow-up session: finish build-validating `proxmox-gui` and `proxmox-pxe-boot` (both killed here after 35-40min on this session's 2GB-RAM machine — not failures, just unfinished) once pve has headroom (item 1) — ideally from a machine with more RAM. `proxmox-server` and `proxmox-tailscale-exit-node` already passed build-only validation in this session, no follow-up needed.
---
## 5. Uncommitted Changes From This Session
This worktree (`worktree-flake-e2e-audit`) currently has:
- `scripts/proxmox/create-proxmox-resource.sh` — the `--agent enabled=1` fix (Finding 4).
- `.sops.yaml` / `secrets/common.yaml` — `lxc-gui`'s new age key registered as a recipient (generated live during this session's testing).
Per this session's standard workflow, these will be committed, pushed, and opened as a draft PR rather than pushed to `main` directly — merging it is the operator's call, and is also **prerequisite to live-redeploying `lxc-gui` successfully** (its build will keep hitting the sops-staleness failure from Finding 2 on pve until this registration is on `origin/main`).
+21 -347
View File
@@ -17,87 +17,11 @@ machines when deployed.
- Validation is limited to evaluation, linting, formatting checks, and - Validation is limited to evaluation, linting, formatting checks, and
`nix build --dry-run --no-link`. `nix build --dry-run --no-link`.
- Do not add secrets, tokens, private keys, or new password hashes to the repo. - Do not add secrets, tokens, private keys, or new password hashes to the repo.
- This repo currently contains **committed password hashes** in - This repo currently contains **committed password hashes** (e.g.
`modules/installer/common.nix` (the auto-installer's own root/nixos login — `prepare.sh`, `hosts/nixos/configuration.nix`) and SSH public keys (e.g.
a deliberate, documented choice, see `docs/auto-installer.md`, not `modules/nix-cache/server.nix`). The hashes are known tech debt — do not use
accidental tech debt) and **SSH public keys** in `variables.nix` them as a template for new hosts, and flag any *new* secret-like string you
(`vars.adminSshKey`, `vars.remoteBuilderAuthorizedKeys`) plus a couple of encounter instead of committing it.
per-host `KEY` values for beszel-agent auth (`hosts/server/host.nix`,
`hosts/nix-cache/host.nix`). Don't use the installer's hardcoded hash as a
template for a *real* host — every other host uses sops-nix
(`hashedPasswordFile`, see "Security Notes" in `README.md`). Flag any *new*
secret-like string you encounter instead of committing it.
- `host-keys/` is gitignored — locally-generated *private* SSH host keys for
the auto-installer (see `docs/auto-installer.md`). Never commit its
contents; if `git status` ever shows it as trackable, something is wrong.
### Two Proxmox nodes: `pve1.sweet.home` (production) and `pve-test.sweet.home` (sandbox)
There are two SSH-reachable Proxmox nodes on the LAN, both defined in
`scripts/env.sh` (`PVE1_HOST` / `PVE_TEST_HOST`), individually targetable
via `scripts/proxmox/create-proxmox-resource.sh --node <host>` or by
overriding `PROXMOX_HOST`. `PROXMOX_HOST` itself still defaults to
`PVE1_HOST` (production) — that default, and every other script behavior,
is unchanged from before `pve-test` existed; the only thing new is that
`pve-test` can now be reached at all. They are **not interchangeable** —
one is real production infrastructure, the other exists specifically so
there's somewhere safe to test. The restriction below is a policy for
Claude specifically, not a change to the tooling's own default or
anything the operator needs to opt into.
#### `pve1.sweet.home` (production — off-limits to Claude)
A real, live Proxmox node hosting production VMs/containers — not a
sandbox, and not Claude's to touch by default.
- **Off-limits at all times unless the operator has given explicit,
same-session instructions to act on this specific host.** That
authorization is scoped to the task it was given for — don't carry it
forward to unrelated later work in the same conversation, and never
assume it from a previous session.
- **Read-only for existing state is always fine, authorization or not.**
You may SSH in (or use `pvesm`, `qm list`, `pct list`, `qm config`, `pct
config`, the Proxmox API, etc.) to inspect the node's config, storage,
and any existing VM/container — including ones this repo didn't create.
- **Never** modify, stop, restart, delete, reconfigure, or create anything
on this node (`qm set`, `pct set`, `qm destroy`, `pct destroy`, `qm
stop`, `pct stop`, `qm create`, `pct create`, snapshot operations,
storage changes, etc.) — including scratch/test resources — without
that explicit go-ahead. Use `pve-test.sweet.home` for anything
exploratory instead; it exists precisely so `pve1` never has to be the
answer to "where do I test this."
- **This is a Claude-specific policy, not something the scripts enforce.**
`scripts/env.sh`/`create-proxmox-resource.sh` default to `pve1` exactly
as they did before `pve-test` existed, with no extra flag or prompt
required — that's deliberate, so the operator's own existing workflows
don't change. Claude, however, must never rely on that default: every
Proxmox action Claude takes on its own initiative — not explicitly
pointed at `pve1` by the operator this session — targets `pve-test`
instead (e.g. `--node "$PVE_TEST_HOST"`, or `PROXMOX_HOST=$PVE_TEST_HOST`).
Claude's own default is `pve-test`, full stop, regardless of what the
tooling's own unqualified default happens to be.
#### `pve-test.sweet.home` (sandbox — Claude's default target)
A separate Proxmox node set aside for testing. The *tooling's* default is
still production (`PROXMOX_HOST` → `PVE1_HOST`, see above) — but
**Claude's own default is this node**: absent an explicit, same-session
instruction to use `pve1`, every Proxmox action Claude initiates targets
`pve-test`. Once targeted, it's safe to create, interrogate, and destroy
resources on without asking first.
- **Test VMs/containers are allowed, but must be torn down.** Create a
scratch VM or container here (e.g. via
`scripts/proxmox/create-proxmox-resource.sh` or raw `qm`/`pct create`)
to validate something. Anything created this way must be destroyed
again in the same session, before ending the task — never leave a test
resource running. Use a VMID/name that's obviously scratch (and doesn't
collide with a real flake target) so it's unambiguous what's safe to
remove.
- **Node-level config is still not yours to change.** Creating/destroying
your own scratch guests is fine; Proxmox host config, storage pools, and
networking on `pve-test` itself are still the operator's call to make
manually, same as on `pve1`.
## Commands ## Commands
@@ -105,22 +29,11 @@ resources on without asking first.
# One-time environment bootstrap (installs Nix if missing, prints hosts) # One-time environment bootstrap (installs Nix if missing, prints hosts)
bash scripts/codex-setup.sh bash scripts/codex-setup.sh
# Changed-files-only validation: secret grep (whole repo), nixpkgs-fmt --check # Full validation: secret grep, nixpkgs-fmt --check, statix lint, eval all hosts
# and statix on changed *.nix files, eval of the hosts/packages those changes
# can affect. This is what CI runs on every push/PR.
bash scripts/codex-maintenance.sh bash scripts/codex-maintenance.sh
# Full sweep: nixpkgs-fmt --check/statix over the whole tree, eval every host # Same, plus a dry-run build (no result symlink) of every host's toplevel
# and package. Slow (minutes) -- CI never runs this; use it locally before a bash scripts/codex-maintenance.sh dry-run
# release or after touching modules/common/*, flake.nix, or variables.nix for
# extra confidence beyond the automatic full-fallback those paths already
# trigger in the default mode (see below).
bash scripts/codex-maintenance.sh --full-check
# Either mode, plus a dry-run build (no result symlink) of every host/package
# in whichever scope is active
bash scripts/codex-maintenance.sh --dry-run
bash scripts/codex-maintenance.sh --full-check --dry-run
# List the hosts the flake currently exposes # List the hosts the flake currently exposes
nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]' nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
@@ -137,197 +50,6 @@ maintenance script pulls them via `nix run github:NixOS/nixpkgs/nixos-25.11#<too
There is no test suite — "correctness" here means the flake evaluates and There is no test suite — "correctness" here means the flake evaluates and
`nixpkgs-fmt`/`statix` are clean. `nixpkgs-fmt`/`statix` are clean.
With no flags, `codex-maintenance.sh` diffs against a base ref (env
`MAINT_BASE_SHA`, else the PR base SHA in CI, else `HEAD^` locally) and scopes
fmt-check/statix to the changed `*.nix` files and eval to the hosts/packages
those changes can affect — a `hosts/<name>/host.nix` edit only evals that
host's targets, a `modules/platforms/<platform>.nix` edit only evals that
platform's hosts, and so on. A change to `flake.nix`, `flake.lock`,
`variables.nix`, `modules/common/*`, or any other `modules/*.nix` file outside
`platforms/`/`build-types/` (whose blast radius isn't safely inferable from
the path alone) falls back to evaluating every host and package, same as
`--full-check` would, just without the whole-tree fmt/statix sweep. This
exists because the whole-tree sweep is what was timing out CI; **CI always
runs the plain, no-flag form and never passes `--full-check`.**
The default mode's diff is against the working tree (uncommitted and staged
edits included, not just committed ones), so it's already the right tool for
an interactive session too: after editing one or two hosts/modules, plain
`bash scripts/codex-maintenance.sh` naturally scopes to just what you
touched. Reserve `--full-check` for changes that plausibly affect every host
(`modules/common/*`, `flake.nix`, `variables.nix` — though the default mode
already falls back to evaluating everything for those paths, `--full-check`
additionally re-checks fmt/statix over the whole tree) or as a final check
before committing.
## Scripts
Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` is
organized by purpose: `scripts/secrets/` (sops/age + SSH host-key
management), `scripts/proxmox/` (Proxmox deployment), `scripts/installer/`
(the auto-installer's own shell script, templated into the image — see
below), `scripts/lib/` (shared helpers, sourced by the scripts below — not
run directly), and a handful of repo-wide scripts left at the top level
(`env.sh`, `bump-nixpkgs-release.sh`, plus `codex-setup.sh`/
`codex-maintenance.sh` above). When adding a new script, put it in the
matching subfolder rather than the top level, and if it duplicates logic
another script already has, lift the shared part into `scripts/lib/`
instead of copying it.
### `scripts/installer/`
- `scripts/installer/auto-install.sh` — the interactive install script
baked into the auto-installer image (see `docs/auto-installer.md`), kept
as a real, version-controlled shell file rather than inline in
`modules/installer/common.nix`'s Nix. It sources `scripts/env.sh` itself
for `LAN_DOMAIN` (`export LAN_DOMAIN`/`: "${LAN_DOMAIN:=...}"`, matching
`variables.nix`'s `lanDomain` — manually kept in sync, same pattern as
`NIX_CACHE_HOST` mirroring `nixCacheHost`), rather than Nix-level string
substitution — that's what makes it work identically whether run
straight from a git checkout or from inside the built installer image.
`common.nix` bakes `scripts/env.sh` in alongside it at a matching
relative path (`/etc/nixos-installer/env.sh` next to
`/etc/nixos-installer/installer/auto-install.sh`) so the script's own
`source "$(dirname ...)/../env.sh"` line resolves the same way in both
contexts — this is also why it's invoked from
`/etc/nixos-installer/installer/auto-install.sh` rather than a flat
`/etc/auto-install.sh`. `#!/usr/bin/env bash`, not
`#!/run/current-system/sw/bin/bash`: the latter only resolves on an
already-activated NixOS system, breaking the checked-out-file case
entirely (confirmed live: "cannot execute: required file not found" on
a non-NixOS box); `/usr/bin/env` is reliably present on both NixOS
(`environment.usrbinenv`'s own default) and any normal Linux distro.
### `scripts/secrets/`
- `scripts/secrets/sync-host-keys.sh` — generates/registers SSH host keys
and their `.sops.yaml`/`secrets/*.yaml` recipients for flake targets,
idempotently (`--all`, `<target>`, `--remove`, `--regenerate-all-keys`,
all with `--dry-run`). The primary tool for provisioning a new host's
secrets access — see "Creating a new machine" in `docs/auto-installer.md`.
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a
key by an arbitrary name without touching `.sops.yaml`. Still useful to
pre-generate a key before its flake target exists yet, since
`sync-host-keys.sh` can only act on targets `nixosConfigurations` already
has.
- `scripts/secrets/rotate-admin-key.sh <backup-admin-key> [--new-key-file
<path>] [--dry-run]` — rotates `.sops.yaml`'s `&admin` age key: decrypts
with a backed-up copy of the key currently trusted as `&admin` (verified
by deriving its public key and comparing, not taken on faith), replaces
the `&admin` line with a new key already present in the environment
(defaults to wherever sops/age itself would look), and runs
`sops updatekeys` on every `secrets/*.yaml`. One-way: the old key can no
longer decrypt anything re-encrypted this way. This is the automation
for the manual steps `sync-host-keys.sh`/`create-proxmox-resource.sh`
print when they bootstrap a brand-new, not-yet-trusted key on a machine
with no prior admin access.
- `scripts/secrets/backup-admin-key.sh <dest-path> [--key-file <path>]
[--force] [--dry-run]` — copies the local sops age key (source
resolution matches sops/age itself: `$SOPS_AGE_KEY` inline, then
`--key-file`, then `$SOPS_AGE_KEY_FILE`, then the XDG default) to an
arbitrary destination path with `0600` permissions, validating it's a
real age identity and round-tripping the public key before and after the
write. Refuses to overwrite an existing `<dest-path>` without `--force`.
Purely a local filesystem copy — never touches `.sops.yaml`/
`secrets/*.yaml` or the repo at all. The resulting file is exactly what
`rotate-admin-key.sh` expects as its backup-key argument.
- `scripts/secrets/sync-nix-cache-host-key.sh [--check] [--dry-run]
[--host <name>]` — detects drift between the ed25519 SSH host key
nix-cache is actually serving right now (via `ssh-keyscan`) and
`vars.nixCacheHostKey` (`variables.nix`), the value
`modules/nix-cache/remote-builder-client.nix` bakes into every real
client's declarative `programs.ssh.knownHosts` and
`configure-nix-cache-client.sh` hardcodes as its own default for
non-NixOS clients. That value has no automatic source of truth — it's
set once from whatever nix-cache's host key happened to be at the time,
and silently goes stale if the host is ever rebuilt/recreated with a new
key, breaking every client's distributed-build SSH trust with no error
that points back here. `--check` (used by `codex-maintenance.sh`, which
treats an unreachable nix-cache — e.g. from a non-LAN CI runner — as a
silent skip rather than a failure) only reports drift; the no-flags form
updates both files in place. Declarative clients still need a rebuild to
pick up the fix.
### `scripts/proxmox/`
- `scripts/proxmox/create-proxmox-resource.sh` — builds a `lxc-*`/
`proxmox-*` target's tarball/disk image and creates it on a real Proxmox
node (`pct create` against the tarball as a CT template / `qm create`+
`importdisk`), or reconfigures an existing resource's cores/memory/disk
size (`--modify`, always requires typing the VMID back to confirm).
Checks for an already-uploaded image on the node before building
(`--force-rebuild` to skip that and always rebuild), and probes
nix-cache's substituter/remote-builder reachability once up front rather
than letting every `nix build` call retry against it individually.
Refuses to create a target whose host identity already exists live on
the node (checked directly via `qm`/`pct`, not any file in this repo)
unless `--allow-duplicate-host` is passed. `--dry-run` throughout both
modes. The first time it has to bootstrap build tooling on a node (i.e.
`nix` wasn't already on its `PATH`), it also runs
`scripts/proxmox/configure-nix-cache-client.sh` there (non-fatally — a
failure just falls back to building from source / `cache.nixos.org`) so
the node substitutes from and can offload builds to nix-cache on every
subsequent run, not just this one.
- `scripts/proxmox/configure-nix-cache-client.sh [--dry-run]
[--no-remote-builder] [--no-restart]` — the non-NixOS equivalent of
`modules/nix-cache/client.nix`/`remote-builder-client.nix`, for a plain
Debian machine with the Nix package manager (not NixOS) already
installed: run as root *on that machine* to add nix-cache as a
substituter in `/etc/nix/nix.conf` (`https://cache.nixos.org/` kept as
fallback) via `extra-substituters`/`extra-trusted-public-keys` so it
layers on top of whatever's already there instead of clobbering it, and,
if `/root/.ssh/nixremote` is already present (see docs/nix-cache.md
"Remote builder SSH keys"), configures it as a distributed-build
machine too and trusts nix-cache's SSH host key in
`/etc/ssh/ssh_known_hosts`. Idempotent (re-running replaces its own
marked block rather than duplicating it); restarts `nix-daemon` by
default so the change takes effect immediately.
### `scripts/lib/`
Sourced by the scripts above, never run directly:
- `nix-bootstrap.sh` — `NIX_CONFIG`/`ensure_nix_profile`, shared by
`codex-setup.sh`/`codex-maintenance.sh` and the remote build commands
`create-proxmox-resource.sh` runs over SSH.
- `nix-eval.sh` — `NIX_EVAL_FLAGS` plus `list_flake_targets`/
`flake_target_hostname` flake-introspection helpers.
- `ssh-host-keys.sh` — `generate_host_ed25519_key`/`ssh_pubkey_to_age`,
shared by `sync-host-keys.sh` and `prepare-host-key.sh`.
- `sops-age.sh` — `age_pubkey_from_identity_file`/`sops_yaml_admin_pubkey`/
`sops_updatekeys` plus the shared sops/age default key-file resolution,
shared by `backup-admin-key.sh`, `rotate-admin-key.sh`, and
`sync-host-keys.sh`.
- `confirm.sh` — `confirm_typed`, the "type X back to confirm" destructive-
action prompt shared by `create-proxmox-resource.sh` and
`sync-host-keys.sh`.
- `sync-host-keys-edit-sops.py` — the `.sops.yaml` anchor/key_groups editor
`sync-host-keys.sh` shells out to (see that script for why: precise,
idempotent YAML edits are impractical in bash).
### Top level
- `scripts/env.sh` — shared config (`PROXMOX_HOST`, storage pool, bridge,
default cores/memory, `NIX_CACHE_HOST`, `LAN_DOMAIN`) sourced by
`create-proxmox-resource.sh` and `scripts/installer/auto-install.sh`. Add
new cross-script config here instead of duplicating it per-script.
- `scripts/bump-nixpkgs-release.sh` — bumps `flake.nix`'s `nixpkgs.url`/
`home-manager.url` in place. Exists because flake input URLs can't
reference `variables.nix` (confirmed empirically — `nix flake metadata`
errors on it), so this is the closest equivalent to a single source of
truth for the tracked release.
`sync-host-keys.sh`, `create-proxmox-resource.sh`, and
`rotate-admin-key.sh` genuinely mutate real state when run for real (not
`--dry-run`): real `secrets/*.yaml` recipients, real Proxmox VMs/
containers, real revocation of decrypt access. They require the
operator's own SSH/sops access, which an agent session doesn't have — but
don't suggest running any of them non-dry-run without the operator's
explicit go-ahead even if it becomes technically reachable.
`backup-admin-key.sh` only writes a key copy to a path the operator gives
it — lower-stakes than the others, but it still handles a real private
key, so treat its destination path choice as the operator's call too.
## Architecture ## Architecture
`flake.nix` is the single entry point. It generates one `flake.nix` is the single entry point. It generates one
@@ -348,15 +70,10 @@ nixosSystem {
} }
``` ```
Platforms: `linode`, `proxmox`, `lxc`, `baremetal`. Build types: `minimal`, Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
`nix-cache`, `server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `server`, `docker`, `gui`, `pxe-boot`. Not every combination is built — e.g.
`tor-relay`. Not every combination is built — e.g. `pxe-boot` has no `linode` `pxe-boot` has no `linode` variant (PXE/DHCP/TFTP need LAN L2 adjacency a
variant (PXE/DHCP/TFTP need LAN L2 adjacency a Linode VPS doesn't have), Linode VPS doesn't have). Treat `flake.nix`'s `generatedTargets` as the source
`tor-relay` currently only exists as `lxc-tor-relay`, and `baremetal`
currently only exists as `baremetal-gui` (the real gui-host hardware —
see `hosts/nixos/host.nix` and `modules/platforms/baremetal.nix`). Treat
`flake.nix`'s
`generatedTargets` as the source
of truth for which hosts exist — `README.md`, `AGENTS.md`, of truth for which hosts exist — `README.md`, `AGENTS.md`,
`docs/flake-lock-automation.md`, and the CI eval workflows `docs/flake-lock-automation.md`, and the CI eval workflows
(`.github/workflows/check-nixos.yml`, `.gitea/workflows/check-nixos.yml`) list (`.github/workflows/check-nixos.yml`, `.gitea/workflows/check-nixos.yml`) list
@@ -371,58 +88,22 @@ removing a host.
of their own beyond narrow parameterized helpers (see of their own beyond narrow parameterized helpers (see
`modules/beszel/host-token.nix` below) — all shared behavior comes from the `modules/beszel/host-token.nix` below) — all shared behavior comes from the
platform/build-type modules composed in `flake.nix`, not from the host file. platform/build-type modules composed in `flake.nix`, not from the host file.
- `modules/platforms/{linode,proxmox,lxc,baremetal}.nix` — platform-specific - `modules/platforms/{linode,proxmox,lxc}.nix` — platform-specific config:
config: boot method, guest tooling, and the hardware config, imported boot method, guest tooling, and (for linode/proxmox) the hypervisor-specific
directly by the platform module itself — **not** wired in from hardware config, imported directly by the platform module itself
`flake.nix`. VM platforms use `../hardware-configuration/vm/{proxmox,linode}.nix`; (`../hardware-configuration/vm/{proxmox,linode}.nix`) — **not** wired in
`baremetal.nix` uses `../hardware-configuration/baremetal.nix` (adapted from `flake.nix`. `lxc.nix` has no hardware-configuration counterpart since
from a real `nixos-generate-config` run on the actual hardware, not a containers share the host kernel.
vm/ file, since it isn't a VM) plus `hardware.enableRedistributableFirmware
= true` for real wifi/GPU/microcode firmware that VMs never needed.
`lxc.nix` has no hardware-configuration counterpart since containers
share the host kernel; instead it imports nixpkgs' own
`virtualisation/proxmox-lxc.nix`, which gives every `lxc-*` host a
`config.system.build.tarball` output — a plain rootfs tarball, used as a
`pct create ... vztmpl` CT template (**not** `pct restore`, which expects
`vzdump` backup-archive metadata this doesn't have), no install step —
see `docs/auto-installer.md`.
- `modules/build-types/*.nix` — what a system is for: - `modules/build-types/*.nix` — what a system is for:
minimal/server/docker/gui/pxe-boot/nix-cache/tailscale-exit-node/tor-relay. minimal/server/docker/gui/pxe-boot/nix-cache.
- `modules/common/configuration.nix` — base NixOS config imported by every - `modules/common/configuration.nix` — base NixOS config imported by every
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
the `nixos` user; the `nixos` workstation (`gui` build type) has its own, the `nixos` user; the `nixos` workstation (`gui` build type) has its own,
other hosts share `modules/common/home.nix`. other hosts share `modules/common/home.nix`.
- `modules/disko/proxmox.nix` — declarative disk layout (GPT: ESP + swap + - `modules/disko/proxmox.nix` — declarative disk layout (GPT: ESP + swap +
ext4 root) via disko, used by all Proxmox-VM hosts (`proxmox-*`, not ext4 root) via disko, used by all Proxmox-VM hosts.
`lxc-*`). Also carries `imageSize`/`imageName`, letting every `proxmox-*`
host be built as a standalone, `qm importdisk`-ready `.raw` image with no
install step — see `docs/proxmox-images.md`.
- `modules/disko/linode.nix` — `linode-*`'s disko config, deliberately
different in kind from the Proxmox one: Linode provisions and sizes
`/dev/sda`/`/dev/sdb` itself as whole, unpartitioned devices before the OS
boots, so this declares them with `destroy = false` (disko never wipes
them) and a bare `filesystem`/`swap` content type instead of a partition
table — idempotent against an already-provisioned disk, never destructive.
- `modules/disko/baremetal.nix` — `baremetal-gui`'s disko config: a ZFS
RAID0 (striped, no redundancy — disko's zpool `mode` defaults to `""`,
which is a plain stripe rather than `"mirror"`/`"raidz"`) root pool
across two disks, ESP + systemd-boot on the first. Device paths
(`vars.guiRootDisk1`/`guiRootDisk2`) are placeholders — fill in stable
`/dev/disk/by-id/...` paths before running disko for real.
`modules/platforms/baremetal.nix` also imports
`modules/services/zfs/enable-service.nix` for this (the `zfs_unstable`
package, autoScrub/autoSnapshot/trim) — the only other importer today is
`server`'s NFS data pool, an unrelated non-root ZFS use.
- `modules/boot/efi.nix` — systemd-boot + EFI vars, paired with the disko module. - `modules/boot/efi.nix` — systemd-boot + EFI vars, paired with the disko module.
- `modules/installer/` — the auto-installer environment (ISO, also served as
PXE netboot): `common.nix` (shared config + the generated
`auto-install.sh`), `iso.nix`, `host-keys.nix` (optionally bakes
`host-keys/` into the image under `--impure`). See
`docs/auto-installer.md`.
- `modules/pxe-boot/stage-installer-artifacts.nix` — builds the installer's
netboot image and stages it on the `pxe-boot` host so its iPXE menu can
chain straight to it. See `docs/pxe-boot.md`.
- `modules/nix-cache/{client,server,remote-builder-client}.nix` — binary cache - `modules/nix-cache/{client,server,remote-builder-client}.nix` — binary cache
substituter + SSH remote-builder wiring; see `docs/nix-cache.md` for the substituter + SSH remote-builder wiring; see `docs/nix-cache.md` for the
full design (per-host local stores, no shared `/nix/store`, and how the full design (per-host local stores, no shared `/nix/store`, and how the
@@ -432,8 +113,7 @@ 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/tor/`, `modules/services/*` — single-purpose, `modules/traefik/`, `modules/services/*` — single-purpose, single-host
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.
@@ -449,12 +129,6 @@ duplicating config.
handling. handling.
- `docs/pxe-boot.md` — the `pxe-boot` host's iPXE/TFTP/HTTP boot chain and - `docs/pxe-boot.md` — the `pxe-boot` host's iPXE/TFTP/HTTP boot chain and
directory layout under `/srv/pxe`. directory layout under `/srv/pxe`.
- `docs/auto-installer.md` — the installer environment (ISO/netboot/Proxmox
LXC), `host-keys/` and the sops-nix pre-seeding problem it solves, and why
`lxc-*` hosts are deliberately excluded from its menu.
- `docs/proxmox-images.md` — building `proxmox-*` hosts as standalone `.raw`
disk images (disko's image builder) instead of installing, and deploying
the result to Proxmox.
- `docs/flake-lock-automation.md` — how `flake.lock` updates flow through CI - `docs/flake-lock-automation.md` — how `flake.lock` updates flow through CI
(scheduled `nix flake update` PR + host-eval-on-PR workflow) and why hosts (scheduled `nix flake update` PR + host-eval-on-PR workflow) and why hosts
should track the committed lock file rather than `nixos-rebuild --upgrade-all`. should track the committed lock file rather than `nixos-rebuild --upgrade-all`.
+17 -74
View File
@@ -8,42 +8,24 @@ workstation.
Targets are named `<platform>-<buildtype>`, generated from two orthogonal Targets are named `<platform>-<buildtype>`, generated from two orthogonal
pieces composed in `flake.nix`: pieces composed in `flake.nix`:
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`, `baremetal` - **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`, `tor-relay` `gui`, `pxe-boot`
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, PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have. The full
`tor-relay` currently only exists as `lxc-tor-relay`, and `baremetal` list:
currently only exists as `baremetal-gui` (the real gui-host hardware). The
full list:
| Target | Purpose | | Target | Purpose |
| --- | --- | | --- | --- |
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS | | `linode-minimal` | Minimal NixOS host profile on a Linode VPS (real, deployed) |
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox — previously the flat `nix-minimal` target | | `proxmox-minimal` | Minimal NixOS host profile on Proxmox (real, deployed — previously the flat `nix-minimal` target) |
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container | | `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder — previously the flat `nix-cache` target | | `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder (`proxmox-nix-cache` is the real, deployed one — previously the flat `nix-cache` target) |
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host — previously the flat `server` target | | `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host (`proxmox-server` is the real, deployed one — previously the flat `server` target) |
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack — previously the flat `docker` target | | `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack (`proxmox-docker` is the real, deployed one — previously the flat `docker` target) |
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation — previously the flat `nixos` target | | `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation (`proxmox-gui` is the real, deployed one — previously the flat `nixos` target) |
| `baremetal-gui` | Same Cinnamon desktop workstation, on the real gui-host hardware — ZFS RAID0 root, systemd-boot | | `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host (`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 |
| `lxc-tor-relay` | Tor middle relay |
Which variant of a given buildtype is actually deployed isn't tracked
anywhere in this repo — that's live infrastructure state, not something a
committed file can keep accurate, and it changes independently of the code.
Check the Proxmox node itself, or `/etc/flake-target` on a running host (see
below), if you need to know what's really out there right now.
`scripts/proxmox/create-proxmox-resource.sh`'s duplicate-host guard works the same
way: it checks the Proxmox node directly rather than any file here.
Real, production deployments live on `pve1.sweet.home`; there's a second
node, `pve-test.sweet.home`, set aside purely for scratch/test resources —
see `scripts/env.sh` (`PVE1_HOST` / `PVE_TEST_HOST`, and the
`--node`/`PROXMOX_HOST` targeting they feed into) and CLAUDE.md's Proxmox
section for which is which.
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity 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
@@ -64,17 +46,17 @@ nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
| Path | Purpose | | Path | Purpose |
| --- | --- | | --- | --- |
| `flake.nix` | Flake inputs, the `mkTarget` platform × build-type generator, and `nixosConfigurations` outputs | | `flake.nix` | Flake inputs, the `mkTarget` platform × build-type generator, and `nixosConfigurations` outputs |
| `variables.nix` | Single source of truth for shared values (LAN domain/CIDR, hostnames, timezone, primary username, storage root, NFS share subpaths/mountpoints, service ports, ...) — passed to every module and Home Manager config as the `vars` argument via `specialArgs`/`extraSpecialArgs` | | `variables.nix` | Single source of truth for shared values (LAN domain/CIDR, hostnames, timezone, primary username, storage root, ...) — passed to every module and Home Manager config as the `vars` argument via `specialArgs`/`extraSpecialArgs` |
| `hosts/<name>/host.nix` | Per-machine identity: hostname, hostId, per-machine secrets, `system.stateVersion` | | `hosts/<name>/host.nix` | Per-machine identity: hostname, hostId, per-machine secrets, `system.stateVersion` |
| `hosts/nixos/home.nix` | Workstation-specific Home Manager config (used by the `gui` build type) | | `hosts/nixos/home.nix` | Workstation-specific Home Manager config (used by the `gui` build type) |
| `modules/platforms/` | Platform-specific config: virtualisation guest tools, boot method, hardware config (`linode.nix`, `proxmox.nix`, `lxc.nix`, `baremetal.nix`) | | `modules/platforms/` | Platform-specific config: virtualisation guest tools, boot method, hardware config (`linode.nix`, `proxmox.nix`, `lxc.nix`) |
| `modules/build-types/` | Build-type-specific config: what makes a system minimal/server/docker/gui/pxe-boot/nix-cache | | `modules/build-types/` | Build-type-specific config: what makes a system minimal/server/docker/gui/pxe-boot/nix-cache |
| `modules/common/` | Shared NixOS config, Home Manager, aliases imported by every host | | `modules/common/` | Shared NixOS config, Home Manager, aliases imported by every host |
| `modules/nix-cache/` | Binary cache and remote builder client/server modules | | `modules/nix-cache/` | Binary cache and remote builder client/server modules |
| `modules/installer/` | Auto-installer environment (ISO, also served as PXE netboot) — see `docs/auto-installer.md` | | `modules/installer/` | Auto-installer environment (ISO/netboot/Proxmox LXC) — see `docs/auto-installer.md` |
| `host-keys/` | Gitignored, locally-generated SSH host keys for the auto-installer — see `docs/auto-installer.md` | | `host-keys/` | Gitignored, locally-generated SSH host keys for the auto-installer — see `docs/auto-installer.md` |
| `docs/` | Operational notes for cache, builders, lock updates, boot services, the auto-installer, and Proxmox image builds | | `docs/` | Operational notes for cache, builders, lock updates, boot services, and the auto-installer |
| `scripts/` | Codex setup, validation, host-key, release-bump, and Proxmox resource helpers | | `scripts/` | Codex setup, validation, and host-key-prep helpers |
## Validation ## Validation
@@ -82,19 +64,10 @@ Safe validation commands for Codex and local review:
```bash ```bash
bash scripts/codex-setup.sh bash scripts/codex-setup.sh
bash scripts/codex-maintenance.sh dry-run
bash scripts/codex-maintenance.sh bash scripts/codex-maintenance.sh
``` ```
`codex-maintenance.sh` with no flags (what CI runs on every push/PR) scopes
fmt-check/statix/eval to files changed against a base ref — fast, but only
as thorough as the diff. For the full sweep (every host, every package,
fmt-check and statix over the whole tree — slow, CI never runs this):
```bash
bash scripts/codex-maintenance.sh --full-check
bash scripts/codex-maintenance.sh --full-check --dry-run
```
For individual host evaluation: For individual host evaluation:
```bash ```bash
@@ -114,29 +87,6 @@ review sessions.
client hosts. client hosts.
- `pxe-boot` serves iPXE boot files over HTTP from `/srv/pxe`. - `pxe-boot` serves iPXE boot files over HTTP from `/srv/pxe`.
### Deploying a new host
Three different paths depending on target, none of them involving a manual
`nixos-rebuild switch` from this repo:
- Most hosts: boot the auto-installer, pick the target from its menu — see
`docs/auto-installer.md`. Every menu target has a Disko config the
installer formats unconditionally (`docs/auto-installer.md`'s "Storage"
section covers how this stays non-destructive for `linode-*`, whose disks
Linode itself provisions ahead of time).
- `lxc-*` targets: not installed at all — build a ready-to-run container
tarball and `pct create` it as a CT template directly. `docs/auto-installer.md`
covers why (and the installer's menu excludes them for the same reason).
- `proxmox-*` targets: can alternatively be built as a standalone `.raw`
disk image and attached to a new VM with no install step — see
`docs/proxmox-images.md`.
`scripts/proxmox/create-proxmox-resource.sh --type lxc|vm --host <name>` automates
either of the last two end to end (host-key registration, building the
image directly on the Proxmox node itself, `pct create`/`qm create`), with
`--dry-run` and a guard against duplicating an already-deployed host's
identity. See its `--help`.
## Security Notes ## Security Notes
Do not commit tokens, private keys, live credentials, or new password hashes Do not commit tokens, private keys, live credentials, or new password hashes
@@ -157,13 +107,6 @@ enabled via `git config core.hooksPath .githooks`, done automatically by
`scripts/codex-setup.sh`) runs `gitleaks protect --staged` to catch mistakes `scripts/codex-setup.sh`) runs `gitleaks protect --staged` to catch mistakes
before they're committed. before they're committed.
The auto-installer environment is the one deliberate exception to
sops-nix-everywhere: it has a hardcoded login password instead (no stable
per-boot host key for sops-nix to derive from on ephemeral media) — see
"Host keys" in `docs/auto-installer.md` for why, and how the private keys it
*does* pre-seed for target hosts stay out of git via the gitignored
`host-keys/` directory.
This repository's git *history* still contains secrets committed before this This repository's git *history* still contains secrets committed before this
migration (see `remove-sensetive-info-refactor.md`) — those are being migration (see `remove-sensetive-info-refactor.md`) — those are being
scrubbed and rotated separately; don't treat the repo as safe to make public scrubbed and rotated separately; don't treat the repo as safe to make public
+46 -158
View File
@@ -5,16 +5,12 @@ install any host exposed by its own `nixosConfigurations`. It was migrated
from a formerly-separate `nix-auto-installer` repo — everything it did now from a formerly-separate `nix-auto-installer` repo — everything it did now
lives here. lives here.
The installer provides a small NixOS install environment (ISO, or the same The installer provides a small NixOS install environment (ISO/netboot/Proxmox
image netbooted via PXE) with SSH access, Git support, and an interactive LXC) with SSH access, Git support, and an interactive installation script.
installation script. Logging in as any user (root or `nixos`) runs `/etc/auto-install.sh`,
Logging in as any user (root or `nixos`) runs discovers available hosts from this same flake, lets the operator choose a
`/etc/nixos-installer/installer/auto-install.sh` (the same file as target, applies that host's Disko storage configuration, installs NixOS, and
`scripts/installer/auto-install.sh` in this repo — see "Installer process" reboots.
below for why it's baked in at that path rather than a flat
`/etc/auto-install.sh`), discovers available hosts from this same flake,
lets the operator choose a target, applies that host's Disko storage
configuration, installs NixOS, and reboots.
**This applies to every `nixosConfigurations` target except `lxc-*` hosts — **This applies to every `nixosConfigurations` target except `lxc-*` hosts —
see "LXC hosts" immediately below for why those are different.** see "LXC hosts" immediately below for why those are different.**
@@ -22,7 +18,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`, `lxc-tailscale-exit-node`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the `lxc-gui`, `lxc-pxe-boot`) 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
@@ -37,76 +33,16 @@ container image, no install step at all:
nix build .#nixosConfigurations.lxc-minimal.config.system.build.tarball nix build .#nixosConfigurations.lxc-minimal.config.system.build.tarball
``` ```
This is a plain rootfs tarball, not a `vzdump` backup archive — restoring it Then, on the Proxmox host, `pct restore` (or the GUI's "Create CT" → upload
with `pct restore` fails ("archive contains no configuration file"), since template flow) that tarball directly as a new container. First boot runs
that command expects backup-archive metadata this tarball doesn't have. Use `boot.postBootCommands` (registers the Nix store DB and system profile) —
it as a CT *template* instead: drop it under Proxmox's template storage there's no separate activation step to run yourself.
(conventionally `/var/lib/vz/template/cache/` for the `local` storage, or
the GUI's "Create CT" → upload-as-template flow) and create a container
from it, supplying all config on the command line since a template has none
of its own:
```sh Host keys still need pre-seeding the same way as any other host (see "Host
pct create <vmid> local:vztmpl/<file>.tar.xz \ keys" below) — the sops-nix activation-vs-first-boot race is identical
--unprivileged 1 --features nesting=1,keyctl=1 \ regardless of how the image reaches the machine. `NIXOS_HOST_KEYS_DIR=...
--rootfs local-lvm:8 --hostname <name> --cores 2 --memory 2048 --swap 2048 \ nix build ... --impure` bakes the matching key into the tarball the same way
--net0 name=eth0,bridge=vmbr0,ip=dhcp it does for the ISO/PXE installer images.
pct start <vmid>
```
Every one of those extra flags is load-bearing, confirmed by actually
booting one:
- `--unprivileged 1` — `modules/platforms/lxc.nix` sets
`proxmoxLXC.privileged = false`, so the image assumes it's running
unprivileged. `pct create`'s own CLI default for this flag is
privileged (unlike the web UI, whose checkbox defaults the other way)
— omit it and you get a privileged container running a NixOS config
that assumes unprivileged, a real mismatch.
- `--features nesting=1,keyctl=1` — required for a modern (v247+)
systemd guest to boot unprivileged at all. Without it, AppArmor denies
the nested user namespaces and credential mounts systemd routinely
uses (even plain getty units) — every getty crash-loops on a denied
`/run/credentials/*` mount every ~3s (this is what garbage on the
console turns out to be) while core services like `nsncd` fail the
same way, and the system never finishes activating.
- `--swap 2048` — `--memory` doesn't touch swap; it silently stays at
Proxmox's own 512M default otherwise. Match it to `--memory` unless
you deliberately want otherwise.
First boot runs `boot.postBootCommands` (registers the Nix store DB and
system profile) — there's no separate activation step to run yourself.
`scripts/proxmox/create-proxmox-resource.sh --type lxc --host <name>` automates all
of this (host-key handling, building the tarball directly on the Proxmox
node itself, `pct create` with the flags above) — see its `--help`.
Host keys still need pre-seeding the same way as any other host — the
sops-nix activation-vs-first-boot race is identical regardless of how the
image reaches the machine. Unlike the ISO/PXE installer (where
`modules/installer/host-keys.nix` bakes *every* `host-keys/` entry into
`/etc/host-keys/` for `auto-install.sh` to pick from and copy at install
time — see "Host keys" below), an `lxc-*` tarball has no install step to
copy anything during, so `modules/platforms/lxc.nix` bakes this *one*
target's key straight into `/etc/ssh/ssh_host_ed25519_key(.pub)` directly,
keyed by its own exact flake target name (`config.environment.etc` can't
be read back from within a module still contributing to it, so this comes
in via `specialArgs.flakeTarget`, set by `flake.nix`'s `mkTarget`):
```sh
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" \
nix build .#nixosConfigurations.lxc-nix-cache.config.system.build.tarball --impure
```
Confirmed the hard way: without this, the tarball's own built-in system
just generates a fresh host key at first boot like any host would, which
can never match whatever `.sops.yaml` actually trusts for that target —
`sops-install-secrets` fails with `Error getting data key: 0 successful
groups required, got 0`, and *every* secret (including this host's own
login) permanently fails to decrypt, silently — no error in the boot log
at all, since the activation step that would install secrets only runs on
a from-scratch first activation and skips silently once `/run/current-system`
already exists. `scripts/proxmox/create-proxmox-resource.sh` always builds with
`NIXOS_HOST_KEYS_DIR` set for this reason.
## Layout ## Layout
@@ -117,47 +53,35 @@ already exists. `scripts/proxmox/create-proxmox-resource.sh` always builds with
`installation-cd-minimal.nix` module plus `common.nix`. Also used, paired `installation-cd-minimal.nix` module plus `common.nix`. Also used, paired
with `netboot-minimal.nix`, to build the PXE netboot variant (see with `netboot-minimal.nix`, to build the PXE netboot variant (see
`docs/pxe-boot.md`). `docs/pxe-boot.md`).
- `modules/installer/proxmox-lxc.nix` — Proxmox LXC-specific: disables
bootloader options (containers don't need one) plus `common.nix`.
- `modules/installer/host-keys.nix` — optionally bakes pre-generated SSH - `modules/installer/host-keys.nix` — optionally bakes pre-generated SSH
host keys into the image; see "Host keys" below. host keys into the image; see "Host keys" below.
- `scripts/secrets/sync-host-keys.sh` — admin-workstation tool that generates, - `scripts/prepare-host-key.sh` — admin-workstation pre-flight tool, see
registers, and (via `--remove`/`--regenerate-all-keys`) retires host "Creating a New Machine" below.
keys; see "Creating a New Machine" below.
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a single
key by an arbitrary name without touching `.sops.yaml`. Still useful for
pre-generating a key *before* its flake target exists (`sync-host-keys.sh`
can only act on targets `nixosConfigurations` already has); otherwise
`sync-host-keys.sh` does the same thing and more.
Flake outputs: Flake outputs:
```nix ```nix
nixosConfigurations.installer # ISO/netboot installer image nixosConfigurations.installer # ISO/netboot installer image
nixosConfigurations.proxmox-lxc # Proxmox LXC-based installer image
packages.x86_64-linux.iso # installer ISO/netboot image packages.x86_64-linux.iso # installer ISO/netboot image
packages.x86_64-linux.lxc # Proxmox LXC installer tarball
packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
packages.x86_64-linux.all # all three of the above, bundled
``` ```
```sh ```sh
nix build .#iso nix build .#iso
nix build .#lxc
nix build .#pxe nix build .#pxe
nix build .#all
``` ```
There's no `nixosConfigurations.proxmox-lxc` (installer-boots-as-an-LXC-
container) or `packages.x86_64-linux.lxc`/`.all` anymore. Both existed only
to let the installer itself run as an LXC container so you could
`nixos-install` some *other* host from within it — but LXC targets are
excluded from the install menu (same bind-mount problem as any LXC
`nixos-install`), and now have their own direct tarball path anyway (see
"LXC hosts" above), which left the installer's own LXC form with no real
use case.
The `pxe` variant is also built automatically as part of the `pxe-boot` host The `pxe` variant is also built automatically as part of the `pxe-boot` host
itself (`modules/pxe-boot/stage-installer-artifacts.nix`) and served over itself (`modules/pxe-boot/stage-installer-artifacts.nix`) and served over
iPXE as the menu's "NixOS Auto-Installer" entry — see `docs/pxe-boot.md`. iPXE — see `docs/pxe-boot.md`.
That same host also builds and serves `packages.x86_64-linux.pxe-minimal`,
a vanilla NixOS minimal netboot image with none of this auto-installer's
wiring, as a separate "NixOS Minimal" menu entry — also documented in
`docs/pxe-boot.md`, not covered further here since it's not this installer.
## Host keys ## Host keys
@@ -195,55 +119,23 @@ default.
`auto-install.sh` still supports the older manual path as a fallback: if a `auto-install.sh` still supports the older manual path as a fallback: if a
host's key isn't baked in (`/etc/host-keys`), it checks `/root/host-keys` host's key isn't baked in (`/etc/host-keys`), it checks `/root/host-keys`
next, where you can `scp` a key in after boot, same as before this migration. next, where you can `scp` a key in after boot, same as before this migration.
If neither has it and the script is running interactively (an actual
operator at the other end of stdin, not an unattended run), it prompts for
an arbitrary directory to check (a mounted USB stick, another filesystem,
etc.) and copies the key pair into `/root/host-keys` from there if found.
## Storage ## Storage
Disk partitioning is handled by Disko — the installer has no hardcoded Disk partitioning is handled by Disko — the installer has no hardcoded
`parted`/`mkfs`/`mkswap`/`mount` commands, and `auto-install.sh` runs `parted`/`mkfs`/`mkswap`/`mount` commands. Each host defines its own layout
`disko --mode destroy,format,mount` unconditionally, no branching on whether via `disko.devices`; a host without one is installed in place via bind-mount
the target has a Disko config. Every host reachable through this menu has (this is how every LXC container target works, since they have no raw block
one: device to partition).
- `proxmox-*` (`modules/disko/proxmox.nix`): a real GPT partition table
(ESP + swap + root) on `/dev/sda`.
- `linode-*` (`modules/disko/linode.nix`): Linode provisions and sizes
`/dev/sda`/`/dev/sdb` itself as whole, unpartitioned block devices before
the OS ever boots, so this declares them with `destroy = false` (skips
disko's wipe stage for these disks entirely — see the option's own docs)
and a bare `filesystem`/`swap` content type with no partition table, and
the format step it does run only calls `mkfs`/`mkswap` if `blkid` shows
the device isn't already formatted — a re-run against an
already-provisioned Linode disk is a no-op, not a wipe.
`lxc-*` is the only category without one — it's excluded from this menu
entirely (see "LXC hosts" above), so it never reaches this code path.
## Installer process ## Installer process
`scripts/installer/auto-install.sh` is a real, version-controlled shell `/etc/auto-install.sh`:
script — not an inline Nix string. It sources `scripts/env.sh` for
`LAN_DOMAIN` itself (same as every other script in `scripts/`), so it
behaves identically whether it's run straight from a git checkout (e.g.
manually, from a stock NixOS ISO that isn't this repo's own installer
image) or from inside the built installer image. That's also why it's
baked in at `/etc/nixos-installer/installer/auto-install.sh` rather than a
flat `/etc/auto-install.sh` — `modules/installer/common.nix` bakes
`scripts/env.sh` in alongside it at `/etc/nixos-installer/env.sh`,
preserving the same relative layout (`installer/auto-install.sh` ->
`../env.sh`) the checked-out repo has, so the script's own
`source ".../env.sh"` line resolves correctly in both places without any
Nix-level templating.
Once running, it:
1. Queries `nixosConfigurations` from this flake over the network (`git+https://<lanDomain>/beatzaplenty/nixos.git`) — this happens at *install* time, not build time, so a generic installer image always sees whatever hosts are currently committed, without needing a rebuild. 1. Queries `nixosConfigurations` from this flake over the network (`git+https://<lanDomain>/beatzaplenty/nixos.git`) — this happens at *install* time, not build time, so a generic installer image always sees whatever hosts are currently committed, without needing a rebuild.
2. Presents them as a menu; confirms the choice. 2. Presents them as a menu; confirms the choice.
3. Skips the `nix-cache` substituter when installing a `nix-cache` host itself (consistent with that host's own runtime config). 3. Skips the `nix-cache` substituter when installing a `nix-cache` host itself (consistent with that host's own runtime config).
4. Runs `disko --mode destroy,format,mount` (see "Storage" above — every host reachable through this menu has a Disko config, so this is unconditional). 4. Runs `disko --mode destroy,format,mount` if the target has a Disko config, otherwise bind-mounts `/` onto `/mnt`.
5. Installs the target's SSH host key from `/etc/host-keys` or `/root/host-keys` (see "Host keys" above). 5. Installs the target's SSH host key from `/etc/host-keys` or `/root/host-keys` (see "Host keys" above).
6. Runs `nixos-install --flake <url>#<choice> --no-root-password`. 6. Runs `nixos-install --flake <url>#<choice> --no-root-password`.
7. Cleans up and reboots. 7. Cleans up and reboots.
@@ -255,28 +147,24 @@ host consumes any sops-nix secret — as of this writing, that's every host
(`modules/common/configuration.nix` puts the root/nixos password hash and the (`modules/common/configuration.nix` puts the root/nixos password hash and the
GitHub token behind sops-nix for all of them). GitHub token behind sops-nix for all of them).
1. **Add the flake target** — `hosts/<name>/host.nix` plus the matching 1. **On your admin workstation**, decide the new machine's flake target name
`mkTarget { ... }` entry in `flake.nix`'s `generatedTargets` (see (`<platform>-<buildtype>`, e.g. `proxmox-server`) and generate + register
"Composition pattern" in `CLAUDE.md`). No secrets involved yet, so this its host key:
is safe to commit on its own if you want a clean history.
2. **On your admin workstation, generate and register its host key:**
```sh ```sh
./scripts/secrets/sync-host-keys.sh <flake-target> ./scripts/prepare-host-key.sh <hostname>
``` ```
This generates `host-keys/<flake-target>_ssh_host_ed25519_key(.pub)`, This generates `host-keys/<hostname>_ssh_host_ed25519_key(.pub)` and
adds it as a new `.sops.yaml` anchor, works out which `secrets/*.yaml` prints the `.sops.yaml` snippet to add, which secrets files to
files this specific host actually references (from its own re-encrypt it into, and the next steps below.
`config.sops.secrets`, not guessed), adds it to each one's
`key_groups`, and re-encrypts them with `sops updatekeys` — no manual
YAML editing. Safe to re-run; it only fills in what's missing.
Doing this for every host that needs one at once — after adding several 2. **Edit `.sops.yaml`** with the printed snippet, then re-encrypt every
new targets, or just to catch up any that were missed — is secrets file you added the new host to:
`./scripts/secrets/sync-host-keys.sh --all`. See `scripts/secrets/sync-host-keys.sh --help`
for its other modes (`--remove`, `--regenerate-all-keys`). ```sh
nix-shell -p sops --run 'sops updatekeys secrets/common.yaml'
```
3. **Commit and push.** The flake build the installer uses has to see the 3. **Commit and push.** The flake build the installer uses has to see the
new recipient before you install, or decryption fails on first boot new recipient before you install, or decryption fails on first boot
+3 -8
View File
@@ -8,14 +8,9 @@ and to verify that declared NixOS hosts still evaluate after dependency updates.
- A scheduled workflow runs `nix flake update` once per week. - A scheduled workflow runs `nix flake update` once per week.
- On GitHub, any resulting `flake.lock` change is proposed through a pull request. - On GitHub, any resulting `flake.lock` change is proposed through a pull request.
- On Gitea, the workflow can commit and push `flake.lock` directly when PR automation is not configured. - On Gitea, the workflow can commit and push `flake.lock` directly when PR automation is not configured.
- A separate CI workflow runs `scripts/codex-maintenance.sh` before merge. - A separate CI workflow evaluates every configured host before merge, listed
Its default mode scopes eval to the hosts/packages a change can affect, dynamically via `nix eval --json .#nixosConfigurations --apply builtins.attrNames`
determined from a git diff against the PR base — but a `flake.lock` change rather than hand-enumerated, so it can't drift as `<platform>-<buildtype>`
is treated as repo-wide and always falls back to evaluating every host, so
a lock-file update PR still gets full coverage. Hosts are still listed
dynamically via
`nix eval --json .#nixosConfigurations --apply builtins.attrNames` rather
than hand-enumerated, so that fallback can't drift as `<platform>-<buildtype>`
targets are added or removed. See `README.md` for the current target list. targets are added or removed. See `README.md` for the current target list.
## Why hosts should stop using `--upgrade-all` ## Why hosts should stop using `--upgrade-all`
+18 -54
View File
@@ -11,70 +11,34 @@ This repository configures `nix-cache` as a **binary cache server** and a **remo
- Client hosts import `modules/nix-cache/client.nix` and, when remote building is enabled, `modules/nix-cache/remote-builder-client.nix`. - Client hosts import `modules/nix-cache/client.nix` and, when remote building is enabled, `modules/nix-cache/remote-builder-client.nix`.
- The `nix-cache` host imports `modules/nix-cache/server.nix`. - The `nix-cache` host imports `modules/nix-cache/server.nix`.
## Binary cache signing key ## Binary cache signing keys (on nix-cache)
`modules/nix-cache/client.nix` hardcodes every client's trust in one
specific public key (`cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4=`).
That means whichever host is currently playing the `nix-cache` role has to
use that *exact* keypair — not a freshly generated one — or no client will
accept substitutes from it (they'd just silently fall back to building
from source). So unlike most per-host secrets, this one can't be
self-generated on first boot; it's managed via sops-nix like every other
secret in this repo, sourced from `secrets/nix-cache.yaml`'s
`cache-priv-key` entry (`modules/nix-cache/server.nix`).
**Adding or rotating the value:**
```bash ```bash
nix-shell -p sops --run 'sops secrets/nix-cache.yaml' sudo install -d -m 0700 /etc/nix
sudo nix-store --generate-binary-cache-key nix-cache-1 /etc/nix/cache-priv.pem /etc/nix/cache-pub.pem
sudo chmod 0600 /etc/nix/cache-priv.pem
sudo chmod 0644 /etc/nix/cache-pub.pem
cat /etc/nix/cache-pub.pem
``` ```
Add (or replace) a `cache-priv-key` entry with the private key file's exact Do not commit private keys.
contents. If you don't have it yet, generate a keypair once: Do not commit new password hashes or live credentials. Existing committed hashes
should be rotated and moved to host-local secret management.
```bash
nix-store --generate-binary-cache-key nix-cache-1 cache-priv.pem cache-pub.pem
```
— paste `cache-priv.pem`'s contents into the `cache-priv-key` entry above,
delete both local files afterward, and update
`trusted-public-keys` in `modules/nix-cache/client.nix` (and every already-built
client) to match `cache-pub.pem` if this is a genuine rotation rather than
a first-time bootstrap. Any `nixos-configurations.*-nix-cache` host picks
the new key up automatically on next activation — no more manual
`/etc/nix/cache-priv.pem` install step.
## Remote builder SSH keys ## Remote builder SSH keys
Each client authenticates as `nixremote` using its **own default root SSH On each client, install the private key used to authenticate as `nixremote`:
identity** (`/root/.ssh/id_ed25519`) — not a separately-named or shared
keypair. If a client doesn't have one yet:
```bash ```bash
sudo ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519 sudo install -d -m 0700 /root/.ssh
sudo install -m 0600 ./nixremote /root/.ssh/nixremote
sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
``` ```
Then add its `.pub` contents as a new entry in `vars.remoteBuilderAuthorizedKeys` On `nix-cache`, install the matching public key used by `nixremote` authorized keys.
(`variables.nix`) and rebuild `nix-cache` to pick it up (that list is
declarative — an imperative `ssh-copy-id nixremote@nix-cache` won't stick;
it gets overwritten on every rebuild). Verify with:
```bash The committed `nixremote` authorized keys are public SSH keys only. Keep the
sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache nix-store --version matching private keys on client hosts and out of the repository.
```
The committed `remoteBuilderAuthorizedKeys` entries are public SSH keys
only. Keep the matching private keys on client hosts and out of the
repository.
nix-cache's own SSH *host* key is trusted declaratively via
`programs.ssh.knownHosts` in `modules/nix-cache/remote-builder-client.nix`,
sourced from `vars.nixCacheHostKey` (`variables.nix`) — every client rebuild
picks it up automatically, so distributed builds don't fail with "Host key
verification failed" on a client that has never manually SSH'd to nix-cache
before. If nix-cache's host key is ever rotated or the host rebuilt from
scratch, update `vars.nixCacheHostKey` to match its new
`/etc/ssh/ssh_host_ed25519_key.pub`.
## Manual verification ## Manual verification
@@ -84,8 +48,8 @@ After deployment:
curl http://nix-cache/nix-cache-info curl http://nix-cache/nix-cache-info
nix store ping --store http://nix-cache nix store ping --store http://nix-cache
nix show-config | grep -E 'substituters|trusted-public-keys|builders-use-substitutes' nix show-config | grep -E 'substituters|trusted-public-keys|builders-use-substitutes'
sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache nix-store --version sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
nix build nixpkgs#hello --builders 'ssh://nixremote@nix-cache x86_64-linux /root/.ssh/id_ed25519 4 2 big-parallel,kvm,nixos-test,benchmark' -L nix build nixpkgs#hello --builders 'ssh://nixremote@nix-cache x86_64-linux /root/.ssh/nixremote 4 2 big-parallel,kvm,nixos-test,benchmark' -L
nix path-info -r nixpkgs#hello nix path-info -r nixpkgs#hello
curl -I "http://nix-cache/$(basename "$(nix path-info nixpkgs#hello)").narinfo" curl -I "http://nix-cache/$(basename "$(nix path-info nixpkgs#hello)").narinfo"
``` ```
-116
View File
@@ -1,116 +0,0 @@
# Proxmox VM disk images
`proxmox-*` hosts (VM platform, not `lxc-*`) can be built as standalone,
ready-to-attach `.raw` disk images via disko's own image-builder — no
`nixos-install`, no live installer boot. This uses the same `disko.devices`
config (`modules/disko/proxmox.nix`) already used to format a real disk on
install, so there's nothing host-specific to write; it's available for every
`proxmox-*` target automatically.
`scripts/proxmox/create-proxmox-resource.sh --type vm --host <name>` automates the
whole walkthrough below (and the equivalent LXC one) end to end, including
host-key handling and building the image directly on the Proxmox node
itself (no local build, no image transfer) — see its `--help`. The steps
here are what it runs under the hood, useful for doing any of it by hand
or understanding what it does before you trust it against real
infrastructure.
## Building
```sh
nix build .#nixosConfigurations.proxmox-server.config.system.build.diskoImagesScript
sudo ./result --build-memory 2048
```
This produces `<hostname>.raw` in the current directory (e.g. `server.raw`
for `proxmox-server`, matching `networking.hostName`, not the flake attribute
name — every `proxmox-*` host gets a distinctly named image instead of all
of them producing an identical `main.raw`). The script builds inside a
temporary QEMU VM and moves the finished image out to the working directory
when done; `--build-memory` controls how much RAM that build VM gets.
`disko.devices.disk.main.imageSize` (currently `20G`, in
`modules/disko/proxmox.nix`) sets the image's total size — disko doesn't
support auto-resizing, so this needs to comfortably fit ESP + swap + root at
build time. Grow the virtual disk (and resize the filesystem) in Proxmox
after attaching if a host needs more than that; this is the normal way to
size these images, not a one-time decision to get exactly right up front.
## Host keys
The disko image script runs a real activation pass inside its temporary
build VM while constructing the image — the same sops-nix
activation-before-first-boot problem the installer and LXC tarball workflows
have (see `docs/auto-installer.md`) applies here too, unmodified. Disko has
a native mechanism for it:
```sh
sudo ./result \
--pre-format-files host-keys/server_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \
--pre-format-files host-keys/server_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \
--build-memory 2048
```
Generate the key first with `scripts/secrets/sync-host-keys.sh <hostname>`, same
as any other host — see `docs/auto-installer.md` for the full walkthrough
(it registers the new key in `.sops.yaml` and re-encrypts the affected
`secrets/*.yaml` files too, no manual editing needed).
## Deploying to Proxmox
The image needs **UEFI (OVMF)**, not Proxmox's default SeaBIOS —
`modules/boot/efi.nix` uses `systemd-boot`, which only works with UEFI
firmware. `virtio-scsi` is safe to use as the disk bus:
`hardware-configuration/vm/proxmox.nix` already includes `virtio_scsi` in
its initrd kernel modules.
1. Copy the image to the Proxmox host:
```sh
scp server.raw root@<proxmox-host>:/var/lib/vz/import/
```
2. Create an empty VM shell (no disk yet) — replace `<vmid>` with a free ID
and `<storage>` with your storage pool's name (`pvesm status` or
Datacenter → Storage in the web UI):
```sh
qm create <vmid> --name proxmox-server --memory 2048 --cores 2 \
--net0 virtio,bridge=vmbr0 \
--bios ovmf --machine q35 \
--scsihw virtio-scsi-pci \
--efidisk0 <storage>:1,efitype=4m,pre-enrolled-keys=0
```
(`--efidisk0` is required for UEFI — it's where OVMF persists boot-entry
NVRAM; without it, systemd-boot's boot entry may not survive a reboot.)
3. Import the raw disk into storage:
```sh
qm importdisk <vmid> /var/lib/vz/import/server.raw <storage>
```
This prints the resulting disk identifier (e.g. `vm-<vmid>-disk-1`).
4. Attach it and set it as the boot disk:
```sh
qm set <vmid> --scsi0 <storage>:vm-<vmid>-disk-1
qm set <vmid> --boot order=scsi0
```
5. Boot it:
```sh
qm start <vmid>
```
No install step — it boots straight into the already-activated system.
## Why not `nix build .#nixosConfigurations.<host>.config.system.build.vm`?
That's a different, unrelated feature — `system.build.vm` (`nixos-rebuild
build-vm`) produces an ephemeral QEMU script for locally testing a
configuration, not a distributable disk image. It's not part of this
workflow.
+14 -36
View File
@@ -1,10 +1,6 @@
# pxe-boot # pxe-boot
The `pxe-boot` host serves HTTP boot assets for iPXE clients — including The `pxe-boot` host serves HTTP boot assets for iPXE clients.
self-staged copies of both this flake's own auto-installer netboot image
(see `docs/auto-installer.md` for what that image actually is and does once
booted) and a vanilla, unmodified NixOS minimal netboot image for plain
rescue/inspection use.
## Host Role ## Host Role
@@ -29,8 +25,7 @@ The host creates these directories with systemd tmpfiles:
/srv/pxe /srv/pxe
/srv/pxe/http /srv/pxe/http
/srv/pxe/http/images /srv/pxe/http/images
/srv/pxe/http/auto-installer /srv/pxe/http/nixos
/srv/pxe/http/nixos-minimal
/srv/pxe/http/systemrescue /srv/pxe/http/systemrescue
/srv/pxe/http/ubuntu /srv/pxe/http/ubuntu
/srv/pxe/http/rescue /srv/pxe/http/rescue
@@ -39,7 +34,7 @@ The host creates these directories with systemd tmpfiles:
Mount shared image storage under `/srv/pxe/http`, preferably Mount shared image storage under `/srv/pxe/http`, preferably
`/srv/pxe/http/images` unless a menu entry expects files in a specific `/srv/pxe/http/images` unless a menu entry expects files in a specific
directory such as `/srv/pxe/http/auto-installer`. directory such as `/srv/pxe/http/nixos`.
The HTTP iPXE chain is: The HTTP iPXE chain is:
@@ -52,37 +47,20 @@ undionly.kpxe or ipxe.efi
The generated menu currently exposes entries for: The generated menu currently exposes entries for:
- NixOS Auto-Installer - NixOS installer
- NixOS Minimal
- SystemRescue environment - SystemRescue environment
- iPXE shell - iPXE shell
- Reboot - Reboot
Both NixOS entries chain-load a `netboot.ipxe` staged into their own The NixOS installer entry chain-loads `/srv/pxe/http/nixos/netboot.ipxe`,
directory (`/srv/pxe/http/auto-installer/netboot.ipxe` and which is nixpkgs' own generated netboot iPXE script (correct `init=`/`initrd=`
`/srv/pxe/http/nixos-minimal/netboot.ipxe`), each nixpkgs' own generated kernel parameters included) rather than a hand-rolled boot line — that script
netboot iPXE script (correct `init=`/`initrd=` kernel parameters included) in turn expects its kernel/initrd siblings in the same directory. All three
rather than a hand-rolled boot line — that script in turn expects its files (`bzImage`, `initrd`, `netboot.ipxe`) are built from this flake's own
kernel/initrd siblings in the same directory. Each directory's three files `modules/installer/iso.nix` netboot image (the same one `nix build .#pxe`
(`bzImage`, `initrd`, `netboot.ipxe`) are built from source and staged produces) and staged automatically by
automatically by `modules/pxe-boot/stage-installer-artifacts.nix` via `modules/pxe-boot/stage-installer-artifacts.nix` via `systemd.tmpfiles.rules`
`systemd.tmpfiles.rules` — no manual operator step required: — no manual operator step required.
- `auto-installer` is this flake's own `netbootSystem` (`flake.nix`) — the
same auto-installer image `nix build .#pxe` produces. See
`docs/auto-installer.md`.
- `nixos-minimal` is `netbootMinimalSystem` (`flake.nix`) — nixpkgs'
`netboot-minimal.nix` composed on its own, with none of this flake's
auto-installer wiring (no `common.nix`, no `auto-install.sh`, no baked
host keys or custom users). Same `nix build .#pxe-minimal` mechanism as
the auto-installer image, just a different module composition. Useful
as a plain rescue/inspection shell that doesn't assume anything about
this flake.
Both images set `networking.hostName` to match their menu entry/staged
directory name (`auto-installer` / `nixos-minimal`), so each one's
generated system name (`nixos-system-<name>-*`) is self-describing rather
than the nixpkgs default of `nixos-system-nixos-*` for both.
The SystemRescue entry expects the source ISO at: The SystemRescue entry expects the source ISO at:
@@ -105,7 +83,7 @@ uses `archiso_http_srv` to fetch the squashfs payload over HTTP.
Safe evaluation check: Safe evaluation check:
```bash ```bash
nix eval .#nixosConfigurations.proxmox-pxe-boot.config.system.build.toplevel.drvPath --raw nix eval .#nixosConfigurations.pxe-boot.config.system.build.toplevel.drvPath --raw
``` ```
After deployment by an operator, basic service checks are: After deployment by an operator, basic service checks are:
+143
View File
@@ -0,0 +1,143 @@
# Spec: Refactor Flake Targets into Platform × Build-Type Matrix
## Context
The flake at `~/nixos` currently defines these output targets (flat, ad-hoc naming):
- `docker`
- `linode-minimal`
- `nix-cache`
- `nix-minimal`
- `nixos`
- `server`
- `pxe-boot`
Some already follow a `platform-buildtype` convention (`linode-minimal`), most don't.
`~/nix-auto-installer` is a related repo and should be checked for any coupling to
these target names (scripts, docs, CI, or install automation that reference them by
name) before renaming anything.
## Goal
Restructure the flake so targets are generated from two orthogonal concepts:
**Build types** (what the system is for):
- `minimal`
- `nix-cache`
- `server`
- `docker`
- `pxe-boot`
- `gui`
**Platforms** (what it's deployed on):
- `linode` (Linode VM)
- `proxmox` (Proxmox VM)
- `lxc` (Proxmox LXC container)
Final targets should be named consistently as `<platform>-<buildtype>`, e.g.:
```
linode-minimal proxmox-minimal lxc-minimal
linode-nix-cache proxmox-nix-cache lxc-nix-cache
linode-server proxmox-server lxc-server
linode-docker proxmox-docker lxc-docker
linode-pxe-boot proxmox-pxe-boot lxc-pxe-boot
linode-gui proxmox-gui lxc-gui
```
That's the full matrix (18 targets) if every build type applies to every platform.
See **Open Questions** below — some combinations may not make sense and should be
confirmed with me before being built out, not silently included or dropped.
## Migration mapping (old → new)
| Old target | New target | Notes |
|--------------------|------------------------------------------------------|-------|
| `linode-minimal` | `linode-minimal` | Already correct, keep as-is |
| `nix-minimal` | likely `proxmox-minimal` or a platform-less base module | Ambiguous — see Open Questions |
| `nix-cache` | base module consumed by `linode-nix-cache`, `proxmox-nix-cache`, `lxc-nix-cache` | Currently platform-less; needs to become a build-type module, not a standalone target |
| `server` | base module consumed by `linode-server`, `proxmox-server`, `lxc-server` | Same as above |
| `docker` | base module consumed by `linode-docker`, `proxmox-docker`, `lxc-docker` | Confirm docker actually makes sense as an LXC/VM guest build vs. a standalone container image — see Open Questions |
| `pxe-boot` | TBD — may stay a single target rather than a per-platform one | See Open Questions |
| `nixos` | TBD — unclear what this maps to in the new scheme | See Open Questions |
## Open Questions (Claude Code: raise these with me before implementing, don't guess)
1. **`nixos` target** — what is this currently used for (bare metal install, dev
shell, template)? It doesn't obviously map to any of the six build types.
2. **`nix-minimal` vs `linode-minimal`** — are these two different things, or is
`nix-minimal` a leftover/duplicate?
3. **`pxe-boot` and `gui` across all three platforms** — does PXE boot make sense
for an LXC container or a cloud VM (Linode), or is it inherently bare-metal/
network-boot only and should remain a single non-platform target? Does `gui`
make sense inside an LXC container?
4. **`docker` as a build type** — is this "a NixOS host configured to run Docker"
(which would sensibly have linode/proxmox/lxc variants), or "a Docker container
image built by the flake" (which wouldn't take a platform prefix at all, since
it doesn't run on Linode/Proxmox/LXC as a guest OS)? These are structurally
different and change how it should be wired in.
5. Confirm whether all 18 combinations should actually exist, or whether this is
meant to produce only the combinations that are genuinely useful (e.g. maybe no
one needs `lxc-pxe-boot`).
## Implementation approach
1. **Inventory first.** Read the current `flake.nix` and any `nixosConfigurations`/
`modules` structure. Map every existing target to what module(s) it actually
pulls in. Don't assume — confirm against the real file contents.
2. **Separate build-type and platform into their own module directories**, e.g.:
```
modules/build-types/minimal.nix
modules/build-types/nix-cache.nix
modules/build-types/server.nix
modules/build-types/docker.nix
modules/build-types/pxe-boot.nix
modules/build-types/gui.nix
modules/platforms/linode.nix
modules/platforms/proxmox.nix
modules/platforms/lxc.nix
```
Build-type modules should contain only what makes a system "minimal" vs
"server" vs "gui", etc. Platform modules should contain only what's specific
to running as a Linode VM vs Proxmox VM vs LXC container (virtualisation
guest tools, boot method, filesystem/image format, LXC-specific constraints
like no kernel modules, etc).
3. **Generate the target matrix programmatically** in `flake.nix` rather than
hand-writing 18 near-identical `nixosConfigurations` entries — e.g. a small
function that takes a platform name and build-type name, composes the two
modules plus any shared base module, and produces the named output. This
keeps future build types/platforms a one-line addition rather than a copy-paste
job.
4. **Only build combinations we've confirmed make sense** (see Open Questions) —
don't emit all 18 by default if some are structurally invalid.
5. **Preserve existing working configs during the transition.** Don't delete the
old target names until their replacements build successfully — rename/alias
at the end, not the start, so there's no window where the flake is broken.
## Verification
For every new target produced:
```bash
nix flake check
nix build .#nixosConfigurations.<target>.config.system.build.toplevel
```
Confirm each builds without evaluation errors before considering it done. If a
target fails to build, report which one and why rather than silently skipping it.
## Deliverables
- Refactored `flake.nix` using the composed module + generated-matrix approach.
- New `modules/build-types/*.nix` and `modules/platforms/*.nix` files.
- Old flat target names removed only after their replacements are verified.
- A short `README.md` (or section in existing docs) listing the final target
names and what each one is for.
- A summary at the end of what changed, what was removed, and any of the Open
Questions above that got resolved differently than expected.
## Out of scope
- Don't touch `~/nix-auto-installer` contents beyond checking it for references
to the old target names — if changes there are needed, flag them, don't make
them without confirming.
- Don't add new build types or platforms beyond the ones listed here.
Generated
+6 -6
View File
@@ -95,11 +95,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1784350909, "lastModified": 1783740085,
"narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=", "narHash": "sha256-qajyHfZY29G2oEQk+uHxmsJcRoBUBXP9maTpFlwP/dI=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3", "rev": "3cd22efe6471dc7365c822bd9ad73a21e55f38fb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -147,11 +147,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1784432872, "lastModified": 1784011430,
"narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=", "narHash": "sha256-lDebytrYdd47IBLwvNOD+6AGeoqZ78CIKlp70hzW280=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870", "rev": "8eeec934ae0dbeca3d7868c059568a65c08b2fc3",
"type": "github" "type": "github"
}, },
"original": { "original": {
+18 -77
View File
@@ -33,9 +33,6 @@
# nix-cache itself consumes the nix-cache substituter and remote # nix-cache itself consumes the nix-cache substituter and remote
# builder. # builder.
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix }: mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix }:
let
flakeTarget = "${platform}-${buildType}";
in
nixpkgs.lib.nixosSystem { nixpkgs.lib.nixosSystem {
inherit system; inherit system;
modules = [ modules = [
@@ -45,7 +42,7 @@
./modules/platforms/${platform}.nix ./modules/platforms/${platform}.nix
./modules/build-types/${buildType}.nix ./modules/build-types/${buildType}.nix
hostPath hostPath
{ environment.etc."flake-target".text = flakeTarget; } { environment.etc."flake-target".text = "${platform}-${buildType}"; }
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
{ {
home-manager = { home-manager = {
@@ -59,13 +56,7 @@
./modules/nix-cache/client.nix ./modules/nix-cache/client.nix
./modules/nix-cache/remote-builder-client.nix ./modules/nix-cache/remote-builder-client.nix
]; ];
# flakeTarget is passed via specialArgs (not read back from specialArgs = { inherit inputs vars netbootSystem; };
# config.environment.etc."flake-target" above) specifically so
# modules/platforms/lxc.nix can use it to select its own host key
# file without a same-option circular dependency (a module
# contributing to environment.etc can't read the merged
# environment.etc it's itself contributing to).
specialArgs = { inherit inputs vars netbootSystem netbootMinimalSystem flakeTarget; };
}; };
# Generated platform x build-type matrix. pxe-boot has no linode # Generated platform x build-type matrix. pxe-boot has no linode
@@ -91,16 +82,9 @@
linode-gui = mkTarget { platform = "linode"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; }; linode-gui = mkTarget { platform = "linode"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
proxmox-gui = mkTarget { platform = "proxmox"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; }; proxmox-gui = mkTarget { platform = "proxmox"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
lxc-gui = mkTarget { platform = "lxc"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; }; lxc-gui = mkTarget { platform = "lxc"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
baremetal-gui = mkTarget { platform = "baremetal"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
proxmox-pxe-boot = mkTarget { platform = "proxmox"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; }; proxmox-pxe-boot = mkTarget { platform = "proxmox"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
lxc-pxe-boot = mkTarget { platform = "lxc"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; }; lxc-pxe-boot = mkTarget { platform = "lxc"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
linode-tailscale-router = mkTarget { platform = "linode"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
proxmox-tailscale-router = mkTarget { platform = "proxmox"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
lxc-tailscale-router = mkTarget { platform = "lxc"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; };
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
}; };
# Auto-install environments (migrated from the former nix-auto-installer # Auto-install environments (migrated from the former nix-auto-installer
@@ -116,94 +100,51 @@
modules = [ ./modules/installer/iso.nix ]; modules = [ ./modules/installer/iso.nix ];
specialArgs = { inherit vars; }; specialArgs = { inherit vars; };
}; };
proxmox-lxc = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./modules/installer/proxmox-lxc.nix ];
specialArgs = { inherit vars; };
};
}; };
# Same installer environment, built as netboot (kernel + initrd + # Same installer environment, built as netboot (kernel + initrd +
# iPXE script) instead of an ISO — this is what packages.pxe bundles. # iPXE script) instead of an ISO — this is what packages.pxe bundles.
#
# Deliberately imports common.nix directly, NOT ./modules/installer/iso.nix
# (which pulls in nixpkgs' installation-cd-minimal.nix) -- confirmed live
# that composing the ISO module together with netboot-minimal.nix hangs
# every boot waiting for a device that can never exist on a netboot
# client ("A start job is running for /dev/disk/by-label/nixos-minimal-...").
# Both installation-cd-base.nix and netboot.nix set fileSystems."/" via
# the identical lib.mkImageMediaOverride (mkOverride 60) priority --
# genuinely conflicting root-filesystem strategies (ISO-by-label vs.
# netboot-tmpfs) at the same priority, and the ISO one was winning.
# netboot-minimal.nix's own chain (netboot-base.nix) already imports
# profiles/installation-device.nix independently, so common.nix's
# initialHashedPassword override (which assumes that profile is
# present) still applies correctly without iso.nix in the mix.
#
# networking.hostName is set explicitly (rather than left at nixpkgs'
# own "nixos" default) so this image's generated system name
# (nixos-system-auto-installer-*) matches its iPXE menu entry —
# see modules/build-types/pxe-boot.nix's :auto-installer item — and
# its staged directory, /srv/pxe/http/auto-installer.
netbootSystem = nixpkgs.lib.nixosSystem { netbootSystem = nixpkgs.lib.nixosSystem {
inherit system; inherit system;
modules = [ modules = [
./modules/installer/common.nix ./modules/installer/iso.nix
({ modulesPath, ... }: { ({ modulesPath, ... }: {
imports = [ imports = [
(modulesPath + "/installer/netboot/netboot-minimal.nix") (modulesPath + "/installer/netboot/netboot-minimal.nix")
]; ];
}) })
{ networking.hostName = "auto-installer"; }
]; ];
specialArgs = { inherit vars; }; specialArgs = { inherit vars; };
}; };
# A genuinely vanilla NixOS minimal netboot image: nixpkgs'
# netboot-minimal.nix on its own, with none of this flake's
# auto-installer wiring (no common.nix — no auto-install.sh, no
# baked host keys, no custom users/passwords). Built from source via
# the same nixosSystem + netboot-minimal.nix path as netbootSystem
# above, so both go through an identical build mechanism; the only
# difference is what's composed in. hostName again matches this
# image's iPXE menu entry (:nixos-minimal) and staged directory
# (/srv/pxe/http/nixos-minimal).
netbootMinimalSystem = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
({ modulesPath, ... }: {
imports = [
(modulesPath + "/installer/netboot/netboot-minimal.nix")
];
})
{ networking.hostName = "nixos-minimal"; }
];
};
in in
{ {
nixosConfigurations = generatedTargets // installerTargets; nixosConfigurations = generatedTargets // installerTargets;
# Buildable auto-installer artifacts (`nix build .#<name>`). No `lxc` # Buildable auto-installer artifacts (`nix build .#<name>`), migrated
# variant (installer-boots-as-an-LXC-container) or `all` bundle # unchanged from nix-auto-installer's interface.
# anymore — lxc-* and proxmox-* hosts deploy via their own tarball/ packages.${system} = rec {
# disk-image outputs instead (see docs/auto-installer.md and
# docs/proxmox-images.md), which left the installer's own LXC form
# with no real use case: it's excluded from the install menu (same
# bind-mount problem as any LXC nixos-install target) and nothing
# else needed booting the installer itself as a container.
packages.${system} = {
iso = installerTargets.installer.config.system.build.isoImage; iso = installerTargets.installer.config.system.build.isoImage;
lxc = installerTargets.proxmox-lxc.config.system.build.tarball;
pxe = pkgs.linkFarm "pxe" [ pxe = pkgs.linkFarm "pxe" [
{ name = "netboot.ipxe"; path = netbootSystem.config.system.build.netbootIpxeScript; } { name = "netboot.ipxe"; path = netbootSystem.config.system.build.netbootIpxeScript; }
{ name = "initrd"; path = netbootSystem.config.system.build.netbootRamdisk; } { name = "initrd"; path = netbootSystem.config.system.build.netbootRamdisk; }
{ name = "kernel"; path = netbootSystem.config.system.build.kernel; } { name = "kernel"; path = netbootSystem.config.system.build.kernel; }
]; ];
# Vanilla NixOS minimal netboot bundle — see netbootMinimalSystem all = pkgs.linkFarm "all" [
# above. Staged onto the pxe-boot host alongside packages.pxe by { name = "iso"; path = iso; }
# modules/pxe-boot/stage-installer-artifacts.nix. { name = "lxc"; path = lxc; }
pxe-minimal = pkgs.linkFarm "pxe-minimal" [ { name = "pxe"; path = pxe; }
{ name = "netboot.ipxe"; path = netbootMinimalSystem.config.system.build.netbootIpxeScript; }
{ name = "initrd"; path = netbootMinimalSystem.config.system.build.netbootRamdisk; }
{ name = "kernel"; path = netbootMinimalSystem.config.system.build.kernel; }
]; ];
}; };
}; };
+4 -8
View File
@@ -19,15 +19,11 @@
nextcloud-client nextcloud-client
# vscode # vscode
chromium chromium
claude-code
fish
sops
]; ];
# Optional: set environment vars # Optional: set environment vars
sessionVariables = { sessionVariables = {
EDITOR = "vim"; EDITOR = "vim";
SOPS_AGE_KEY_FILE = "~/.config/sops/age/keys.txt";
}; };
file = { file = {
@@ -35,7 +31,7 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Proxmox (Chromium) Name=Proxmox (Chromium)
Exec=chromium --app=https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --window-size=1920,1080 --window-position=0,0 Exec=chromium --app=https://pve.${vars.homeDomain}:${toString vars.pveWebPort} --window-size=1920,1080 --window-position=0,0
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false Terminal=false
Categories=Hypervisor; Categories=Hypervisor;
@@ -45,7 +41,7 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Proxmox Backup Server (Chromium) Name=Proxmox Backup Server (Chromium)
Exec=chromium --app=https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --window-size=1920,1080 --window-position=0,0 Exec=chromium --app=https://${vars.pbsIp}:${toString vars.pbsWebPort} --window-size=1920,1080 --window-position=0,0
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false Terminal=false
Categories=backup; Categories=backup;
@@ -55,7 +51,7 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Proxmox (Firefox) Name=Proxmox (Firefox)
Exec=firefox --new-instance https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --profile ProxmoxWebApp --window-size=1920,1080 --class ProxmoxWebApp Exec=firefox --new-instance https://pve.${vars.homeDomain}:${toString vars.pveWebPort} --profile ProxmoxWebApp --window-size=1920,1080 --class ProxmoxWebApp
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false Terminal=false
Categories=Hypervisor; Categories=Hypervisor;
@@ -65,7 +61,7 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=Proxmox Backup Server (Firefox) Name=Proxmox Backup Server (Firefox)
Exec=firefox --new-window https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --profile PbsWebApp --window-size=1920,1080 --class PbsWebApp Exec=firefox --new-window https://${vars.pbsIp}:${toString vars.pbsWebPort} --profile PbsWebApp --window-size=1920,1080 --class PbsWebApp
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false Terminal=false
Categories=backup; Categories=backup;
-9
View File
@@ -1,17 +1,8 @@
_: _:
{ {
imports = [
../../modules/networking/wifi.nix
];
networking.hostName = "nixos"; networking.hostName = "nixos";
# Only needed now that baremetal-gui exists (ZFS root) -- harmless on the
# ext4-rooted linode/proxmox/lxc-gui variants, so set unconditionally
# rather than only on the baremetal platform.
networking.hostId = "de6a9ffc";
# Preserved from the pre-refactor `nixos` target — stateVersion must never # Preserved from the pre-refactor `nixos` target — stateVersion must never
# be bumped on an already-installed machine. # be bumped on an already-installed machine.
system.stateVersion = "25.05"; system.stateVersion = "25.05";
+1 -1
View File
@@ -14,7 +14,7 @@
services.beszel.agent.environment = { services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375"; #DOCKER_HOST = "tcp://docker-socket-proxy:2375";
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG"; KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}"; EXTRA_FILESYSTEMS = "${vars.storageRoot}/docker/volumes";
LOG_LEVEL = "debug"; LOG_LEVEL = "debug";
}; };
-10
View File
@@ -1,10 +0,0 @@
_:
{
networking.hostName = "tailscale-router";
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
# for pool-import safety, and this host does neither.
system.stateVersion = "26.05";
}
-23
View File
@@ -1,23 +0,0 @@
{ ... }:
{
imports = [
(import ../../modules/beszel/host-token.nix {
name = "tor-relay";
sopsFile = ../../secrets/tor-relay.yaml;
})
];
networking.hostName = "tor-relay";
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
# for pool-import safety, and this host does neither.
services.beszel.agent.environment = {
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
};
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
# flake's current nixpkgs release rather than being pinned to an older one.
system.stateVersion = "26.05";
}
+1 -10
View File
@@ -4,15 +4,6 @@
services.beszel.agent.enable = true; services.beszel.agent.enable = true;
services.beszel.agent.environment = { services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375"; #DOCKER_HOST = "tcp://docker-socket-proxy:2375";
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}"; HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.beszelHubPort}";
}; };
# The upstream module runs beszel-agent under DynamicUser with
# ProtectSystem = "strict" and no StateDirectory, so /var/lib/beszel-agent
# (where the agent persists its hub-pairing fingerprint, per
# https://github.com/henrygd/beszel/discussions/1542) isn't writable --
# every restart silently fails to save it and regenerates a fresh one in
# memory, permanently desyncing from whatever the hub has on record after
# the very first successful pairing. Give it real persistent storage.
systemd.services.beszel-agent.serviceConfig.StateDirectory = "beszel-agent";
} }
+5 -10
View File
@@ -28,18 +28,13 @@
boot.supportedFilesystems = [ "nfs" ]; boot.supportedFilesystems = [ "nfs" ];
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
"L+ /home/${vars.primaryUser}/docker - - - - ${vars.nfsShares.dockerConfig.mountpoint}" "L+ /home/nixos/docker - - - - /mnt/docker/config"
"d /mnt/docker 0755 ${vars.primaryUser} users -" "d /mnt/docker 0755 nixos users -"
"d ${vars.nfsShares.raspiVolumes.mountpoint} 0755 ${vars.primaryUser} users -" "d /mnt/raspi-backup 0755 nixos users -"
]; ];
users.users.${vars.primaryUser}.extraGroups = [ "docker" ]; users.users.nixos.extraGroups = [ "docker" ];
services.openssh.settings.PermitRootLogin = "yes"; services.openssh.settings.PermitRootLogin = "yes";
networking.firewall.allowedTCPPorts = [ networking.firewall.allowedTCPPorts = [ 80 8080 443 vars.beszelHubPort ];
vars.ports.dockerHttp
vars.ports.dockerExtra
vars.ports.dockerHttps
vars.ports.beszelHub
];
} }
+3 -3
View File
@@ -1,4 +1,4 @@
{ config, pkgs, lib, inputs, vars, ... }: { config, pkgs, lib, inputs, ... }:
{ {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
@@ -18,7 +18,7 @@
]; ];
boot.loader.grub.useOSProber = true; boot.loader.grub.useOSProber = true;
programs.direnv.enable = true;
services = { services = {
xserver = { xserver = {
enable = true; enable = true;
@@ -65,7 +65,7 @@ programs.direnv.enable = true;
# The networkmanager group only exists when NM is actually enabled — the # The networkmanager group only exists when NM is actually enabled — the
# lxc platform module force-disables it, so don't add the user to a group # lxc platform module force-disables it, so don't add the user to a group
# that won't exist there. # that won't exist there.
users.users.${vars.primaryUser}.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ]; users.users.nixos.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
programs.firefox.enable = true; programs.firefox.enable = true;
+2 -2
View File
@@ -1,4 +1,4 @@
{ lib, pkgs, config, vars, ... }: { lib, pkgs, config, ... }:
{ {
networking.networkmanager.enable = true; networking.networkmanager.enable = true;
@@ -6,7 +6,7 @@
# The networkmanager group only exists when NM is actually enabled — the # The networkmanager group only exists when NM is actually enabled — the
# lxc platform module force-disables it, so don't add the user to a group # lxc platform module force-disables it, so don't add the user to a group
# that won't exist there. # that won't exist there.
users.users.${vars.primaryUser}.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ]; users.users.nixos.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
inetutils inetutils
+6 -11
View File
@@ -68,19 +68,15 @@ let
set base ${pxeBaseUrl} set base ${pxeBaseUrl}
menu PXE Boot Menu menu PXE Boot Menu
item auto-installer NixOS Auto-Installer item nixos NixOS Installer
item nixos-minimal NixOS Minimal
item rescue Rescue Environment item rescue Rescue Environment
item shell iPXE Shell item shell iPXE Shell
item reboot Reboot item reboot Reboot
choose target && goto ''${target} choose target && goto ''${target}
:auto-installer :nixos
chain ''${base}/auto-installer/netboot.ipxe chain ''${base}/nixos/netboot.ipxe
:nixos-minimal
chain ''${base}/nixos-minimal/netboot.ipxe
:rescue :rescue
chain ''${base}/systemrescue.ipxe chain ''${base}/systemrescue.ipxe
@@ -133,8 +129,7 @@ in
"d ${pxeRoot} 0755 root root -" "d ${pxeRoot} 0755 root root -"
"d ${httpRoot} 0755 root root -" "d ${httpRoot} 0755 root root -"
"d ${httpRoot}/images 0755 root root -" "d ${httpRoot}/images 0755 root root -"
"d ${httpRoot}/auto-installer 0755 root root -" "d ${httpRoot}/nixos 0755 root root -"
"d ${httpRoot}/nixos-minimal 0755 root root -"
"d ${httpRoot}/systemrescue 0755 root root -" "d ${httpRoot}/systemrescue 0755 root root -"
"d ${httpRoot}/ubuntu 0755 root root -" "d ${httpRoot}/ubuntu 0755 root root -"
"d ${httpRoot}/rescue 0755 root root -" "d ${httpRoot}/rescue 0755 root root -"
@@ -160,6 +155,6 @@ in
}; };
}; };
networking.firewall.allowedTCPPorts = [ vars.ports.pxeBootHttp ]; networking.firewall.allowedTCPPorts = [ 80 ];
networking.firewall.allowedUDPPorts = [ vars.ports.pxeBootTftp ]; networking.firewall.allowedUDPPorts = [ 69 ];
} }
+8 -70
View File
@@ -1,74 +1,12 @@
{ vars, lib, pkgs, ... }: { vars, lib, ... }:
let
poolName = lib.removePrefix "/" vars.storageRoot;
# For each NFS share subpath, generate every ancestor path so ZFS datasets
# are created parent-first. e.g. "docker/config" → ["docker" "docker/config"]
ancestors = path:
let parts = lib.splitString "/" path;
in lib.imap1 (i: _: lib.concatStringsSep "/" (lib.take i parts)) parts;
poolDatasets = lib.unique (
lib.concatMap (share: ancestors share.subpath) (lib.attrValues vars.nfsShares)
);
in
{ {
imports = [ imports = [
../beszel/enable-agent.nix ../beszel/enable-agent.nix
../services/zfs/enable-service.nix ../services/zfs/enable-service.nix
]; ];
boot.zfs.extraPools = [ poolName ]; boot.zfs.extraPools = [ (lib.removePrefix "/" vars.storageRoot) ];
# On a fresh image deploy the data disk (scsi1) starts blank — no pool
# exists yet, so zfs-import-tank.service would spin for 60 s and fail.
# This service runs first: if the pool is already present it exits instantly;
# otherwise it creates it (with all required datasets) so the standard
# import service finds it ready on the very first boot.
systemd.services."zfs-init-${poolName}" = {
description = "Initialize '${poolName}' ZFS pool on first boot if not present";
wantedBy = [ "zfs-import-${poolName}.service" ];
before = [ "zfs-import-${poolName}.service" ];
after = [ "systemd-udev-settle.service" ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [ pkgs.zfs_unstable ];
script = ''
# Already imported — nothing to do.
if zpool list "${poolName}" >/dev/null 2>&1; then
exit 0
fi
# Pool exists on a device but not yet imported — let the standard
# zfs-import-${poolName}.service handle it normally.
if zpool import -d /dev/disk/by-id -N "${poolName}" 2>/dev/null; then
exit 0
fi
# No pool found at all. Create it on the Proxmox data disk (scsi1),
# which appears as /dev/disk/by-id/scsi-*drive-scsi1 inside the VM.
DATA_DISK=""
for candidate in /dev/disk/by-id/scsi-*drive-scsi1; do
[[ "$candidate" == *-part* ]] && continue
[ -b "$candidate" ] && DATA_DISK="$candidate" && break
done
if [ -z "$DATA_DISK" ]; then
echo "zfs-init-${poolName}: no data disk found (expected /dev/disk/by-id/scsi-*drive-scsi1)" >&2
exit 1
fi
echo "zfs-init-${poolName}: creating pool on $DATA_DISK"
zpool create -f "${poolName}" "$DATA_DISK"
${lib.concatMapStrings (ds: ''
zfs create "${poolName}/${ds}"
'') poolDatasets}
'';
};
systemd.services.nfs-server = { systemd.services.nfs-server = {
after = [ "zfs-mount.service" ]; after = [ "zfs-mount.service" ];
@@ -78,13 +16,13 @@ in
services.nfs.server = { services.nfs.server = {
enable = true; enable = true;
exports = '' exports = ''
${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash) ${vars.storageRoot}/docker/config ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash) ${vars.storageRoot}/docker/volumes ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash) ${vars.storageRoot}/docker/databases ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash) ${vars.storageRoot}/docker/nextcloud-data ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash) ${vars.storageRoot}/raspi/volumes ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
''; '';
}; };
networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd ]; networking.firewall.allowedTCPPorts = [ 111 2049 ];
} }
-19
View File
@@ -1,19 +0,0 @@
{ ... }:
{
imports = [
../tailscale/subnet-router.nix
];
# "server", not "both": this build type advertises LAN subnet routes but
# doesn't use another tailscale exit node itself, so it doesn't need the
# "client"-side loose reverse-path filtering that "both" would also enable.
# Deliberately kept explicit here (not just relying on subnet-router.nix's
# own setting) so the intent is clear at the build-type level.
services.tailscale.useRoutingFeatures = "server";
# Forwarded subnet-router traffic arrives on tailscale0 already
# tailscale-authenticated -- the firewall's normal per-port allow-list
# would otherwise drop it. Standard NixOS/Tailscale subnet-router guidance.
networking.firewall.trustedInterfaces = [ "tailscale0" ];
}
-8
View File
@@ -1,8 +0,0 @@
{ ... }:
{
imports = [
../tor/enable-relay.nix
../beszel/enable-agent.nix
];
}
-21
View File
@@ -17,26 +17,6 @@ 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 = {
@@ -45,6 +25,5 @@ in
"Switch-nix" = mySwitchCmd; "Switch-nix" = mySwitchCmd;
"Test-nix" = myTestCmd; "Test-nix" = myTestCmd;
}; };
initExtra = buildImageFn;
}; };
} }
-2
View File
@@ -31,7 +31,6 @@
btop btop
git git
gcr gcr
jq
]; ];
# Secrets shared by every host, decrypted at activation via each host's # Secrets shared by every host, decrypted at activation via each host's
@@ -77,7 +76,6 @@
openssh.authorizedKeys.keys = [ openssh.authorizedKeys.keys = [
vars.adminSshKey vars.adminSshKey
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGygkCljN6uKpdJbHTOQtn8ZnH+wKXDLAwrDFbLrE/65 nixos@nixos"
]; ];
}; };
-87
View File
@@ -1,87 +0,0 @@
{ vars, ... }:
{
# ZFS RAID0 (striped, no redundancy) root pool for the bare-metal gui
# host — two disks, each contributing its own top-level vdev. disko's
# zpool `mode` defaults to "" (plain stripe) when left unset, which is
# what gives RAID0 semantics here rather than mirror/raidz.
#
# Device paths are placeholders until the real hardware profile lands —
# fill in vars.guiRootDisk1/guiRootDisk2 (stable /dev/disk/by-id/...
# paths, not /dev/sdX) before running disko against real hardware. Swap
# is deliberately left out for now — sizing that sensibly needs the
# box's actual RAM size, which comes with the hardware profile too.
#
# Not yet imported anywhere: this awaits the new bare-metal platform
# module (alongside modules/boot/efi.nix for systemd-boot, matching
# modules/platforms/proxmox.nix's pattern) once the hardware config is
# in hand.
disko.devices = {
disk = {
disk1 = {
type = "disk";
device = vars.guiRootDisk1;
content = {
type = "gpt";
partitions = {
esp = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
zfs = {
size = "100%";
content = {
type = "zfs";
pool = "rpool";
};
};
};
};
};
disk2 = {
type = "disk";
device = vars.guiRootDisk2;
content = {
type = "gpt";
partitions = {
zfs = {
size = "100%";
content = {
type = "zfs";
pool = "rpool";
};
};
};
};
};
};
zpool.rpool = {
type = "zpool";
rootFsOptions = {
compression = "zstd";
"com.sun:auto-snapshot" = "false";
};
mountpoint = "/";
options.ashift = "12";
};
};
}
-35
View File
@@ -1,35 +0,0 @@
_:
{
# Linode provisions and sizes these disks itself (via the Linode
# dashboard/API) before the OS ever boots, and presents them as whole,
# unpartitioned block devices — /dev/sda is the root filesystem directly,
# /dev/sdb is swap directly, no partition table on either. Nothing here
# should ever repartition or resize them:
# - `destroy = false` skips each disk entirely during disko's destroy
# stage (see disko's disk.destroy option) — no wipefs, ever.
# - the filesystem content type's own create step only runs mkfs if the
# device isn't already formatted (checked via `blkid`), so re-running
# this against an already-provisioned Linode disk is a no-op.
disko.devices.disk = {
main = {
device = "/dev/sda";
destroy = false;
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
swap = {
device = "/dev/sdb";
destroy = false;
content = {
type = "swap";
};
};
};
}
+1 -11
View File
@@ -1,4 +1,4 @@
{ config, vars, ... }: _:
{ {
disko.devices = { disko.devices = {
@@ -6,16 +6,6 @@
type = "disk"; type = "disk";
device = "/dev/sda"; device = "/dev/sda";
# Only used when building a standalone disk image directly (`nix build
# .#nixosConfigurations.<host>.config.system.build.diskoImagesScript`)
# rather than formatting a real device — see docs/proxmox-images.md.
# imageSize sets the .raw file's total size (root's "100%" below fills
# whatever's left after ESP + swap within it); imageName keeps each
# host's image distinctly named instead of every proxmox-* host
# producing an identical "main.raw".
imageSize = vars.proxmoxImageSize;
imageName = config.networking.hostName;
content = { content = {
type = "gpt"; type = "gpt";
+20 -39
View File
@@ -1,84 +1,65 @@
{ 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" ];
# A bare hostname here never resolves reliably: systemd-resolved only
# ever tries LLMNR for single-label names (never DNS, regardless of any
# configured search domain), and a *global* search domain (the first fix
# attempted here) backfires worse -- confirmed live on lxc-docker, adding
# `networking.search` made systemd-resolved prioritize its domain-matched
# but server-less global scope over eth0's correctly-configured one for
# every "*.sweet.home" query, silently sending them to public fallback
# DNS instead. `resolvectl query --interface=eth0 server.sweet.home`
# resolved fine throughout, proving the LAN DNS server was never the
# problem -- only the ambient, unqualified device string was. Using the
# FQDN directly sidesteps all of that, matching the pattern
# ../raspi/mount-data.nix already uses for the same reason.
nfsServer = "${vars.nfsServerHost}.${vars.homeDomain}";
in
{ {
fileSystems = { fileSystems = {
${vars.nfsShares.dockerConfig.mountpoint} = { "/mnt/docker/config" = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath}"; device = "${vars.nfsServerHost}:${vars.storageRoot}/docker/config";
fsType = "nfs"; fsType = "nfs";
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
] ++ automountOpts; ];
}; };
${vars.nfsShares.dockerDatabases.mountpoint} = { "/mnt/docker/databases" = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}"; device = "${vars.nfsServerHost}:${vars.storageRoot}/docker/databases";
fsType = "nfs"; fsType = "nfs";
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
] ++ automountOpts; ];
}; };
${vars.nfsShares.dockerVolumes.mountpoint} = { "/mnt/docker/volumes" = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}"; device = "${vars.nfsServerHost}:${vars.storageRoot}/docker/volumes";
fsType = "nfs"; fsType = "nfs";
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
] ++ automountOpts; ];
}; };
${vars.nfsShares.nextcloudData.mountpoint} = { "/mnt/nextcloud-data" = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}"; device = "${vars.nfsServerHost}:${vars.storageRoot}/docker/nextcloud-data";
fsType = "nfs"; fsType = "nfs";
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
] ++ automountOpts; ];
}; };
${vars.nfsShares.raspiVolumes.mountpoint} = { "/mnt/raspi-backup" = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}"; device = "${vars.nfsServerHost}:${vars.storageRoot}/raspi/volumes";
fsType = "nfs"; fsType = "nfs";
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
] ++ automountOpts; ];
}; };
}; };
} }
@@ -1,23 +0,0 @@
# Adapted from the output of `nixos-generate-config`, run from a live GUI
# ISO boot on the actual gui-host hardware (AMD CPU). fileSystems and
# swapDevices are deliberately omitted -- the live ISO had no formatted
# disks to detect, and disko (modules/disko/baremetal.nix) generates both
# from the declarative zpool layout anyway.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
];
boot = {
initrd.availableKernelModules = [ "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ];
initrd.kernelModules = [ ];
kernelModules = [ "kvm-amd" ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
+8 -5
View File
@@ -33,11 +33,14 @@
}; };
}; };
# fileSystems."/" and swapDevices are now owned by disko fileSystems."/" =
# (../disko/linode.nix, imported from ../platforms/linode.nix) — same {
# /dev/sda root + /dev/sdb swap layout, declared there instead so disko's device = "/dev/sda";
# (idempotent, non-destructive — see that file) format/mount scripts stay fsType = "ext4";
# in sync with what NixOS actually mounts. };
swapDevices =
[{ device = "/dev/sdb"; }];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
} }
+145 -15
View File
@@ -45,21 +45,151 @@
disko disko
]; ];
# Auto-install script, kept as a real, version-controlled shell file at # Write auto-install script to /root
# scripts/installer/auto-install.sh rather than an inline Nix string. etc."auto-install.sh" = {
# It sources scripts/env.sh itself (for LAN_DOMAIN, same as every other text = ''
# script in this repo) rather than relying on Nix-level templating, so #!/run/current-system/sw/bin/bash
# it behaves identically whether it's run straight from a git checkout set -eux
# or from here -- baking scripts/env.sh in alongside it at a matching
# relative path (installer/auto-install.sh -> ../env.sh) is what makes
# that resolve correctly in both places.
etc = {
"nixos-installer/env.sh".source = ../../scripts/env.sh;
"nixos-installer/installer/auto-install.sh" = { set -euo pipefail
source = ../../scripts/installer/auto-install.sh;
mode = "0755"; export FLAKE_BASE_URL="git+https://${vars.lanDomain}/beatzaplenty/nixos.git"
};
echo "Fetching available NixOS hosts from flake..."
# Two categories deliberately excluded from the menu:
# lxc-* — these build a config.system.build.tarball
# meant for `pct restore` on Proxmox
# directly, not an install. Running
# nixos-install against one here would
# bind-mount / onto /mnt and then refuse to
# touch the filesystem it's currently
# running on — see docs/auto-installer.md.
# installer/proxmox-lxc — these *are* the installer image's own
# flake targets, not deployable hosts;
# "installing" one means nixos-install-ing
# a copy of the installer into itself.
mapfile -t options < <(
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
"''${FLAKE_BASE_URL}#nixosConfigurations" \
--apply builtins.attrNames \
| jq -r '.[]
| select(startswith("lxc-") | not)
| select(. != "installer")
| select(. != "proxmox-lxc")'
)
if [[ ''${#options[@]} -eq 0 ]]; then
echo "ERROR: No NixOS hosts found in ''${FLAKE_BASE_URL}#nixosConfigurations" >&2
exit 1
fi
echo "Note: lxc-* targets aren't installed this way — build them with"
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
echo "Choose the flake profile to install:"
select choice in "''${options[@]}"; do
if [[ -n "$choice" ]]; then
echo "You selected: $choice"
break
else
echo "Invalid selection. Try again."
fi
done
echo "Starting install with flake: ''${FLAKE_BASE_URL}#''${choice}"
# Optional: confirm before proceeding
read -rp "Proceed with installation? (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
# A nix-cache host is *the* substituter/remote-builder for every other
# host once installed (its own config explicitly excludes itself from
# using either — see buildType != "nix-cache" in the nixos flake.nix).
# Installing one shouldn't depend on a nix-cache substituter either,
# for the same reason — plus in practice "nix-cache" only resolves over
# Tailscale, which a fresh installer environment was never connected to
# anyway, so it's dead weight even for non-nix-cache installs until
# that's sorted out. Override it away here specifically for nix-cache
# targets to keep install-time behaviour consistent with run-time.
nix_extra_opts=()
if [[ "''${choice}" == *-nix-cache ]]; then
echo "Installing a nix-cache host — skipping the nix-cache substituter."
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
fi
if nix eval --refresh --json --extra-experimental-features "flakes nix-command" "''${nix_extra_opts[@]}" "''${FLAKE_BASE_URL}#nixosConfigurations.''${choice}.config.disko.devices.disk.main.device" >/dev/null 2>&1; then
disko --mode destroy,format,mount \
--flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes
else
# No raw disk to partition — true for every LXC container, which has
# no block device visible from inside it, only its already-mounted
# root filesystem. nixos-install defaults to installing into /mnt; if
# nothing points /mnt at the real root, the new system gets built and
# written to a disconnected empty directory, never actually becomes
# bootable, and the container silently keeps running this installer
# environment forever. Bind-mount / onto /mnt so the install actually
# lands on the filesystem this container boots from.
echo "Selected host has no Disko configuration — installing in place via bind mount."
mount --bind / /mnt
fi
# sops-nix derives this host's decryption key from its own SSH host key
# at *activation* time, which runs before systemd would otherwise
# generate one on first boot. Without pre-seeding it here, secrets
# (including the login password) fail to decrypt on first boot.
# Generate the key with scripts/prepare-host-key.sh first.
#
# Two places a key can come from, checked in order:
# /etc/host-keys — baked into this image at build time (see
# modules/installer/host-keys.nix; only present
# if built with NIXOS_HOST_KEYS_DIR set)
# /root/host-keys — scp'd in manually after boot (older fallback,
# still supported for images built without keys)
mkdir -p /root/host-keys
if [[ -f "/etc/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
echo "Found baked-in SSH host key for ''${choice}, installing to target..."
install -D -m 0600 "/etc/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
install -D -m 0644 "/etc/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
elif [[ -f "/root/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
echo "Found pre-seeded SSH host key for ''${choice}, installing to target..."
install -D -m 0600 "/root/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
install -D -m 0644 "/root/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
else
echo "WARNING: no SSH host key found for ''${choice} (checked /etc/host-keys and /root/host-keys)"
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
echo "Run scripts/prepare-host-key.sh for host ''${choice} on your admin workstation first,"
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, or scp the result to"
echo "/root/host-keys/ on this machine."
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
mkdir -p /mnt/install-tmp
export TMPDIR=/mnt/install-tmp
nixos-install \
--flake "''${FLAKE_BASE_URL}#''${choice}" \
"''${nix_extra_opts[@]}" \
--no-root-password
rm -rf /mnt/install-tmp
# Redundant copy of the host's private key — the real one is now at
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
# up on its own since it was written imperatively, not declaratively.
rm -rf /root/host-keys
sleep 10
reboot
'';
mode = "0755";
}; };
}; };
@@ -73,7 +203,7 @@
# file-copying/chown. # file-copying/chown.
programs.bash.loginShellInit = '' programs.bash.loginShellInit = ''
if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then
sudo /etc/nixos-installer/installer/auto-install.sh sudo /etc/auto-install.sh
touch "$HOME/.auto_install_ran" touch "$HOME/.auto_install_ran"
fi fi
''; '';
+12
View File
@@ -0,0 +1,12 @@
{ modulesPath, ... }:
{
imports = [
./common.nix
(modulesPath + "/virtualisation/proxmox-lxc.nix")
];
# LXC has no bootloader
boot.loader.grub.enable = false;
boot.loader.systemd-boot.enable = false;
}
-43
View File
@@ -1,43 +0,0 @@
{ config, lib, vars, ... }:
{
# Prestages a NetworkManager connection profile for vars.wifiSsid so the
# host associates on first boot with no manual nmtui/nmcli step. Guarded
# on a non-empty SSID so leaving the placeholder blank in variables.nix
# is a no-op rather than an empty, broken profile — fill it in once the
# network is known.
#
# The password itself lives in secrets/gui.yaml, not variables.nix --
# NetworkManager's ensureProfiles renders `psk = "$WIFI_PASSWORD"`
# literally into the store (see nixpkgs' own ensureProfiles example,
# which does the same for exactly this reason) and its systemd service
# envsubst-expands it from environmentFiles at activation time, so the
# real value only ever touches /run (root-only, UMask 0177), never the
# Nix store.
sops.secrets."wifi-password" = lib.mkIf (vars.wifiSsid != "") {
sopsFile = ../../secrets/gui.yaml;
};
sops.templates."wifi-password.env" = lib.mkIf (vars.wifiSsid != "") {
content = "WIFI_PASSWORD=${config.sops.placeholder."wifi-password"}";
};
networking.networkmanager.ensureProfiles = lib.mkIf (vars.wifiSsid != "") {
environmentFiles = [ config.sops.templates."wifi-password.env".path ];
profiles.${vars.wifiSsid} = {
connection = {
id = vars.wifiSsid;
type = "wifi";
};
wifi = {
mode = "infrastructure";
ssid = vars.wifiSsid;
};
wifi-security = {
key-mgmt = "wpa-psk";
psk = "$WIFI_PASSWORD";
};
};
};
}
+1 -1
View File
@@ -3,7 +3,7 @@
{ {
nix.settings = { nix.settings = {
substituters = [ substituters = [
"http://${vars.nixCacheHost}.${vars.homeDomain}" "http://${vars.nixCacheHost}"
"https://cache.nixos.org/" "https://cache.nixos.org/"
]; ];
trusted-public-keys = [ trusted-public-keys = [
+6 -18
View File
@@ -1,30 +1,18 @@
{ pkgs, vars, ... }: { pkgs, vars, ... }:
{ {
# Authenticate as nixremote using the client host's own default root SSH # Install the remote builder key on each client host (do not commit private keys):
# identity (/root/.ssh/id_ed25519) rather than a separately-named key -- # sudo install -d -m 0700 /root/.ssh
# matches vars.remoteBuilderAuthorizedKeys, which already authorizes # sudo install -m 0600 ./nixremote /root/.ssh/nixremote
# each host's own default key (one entry per host, not a shared # sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
# dedicated keypair). If this host doesn't have one yet:
# sudo -u root ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
# # then add its .pub to vars.remoteBuilderAuthorizedKeys and rebuild nix-cache
# sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache.sweet.home 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}.${vars.homeDomain}" = {
hostNames = [ "${vars.nixCacheHost}.${vars.homeDomain}" ];
publicKey = vars.nixCacheHostKey;
};
nix = { nix = {
distributedBuilds = true; distributedBuilds = true;
buildMachines = [ buildMachines = [
{ {
hostName = "${vars.nixCacheHost}.${vars.homeDomain}"; hostName = vars.nixCacheHost;
sshUser = vars.remoteBuilderUser; sshUser = vars.remoteBuilderUser;
sshKey = "/root/.ssh/id_ed25519"; sshKey = "/root/.ssh/${vars.remoteBuilderUser}";
inherit (pkgs.stdenv.hostPlatform) system; inherit (pkgs.stdenv.hostPlatform) system;
maxJobs = 4; maxJobs = 4;
speedFactor = 2; speedFactor = 2;
+12 -14
View File
@@ -1,26 +1,24 @@
{ config, pkgs, vars, ... }: { config, pkgs, vars, ... }:
{ {
# nix-serve's signing key has to be the *same* key on every host that # Generate the binary cache key pair on the nix-cache host:
# ever plays the nix-cache role -- modules/nix-cache/client.nix hardcodes # sudo install -d -m 0700 /etc/nix
# every client's trust in one specific public key ("cache.local-1:..."), # sudo nix-store --generate-binary-cache-key nix-cache-1 \
# so a freshly self-generated key here wouldn't be trusted by anyone. # /etc/nix/cache-priv.pem \
# Managed via sops-nix like every other secret in this repo instead of # /etc/nix/cache-pub.pem
# the old manual `nix-store --generate-binary-cache-key` step -- see # sudo chmod 0600 /etc/nix/cache-priv.pem
# "Binary cache signing key" in docs/nix-cache.md for how to add/rotate # sudo chmod 0644 /etc/nix/cache-pub.pem
# the value in secrets/nix-cache.yaml. # cat /etc/nix/cache-pub.pem
sops.secrets."cache-priv-key".sopsFile = ../../secrets/nix-cache.yaml;
services = { services = {
nix-serve = { nix-serve = {
enable = true; enable = true;
secretKeyFile = config.sops.secrets."cache-priv-key".path; secretKeyFile = "/etc/nix/cache-priv.pem";
}; };
nginx = { nginx = {
enable = true; enable = true;
recommendedProxySettings = true; recommendedProxySettings = true;
virtualHosts."${vars.nixCacheHost}.${vars.homeDomain}" = { virtualHosts.${vars.nixCacheHost} = {
locations."/" = { locations."/" = {
proxyPass = "http://${config.services.nix-serve.bindAddress}:${toString config.services.nix-serve.port}"; proxyPass = "http://${config.services.nix-serve.bindAddress}:${toString config.services.nix-serve.port}";
}; };
@@ -30,7 +28,7 @@
openssh.enable = true; openssh.enable = true;
}; };
networking.firewall.allowedTCPPorts = [ vars.ports.nixCacheHttp ]; networking.firewall.allowedTCPPorts = [ 80 ];
users.groups.${vars.remoteBuilderUser} = { }; users.groups.${vars.remoteBuilderUser} = { };
@@ -56,6 +54,6 @@
nix.gc = { nix.gc = {
automatic = true; automatic = true;
dates = "weekly"; dates = "weekly";
options = "--delete-older-than ${vars.nixCacheGcMaxAge}"; options = "--delete-older-than 30d";
}; };
} }
-38
View File
@@ -1,38 +0,0 @@
{ ... }:
{
imports = [
../hardware-configuration/baremetal.nix
../boot/efi.nix
../disko/baremetal.nix
../services/zfs/enable-service.nix
];
# Needed for real wifi/bluetooth/GPU firmware blobs and CPU microcode
# updates (hardware-configuration/baremetal.nix's amd.updateMicrocode
# keys off this) -- irrelevant on the linode/proxmox/lxc platforms,
# which are all VMs with no real hardware to load firmware for.
hardware.enableRedistributableFirmware = true;
# AMD GPU: the amdgpu kernel driver autoloads from the PCI ID with no
# extra boot.kernelModules entry needed; this is the userspace half --
# the dedicated Xorg driver (not just the generic modesetting fallback)
# plus Mesa OpenGL/Vulkan (amdgpu/RADV), same firmware blobs as above.
# 32-bit support is for compatibility with 32-bit apps/games.
services.xserver.videoDrivers = [ "amdgpu" ];
hardware.graphics = {
enable = true;
enable32Bit = true;
};
# The systemd-based initrd (default here since this host has a ZFS root --
# see modules/disko/baremetal.nix) locks the root account by default, so
# sulogin refuses to hand over a shell if something in the initrd (e.g.
# the ZFS pool import) fails and it drops to emergency mode -- confirmed
# live: it just loops re-entering the target instead of prompting. This
# only affects the pre-switch-root initrd shell, not the installed
# system's own login, and is worth the tradeoff on a box already reachable
# at the physical console.
boot.initrd.systemd.emergencyAccess = true;
}
-1
View File
@@ -3,7 +3,6 @@
{ {
imports = [ imports = [
../hardware-configuration/vm/linode.nix ../hardware-configuration/vm/linode.nix
../disko/linode.nix
]; ];
networking = { networking = {
+3 -167
View File
@@ -1,44 +1,5 @@
{ config, lib, modulesPath, flakeTarget, ... }: { lib, modulesPath, ... }:
let
# Bakes this exact flake target's pre-generated SSH host key straight
# into /etc/ssh/ -- mirrors modules/installer/host-keys.nix's
# builtins.getEnv pattern (impure and empty under normal `nix
# build`/`nix eval`, so this is a no-op unless explicitly opted into
# with NIXOS_HOST_KEYS_DIR=... --impure), but places the key directly
# rather than staging it under /etc/host-keys/ for a later manual copy
# -- this is the whole system for a `lxc-*` host, built straight to a
# pct-restorable tarball with no install step, so there's no later copy
# step to stage for.
#
# Without this, config.system.build.tarball's built-in system just
# generates a fresh host key at first boot like any other host would --
# but sops-nix derives its decryption key from *this* file, and
# .sops.yaml only trusts whatever key scripts/secrets/sync-host-keys.sh already
# registered for this exact target name. A freshly-generated key can
# never match that, so every secret (including this host's own login)
# permanently fails to decrypt. Confirmed live: sops-install-secrets
# errored with "Error getting data key: 0 successful groups required,
# got 0" -- the container's actual host key's age fingerprint didn't
# match the one registered in .sops.yaml at all.
hostKeysDirStr = builtins.getEnv "NIXOS_HOST_KEYS_DIR";
hasHostKeysDir = hostKeysDirStr != "" && builtins.pathExists hostKeysDirStr;
hostKeysDir = /. + hostKeysDirStr;
# flakeTarget ("${platform}-${buildType}") comes in via specialArgs from
# flake.nix's mkTarget -- exactly the name scripts/secrets/sync-host-keys.sh
# registers keys under. Deliberately not read back from
# config.environment.etc."flake-target" (which is set to the same value)
# -- this module also *contributes* to environment.etc below, and a
# module reading the merged value of an option it's still defining is a
# circular dependency (confirmed: "infinite recursion encountered").
privKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key";
pubKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key.pub";
hasKeyForThisTarget =
hasHostKeysDir
&& builtins.pathExists privKeyFile
&& builtins.pathExists pubKeyFile;
in
{ {
# LXC containers share the host kernel — Proxmox starts them by exec'ing # LXC containers share the host kernel — Proxmox starts them by exec'ing
# /sbin/init directly, no bootloader/initrd involved — and Proxmox has its # /sbin/init directly, no bootloader/initrd involved — and Proxmox has its
@@ -58,28 +19,8 @@ in
# host.nix declares each host's real hostname (networking.hostName); # host.nix declares each host's real hostname (networking.hostName);
# keep that instead of letting Proxmox's ambient container config win. # keep that instead of letting Proxmox's ambient container config win.
manageHostName = true; manageHostName = true;
# Unprivileged by default -- matches how these containers are actually # Unprivileged matches how these containers are actually created.
# created (scripts/proxmox/create-proxmox-resource.sh reads this value privileged = false;
# back to decide `pct create`'s --unprivileged flag, so the two stay
# in sync).
#
# lxc-docker is the one exception: the kernel's NFS client doesn't set
# FS_USERNS_MOUNT, so mounting NFS from inside *any* non-init user
# namespace -- which is exactly what an unprivileged container's
# UID-mapped root runs in -- is rejected at the VFS layer with EPERM,
# no matter what Proxmox's own `mount=nfs;nfs4` container feature
# allows at the AppArmor layer (confirmed live: TCP to the NFS server
# succeeds, the server's export table matches the container's IP, and
# `mount.nfs: Operation not permitted` still fires immediately with no
# corresponding denial anywhere in the server's logs -- a kernel-level
# rejection, not a network or export-permission one). Keying off
# hostName rather than something docker-build-type-specific because
# modules/build-types/docker.nix is also composed for linode-docker/
# proxmox-docker, which don't import proxmox-lxc.nix at all --setting
# this option there would break their eval with "option does not
# exist" regardless of any mkIf guard, since mkIf only makes a value
# conditional, not whether the option needs to exist somewhere.
privileged = config.networking.hostName == "docker";
}; };
boot.loader = { boot.loader = {
@@ -94,109 +35,4 @@ in
# for the same reason; it just doesn't disable NetworkManager itself, # for the same reason; it just doesn't disable NetworkManager itself,
# which modules/common/configuration.nix enables for every host. # which modules/common/configuration.nix enables for every host.
networking.networkmanager.enable = lib.mkForce false; networking.networkmanager.enable = lib.mkForce false;
environment.etc = lib.mkIf hasKeyForThisTarget {
"ssh/ssh_host_ed25519_key" = {
source = privKeyFile;
mode = "0600";
};
"ssh/ssh_host_ed25519_key.pub" = {
source = pubKeyFile;
mode = "0644";
};
};
# NixOS's etc activation removes any /etc file that was in the previous
# generation's environment.etc but is absent from the current one — even
# real (non-symlink) copies. On every routine nixos-rebuild switch/test that
# lacks NIXOS_HOST_KEYS_DIR the key is absent from environment.etc, so it
# gets removed as "obsolete". sops-nix derives its age decryption key from
# /etc/ssh/ssh_host_ed25519_key; deletion cascades into every sops secret
# failing with "Error getting data key: 0 successful groups required, got 0".
#
# Fix: two activation scripts that bracket the etc step.
# preserveSshHostKey — no deps, runs before etc — saves the live key to
# /run (tmpfs) before etc can delete it.
# restoreSshHostKey — deps=[etc], runs after etc — reinstalls the key via
# `install` (atomic, sets mode) if etc removed it.
# The resulting file is not registered in environment.etc
# for either the previous or current generation, so
# subsequent rebuilds leave it alone permanently.
system.activationScripts.preserveSshHostKey = ''
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
fi
'';
system.activationScripts.restoreSshHostKey = {
deps = [ "etc" ];
text = ''
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
fi
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
'';
};
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
# store DB via a systemd service (register-nix-paths) -- it never runs
# an activation script at all. Confirmed live this means neither
# sops-nix's "for users" secrets (password hashes -- installed by the
# activation script itself, not a systemd service, since they need to
# exist *before* user creation) nor the user-creation step that
# consumes them ever run on a real lxc-* boot. Regular secrets
# (nix-serve's key, beszel's token, etc.) work anyway because sops-nix
# provides its own systemd service for those.
#
# A systemd service, not boot.postBootCommands: tried that first (it's
# a genuine, generally-invoked hook -- nixos/modules/system/boot/stage-2-init.sh,
# which becomes this container's actual /sbin/init, unconditionally
# runs it) but switch-to-configuration behaves differently that early in
# boot (raw stage-2-init.sh, before systemd itself has even started) --
# confirmed live it silently failed to rewrite /etc/shadow from there
# even in "test" mode, despite the exact same command working reliably
# every time when run post-boot (i.e. as a normal systemd service, which
# is what this is). Not fully root-caused why the early context
# specifically breaks it; a real systemd service sidesteps needing to.
#
# /etc/shadow already has PLACEHOLDER entries for every declared user
# baked in at build time (part of constructing the system closure).
# update-users-groups.pl deliberately never overwrites an *existing*
# shadow entry -- a correct safety property in general (don't clobber a
# real user's real password on a config rebuild) -- but on a genuine
# first boot that only means the real hashedPasswordFile-derived hash
# never gets the chance to be applied either, since the placeholder is
# already "seen". Safe to clear here specifically: there is no real
# password yet to protect on a first boot.
#
# "test" mode, not "boot": confirmed live "boot" mode aborts partway
# through (before rewriting /etc/shadow) on a warning that "/boot" is on
# a different filesystem -- a real check for a host with a bootloader to
# update, meaningless for a container that has none
# (boot.loader.{grub,systemd-boot}.enable are both false above), but it
# still aborts the script. "test" runs every activation step without
# touching boot-loader state at all.
#
# ConditionPathExists (systemd-native, not a bash-level check) means
# this only ever runs once, on the genuine first boot -- systemd itself
# skips even starting it on every later boot once the marker exists.
# switch-to-configuration is otherwise the operator's call per this
# repo's own safety rules, not something to run on every boot.
systemd.services.nixos-lxc-first-boot-activate = {
description = "Complete first-boot NixOS activation (users, secrets) for this LXC container";
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionPathExists = "!/var/lib/nixos-lxc-first-boot-activated";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
rm -f /etc/shadow
/run/current-system/bin/switch-to-configuration test
mkdir -p /var/lib
touch /var/lib/nixos-lxc-first-boot-activated
'';
};
} }
+14 -23
View File
@@ -1,32 +1,23 @@
{ netbootSystem, netbootMinimalSystem, ... }: { netbootSystem, ... }:
let let
# config.system.build.kernel and .netbootRamdisk are directories, not the # config.system.build.kernel and .netbootRamdisk are directories, not the
# files themselves — nixpkgs' own system.build.kexecTree does the same # files themselves — nixpkgs' own system.build.kexecTree does the same
# ${...}/<file> dereference for the same reason. # ${...}/<file> dereference for the same reason.
mkStageRules = { dirName, system }: inherit (netbootSystem.config.system.boot.loader) kernelFile;
let
inherit (system.config.system.boot.loader) kernelFile;
dir = "/srv/pxe/http/${dirName}";
in
[
# Declared here too (not just in build-types/pxe-boot.nix) so this
# module's C+ rules don't depend on cross-module list-merge ordering —
# tmpfiles' C type needs the target directory to already exist.
"d ${dir} 0755 root root -"
"C+ ${dir}/${kernelFile} 0644 root root - ${system.config.system.build.kernel}/${kernelFile}"
"C+ ${dir}/initrd 0644 root root - ${system.config.system.build.netbootRamdisk}/initrd"
"C+ ${dir}/netboot.ipxe 0644 root root - ${system.config.system.build.netbootIpxeScript}/netboot.ipxe"
];
in in
{ {
# Builds this flake's own installer netboot image (the same one # Builds this flake's own installer netboot image (the same one
# `nix build .#pxe` produces) plus the vanilla NixOS minimal netboot image # `nix build .#pxe` produces) and stages it where menu.ipxe's :nixos
# (`nix build .#pxe-minimal`), and stages both where menu.ipxe's # entry expects it, so the pxe-boot host is self-contained — no manual
# :auto-installer / :nixos-minimal entries expect them, so the pxe-boot # operator step to populate /srv/pxe/http/nixos after deploy.
# host is self-contained — no manual operator step to populate systemd.tmpfiles.rules = [
# /srv/pxe/http after deploy. # Declared here too (not just in build-types/pxe-boot.nix) so this
systemd.tmpfiles.rules = # module's C+ rules don't depend on cross-module list-merge ordering —
mkStageRules { dirName = "auto-installer"; system = netbootSystem; } # tmpfiles' C type needs the target directory to already exist.
++ mkStageRules { dirName = "nixos-minimal"; system = netbootMinimalSystem; }; "d /srv/pxe/http/nixos 0755 root root -"
"C+ /srv/pxe/http/nixos/${kernelFile} 0644 root root - ${netbootSystem.config.system.build.kernel}/${kernelFile}"
"C+ /srv/pxe/http/nixos/initrd 0644 root root - ${netbootSystem.config.system.build.netbootRamdisk}/initrd"
"C+ /srv/pxe/http/nixos/netboot.ipxe 0644 root root - ${netbootSystem.config.system.build.netbootIpxeScript}/netboot.ipxe"
];
} }
+6 -12
View File
@@ -1,23 +1,14 @@
{ config, lib, vars, ... }: { vars, ... }:
{ {
fileSystems.${vars.raspiMountpoint} = { fileSystems."/mnt/raspi" = {
device = "${vars.raspberryPiHost}.${vars.tailnetDomain}:${vars.raspiNfsPath}"; device = "raspberrypi.${vars.tailnetDomain}:/home/raspi/raspi";
fsType = "nfs4"; fsType = "nfs4";
options = [ options = [
"nofail" "nofail"
"_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"
@@ -26,6 +17,9 @@
# 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"
]; ];
}; };
+12
View File
@@ -0,0 +1,12 @@
{ vars, ... }:
{
services.tailscale = {
enable = true;
extraUpFlags = [
"--advertise-exit-node"
"--advertise-routes=${vars.lanCidr}"
];
};
}
-15
View File
@@ -1,15 +0,0 @@
_:
{
imports = [ ./enable-service.nix ];
services.tailscale = {
# Enables the sysctl forwarding settings subnet routers need;
# without this, --advertise-routes has no effect.
useRoutingFeatures = "server";
# Lets peers reach this node directly over the tailscale UDP port
# instead of relaying through DERP.
openFirewall = true;
};
}
-35
View File
@@ -1,35 +0,0 @@
{ 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 ];
}
+4 -4
View File
@@ -1,14 +1,14 @@
{ vars, ... }: _:
{ {
services.logrotate = { services.logrotate = {
enable = true; enable = true;
settings = { settings = {
"${vars.nfsShares.dockerVolumes.mountpoint}/traefik-data/logs/*.log" = { "/mnt/docker/volumes/traefik-data/logs/*.log" = {
daily = true; daily = true;
size = vars.traefikLogRotate.maxSize; size = "100M";
rotate = vars.traefikLogRotate.keep; rotate = 20;
compress = true; compress = true;
missingok = true; missingok = true;
notifempty = true; notifempty = true;
Binary file not shown.
Executable
+30
View File
@@ -0,0 +1,30 @@
#create MBR table
parted /dev/sda -- mklabel msdos
#create nixos partition
parted /dev/sda -- mkpart primary 1MB -8GB
#set nixos partition to bootable
parted /dev/sda -- set 1 boot on
# create swap partition
parted /dev/sda -- mkpart primary linux-swap -8GB 100%
#format OS partition
mkfs.ext4 -L nixos /dev/sda1
#format swap
mkswap -L swap /dev/sda2
#activate swap
swapon /dev/sda2
#mount nixos partition
mount /dev/disk/by-label/nixos /mnt
export TMPDIR=/mnt/install-tmp
mkdir -p /mnt/install-tmp
#Generate config
#nixos-generate-config --root /mnt/
#copy customised configuration over
#cp configuration.nix /mnt/etc/nixos/configuration.nix
#nixos-install --no-root-passwd
#reboot
+134
View File
@@ -0,0 +1,134 @@
# 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)
-134
View File
@@ -1,134 +0,0 @@
#!/usr/bin/env bash
# Bumps the NixOS release branch this flake tracks — flake.nix's
# `nixpkgs.url` and `home-manager.url` — in one place, via targeted
# substitution of just those two lines. Deliberately does NOT touch any
# `system.stateVersion` anywhere in the repo: per NixOS's own docs, that
# value must stay fixed at whatever it was on a host's first install (it
# pins on-disk data-format defaults, not "which nixpkgs release am I on"),
# so it's never something a channel bump should follow.
#
# scripts/codex-maintenance.sh's own `nixos-25.11` pin (used only to fetch
# nixpkgs-fmt/statix — see CLAUDE.md) is a separate, independently-versioned
# reference on purpose: it doesn't have to track the flake's own nixpkgs
# input, since the tooling just needs to build, not match. Bump it with
# --tooling if you want it moved too; the default run leaves it alone.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
flake_nix="${repo_root}/flake.nix"
maintenance_sh="${repo_root}/scripts/codex-maintenance.sh"
claude_md="${repo_root}/CLAUDE.md"
usage() {
cat <<EOF
Usage: $0 <release> [--tooling <release>]
<release> New NixOS release for flake.nix's nixpkgs.url and
home-manager.url, e.g. 26.11
--tooling <release> Also bump scripts/codex-maintenance.sh's separate
nixpkgs-fmt/statix pin (and its mention in
CLAUDE.md) to this release. Independent of the
first argument — pass the same value if you want
both in sync, a different one if you don't.
Examples:
$0 26.11
$0 26.11 --tooling 26.11
EOF
}
release_re='^[0-9]{2}\.(05|11)$'
if [[ $# -eq 0 || "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
fi
new_release="$1"
shift
tooling_release=""
while [[ $# -gt 0 ]]; do
case "$1" in
--tooling)
tooling_release="${2:?--tooling requires a release argument}"
shift 2
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
check_release_format() {
local release="$1"
if [[ ! "$release" =~ $release_re ]]; then
echo "ERROR: '$release' doesn't look like a NixOS release (expected e.g. 26.11)" >&2
exit 1
fi
}
check_branch_exists() {
local repo_url="$1" branch="$2"
echo "Checking '$branch' exists on $repo_url..."
if ! git ls-remote --exit-code --heads "$repo_url" "$branch" >/dev/null; then
echo "ERROR: branch '$branch' not found on $repo_url. Typo, or not cut yet?" >&2
exit 1
fi
}
check_release_format "$new_release"
current_release="$(grep -oE 'nixos-[0-9]{2}\.[0-9]{2}' "$flake_nix" | head -1 | sed 's/^nixos-//')"
if [[ -z "$current_release" ]]; then
echo "ERROR: couldn't find flake.nix's current nixpkgs release" >&2
exit 1
fi
if [[ "$current_release" == "$new_release" ]]; then
echo "flake.nix is already on $new_release."
else
echo "Bumping flake.nix's nixpkgs/home-manager release: $current_release -> $new_release"
check_branch_exists "https://github.com/NixOS/nixpkgs.git" "nixos-$new_release"
check_branch_exists "https://github.com/nix-community/home-manager.git" "release-$new_release"
sed -i \
-e "s|github:NixOS/nixpkgs/nixos-${current_release}|github:NixOS/nixpkgs/nixos-${new_release}|" \
-e "s|github:nix-community/home-manager/release-${current_release}|github:nix-community/home-manager/release-${new_release}|" \
"$flake_nix"
echo "Updated:"
grep -n "nixos-${new_release}\|release-${new_release}" "$flake_nix"
fi
if [[ -n "$tooling_release" ]]; then
check_release_format "$tooling_release"
current_tooling_release="$(grep -oE 'nixos-[0-9]{2}\.[0-9]{2}' "$maintenance_sh" | head -1 | sed 's/^nixos-//')"
if [[ "$current_tooling_release" == "$tooling_release" ]]; then
echo "codex-maintenance.sh's tooling pin is already on $tooling_release."
else
echo "Bumping codex-maintenance.sh's nixpkgs-fmt/statix pin: $current_tooling_release -> $tooling_release"
check_branch_exists "https://github.com/NixOS/nixpkgs.git" "nixos-$tooling_release"
sed -i "s|github:NixOS/nixpkgs/nixos-${current_tooling_release}|github:NixOS/nixpkgs/nixos-${tooling_release}|g" \
"$maintenance_sh"
sed -i "s|nixos-${current_tooling_release}|nixos-${tooling_release}|g" \
"$claude_md"
echo "Updated:"
grep -n "nixos-${tooling_release}" "$maintenance_sh" "$claude_md"
fi
fi
echo
echo "flake.lock still points at the old input revisions until refreshed. Either:"
echo " nix flake update nixpkgs home-manager # just these two inputs"
echo " nix flake update # everything — see docs/flake-lock-automation.md"
echo
echo "Then run 'bash scripts/codex-maintenance.sh --full-check --dry-run' before"
echo "committing — a channel bump can shift option defaults across every host,"
echo "and only --dry-run actually builds anything to catch that."
+31 -282
View File
@@ -1,73 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Validation entry point for CI and local/agent review.
#
# Default mode (what CI runs on every push/PR): fmt-check, statix, and eval
# are scoped to files that actually changed against a base ref, plus
# whichever hosts/packages those changes can affect. This exists because
# the unscoped sweep below is slow enough to time out CI runners -- see
# --full-check.
#
# --full-check: the historical full sweep (every host, every package,
# fmt --check ./statix check . over the whole tree). Slow -- minutes, not
# seconds. CI never passes this; run it locally before a release or after
# touching modules/common/*, flake.nix, or variables.nix if you want extra
# confidence beyond what the changed-files scope already covers for those
# paths (see below).
#
# --dry-run: adds `nix build --dry-run --no-link` for whatever scope is
# active (changed-files scope by default, full scope under --full-check).
#
# Per-host/per-package eval and dry-run build calls run concurrently (see
# scripts/lib/nix-parallel.sh) since they're independent of each other.
# Concurrency defaults to core count capped by available memory (~1GB/job)
# rather than plain core count, since each concurrent `nix eval` evaluates a
# whole NixOS system closure and can OOM a small/memory-constrained CI
# runner otherwise; override via NIX_PARALLEL_JOBS if a runner has more (or
# less) room than that estimate assumes.
set -euo pipefail set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export NIX_CONFIG="${NIX_CONFIG:-}
# shellcheck source=lib/nix-bootstrap.sh experimental-features = nix-command flakes
source "${script_dir}/lib/nix-bootstrap.sh" accept-flake-config = false
# shellcheck source=lib/nix-eval.sh warn-dirty = false
source "${script_dir}/lib/nix-eval.sh" "
# shellcheck source=lib/nix-parallel.sh
source "${script_dir}/lib/nix-parallel.sh"
repo_root="$(cd "${script_dir}/.." && pwd)" MODE="${1:-validate}"
cd "$repo_root"
full_check=false ensure_nix_profile() {
dry_run=false 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
usage() { elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
cat <<'EOF' . "$HOME/.nix-profile/etc/profile.d/nix.sh"
Usage: scripts/codex-maintenance.sh [--full-check] [--dry-run] fi
--full-check Run the full sweep: fmt-check and statix over the whole
repo, eval every host and package. Slow. Never run by CI.
--dry-run Additionally run `nix build --dry-run --no-link` for
whatever scope is active.
With neither flag (the CI default), fmt-check/statix/eval are scoped to
files changed against a base ref (env MAINT_BASE_SHA, else the PR base,
else HEAD^), plus the hosts/packages those changes can affect.
EOF
} }
for arg in "$@"; do
case "$arg" in
--full-check) full_check=true ;;
--dry-run) dry_run=true ;;
-h|--help) usage; exit 0 ;;
*)
echo "Unknown argument: $arg" >&2
usage >&2
exit 1
;;
esac
done
ensure_nix_profile ensure_nix_profile
if ! command -v nix >/dev/null 2>&1; then if ! command -v nix >/dev/null 2>&1; then
@@ -75,6 +24,13 @@ 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="$(echo "$hosts_json" | jq -r '.[]')"
echo "Hosts:"
echo "$hosts"
echo
echo "Checking for obvious committed secrets..." echo "Checking for obvious committed secrets..."
if grep -RInE 'github_pat_|ghp_|access-tokens|hashedPassword[[:space:]]*=' \ if grep -RInE 'github_pat_|ghp_|access-tokens|hashedPassword[[:space:]]*=' \
--exclude-dir=.git \ --exclude-dir=.git \
@@ -86,235 +42,28 @@ else
echo "No obvious token patterns found." echo "No obvious token patterns found."
fi fi
mapfile -t all_hosts < <(list_flake_targets .)
mapfile -t all_packages < <(nix eval --json "${NIX_EVAL_FLAGS[@]}" .#packages.x86_64-linux --apply builtins.attrNames | jq -r '.[]')
# host_targets_for_dir <hosts-subdir-name>
# Prints the nixosConfigurations target names whose hostPath is
# ./hosts/<dir>/host.nix, derived straight from flake.nix's generatedTargets
# (one mkTarget { ... } call per line) rather than a hand-maintained table,
# so it can't drift the way a copied mapping would.
host_targets_for_dir() {
local dir="$1"
grep -oE '^[[:space:]]*[A-Za-z0-9_-]+ = mkTarget \{[^}]*hostPath = \./hosts/'"${dir}"'/host\.nix;[^}]*\};' flake.nix \
| sed -E 's/^[[:space:]]*([A-Za-z0-9_-]+) = mkTarget.*/\1/' \
|| true
}
declare -a changed_files=()
scope_desc="full repo"
if ! $full_check; then
resolve_base_ref() {
if [[ -n "${MAINT_BASE_SHA:-}" ]] && git cat-file -e "${MAINT_BASE_SHA}^{commit}" 2>/dev/null; then
echo "$MAINT_BASE_SHA"
return
fi
if git rev-parse --verify -q HEAD^ >/dev/null 2>&1; then
echo "HEAD^"
return
fi
git hash-object -t tree /dev/null
}
base_ref="$(resolve_base_ref)"
echo
echo "Changed-files scope: diffing against ${base_ref}"
mapfile -t changed_files < <(git diff --name-only --diff-filter=ACMR "$base_ref" -- . | sort -u)
if [[ ${#changed_files[@]} -eq 0 ]]; then
echo "No changed files detected."
else
printf ' %s\n' "${changed_files[@]}"
fi
scope_desc="changed files only (base: ${base_ref})"
fi
# Whole-tree fmt/lint always run under --full-check; otherwise scoped below.
declare -a changed_nix_files=()
for f in "${changed_files[@]:-}"; do
[[ "$f" == *.nix && -f "$f" ]] && changed_nix_files+=("$f")
done
echo echo
echo "Checking Nix formatting with nixpkgs-fmt..." echo "Checking Nix formatting with nixpkgs-fmt..."
if $full_check; then 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 .
elif [[ ${#changed_nix_files[@]} -gt 0 ]]; then
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check "${changed_nix_files[@]}"
else
echo "No changed .nix files; skipping."
fi
echo echo
echo "Running statix lint..." echo "Running statix lint..."
if $full_check; then 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 .
elif [[ ${#changed_nix_files[@]} -gt 0 ]]; then
for f in "${changed_nix_files[@]}"; do
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check "$f"
done
else
echo "No changed .nix files; skipping."
fi
# Figure out which hosts/packages this run needs to eval (and, under
# --dry-run, build). full_check always means "everything"; otherwise a
# change to flake.nix/flake.lock/variables.nix/modules/common/* (repo-wide
# inputs) or to any other modules/*.nix outside platforms//build-types
# (whose blast radius isn't safely inferable from the path alone -- see
# CLAUDE.md's "Grep modules/build-types/*.nix for each build type's imports
# list") also falls back to everything, on the same reasoning CLAUDE.md
# already gives interactive sessions for when to run the full sweep.
# Anything more targeted -- a host.nix, a platform module, a build-type
# module -- narrows to just the hosts it can affect.
declare -A affected_hosts=()
eval_packages=false
if $full_check; then
for h in "${all_hosts[@]}"; do affected_hosts[$h]=1; done
eval_packages=true
else
full_fallback=false
for f in "${changed_files[@]:-}"; do
case "$f" in
flake.nix|flake.lock|variables.nix|modules/common/*)
full_fallback=true
;;
esac
done
if ! $full_fallback; then
for f in "${changed_files[@]:-}"; do
case "$f" in
hosts/*/*)
hostdir="${f#hosts/}"
hostdir="${hostdir%%/*}"
while IFS= read -r t; do
[[ -n "$t" ]] && affected_hosts[$t]=1
done < <(host_targets_for_dir "$hostdir")
;;
modules/platforms/*.nix)
platform="$(basename "$f" .nix)"
for h in "${all_hosts[@]}"; do
[[ "$h" == "${platform}-"* ]] && affected_hosts[$h]=1
done
;;
modules/build-types/*.nix)
buildtype="$(basename "$f" .nix)"
for h in "${all_hosts[@]}"; do
[[ "$h" == *"-${buildtype}" ]] && affected_hosts[$h]=1
done
;;
modules/installer/*)
# iso.nix (imported by both the "installer" nixosConfigurations
# target and netbootSystem, which backs packages.pxe) pulls in
# common.nix, so a common.nix change reaches all three.
affected_hosts[installer]=1
eval_packages=true
;;
modules/pxe-boot/*)
# stage-installer-artifacts.nix is imported by
# modules/build-types/pxe-boot.nix only -- same blast radius as a
# build-types/*.nix change, not a packages one.
for h in "${all_hosts[@]}"; do
[[ "$h" == *"-pxe-boot" ]] && affected_hosts[$h]=1
done
;;
modules/*)
full_fallback=true
;;
esac
done
fi
if $full_fallback; then
echo
echo "Changed files affect shared config; falling back to evaluating every host/package."
for h in "${all_hosts[@]}"; do affected_hosts[$h]=1; done
eval_packages=true
fi
fi
mapfile -t hosts < <(for h in "${!affected_hosts[@]}"; do echo "$h"; done | sort)
echo echo
echo "Checking nix-cache host key for drift..." echo "Evaluating host toplevel derivations..."
if bash "${script_dir}/secrets/sync-nix-cache-host-key.sh" --check; then for host in $hosts; do
: echo "==> $host"
else nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
drift_status=$? done
if [[ "$drift_status" -eq 2 ]]; then
echo "nix-cache unreachable from here -- skipping host-key drift check."
else
echo "WARNING: nix-cache's host key has drifted from variables.nix (see above)." >&2
echo " Run 'bash scripts/secrets/sync-nix-cache-host-key.sh' to fix." >&2
fi
fi
echo if [[ "$MODE" == "dry-run" ]]; then
if [[ ${#hosts[@]} -eq 0 ]]; then
echo "No hosts affected by changed files; skipping host eval."
else
echo "Evaluating host toplevel derivations (${scope_desc}, up to ${NIX_PARALLEL_JOBS} at a time)..."
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of
# nixos-install (see docs/auto-installer.md); proxmox-* hosts can
# alternatively be built as a standalone disk image (see
# docs/proxmox-images.md). Both are otherwise-unvalidated buildable
# surface, easy to silently break without this.
declare -a host_eval_jobs=()
for host in "${hosts[@]}"; do
host_eval_jobs+=("${host}${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.toplevel.drvPath")
case "$host" in
lxc-*)
host_eval_jobs+=("${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball.drvPath")
;;
proxmox-*)
host_eval_jobs+=("${host} (diskoImagesScript)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath")
;;
esac
done
run_nix_parallel host_eval_jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
fi
echo
if ! $eval_packages; then
echo "No packages affected by changed files; skipping package eval."
else
echo "Evaluating buildable packages (up to ${NIX_PARALLEL_JOBS} at a time)..."
declare -a package_eval_jobs=()
for pkg in "${all_packages[@]}"; do
package_eval_jobs+=("packages.x86_64-linux.${pkg}${NIX_PARALLEL_SEP}.#packages.x86_64-linux.${pkg}")
done
run_nix_parallel package_eval_jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
fi
if $dry_run; then
echo echo
echo "Running dry-run builds for the active scope (up to ${NIX_PARALLEL_JOBS} at a time). This will not create result symlinks." echo "Running dry-run builds for all hosts. This will not create result symlinks."
declare -a host_build_jobs=() for host in $hosts; do
for host in "${hosts[@]:-}"; do echo "==> Dry-run build: $host"
host_build_jobs+=("Dry-run build: ${host}${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.toplevel") nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel"
case "$host" in
lxc-*)
host_build_jobs+=("Dry-run build: ${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball")
;;
proxmox-*)
host_build_jobs+=("Dry-run build: ${host} (diskoImagesScript)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.diskoImagesScript")
;;
esac
done done
run_nix_parallel host_build_jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
if $eval_packages; then
echo
echo "Running dry-run builds for packages."
declare -a package_build_jobs=()
for pkg in "${all_packages[@]}"; do
package_build_jobs+=("Dry-run build: packages.x86_64-linux.${pkg}${NIX_PARALLEL_SEP}.#packages.x86_64-linux.${pkg}")
done
run_nix_parallel package_build_jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
fi
fi fi
echo echo
+22 -19
View File
@@ -1,11 +1,19 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export NIX_CONFIG="${NIX_CONFIG:-}
# shellcheck source=lib/nix-bootstrap.sh experimental-features = nix-command flakes
source "${script_dir}/lib/nix-bootstrap.sh" accept-flake-config = false
# shellcheck source=lib/nix-eval.sh warn-dirty = false
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
@@ -41,17 +49,6 @@ 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
@@ -68,7 +65,6 @@ cat > "$HOME/.config/nix/nix.conf" <<'EOF'
experimental-features = nix-command flakes experimental-features = nix-command flakes
accept-flake-config = false accept-flake-config = false
warn-dirty = false warn-dirty = false
build-users-group =
EOF EOF
echo "Nix version:" echo "Nix version:"
@@ -83,6 +79,13 @@ if ! command -v jq >/dev/null 2>&1; then
fi fi
echo "Available NixOS hosts:" echo "Available NixOS hosts:"
list_flake_targets . hosts="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')"
echo "$hosts"
echo "Codex setup complete. Run bash scripts/codex-maintenance.sh to validate changes." echo "Evaluating all host toplevel derivations..."
for host in $hosts; do
echo "==> Evaluating $host"
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
done
echo "Codex setup complete."
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
#boot to rescue mode
# set root password
scp $RESULT_ISO root@$LINODE_IP:/tmp/nixos-auto.iso
#in LISH or ssh to rescue mode
dd if=/tmp/nixos.iso of=/dev/sda bs=4M status=progress conv=fsync
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env bash
# Shared config for scripts/*.sh. Source this instead of hardcoding a
# second copy of these values in every script:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
# Every variable can still be overridden per-invocation via the
# environment (e.g. PROXMOX_STORAGE=tank-nvme ./scripts/proxmox/create-proxmox-resource.sh ...)
# since each one only sets a default if unset.
# Two SSH-reachable Proxmox nodes exist on the LAN:
# - pve1.sweet.home -- production. Real, live VMs/containers.
# - pve-test.sweet.home -- sandbox/test node, for scratch VMs/containers
# that don't belong on production.
#
# PROXMOX_HOST is what scripts/proxmox/create-proxmox-resource.sh actually
# targets by default -- overridable per-invocation with --node <hostname>,
# or per-variable as usual (e.g. PROXMOX_HOST=$PVE_TEST_HOST). It defaults
# to production, matching this repo's behavior before pve-test existed --
# see CLAUDE.md's "Two Proxmox nodes" section for the policy on which
# situations should target which node (in particular: Claude defaults to
# pve-test, not this variable's own default, unless explicitly told
# otherwise).
: "${PVE1_HOST:=pve1.sweet.home}"
: "${PVE_TEST_HOST:=pve-test.sweet.home}"
: "${PROXMOX_HOST:=$PVE1_HOST}"
: "${PROXMOX_SSH_USER:=wayne}"
# Where this flake repo lives on the Proxmox node itself.
# scripts/proxmox/create-proxmox-resource.sh builds images directly on the node
# instead of transferring them over the network -- it clones the repo here
# (from this checkout's own `origin` remote) the first time it doesn't
# find it, installing build tooling via scripts/codex-setup.sh, then
# `git pull`s it before every subsequent build.
: "${PROXMOX_REMOTE_REPO_DIR:=/home/${PROXMOX_SSH_USER}/nixos}"
# Storage pool names -- Proxmox's own stock-install defaults, but this
# varies a lot by setup (ZFS pool name, custom LVM-thin volume, etc.).
# Verify with `pvesm status` on the node and correct these if wrong.
: "${PROXMOX_STORAGE:=local-zfs}" # VM disks / CT rootfs
: "${PROXMOX_ISO_STORAGE:=local}" # uploaded images/ISOs/CT templates
: "${PROXMOX_BRIDGE:=vmbr0}"
# Fallback resource sizing when a script doesn't get --cores/--memory.
: "${PROXMOX_DEFAULT_CORES:=2}"
: "${PROXMOX_DEFAULT_MEMORY_MB:=2048}"
# `pct create` (unlike `pct restore`) requires an explicit rootfs size --
# no backup metadata to infer it from. Matches Proxmox's own GUI default.
: "${PROXMOX_DEFAULT_LXC_DISK_GB:=8}"
# `pct create --memory` only sets RAM -- swap is a wholly separate
# parameter that otherwise silently stays at Proxmox's own 512M default
# regardless of --memory (confirmed: creating with --memory 2048 left
# swap at 512). create-proxmox-resource.sh defaults --swap to whatever
# --memory resolves to at runtime rather than a static value here, so it
# tracks a --memory picked at the CLI too, not just the default above.
# Required for a modern (v247+) systemd guest to actually boot as an
# unprivileged container: systemd's routine use of nested user namespaces
# and credential mounts (LoadCredential=, DynamicUser=, etc. -- used even
# by plain getty units) gets denied by AppArmor's default LXC confinement
# without these. Confirmed live: without them, every getty unit
# crash-loops on a denied `/run/credentials/*` mount every ~3s (visible
# as garbage on the console) and core services like nsncd fail the same
# way on userns_create; system.build.tarball never finishes activating.
#
# 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 PVE1_HOST PVE_TEST_HOST PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE \
PROXMOX_ISO_STORAGE PROXMOX_BRIDGE PROXMOX_DEFAULT_CORES \
PROXMOX_DEFAULT_MEMORY_MB PROXMOX_DEFAULT_LXC_DISK_GB \
PROXMOX_DEFAULT_LXC_FEATURES PROXMOX_REMOTE_REPO_DIR
# Matches variables.nix's nixCacheHost -- update both if it ever changes.
: "${NIX_CACHE_HOST:=nix-cache}"
export NIX_CACHE_HOST
# Matches variables.nix's lanDomain (the Gitea host this flake's own repo
# is served from -- see scripts/installer/auto-install.sh's FLAKE_BASE_URL)
# -- update both if it ever changes.
: "${LAN_DOMAIN:=gitea.lan.ddnsgeek.com}"
export LAN_DOMAIN
# nix_extra_opts: call as a plain statement (NOT inside $(...)/<(...) --
# that forks a subshell, and the whole point is exporting a decision back
# into *this* shell) to populate the global NIX_OPTS array with whatever
# extra `nix`/`nix-shell` CLI options are needed to avoid nix-cache when
# it's unreachable:
# nix_extra_opts
# nix build "${NIX_OPTS[@]}" ...
#
# Without this, every single `nix eval`/`nix build` call retries each
# store path against a dead substituter up to 5 times with backoff
# (confirmed: ~15s+ per lookup even with a short connect-timeout, because
# nix's own retry count isn't controllable that way), and separately
# tries it as a remote builder too -- both fail independently, so both
# are checked.
#
# Checked with a single fast `curl`/TCP probe (bypassing nix's retry logic
# entirely) the first time this is called in a given process, and the
# result is exported as NIX_EXTRA_OPTS so a script that shells out to
# another script in this repo (e.g. create-proxmox-resource.sh calling
# sync-host-keys.sh) reuses the same decision instead of probing twice.
declare -a NIX_OPTS=()
nix_extra_opts() {
if [[ -n "${NIX_EXTRA_OPTS_DECIDED:-}" ]]; then
if [[ -n "${NIX_EXTRA_OPTS:-}" ]]; then
eval "NIX_OPTS=(${NIX_EXTRA_OPTS})"
else
NIX_OPTS=()
fi
return
fi
export NIX_EXTRA_OPTS_DECIDED=1
NIX_OPTS=()
# Retry a couple of times, 1s apart, before believing either check --
# belt-and-suspenders against a genuine multi-second blip (nix-cache
# restarting), on top of the fix below. Worst case (~11s total, host
# genuinely gone) is still nowhere near the 15s+ *per lookup* nix's own
# substituter retries would cost if this check didn't exist at all.
local attempt cache_up=0 builder_up=0
for attempt in 1 2 3; do
if curl --silent --fail --max-time 3 "http://${NIX_CACHE_HOST}/nix-cache-info" >/dev/null 2>&1; then
cache_up=1
break
fi
[[ "$attempt" -lt 3 ]] && sleep 1
done
if [[ "$cache_up" -eq 0 ]]; then
echo "nix-cache (http://${NIX_CACHE_HOST}) is unreachable -- skipping it (substituter + remote builder) for the rest of this run." >&2
NIX_OPTS=(--option substituters "https://cache.nixos.org/" --builders "")
else
for attempt in 1 2 3; do
# `exec 3<>/dev/tcp/...` just opens the fd and returns -- it does NOT
# read from it. Confirmed live this is load-bearing, not stylistic:
# the previous `cat < /dev/tcp/.../22` blocked forever and always hit
# the timeout even against a perfectly healthy nix-cache, because
# sshd sends its banner and then holds the connection open waiting
# for the client to speak next -- `cat` never sees EOF, so this
# check reported "unreachable" unconditionally, 100% of the time,
# regardless of whether the remote builder was actually up.
if timeout 3 bash -c "exec 3<>/dev/tcp/${NIX_CACHE_HOST}/22" 2>/dev/null; then
builder_up=1
break
fi
[[ "$attempt" -lt 3 ]] && sleep 1
done
if [[ "$builder_up" -eq 0 ]]; then
echo "nix-cache's SSH remote builder (nixremote@${NIX_CACHE_HOST}:22) is unreachable -- disabling remote builds for the rest of this run." >&2
NIX_OPTS=(--builders "")
fi
fi
# `printf '%q '` with a genuinely empty NIX_OPTS still runs one format
# pass over a missing argument and yields the literal `'' ` rather than
# an empty string (confirmed live) -- a subprocess that later does
# `eval "NIX_OPTS=(${NIX_EXTRA_OPTS})"` (the branch above, for e.g.
# sync-host-keys.sh reusing this process's decision) would then rebuild
# a 1-element array holding an empty string instead of a 0-element
# array, and `nix-shell "${NIX_OPTS[@]}" -p <pkg>` chokes on that stray
# element as a bogus positional argument. Guard the empty case
# explicitly so nix-cache being reachable (NIX_OPTS legitimately empty)
# round-trips as truly empty instead.
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
printf -v NIX_EXTRA_OPTS '%q ' "${NIX_OPTS[@]}"
else
NIX_EXTRA_OPTS=""
fi
export NIX_EXTRA_OPTS
}
-206
View File
@@ -1,206 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p jq disko nixos-install-tools zfs
# shellcheck shell=bash
# The only genuinely external tools this script calls directly: `jq`
# (parsing the `nix eval` host list), `disko`/`nixos-install` (the
# install itself), and `zpool` (exporting a ZFS root pool before reboot,
# see the comment above that call below). Everything disko shells out to
# internally (parted/sgdisk/mkfs.*/zfs/...) is self-contained -- disko's
# own generated scripts hardcode absolute Nix store paths for those, they
# don't rely on this script's PATH at all (confirmed by inspecting a
# generated system.build.formatScript). The built installer image
# (modules/installer/common.nix, plus the upstream
# installation-cd-minimal.nix it imports via iso.nix) already has all
# four in environment.systemPackages, so this nix-shell wrapper is a
# fast no-op there; it's what makes the script also work standalone
# (e.g. run directly from a checkout on a stock ISO), where they aren't
# guaranteed.
set -eux
set -euo pipefail
# shellcheck source=../env.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/env.sh"
export FLAKE_BASE_URL="git+https://${LAN_DOMAIN}/beatzaplenty/nixos.git"
echo "Fetching available NixOS hosts from flake..."
# Two categories deliberately excluded from the menu:
# lxc-* — these build a config.system.build.tarball meant for
# `pct restore` on Proxmox directly, not an install.
# Running nixos-install against one here would
# bind-mount / onto /mnt and then refuse to touch the
# filesystem it's currently running on — see
# docs/auto-installer.md.
# installer — this *is* the installer image's own flake target,
# not a deployable host; "installing" it means
# nixos-install-ing a copy of the installer into
# itself.
mapfile -t options < <(
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
"${FLAKE_BASE_URL}#nixosConfigurations" \
--apply builtins.attrNames \
| jq -r '.[]
| select(startswith("lxc-") | not)
| select(. != "installer")'
)
if [[ ${#options[@]} -eq 0 ]]; then
echo "ERROR: No NixOS hosts found in ${FLAKE_BASE_URL}#nixosConfigurations" >&2
exit 1
fi
echo "Note: lxc-* targets aren't installed this way — build them with"
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
echo "Choose the flake profile to install:"
select choice in "${options[@]}"; do
if [[ -n "$choice" ]]; then
echo "You selected: $choice"
break
else
echo "Invalid selection. Try again."
fi
done
echo "Starting install with flake: ${FLAKE_BASE_URL}#${choice}"
# Optional: confirm before proceeding
read -rp "Proceed with installation? (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
# A nix-cache host is *the* substituter/remote-builder for every other
# host once installed (its own config explicitly excludes itself from
# using either — see buildType != "nix-cache" in the nixos flake.nix).
# Installing one shouldn't depend on a nix-cache substituter either,
# for the same reason — plus in practice "nix-cache" only resolves over
# Tailscale, which a fresh installer environment was never connected to
# anyway, so it's dead weight even for non-nix-cache installs until
# that's sorted out. Override it away here specifically for nix-cache
# targets to keep install-time behaviour consistent with run-time.
nix_extra_opts=()
if [[ "${choice}" == *-nix-cache ]]; then
echo "Installing a nix-cache host — skipping the nix-cache substituter."
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
fi
# Every host reachable through this menu has a Disko config (lxc-*
# is filtered out above, and is the only category that doesn't —
# see docs/auto-installer.md), so this can run unconditionally: no
# need to probe the flake first and branch on whether Disko applies.
disko --mode destroy,format,mount \
--flake "${FLAKE_BASE_URL}#${choice}" "${nix_extra_opts[@]}" --yes-wipe-all-disks
# sops-nix derives this host's decryption key from its own SSH host key
# at *activation* time, which runs before systemd would otherwise
# generate one on first boot. Without pre-seeding it here, secrets
# (including the login password) fail to decrypt on first boot.
# Generate the key with scripts/secrets/prepare-host-key.sh first.
#
# Two places a key can come from, checked in order:
# /etc/host-keys — baked into this image at build time (see
# modules/installer/host-keys.nix; only present
# if built with NIXOS_HOST_KEYS_DIR set)
# /root/host-keys — scp'd in manually after boot (older fallback,
# still supported for images built without keys)
mkdir -p /root/host-keys
if [[ -f "/etc/host-keys/${choice}_ssh_host_ed25519_key" ]]; then
echo "Found baked-in SSH host key for ${choice}, installing to target..."
install -D -m 0600 "/etc/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
install -D -m 0644 "/etc/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
elif [[ -f "/root/host-keys/${choice}_ssh_host_ed25519_key" ]]; then
echo "Found pre-seeded SSH host key for ${choice}, installing to target..."
install -D -m 0600 "/root/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
install -D -m 0644 "/root/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
else
# Third place a key can come from: an arbitrary path the operator
# points at interactively (e.g. a USB stick, a mount from another
# machine) -- only offered when there's an actual human at the other
# end of stdin to ask, never in a non-interactive run.
key_copied=0
if [[ -t 0 ]]; then
echo "No SSH host key found for ${choice} (checked /etc/host-keys and /root/host-keys)."
read -rp "Path to a directory containing ${choice}_ssh_host_ed25519_key(.pub) (blank to skip): " key_src_dir
if [[ -n "$key_src_dir" && -f "${key_src_dir}/${choice}_ssh_host_ed25519_key" && -f "${key_src_dir}/${choice}_ssh_host_ed25519_key.pub" ]]; then
cp "${key_src_dir}/${choice}_ssh_host_ed25519_key" "${key_src_dir}/${choice}_ssh_host_ed25519_key.pub" /root/host-keys/
key_copied=1
elif [[ -n "$key_src_dir" ]]; then
echo "WARNING: ${choice}_ssh_host_ed25519_key(.pub) not found in ${key_src_dir}."
fi
fi
if [[ "$key_copied" -eq 1 ]]; then
echo "Copied SSH host key for ${choice} from ${key_src_dir}, installing to target..."
install -D -m 0600 "/root/host-keys/${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
install -D -m 0644 "/root/host-keys/${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
else
echo "WARNING: no SSH host key found for ${choice} (checked /etc/host-keys and /root/host-keys)"
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
echo "Run scripts/secrets/prepare-host-key.sh for host ${choice} on your admin workstation first,"
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, scp the result to"
echo "/root/host-keys/ on this machine, or point at it when prompted above."
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
fi
mkdir -p /mnt/install-tmp
export TMPDIR=/mnt/install-tmp
nixos-install \
--flake "${FLAKE_BASE_URL}#${choice}" \
"${nix_extra_opts[@]}" \
--no-root-password
rm -rf /mnt/install-tmp
# Redundant copy of the host's private key — the real one is now at
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
# up on its own since it was written imperatively, not declaratively.
rm -rf /root/host-keys
# disko's --mode ...,mount left any ZFS root pool imported (that's what
# let nixos-install write into /mnt). If we reboot with it still
# imported, it isn't just "not exported" -- it's stamped with *this*
# live installer environment's hostid, which almost never matches the
# target's own networking.hostId (see hosts/*/host.nix; the installer
# itself sets none). modules/services/zfs/enable-service.nix and
# modules/common/configuration.nix both set boot.zfs.forceImportRoot =
# false deliberately (the safe option per that setting's own docs), so
# the freshly-installed system's first real boot sees a pool "in use by
# another system" and refuses to import it without -f -- which is what
# makes boot stall waiting on the ZFS import. Exporting here (a no-op
# if the chosen host has no ZFS root, e.g. proxmox-*/linode-*) clears
# that in-use state so the next import, from any hostid, succeeds.
#
# Anything still mounted under /mnt -- nixos-install's own leftover
# chroot bind mounts for running the target's activation script
# (/mnt/dev, /mnt/proc, /mnt/sys, /mnt/run), and disko's own /mnt/boot
# ESP mount (modules/disko/baremetal.nix) -- blocks ZFS from unmounting
# its root dataset at /mnt, the same way any nested mount blocks
# unmounting its parent. Confirmed live: zpool export failed with
# "cannot unmount '/mnt': pool or dataset busy" even after handling the
# chroot mounts alone, because /mnt/boot was still mounted too. Because
# of this script's `set -e`, that killed the script before it ever
# reached reboot, silently defeating the whole point of exporting first.
# Unmounting everything under /mnt up front (recursively, so nested
# mounts like /mnt/dev/pts come along for free) sidesteps needing to
# enumerate every mount disko/nixos-install might leave behind.
if mountpoint -q /mnt; then
umount -R /mnt
fi
if [[ -n "$(zpool list -H -o name 2>/dev/null)" ]]; then
echo "Exporting ZFS pool(s) before reboot..."
zpool export -a
fi
sleep 10
reboot
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
# Shared "type X to confirm" prompt for scripts/proxmox/create-proxmox-resource.sh
# (--modify, and replacing an existing --allow-duplicate-host resource) and
# scripts/secrets/sync-host-keys.sh (--regenerate-all-keys) -- three destructive
# confirmations that all work the same way (echo the expected value back
# exactly), kept in one place so the prompt/comparison logic can't drift.
# Source alongside env.sh:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/confirm.sh"
#
# Deliberately does NOT print anything on mismatch or decide exit-vs-return
# -- callers vary on both (a top-level script exits, a subcommand function
# returns; wording differs too), so that stays at the call site.
# confirm_typed <expected> <prompt>
# Prints <prompt> via `read -rp`, then reports (via exit status) whether the
# typed input matched <expected> exactly.
confirm_typed() {
local expected="$1" prompt="$2" input
read -rp "$prompt" input
[[ "$input" == "$expected" ]]
}
-20
View File
@@ -1,20 +0,0 @@
#!/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
}
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env bash
# Shared flake-introspection helpers for scripts/*.sh. Source alongside
# env.sh:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-eval.sh"
#
# NIX_EVAL_FLAGS: --no-use-registries so a call here never resolves through
# the user's global flake registry (every call targets this repo's own
# flake, or an explicit github: ref, not a registry alias); --no-accept-flake-config
# so a flake input's own nixConfig (e.g. a dependency's substituters) is
# never honored -- matches accept-flake-config = false already set repo-wide
# (see lib/nix-bootstrap.sh / CLAUDE.md). Reuse this array rather than
# retyping the two flags at each call site.
declare -a NIX_EVAL_FLAGS=(--no-use-registries --no-accept-flake-config)
# list_flake_targets <flake_ref>
# Prints the attribute names under <flake_ref>#nixosConfigurations, one per
# line, e.g.:
# list_flake_targets . # from inside the repo
# list_flake_targets "$repo_root" # from anywhere
list_flake_targets() {
local flake_ref="$1"
nix eval --json "${NIX_EVAL_FLAGS[@]}" \
"${flake_ref}#nixosConfigurations" --apply builtins.attrNames \
| jq -r '.[]'
}
# flake_target_hostname <flake_ref> <target>
# Prints one nixosConfigurations target's config.networking.hostName.
# Empty (not an error under set -e) if the target doesn't exist or the
# eval otherwise fails -- callers that need to distinguish "empty" from
# "eval failed" should check $? themselves instead of relying on this.
flake_target_hostname() {
local flake_ref="$1" target="$2"
nix eval --raw "${NIX_EVAL_FLAGS[@]}" \
"${flake_ref}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null
}
# flake_target_lxc_privileged <flake_ref> <target>
# Prints "true" or "false" for one lxc-* target's config.proxmoxLXC.privileged
# (modules/platforms/lxc.nix is the single source of truth -- e.g.
# lxc-docker sets this true so it can NFS-mount; every other lxc-* host
# stays unprivileged). Only meaningful for lxc-* targets -- the option
# doesn't exist for linode-*/proxmox-* (nixpkgs' proxmox-lxc.nix, which
# declares it, is only ever imported by modules/platforms/lxc.nix). Empty
# (not an error under set -e) if the eval fails.
flake_target_lxc_privileged() {
local flake_ref="$1" target="$2"
# Not --raw: the option is a Nix boolean, and --raw can only coerce
# strings ("cannot coerce a Boolean to a string"). Plain `nix eval`
# prints a bare `true`/`false` for a boolean, which is exactly the
# string this needs.
nix eval "${NIX_EVAL_FLAGS[@]}" \
"${flake_ref}#nixosConfigurations.${target}.config.proxmoxLXC.privileged" 2>/dev/null
}
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env bash
# Shared parallel-nix-invocation helper for scripts/codex-maintenance.sh.
# Source alongside nix-eval.sh:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-parallel.sh"
#
# The per-host/per-package `nix eval`/`nix build --dry-run` calls in
# codex-maintenance.sh are independent of each other, so running them one at
# a time leaves most cores idle for most of the sweep -- run_nix_parallel
# fans a batch of them out across up to NIX_PARALLEL_JOBS processes instead.
# NIX_PARALLEL_JOBS: how many `nix` invocations run_nix_parallel runs at
# once. Defaults to core count capped by available memory (~1GB/job,
# floor 1) rather than plain `nproc` -- each concurrent `nix eval` here
# evaluates a whole NixOS system closure from scratch, and on a small/
# memory-constrained CI runner, `nproc` concurrent evals can OOM-kill each
# other (confirmed empirically: on a 4GB/6-core box, 5-6 concurrent evals
# started getting killed while 3-4 ran clean and were still ~2x faster than
# serial). Override via env if a given machine/CI runner has room to spare
# or needs a tighter cap.
default_nix_parallel_jobs() {
local cores mem_avail_kb mem_cap
cores="$(nproc 2>/dev/null || echo 4)"
mem_avail_kb="$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo 2>/dev/null)"
if [[ -z "$mem_avail_kb" ]]; then
echo "$cores"
return
fi
mem_cap=$((mem_avail_kb / 1024 / 1024))
((mem_cap < 1)) && mem_cap=1
((mem_cap < cores)) && echo "$mem_cap" || echo "$cores"
}
NIX_PARALLEL_JOBS="${NIX_PARALLEL_JOBS:-$(default_nix_parallel_jobs)}"
# Separator between a job's label and its flake attr in the arrays
# run_nix_parallel takes -- a control character so it can't collide with
# anything a label or attr path would plausibly contain.
NIX_PARALLEL_SEP=$'\x1f'
# run_nix_parallel <jobs_array_name> <nix subcommand + flags...>
#
# jobs_array_name: name of an already-populated bash array whose entries are
# "<label>${NIX_PARALLEL_SEP}<attr>" pairs, e.g.
# jobs=("proxmox-docker${NIX_PARALLEL_SEP}.#nixosConfigurations.proxmox-docker...drvPath")
# Remaining args are passed to `nix` before the attr, e.g.:
# run_nix_parallel jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
# run_nix_parallel jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
#
# Prints "==> <label>" followed by that job's stdout+stderr for every job,
# in submission order (not completion order) so a run stays readable and
# diffable across invocations even though the work itself doesn't finish in
# that order. Returns non-zero if any job failed, only after every job has
# finished and been printed -- same "surface everything, then fail" contract
# a `set -e` caller gets, just parallelized instead of stopping at the first
# failure.
run_nix_parallel() {
local -n jobs_ref="$1"
shift
local -a nix_args=("$@")
local n=${#jobs_ref[@]}
[[ $n -eq 0 ]] && return 0
local tmp_dir
tmp_dir="$(mktemp -d)"
local i=0 running=0
for job in "${jobs_ref[@]}"; do
local attr="${job#*"${NIX_PARALLEL_SEP}"}"
printf '%s\n' "${job%%"${NIX_PARALLEL_SEP}"*}" >"${tmp_dir}/${i}.label"
(
if nix "${nix_args[@]}" "$attr" >"${tmp_dir}/${i}.out" 2>&1; then
echo 0 >"${tmp_dir}/${i}.status"
else
echo 1 >"${tmp_dir}/${i}.status"
fi
) &
i=$((i + 1))
running=$((running + 1))
if ((running >= NIX_PARALLEL_JOBS)); then
wait -n
running=$((running - 1))
fi
done
wait
local failed=0 j
for ((j = 0; j < n; j++)); do
echo "==> $(cat "${tmp_dir}/${j}.label")"
cat "${tmp_dir}/${j}.out"
[[ "$(cat "${tmp_dir}/${j}.status")" -ne 0 ]] && failed=1
done
rm -rf "$tmp_dir"
return $failed
}
-52
View File
@@ -1,52 +0,0 @@
#!/usr/bin/env bash
# Shared sops/age helpers for scripts/secrets/backup-admin-key.sh,
# scripts/secrets/rotate-admin-key.sh, and scripts/secrets/sync-host-keys.sh -- all three
# derive an age public key from a private identity file the same way, two
# of them resolve the same sops/age default key-file path, and two of them
# run `sops updatekeys` the same way. Kept in one place so they can't drift
# apart. Source alongside env.sh:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/sops-age.sh"
#
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
# nix_extra_opts) if the caller has already set it, same convention as
# lib/ssh-host-keys.sh. Falls back to no extra options if the caller never
# sourced env.sh.
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
declare -a NIX_OPTS=()
fi
# sops/age's own default identity-file resolution order, minus $SOPS_AGE_KEY
# itself (an inline identity, not a path -- callers that accept it check it
# separately, before falling back to this).
: "${DEFAULT_SOPS_AGE_KEY_FILE:=${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}}"
# age_pubkey_from_identity_file <identity-file>
# Prints the age public key for a private identity file (age-keygen -y).
age_pubkey_from_identity_file() {
local identity_file="$1"
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '${identity_file}'"
}
# sops_yaml_admin_pubkey <sops-yaml-path>
# Prints .sops.yaml's current &admin age public key, or empty (not an error
# under set -e) if no such anchor line exists -- callers that need to treat
# "missing" as fatal check for an empty result themselves.
sops_yaml_admin_pubkey() {
local sops_yaml="$1"
grep -E '^ - &admin age1' "$sops_yaml" 2>/dev/null | awk '{print $NF}' || true
}
# sops_updatekeys <secrets-file> [key-file]
# Re-encrypts <secrets-file> for .sops.yaml's current recipient set. If
# <key-file> is given, decrypts with that identity (SOPS_AGE_KEY_FILE)
# instead of whatever's ambient -- needed when the ambient default key
# doesn't match yet (e.g. mid-rotation, decrypting with the outgoing key).
sops_updatekeys() {
local secrets_file="$1" key_file="${2:-}"
if [[ -n "$key_file" ]]; then
SOPS_AGE_KEY_FILE="$key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
"sops updatekeys --yes '${secrets_file}'"
else
nix-shell "${NIX_OPTS[@]}" -p sops --run "sops updatekeys --yes '${secrets_file}'"
fi
}
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env bash
# Shared SSH-host-key / age-conversion helpers for scripts/secrets/sync-host-keys.sh
# and scripts/secrets/prepare-host-key.sh -- both generate the same kind of key
# (ed25519, no passphrase, the sops-nix age-derivation input) and convert it
# to an age recipient the same way; kept in one place so the two can't
# drift apart.
#
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
# nix_extra_opts) if the caller has already set it, so a decision to avoid
# an unreachable nix-cache is reused here instead of probed again. Falls
# back to no extra options if the caller never sourced env.sh.
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
declare -a NIX_OPTS=()
fi
# generate_host_ed25519_key <hostname> <keyfile>
# Writes <keyfile> and <keyfile>.pub. Caller is responsible for refusing to
# overwrite an existing keyfile -- this always runs ssh-keygen fresh.
generate_host_ed25519_key() {
local hostname="$1" keyfile="$2"
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
"ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
}
# ssh_pubkey_to_age <pubkeyfile>
# Prints the age public key derived from an ed25519 SSH public key file.
ssh_pubkey_to_age() {
local pubkeyfile="$1"
nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${pubkeyfile}'"
}
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env python3
# Companion to scripts/sync-host-keys.sh. Applies a set of additive edits
# to .sops.yaml via targeted line insertion -- not a full YAML parse and
# re-serialize -- so every untouched byte of the file is guaranteed to stay
# exactly as it was (a full round-trip through a generic YAML library was
# tested and silently reformatted the whole file's indentation style).
#
# Reads a JSON edit plan from stdin:
# {
# "add_keys": [{"host": "lxc-server", "age_key": "age1..."}],
# "add_aliases": [{"host": "lxc-server", "basename": "common.yaml"}],
# "remove_keys": ["lxc-server"],
# "remove_aliases_for_hosts": ["lxc-server"]
# }
# Idempotent: an anchor or alias that's already present/absent is left
# alone -- adding what's already there, or removing what's already gone,
# is a no-op rather than an error.
# Prints a JSON summary of what actually changed to stdout, so the caller
# knows which secrets/*.yaml files need `sops updatekeys` and which don't.
import json
import re
import sys
KEY_LINE_RE = re.compile(r"^ - &\S+ age1")
def add_keys(lines, add_keys):
existing_anchors = set()
for line in lines:
m = re.match(r"^ - &(\S+) age1", line)
if m:
existing_anchors.add(m.group(1))
new = [e for e in add_keys if e["host"] not in existing_anchors]
if not new:
return lines, []
last_idx = None
for i, line in enumerate(lines):
if KEY_LINE_RE.match(line):
last_idx = i
if last_idx is None:
print("ERROR: no ' - &<name> age1...' line found under keys: in .sops.yaml", file=sys.stderr)
sys.exit(1)
insert = [f" - &{e['host']} {e['age_key']}\n" for e in new]
lines = lines[: last_idx + 1] + insert + lines[last_idx + 1 :]
return lines, [e["host"] for e in new]
def add_aliases(lines, add_aliases):
changed_files = []
for e in add_aliases:
host = e["host"]
basename = e["basename"]
target = "path_regex: secrets/" + basename.replace(".", r"\.") + "$"
rule_start = None
for i, line in enumerate(lines):
if target in line:
rule_start = i
break
if rule_start is None:
print(
f"WARNING: no creation_rule for secrets/{basename} in .sops.yaml "
f"(needed by {host}) -- add one manually, then re-run.",
file=sys.stderr,
)
continue
rule_end = len(lines)
for i in range(rule_start + 1, len(lines)):
if re.match(r"^ - path_regex:", lines[i]):
rule_end = i
break
block = lines[rule_start:rule_end]
if any(re.search(r"\*" + re.escape(host) + r"\s*$", line) for line in block):
continue # already present
last_alias_idx = None
last_alias_indent = None
for i in range(rule_start, rule_end):
m = re.match(r"^(\s*)- \*\S+\s*$", lines[i])
if m:
last_alias_idx = i
last_alias_indent = m.group(1)
if last_alias_idx is None:
print(
f"WARNING: creation_rule for secrets/{basename} has no existing "
f"'- *alias' line to anchor the insertion point -- add {host} manually.",
file=sys.stderr,
)
continue
new_line = f"{last_alias_indent}- *{host}\n"
lines = lines[: last_alias_idx + 1] + [new_line] + lines[last_alias_idx + 1 :]
changed_files.append(basename)
return lines, changed_files
def remove_keys(lines, hosts):
hosts = set(hosts)
removed = []
kept = []
for line in lines:
m = re.match(r"^ - &(\S+) age1", line)
if m and m.group(1) in hosts:
removed.append(m.group(1))
continue
kept.append(line)
return kept, removed
def remove_aliases_for_hosts(lines, hosts):
hosts = set(hosts)
if not hosts:
return lines, []
changed_files = []
result = []
current_basename = None
current_block_changed = False
def flush():
if current_block_changed and current_basename:
changed_files.append(current_basename)
for line in lines:
if re.match(r"^ - path_regex:", line):
flush()
current_block_changed = False
m = re.search(r"path_regex: secrets/(.+)\$", line)
current_basename = m.group(1).replace(r"\.", ".") if m else None
result.append(line)
continue
if current_basename is not None and any(
re.search(r"\*" + re.escape(h) + r"\s*$", line) for h in hosts
):
current_block_changed = True
continue # drop this alias line
result.append(line)
flush()
return result, changed_files
def main():
sops_path = sys.argv[1]
edits = json.load(sys.stdin)
with open(sops_path) as f:
lines = f.readlines()
lines, added_keys = add_keys(lines, edits.get("add_keys", []))
lines, added_alias_files = add_aliases(lines, edits.get("add_aliases", []))
lines, removed_keys = remove_keys(lines, edits.get("remove_keys", []))
lines, removed_alias_files = remove_aliases_for_hosts(
lines, edits.get("remove_aliases_for_hosts", [])
)
with open(sops_path, "w") as f:
f.writelines(lines)
changed_files = sorted(set(added_alias_files) | set(removed_alias_files))
json.dump(
{
"added_keys": added_keys,
"removed_keys": removed_keys,
"changed_secrets_files": changed_files,
},
sys.stdout,
)
print()
if __name__ == "__main__":
main()
@@ -1,13 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Generates a new machine's SSH host key by an arbitrary name, before it # Generates a new machine's SSH host key ahead of installing it, so
# necessarily has a flake target yet -- prints the .sops.yaml snippet to # sops-nix has something to derive an age key from before the target
# add by hand. For any host that already has a flake target, # ever boots.
# scripts/secrets/sync-host-keys.sh <target> does this same job plus the
# .sops.yaml/key_groups registration and re-encryption automatically; use
# this script only to pre-generate a key ahead of adding the flake target
# itself.
# #
# Why a host key is needed at all: sops-nix derives each host's decryption key from # Why this is needed: sops-nix derives each host's decryption key from
# its own /etc/ssh/ssh_host_ed25519_key at *activation* time, but that # its own /etc/ssh/ssh_host_ed25519_key at *activation* time, but that
# activation runs before systemd would otherwise generate this key on # activation runs before systemd would otherwise generate this key on
# first boot (sshd-keygen is a normal systemd service gated behind # first boot (sshd-keygen is a normal systemd service gated behind
@@ -22,17 +18,14 @@
# new machine. # new machine.
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/secrets/prepare-host-key.sh <hostname>}" hostname="${1:?usage: scripts/prepare-host-key.sh <hostname> [path-to-nixos-repo]}"
sops_yaml="${repo_root}/.sops.yaml" nixos_repo="${2:-$repo_root}"
sops_yaml="${nixos_repo}/.sops.yaml"
if [[ ! -f "$sops_yaml" ]]; then if [[ ! -f "$sops_yaml" ]]; then
echo "ERROR: $sops_yaml not found — is this script still under nixos/scripts/?" >&2 echo "ERROR: $sops_yaml not found. Pass the nixos repo path as the 2nd argument." >&2
exit 1 exit 1
fi fi
@@ -45,10 +38,9 @@ if [[ -f "$keyfile" ]]; then
exit 1 exit 1
fi fi
nix_extra_opts nix-shell -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
generate_host_ed25519_key "$hostname" "$keyfile"
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")" age_pub="$(nix-shell -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")"
cat <<EOF cat <<EOF
@@ -63,7 +55,7 @@ Generated: ${keyfile}(.pub)
nix-cache/server.) nix-cache/server.)
=== 3. Re-encrypt every secrets file you just added it to === === 3. Re-encrypt every secrets file you just added it to ===
nix-shell -p sops --run 'sops updatekeys ${repo_root}/secrets/common.yaml' nix-shell -p sops --run 'sops updatekeys ${nixos_repo}/secrets/common.yaml'
=== 4. Commit + push this repo so the flake build picks up the new recipient === === 4. Commit + push this repo so the flake build picks up the new recipient ===
@@ -71,8 +63,8 @@ Generated: ${keyfile}(.pub)
a) Rebuild the installer image with all host-keys/ baked in (see a) Rebuild the installer image with all host-keys/ baked in (see
docs/auto-installer.md): docs/auto-installer.md):
NIXOS_HOST_KEYS_DIR="${keydir}" nix build .#iso --impure NIXOS_HOST_KEYS_DIR="${keydir}" nix build .#iso --impure
(or .#pxe — --impure is required since host-keys/ is gitignored and (or .#lxc / .#pxe / .#all — --impure is required since host-keys/
flakes can't see it otherwise) is gitignored and flakes can't see it otherwise)
b) Or, for an image already built without keys, scp it in after boot: b) Or, for an image already built without keys, scp it in after boot:
scp ${keyfile}{,.pub} root@<target-ip>:/root/host-keys/ scp ${keyfile}{,.pub} root@<target-ip>:/root/host-keys/
-221
View File
@@ -1,221 +0,0 @@
#!/usr/bin/env bash
# Ad hoc clone of a single VM/CT from pve1 (production) to pve-test
# (sandbox), via vzdump + qmrestore/pct restore -- not a general-purpose
# backup tool, just a quick "give me a disposable copy of this thing on
# pve-test" for testing against real-ish data without touching prod.
#
# Flow:
# 1. vzdump the resource on pve1 into its "local" storage (--mode
# snapshot by default, so the source keeps running throughout --
# see --mode below for when that's not possible).
# 2. Stream the resulting archive straight from pve1 to pve-test
# (ssh pve1 cat ... | ssh pve-test cat > ...) -- this machine is
# just the relay, no separate on-disk staging copy here.
# 3. qmrestore / pct restore it on pve-test under --new-vmid (default:
# same VMID as the source -- pve-test is a separate node/cluster, so
# no collision unless that VMID is already in use there too).
# Always restored with --unique 1 (fresh MAC addresses) since the
# source is typically still running on the same LAN -- restoring
# with the *same* MAC would put two live guests on the wire with
# identical hardware addresses.
# 4. Delete the vzdump archive from pve1's local storage and the
# relayed copy on pve-test, so neither node accumulates ad hoc
# backup files from this script. Only the pve1 original is
# preserved on any failure after step 1, so a failed
# transfer/restore can be retried without re-running the backup.
#
# This script's own defaults are pve1 -> pve-test, unlike
# create-proxmox-resource.sh's --node (which defaults to production) --
# see CLAUDE.md's "Two Proxmox nodes" section. pve1 is only ever touched
# here after typing the source VMID back to confirm; pve-test is treated
# as disposable, matching this repo's usual policy for that node.
#
# See --help for the full option list.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/confirm.sh
source "${repo_root}/scripts/lib/confirm.sh"
usage() {
cat <<EOF
Usage: $0 --vmid <n> [options]
--vmid <n> Required: VMID on the source node to clone.
Kind (qemu VM vs LXC CT) is auto-detected.
--new-vmid <n> VMID to restore as on the target node
(default: same as --vmid).
--mode snapshot|suspend|stop
vzdump backup mode (default: snapshot -- the
source resource keeps running throughout;
requires snapshot-capable storage, e.g.
ZFS/LVM-thin/Ceph/qcow2). Fall back to
"suspend" (brief pause) or "stop" (source
goes down for the duration) if the source's
storage doesn't support live snapshots --
vzdump's own error will say so.
--source-node <host> (default: \$PVE1_HOST, ${PVE1_HOST})
--target-node <host> (default: \$PVE_TEST_HOST, ${PVE_TEST_HOST})
--source-storage <pool> Where vzdump writes the backup on the
source node (default: local).
--target-storage <pool> Where the restored disk/rootfs lands on
the target node (default: \$PROXMOX_STORAGE, ${PROXMOX_STORAGE}).
--keep-backup Don't delete the vzdump archive from
either node afterward (debugging aid).
--yes Skip the typed VMID confirmation
before touching the source node.
--dry-run Print the full plan and skip every
mutating step (vzdump, transfer,
restore, delete) and the confirm
prompt. Still makes read-only SSH
calls to look up the source kind
and check the target VMID is free
-- harmless on either node.
-h, --help
EOF
}
vmid=""
new_vmid=""
mode="snapshot"
source_node="$PVE1_HOST"
target_node="$PVE_TEST_HOST"
source_storage="local"
target_storage="$PROXMOX_STORAGE"
keep_backup=0
skip_confirm=0
dry_run=0
while [[ $# -gt 0 ]]; do
case "$1" in
--vmid) vmid="$2"; shift 2 ;;
--new-vmid) new_vmid="$2"; shift 2 ;;
--mode) mode="$2"; shift 2 ;;
--source-node) source_node="$2"; shift 2 ;;
--target-node) target_node="$2"; shift 2 ;;
--source-storage) source_storage="$2"; shift 2 ;;
--target-storage) target_storage="$2"; shift 2 ;;
--keep-backup) keep_backup=1; shift ;;
--yes) skip_confirm=1; shift ;;
--dry-run) dry_run=1; shift ;;
-h | --help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
done
if [[ -z "$vmid" ]]; then
echo "ERROR: --vmid is required." >&2
usage >&2
exit 1
fi
if [[ "$mode" != "snapshot" && "$mode" != "suspend" && "$mode" != "stop" ]]; then
echo "ERROR: --mode must be snapshot, suspend, or stop." >&2
exit 1
fi
[[ -z "$new_vmid" ]] && new_vmid="$vmid"
source_target="${PROXMOX_SSH_USER}@${source_node}"
target_target="${PROXMOX_SSH_USER}@${target_node}"
# No dry-run wrapper needed for the calls below: every mutating step
# (vzdump, transfer, restore, delete) is reached only after the --dry-run
# early-exit further down, so a plain `ssh` call is never in the dry-run
# path.
# --- identify the resource kind on the source node -----------------------
echo "==> Looking up VMID ${vmid} on ${source_node}..."
kind=""
if ssh "$source_target" "qm status ${vmid}" >/dev/null 2>&1; then
kind="vm"
elif ssh "$source_target" "pct status ${vmid}" >/dev/null 2>&1; then
kind="lxc"
else
echo "ERROR: VMID ${vmid} doesn't exist on ${source_node} as either a VM or CT." >&2
exit 1
fi
echo "VMID ${vmid} on ${source_node} is a ${kind}."
# --- refuse to clobber an existing resource on the target node -----------
if ssh "$target_target" "qm status ${new_vmid}" >/dev/null 2>&1 \
|| ssh "$target_target" "pct status ${new_vmid}" >/dev/null 2>&1; then
echo "ERROR: VMID ${new_vmid} already exists on ${target_node}. Pass --new-vmid" >&2
echo "with a free ID, or remove the existing resource there first." >&2
exit 1
fi
echo
echo "Plan:"
echo " source: ${kind} VMID ${vmid} on ${source_node} (storage: ${source_storage}, mode: ${mode})"
echo " target: VMID ${new_vmid} on ${target_node} (storage: ${target_storage}, fresh MAC via --unique)"
[[ "$keep_backup" -eq 1 ]] && echo " backup archives are kept on both nodes afterward (--keep-backup)"
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] No backup, transfer, restore, or delete was performed."
exit 0
fi
if [[ "$skip_confirm" -ne 1 ]]; then
echo
if ! confirm_typed "$vmid" "Type the source VMID (${vmid}) to confirm backing it up from ${source_node}: "; then
echo "Cancelled -- input didn't match ${vmid}." >&2
exit 1
fi
fi
# --- vzdump on the source node --------------------------------------------
echo
echo "==> Backing up VMID ${vmid} on ${source_node} (mode=${mode}, storage=${source_storage})..."
vzdump_log="$(ssh "$source_target" \
"vzdump ${vmid} --mode ${mode} --storage ${source_storage} --compress zstd" 2>&1)" \
|| {
echo "$vzdump_log" >&2
echo "ERROR: vzdump failed on ${source_node}." >&2
exit 1
}
echo "$vzdump_log"
archive="$(echo "$vzdump_log" | grep -oP "creating vzdump archive '\K[^']+" | tail -n1)"
if [[ -z "$archive" ]]; then
echo "ERROR: couldn't find the archive path in vzdump's output above." >&2
exit 1
fi
archive_basename="$(basename "$archive")"
target_tmp_archive="/var/tmp/${archive_basename}"
echo "Archive: ${archive}"
# Always clean up the relayed copy on the target node, success or failure
# -- it's only ever a working copy, restored or not.
cleanup_target_tmp() {
if [[ "$keep_backup" -ne 1 ]]; then
ssh "$target_target" "rm -f '${target_tmp_archive}'" >/dev/null 2>&1 || true
fi
}
trap cleanup_target_tmp EXIT
# --- relay the archive from source to target ------------------------------
echo
echo "==> Transferring archive to ${target_node}..."
ssh "$source_target" "cat '${archive}'" | ssh "$target_target" "cat > '${target_tmp_archive}'"
# --- restore on the target node --------------------------------------------
echo
echo "==> Restoring as VMID ${new_vmid} on ${target_node} (storage=${target_storage})..."
if [[ "$kind" == "vm" ]]; then
ssh "$target_target" "qmrestore '${target_tmp_archive}' ${new_vmid} --storage ${target_storage} --unique 1"
else
ssh "$target_target" "pct restore ${new_vmid} '${target_tmp_archive}' --storage ${target_storage} --unique 1"
fi
# --- clean up the source backup now that the restore succeeded -----------
if [[ "$keep_backup" -ne 1 ]]; then
echo
echo "==> Deleting backup archive from ${source_node}'s ${source_storage} storage..."
ssh "$source_target" "rm -f '${archive}' '${archive}.notes' '${archive}.log'" >/dev/null 2>&1 || true
fi
echo
echo "Done. VMID ${new_vmid} (${kind}) is now on ${target_node}, cloned from" \
"VMID ${vmid} on ${source_node}."
@@ -1,199 +0,0 @@
#!/usr/bin/env bash
# Points a non-NixOS Debian machine's Nix install at nix-cache: adds it as
# a substituter (with cache.nixos.org kept as fallback) and, once the
# remote-builder private key is installed, as a distributed-build machine
# too.
#
# This is the non-NixOS equivalent of modules/nix-cache/client.nix +
# modules/nix-cache/remote-builder-client.nix -- those two only apply to
# hosts built from this flake. A plain Debian box with Nix installed has no
# NixOS module system to pick that config up, so this edits nix.conf by hand.
#
# Two modes depending on who runs it:
#
# root (multi-user / daemon install):
# Writes /etc/nix/nix.conf, /etc/ssh/ssh_known_hosts, restarts nix-daemon.
# Requires /etc/nix/nix.conf to already exist (i.e. nix-daemon is set up).
# Run as: sudo ./configure-nix-cache-client.sh [options]
#
# non-root (single-user install):
# Writes ~/.config/nix/nix.conf, ~/.ssh/known_hosts. No daemon to restart.
# Run as: ./configure-nix-cache-client.sh [options]
#
# The values below mirror variables.nix / modules/nix-cache/client.nix in
# this repo -- update both if nix-cache is ever rebuilt with a new host
# key or the cache signing key is rotated (see docs/nix-cache.md).
#
# REMOTE_BUILDER_KEY defaults to the running user's default SSH identity
# (root: /root/.ssh/id_ed25519, other user: ~/.ssh/id_ed25519). That key
# must be listed in vars.remoteBuilderAuthorizedKeys in this repo and
# nix-cache rebuilt before remote building works.
#
# Usage:
# ./configure-nix-cache-client.sh [--dry-run] [--no-remote-builder] [--no-restart]
#
# Env overrides (defaults match variables.nix):
# NIX_CACHE_HOST, NIX_CACHE_HOST_KEY, REMOTE_BUILDER_USER, REMOTE_BUILDER_KEY
set -euo pipefail
: "${NIX_CACHE_HOST:=nix-cache}"
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuHUxGNH6ei3BZD+EfZs3l4X8uJNcjQiOsM/G4yo4O/ lxc-nix-cache}"
: "${REMOTE_BUILDER_USER:=nixremote}"
CACHE_PUB_KEY="cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
FALLBACK_URL="https://cache.nixos.org/"
FALLBACK_PUB_KEY="cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
MARKER_BEGIN="# BEGIN nix-cache client config (configure-nix-cache-client.sh)"
MARKER_END="# END nix-cache client config"
# Mode: root uses system-wide paths and restarts the daemon; non-root uses
# user-level paths and has no daemon to restart.
if [[ "$EUID" -eq 0 ]]; then
install_mode="multi"
NIX_CONF="/etc/nix/nix.conf"
KNOWN_HOSTS="/etc/ssh/ssh_known_hosts"
: "${REMOTE_BUILDER_KEY:=/root/.ssh/id_ed25519}"
else
install_mode="single"
NIX_CONF="${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf"
KNOWN_HOSTS="$HOME/.ssh/known_hosts"
: "${REMOTE_BUILDER_KEY:=$HOME/.ssh/id_ed25519}"
fi
dry_run=0
with_remote_builder=1
restart_daemon=1
for arg in "$@"; do
case "$arg" in
--dry-run) dry_run=1 ;;
--no-remote-builder) with_remote_builder=0 ;;
--no-restart) restart_daemon=0 ;;
-h|--help)
sed -n '2,37p' "$0"
exit 0
;;
*)
echo "ERROR: unknown argument: $arg" >&2
exit 1
;;
esac
done
if ! command -v nix >/dev/null 2>&1; then
echo "ERROR: no 'nix' binary on PATH -- install the Nix package manager first." >&2
exit 1
fi
if [[ "$install_mode" == "multi" && ! -f "$NIX_CONF" ]]; then
echo "ERROR: $NIX_CONF not found -- expected an existing multi-user Nix install." >&2
exit 1
fi
# Single-user: create the config file if it doesn't exist yet.
if [[ "$install_mode" == "single" && "$dry_run" -eq 0 ]]; then
mkdir -p "$(dirname "$NIX_CONF")"
[[ -f "$NIX_CONF" ]] || touch "$NIX_CONF"
fi
builder_line=""
if [[ "$with_remote_builder" -eq 1 ]]; then
if [[ -f "$REMOTE_BUILDER_KEY" ]]; then
case "$(uname -m)" in
x86_64) nix_system="x86_64-linux" ;;
aarch64) nix_system="aarch64-linux" ;;
*)
echo "WARNING: unrecognized architecture '$(uname -m)' -- skipping remote builder, keeping substituter config." >&2
with_remote_builder=0
;;
esac
if [[ "$with_remote_builder" -eq 1 ]]; then
builder_line="builders = ssh://${REMOTE_BUILDER_USER}@${NIX_CACHE_HOST} ${nix_system} ${REMOTE_BUILDER_KEY} 4 2 big-parallel,kvm,nixos-test,benchmark"
fi
else
echo "WARNING: $REMOTE_BUILDER_KEY not found -- skipping remote builder config (substituter still configured)." >&2
echo " See docs/nix-cache.md 'Remote builder SSH keys' for how to install it, then re-run this script." >&2
with_remote_builder=0
fi
fi
block="$(cat <<EOF
$MARKER_BEGIN
extra-substituters = http://${NIX_CACHE_HOST} ${FALLBACK_URL}
extra-trusted-public-keys = ${CACHE_PUB_KEY} ${FALLBACK_PUB_KEY}
EOF
)"
if [[ "$with_remote_builder" -eq 1 ]]; then
block="${block}
builders-use-substitutes = true
${builder_line}"
fi
block="${block}
$MARKER_END"
echo "== nix.conf block to install ($NIX_CONF) =="
echo "$block"
echo "================================"
if [[ "$dry_run" -eq 1 ]]; then
echo "(--dry-run: not writing $NIX_CONF)"
else
tmp_conf="$(mktemp)"
trap 'rm -f "$tmp_conf"' EXIT
if grep -qF "$MARKER_BEGIN" "$NIX_CONF"; then
awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" -v block="$block" '
$0 == begin { print block; skip = 1; next }
$0 == end { skip = 0; next }
skip { next }
{ print }
' "$NIX_CONF" > "$tmp_conf"
else
cp "$NIX_CONF" "$tmp_conf"
printf '\n%s\n' "$block" >> "$tmp_conf"
fi
cp "$NIX_CONF" "${NIX_CONF}.bak.$(date +%Y%m%d%H%M%S)"
install -m 0644 "$tmp_conf" "$NIX_CONF"
echo "Updated $NIX_CONF (backup saved alongside it)."
fi
if [[ "$with_remote_builder" -eq 1 ]]; then
known_hosts_line="${NIX_CACHE_HOST} ${NIX_CACHE_HOST_KEY}"
if [[ "$dry_run" -eq 1 ]]; then
echo "(--dry-run: would ensure this line is present in $KNOWN_HOSTS)"
echo " $known_hosts_line"
else
mkdir -p "$(dirname "$KNOWN_HOSTS")"
touch "$KNOWN_HOSTS"
if ! grep -qF "$known_hosts_line" "$KNOWN_HOSTS" 2>/dev/null; then
echo "$known_hosts_line" >> "$KNOWN_HOSTS"
echo "Added nix-cache's SSH host key to $KNOWN_HOSTS."
fi
fi
fi
# Only restart the daemon for multi-user installs -- single-user has no daemon.
if [[ "$dry_run" -eq 0 && "$restart_daemon" -eq 1 && "$install_mode" == "multi" ]]; then
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nix-daemon 2>/dev/null; then
systemctl restart nix-daemon
echo "Restarted nix-daemon to pick up the new config."
else
echo "nix-daemon not managed by systemd (or not running) -- restart it manually to pick up the new config."
fi
fi
cat <<EOF
Done. Verify with:
curl http://${NIX_CACHE_HOST}/nix-cache-info
nix show-config | grep -E 'substituters|trusted-public-keys|builders'
EOF
if [[ "$with_remote_builder" -eq 1 ]]; then
cat <<EOF
ssh -i ${REMOTE_BUILDER_KEY} ${REMOTE_BUILDER_USER}@${NIX_CACHE_HOST} nix-store --version
nix build nixpkgs#hello -L
EOF
fi
-860
View File
@@ -1,860 +0,0 @@
#!/usr/bin/env bash
# Creates new Proxmox VMs/LXC containers from this flake, and reconfigures
# existing ones -- the manual workflows in docs/proxmox-images.md (VM) and
# docs/auto-installer.md's "LXC hosts" section (container), automated.
#
# Images are built directly on the Proxmox node (PROXMOX_REMOTE_REPO_DIR /
# --remote-repo-dir in scripts/env.sh), not on whatever machine runs this
# script -- there's no multi-gigabyte image to transfer afterward. The first
# time a node doesn't have that repo path yet, it's bootstrapped: cloned from
# this checkout's own `origin` remote, then scripts/codex-setup.sh installs
# the build tooling (Nix, etc.). Every run after that just `git pull`s it and
# copies over the locally-managed host-keys/ (gitignored, so a git pull
# alone wouldn't carry it) before building.
#
# --node (default: $PROXMOX_HOST, see scripts/env.sh) picks which of the two
# LAN Proxmox nodes this runs against: production, pve1.sweet.home
# ($PVE1_HOST, PROXMOX_HOST's own default), or the sandbox node,
# pve-test.sweet.home ($PVE_TEST_HOST) -- pass --node "$PVE_TEST_HOST" (or
# set PROXMOX_HOST=$PVE_TEST_HOST) to target the sandbox instead. See
# CLAUDE.md's "Two Proxmox nodes" section: an agent session should default
# to pve-test and only touch pve1 when the operator has explicitly said so
# for the current task -- this script itself doesn't enforce that (its own
# default is production, matching this repo's behavior before pve-test
# existed), it's a policy for whoever/whatever is driving it.
#
# Usage:
# scripts/proxmox/create-proxmox-resource.sh --type lxc|vm --host <name> [options]
# scripts/proxmox/create-proxmox-resource.sh --type lxc|vm --list
# scripts/proxmox/create-proxmox-resource.sh --modify --vmid <n> [--cores N] [--memory MB] [--grow-disk GB]
#
# SAFETY:
# - The default (create) mode only ever creates a NEW resource -- it
# refuses to run if the target VMID already exists on the node, or if
# a VM/CT identified as --host already exists under any other VMID
# (checked live against the node; --allow-duplicate-host overrides).
# - --allow-duplicate-host distinguishes an exact match (same --type
# *and* --host, e.g. re-running --type lxc --host docker while an
# lxc-docker container already exists -- almost always a redeploy of
# the same target to pick up a rebuilt image) from a cross-type match
# (a different platform sharing the same host identity, e.g. a
# proxmox-docker VM coexisting with lxc-docker). Only the exact match
# is destroyed and replaced, after typing the hostname back to
# confirm (outside --dry-run) -- a cross-type match is always left
# untouched, matching-or-not.
# - --modify only ever touches a resource you name explicitly via
# --vmid, shows exactly what will change first, and (outside
# --dry-run) always requires typing that VMID back to confirm before
# anything is sent to the node. There is no bulk/implicit modify.
# - Outside of --allow-duplicate-host's exact-match replace above,
# neither mode can start/stop/delete a resource.
#
# See --help for the full option list.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/nix-eval.sh
source "${repo_root}/scripts/lib/nix-eval.sh"
# shellcheck source=../lib/confirm.sh
source "${repo_root}/scripts/lib/confirm.sh"
sync_keys="${repo_root}/scripts/secrets/sync-host-keys.sh"
usage() {
cat <<EOF
Usage: $0 --type lxc|vm --host <name> [options] (create)
$0 --type lxc|vm --list (list --host values)
$0 --modify --vmid <n> [options] (reconfigure)
Create mode (default):
--type lxc|vm lxc = container, built as a CT template tarball.
vm = VM, built as a Disko .raw disk image (UEFI/OVMF).
--host <name> Which host identity to deploy -- matches
config.networking.hostName (server, docker,
nix-cache, nixos, pxe-boot, nix-minimal). Use
--list to see what's available for --type.
--name <name> Proxmox display name/hostname (default: --host's
value, e.g. nix-cache -- for lxc this becomes the
guest's real networking.hostName too, since
proxmoxLXC.manageHostName pulls it from Proxmox's
own container config, so it must match host.nix
regardless of build type)
--vmid <n> Numeric VMID (default: next free, via
\`pvesh get /cluster/nextid\` on the node).
Refuses to run if this ID already exists.
--disk-size <GB> lxc only: rootfs size for \`pct create\`
(default: \$PROXMOX_DEFAULT_LXC_DISK_GB, ${PROXMOX_DEFAULT_LXC_DISK_GB}).
--image <path> Use this local image/tarball (uploaded to the
node via scp) instead of checking the node /
building one there from the flake.
--force-rebuild Skip the "does the node already have this
image" check -- always build fresh and
overwrite what's there.
--remote-repo-dir <path> Where this flake repo lives (or gets
cloned) on the node, and is built from
(default: \$PROXMOX_REMOTE_REPO_DIR, ${PROXMOX_REMOTE_REPO_DIR}).
--allow-duplicate-host Required if a VM/CT identified as --host
already exists on the node (checked live via
qm/pct, not any file in this repo) --
otherwise refused, since it'd share that
host's hostName/hostId. An existing resource
of this *same* --type (e.g. re-running --type
lxc --host docker over an existing lxc-docker)
is destroyed and replaced, after confirming --
a different --type sharing the same --host
(e.g. a proxmox-docker VM) is always left
untouched.
Modify mode (reconfigure an EXISTING resource -- requires --modify):
--modify Switch to modify mode.
--vmid <n> Required: which existing resource to change.
Type/VM-vs-CT is auto-detected on the node.
--grow-disk <GB> Grow the primary disk by this many GB
(qm/pct resize; Proxmox only supports
growing, never shrinking, an existing disk).
At least one of --cores / --memory / --grow-disk is required. Always
prints the current -> new values and requires typing the VMID back to
confirm, even outside --dry-run.
Shared:
--cores <n> create: default \$PROXMOX_DEFAULT_CORES (${PROXMOX_DEFAULT_CORES}).
modify: omit to leave unchanged.
--memory <MB> create: default \$PROXMOX_DEFAULT_MEMORY_MB (${PROXMOX_DEFAULT_MEMORY_MB}).
modify: omit to leave unchanged.
--swap <MB> lxc only, create time: \`--memory\` doesn't
touch swap -- it silently stays at Proxmox's
own 512M default otherwise. (default: matches
whatever --memory resolves to)
--storage <pool> (default: \$PROXMOX_STORAGE, ${PROXMOX_STORAGE})
--iso-storage <pool> (default: \$PROXMOX_ISO_STORAGE, ${PROXMOX_ISO_STORAGE})
--bridge <bridge> (default: \$PROXMOX_BRIDGE, ${PROXMOX_BRIDGE})
--node <host> Proxmox node to SSH into (default:
\$PROXMOX_HOST, ${PROXMOX_HOST} --
production; the sandbox node is
\$PVE_TEST_HOST, ${PVE_TEST_HOST}).
--dry-run Print the full plan; touch nothing
local or remote, no prompts.
-h, --help
Config for --storage/--bridge/--node/etc. lives in scripts/env.sh -- edit
that instead of passing the same flag every time.
EOF
}
dry_run=0
modify=0
type=""
host=""
name=""
vmid=""
cores=""
memory=""
swap=""
disk_size=""
grow_disk=""
image=""
storage="$PROXMOX_STORAGE"
iso_storage="$PROXMOX_ISO_STORAGE"
bridge="$PROXMOX_BRIDGE"
node="$PROXMOX_HOST"
remote_repo_dir="$PROXMOX_REMOTE_REPO_DIR"
do_list=0
allow_duplicate_host=0
force_rebuild=0
while [[ $# -gt 0 ]]; do
case "$1" in
--type) type="$2"; shift 2 ;;
--host) host="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--vmid) vmid="$2"; shift 2 ;;
--cores) cores="$2"; shift 2 ;;
--memory) memory="$2"; shift 2 ;;
--swap) swap="$2"; shift 2 ;;
--disk-size) disk_size="$2"; shift 2 ;;
--grow-disk) grow_disk="$2"; shift 2 ;;
--image) image="$2"; shift 2 ;;
--storage) storage="$2"; shift 2 ;;
--iso-storage) iso_storage="$2"; shift 2 ;;
--bridge) bridge="$2"; shift 2 ;;
--node) node="$2"; shift 2 ;;
--remote-repo-dir) remote_repo_dir="$2"; shift 2 ;;
--list) do_list=1; shift ;;
--allow-duplicate-host) allow_duplicate_host=1; shift ;;
--force-rebuild) force_rebuild=1; shift ;;
--modify) modify=1; shift ;;
--dry-run) dry_run=1; shift ;;
-h | --help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
done
ssh_target="${PROXMOX_SSH_USER}@${node}"
# Proxmox tools (pvesh, qm, pct) require root access to the cluster IPC
# socket. When SSH-ing as a non-root user with sudo, prefix every remote
# Proxmox command with sudo.
sudo_prefix=""
sudo_display=""
if [[ "$PROXMOX_SSH_USER" != "root" ]]; then
sudo_prefix="sudo"
sudo_display="sudo "
fi
remote() {
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] ssh ${ssh_target} -- $*"
else
ssh "$ssh_target" "$@"
fi
}
# ============================================================ modify mode
cmd_modify() {
if [[ -z "$vmid" ]]; then
echo "ERROR: --modify requires --vmid." >&2
exit 1
fi
if [[ -z "$cores" && -z "$memory" && -z "$grow_disk" ]]; then
echo "ERROR: --modify needs at least one of --cores / --memory / --grow-disk." >&2
exit 1
fi
echo "Looking up VMID ${vmid} on ${node}..."
local kind current_cores current_memory disk_key
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1; then
kind="vm"
disk_key="scsi0"
elif ssh "$ssh_target" "${sudo_prefix} pct status ${vmid}" >/dev/null 2>&1; then
kind="lxc"
disk_key="rootfs"
else
echo "ERROR: VMID ${vmid} doesn't exist on ${node} -- nothing to modify." >&2
exit 1
fi
local config_cmd="${sudo_prefix} qm config ${vmid}"
[[ "$kind" == "lxc" ]] && config_cmd="${sudo_prefix} pct config ${vmid}"
local current_config
current_config="$(ssh "$ssh_target" "$config_cmd")"
current_cores="$(echo "$current_config" | grep -oP '^cores:\s*\K\S+' || echo '?')"
current_memory="$(echo "$current_config" | grep -oP '^memory:\s*\K\S+' || echo '?')"
echo
echo "VMID ${vmid} is a ${kind} on ${node}. Planned changes:"
[[ -n "$cores" ]] && echo " cores: ${current_cores} -> ${cores}"
[[ -n "$memory" ]] && echo " memory: ${current_memory} MB -> ${memory} MB"
[[ -n "$grow_disk" ]] && echo " ${disk_key}: grow by +${grow_disk}G (Proxmox can only grow, not shrink, an existing disk)"
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] Nothing was changed."
return
fi
echo
if ! confirm_typed "$vmid" "Type the VMID (${vmid}) to confirm these changes: "; then
echo "Cancelled -- input didn't match ${vmid}."
exit 1
fi
local set_cmd="${sudo_prefix} qm set"
local resize_cmd="${sudo_prefix} qm resize"
[[ "$kind" == "lxc" ]] && set_cmd="${sudo_prefix} pct set" && resize_cmd="${sudo_prefix} pct resize"
if [[ -n "$cores" || -n "$memory" ]]; then
local args=""
[[ -n "$cores" ]] && args="${args} --cores ${cores}"
[[ -n "$memory" ]] && args="${args} --memory ${memory}"
remote "${set_cmd} ${vmid}${args}"
fi
if [[ -n "$grow_disk" ]]; then
remote "${resize_cmd} ${vmid} ${disk_key} +${grow_disk}G"
fi
echo
echo "Done. VMID ${vmid} updated."
}
if [[ "$modify" -eq 1 ]]; then
cmd_modify
exit 0
fi
# ============================================================= create mode
if [[ "$type" != "lxc" && "$type" != "vm" ]]; then
echo "ERROR: --type must be 'lxc' or 'vm'." >&2
usage >&2
exit 1
fi
platform_prefix="lxc"
[[ "$type" == "vm" ]] && platform_prefix="proxmox"
[[ -z "$cores" ]] && cores="$PROXMOX_DEFAULT_CORES"
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
# --- discover / resolve the flake target from --host --------------------
# Emits "<target>\t<hostName>" pairs for every ${platform_prefix}-* flake
# target -- the one source both --list and the --host lookup below read
# from, so they can never see a different set of targets from each other.
targets_for_platform() {
local target
for target in $(list_flake_targets "$repo_root" 2>/dev/null | grep -- "^${platform_prefix}-"); do
printf '%s\t%s\n' "$target" "$(flake_target_hostname "$repo_root" "$target")"
done
}
list_hosts() {
local target hostname
while IFS=$'\t' read -r target hostname; do
printf ' %-12s -> %s\n' "$hostname" "$target"
done < <(targets_for_platform)
}
if [[ "$do_list" -eq 1 ]]; then
echo "Available --host values for --type ${type}:"
list_hosts
exit 0
fi
if [[ -z "$host" ]]; then
echo "ERROR: --host is required (or use --list to see options)." >&2
exit 1
fi
flake_target=""
while IFS=$'\t' read -r target hostname; do
if [[ "$hostname" == "$host" ]]; then
flake_target="$target"
break
fi
done < <(targets_for_platform)
if [[ -z "$flake_target" ]]; then
echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2
echo "Available:" >&2
list_hosts >&2
exit 1
fi
# The container/VM's real identity is --host (e.g. "nix-cache"), validated
# above against config.networking.hostName -- not the flake target name
# (e.g. "lxc-nix-cache"), which is build-type-specific and only exists to
# pick which platform variant to build. Defaulting --name to the flake
# target would make lxc's --hostname (which proxmoxLXC.manageHostName
# feeds straight into the guest's real hostname) disagree with host.nix.
[[ -z "$name" ]] && name="$host"
# --- refuse to duplicate a host that's already live on the node ---------
# Queries the node itself (qm/pct's own name/hostname config), not any
# static list in this repo -- a file can't track whether a resource still
# actually exists, and this used to be checked against variables.nix's
# deployedTargets, which drifted stale (it kept naming a VM as "the real
# deployment" well after that VM had been destroyed, blocking its own
# redeploy) until that list was dropped in favour of this live check. This
# only catches guests identified with the default --name (== --host, what
# this script itself always uses unless --name is overridden) -- a guest
# manually renamed on the node afterwards wouldn't match, but nothing here
# creates guests that way.
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] would check ${node} for an existing VM/CT identified as '${host}'"
if [[ "$allow_duplicate_host" -eq 1 ]]; then
echo "[dry-run] --allow-duplicate-host: an existing ${type} named '${host}' would be" \
"destroyed and replaced; a different-type match would be left untouched"
fi
else
echo
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
ssh_check_status=0
existing="$(ssh "$ssh_target" bash -s -- "$host" "$sudo_prefix" <<'REMOTE_SCRIPT'
target="$1"
sudo_pfx="$2"
for id in $($sudo_pfx qm list 2>/dev/null | awk 'NR>1{print $1}'); do
n="$($sudo_pfx qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
[[ "$n" == "$target" ]] && echo "vm ${id} ${n}"
done
for id in $($sudo_pfx pct list 2>/dev/null | awk 'NR>1{print $1}'); do
n="$($sudo_pfx pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
done
exit 0
REMOTE_SCRIPT
)" || ssh_check_status=$?
if [[ "$ssh_check_status" -ne 0 ]]; then
echo "ERROR: couldn't reach ${node} (ssh exited ${ssh_check_status}) to check for an" >&2
echo "existing '${host}' resource -- refusing to guess. Fix connectivity and retry," >&2
echo "or pass --allow-duplicate-host if you're sure none exists (this skips the" >&2
echo "check entirely)." >&2
exit 1
fi
# Split into "exact" (same resource kind as --type -- i.e. literally this
# same host+platform combo already exists, almost always a redeploy of
# the same target to test a rebuilt image) vs "cross-type" (a different
# platform sharing this host identity, e.g. a stopped proxmox-docker VM
# coexisting with an lxc-docker container -- a deliberate, valid setup
# this script has never managed and still won't). Read via a herestring
# (not a pipe) so the appends below survive outside the loop.
this_kind="$type"
exact_matches=""
cross_matches=""
if [[ -n "$existing" ]]; then
while read -r kind id n; do
[[ -z "$kind" ]] && continue
if [[ "$kind" == "$this_kind" ]]; then
exact_matches+="${kind} ${id} ${n}"$'\n'
else
cross_matches+="${kind} ${id} ${n}"$'\n'
fi
done <<<"$existing"
fi
if [[ -n "$exact_matches" && "$allow_duplicate_host" -ne 1 ]]; then
echo "ERROR: '${host}' already exists on ${node} as this same resource type:" >&2
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})" >&2
done
echo "Refusing to create a second ${this_kind} sharing this identity. Pass" >&2
echo "--allow-duplicate-host to destroy it and create a fresh one in its place" >&2
echo "(after confirming), or use --modify to reconfigure the existing one instead." >&2
exit 1
fi
if [[ -n "$cross_matches" && "$allow_duplicate_host" -ne 1 ]]; then
echo "ERROR: '${host}' already exists on ${node} as a different resource type:" >&2
echo "$cross_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})" >&2
done
echo "Refusing to create a second resource sharing this identity. Pass" >&2
echo "--allow-duplicate-host to create one anyway (it gets its own distinct" >&2
echo "sops key and VMID -- the existing resource above is left untouched)," >&2
echo "or use --modify to reconfigure the existing one instead." >&2
exit 1
fi
if [[ -n "$cross_matches" ]]; then
echo "--allow-duplicate-host: '${host}' also exists on ${node} as a different resource" \
"type -- leaving it untouched:"
echo "$cross_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})"
done
fi
if [[ -n "$exact_matches" ]]; then
echo "--allow-duplicate-host: '${host}' already exists on ${node} as this same resource" \
"type -- it will be destroyed and replaced:"
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})"
done
echo
if ! confirm_typed "$host" "Type the hostname (${host}) to confirm destroying the above and replacing it: "; then
echo "Cancelled -- input didn't match ${host}." >&2
exit 1
fi
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo "==> Destroying ${kind} VMID ${id} (${n})..."
if [[ "$kind" == "vm" ]]; then
# qm destroy has no --force to stop-then-destroy in one call (pct's
# does) -- stop explicitly first if it's running.
if ssh "$ssh_target" "${sudo_prefix} qm status ${id}" 2>/dev/null | grep -q running; then
ssh "$ssh_target" "${sudo_prefix} qm stop ${id}"
fi
ssh "$ssh_target" "${sudo_prefix} qm destroy ${id} --purge 1"
else
ssh "$ssh_target" "${sudo_prefix} pct destroy ${id} --force 1 --purge 1"
fi
done
fi
fi
echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'"
# Decide on nix-cache once, here -- this is the earliest point that needs
# it (sync-host-keys.sh below needs nix-shell packages regardless of
# whether an image ends up getting built later), and the decision is
# exported so that subprocess -- and this script's own later build step,
# if it gets there -- both reuse it instead of probing again.
nix_extra_opts
# --- make sure this target has a registered host key --------------------
echo
echo "==> Ensuring host key exists and is registered..."
sync_args=("$flake_target")
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
bash "$sync_keys" "${sync_args[@]}"
# --- VMID: pick one, and refuse to touch anything that already exists ---
echo
if [[ -z "$vmid" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
vmid="<next-free-vmid>"
echo "[dry-run] would ask ${node} for the next free VMID (pvesh get /cluster/nextid)"
else
vmid="$(ssh "$ssh_target" "${sudo_prefix} pvesh get /cluster/nextid" | tr -d '[:space:]')"
echo "Auto-assigned VMID: ${vmid}"
fi
else
echo "Requested VMID: ${vmid}"
fi
if [[ "$dry_run" -eq 0 ]]; then
# qm/pct status exits non-zero (and prints "does not exist") for a free
# ID on that resource type -- but a VMID could exist as the OTHER
# resource type (e.g. requested a CT id that's actually a VM), so check
# both. Any success here means something is already using this ID --
# refuse to go anywhere near it. (Reconfiguring an existing resource is
# --modify's job, not this one's.)
if ssh "$ssh_target" "${sudo_prefix} qm status ${vmid}" >/dev/null 2>&1 \
|| ssh "$ssh_target" "${sudo_prefix} pct status ${vmid}" >/dev/null 2>&1; then
echo "ERROR: VMID ${vmid} already exists on ${node}. Refusing to touch an" >&2
echo "existing resource here -- use --modify to reconfigure it, pick a" >&2
echo "different --vmid, or omit it to auto-assign." >&2
exit 1
fi
fi
# --- resolve the remote path -- fixed naming (not the nix store's own
# derivation-hash-based filename), so a later run can check for it by name.
# lxc uploads as a CT *template* (Proxmox's "vztmpl" content type, under
# iso_storage) -- config.system.build.tarball is a plain rootfs tarball,
# not a vzdump backup archive, so it's created with `pct create ... vztmpl`,
# not restored with `pct restore` (that expects backup-archive metadata
# this tarball doesn't have, and fails with "archive contains no
# configuration file").
remote_dir="/var/lib/vz/import"
remote_filename="${flake_target}.raw"
if [[ "$type" == "lxc" ]]; then
remote_dir="/var/lib/vz/template/cache"
remote_filename="${flake_target}.tar.xz"
fi
remote_path="${remote_dir}/${remote_filename}"
# --- ensure the flake repo (+ tooling) exists on the node, and is current --
# Bootstraps once (git clone from this checkout's own `origin`, then
# scripts/codex-setup.sh installs Nix + friends) if ${remote_repo_dir}
# doesn't exist yet on the node; otherwise just `git pull`s it, so the image
# built there reflects what's actually committed and pushed. Only called
# right before an actual remote build below -- reusing an image already on
# the node, or an explicit --image, never touch the node's checkout at all.
ensure_remote_repo() {
echo
echo "==> Ensuring ${remote_repo_dir} exists and is current on ${node}..."
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would ensure ${remote_repo_dir} exists on ${node} (clone if missing, git pull if present), and would verify/bootstrap build tooling there (scripts/codex-setup.sh) if \`nix\` isn't already on PATH -- and if that bootstrap actually ran, would also configure ${node} as a nix-cache client (scripts/proxmox/configure-nix-cache-client.sh)"
return
fi
if ssh "$ssh_target" "test -d '${remote_repo_dir}/.git'"; then
echo "Repo present -- pulling latest..."
ssh "$ssh_target" "cd '${remote_repo_dir}' && git pull --ff-only"
else
local origin_url
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
if [[ -z "$origin_url" ]]; then
echo "ERROR: ${remote_repo_dir} doesn't exist on ${node}, and this checkout has no" >&2
echo "'origin' remote to clone from. Set one (git remote add origin <url>) or create" >&2
echo "${remote_repo_dir} on ${node} yourself (e.g. git clone), then re-run." >&2
exit 1
fi
echo "Not present -- cloning from ${origin_url}..."
ssh "$ssh_target" "git clone '${origin_url}' '${remote_repo_dir}'"
fi
# Trivial check, run every time (not just right after a fresh clone) --
# confirmed live: a first bootstrap can clone the repo successfully and
# still leave the node without a working `nix` (e.g. the node had no
# `sudo`, which the Nix installer's root path depends on -- see the fix
# in scripts/codex-setup.sh), and a later run with the repo already
# present would otherwise never retry it. Sources
# scripts/lib/nix-bootstrap.sh's ensure_nix_profile first -- a
# single-user Nix install typically only gets sourced into login shells,
# and ssh's non-interactive command execution is neither, so a
# freshly-installed `nix` still wouldn't be on PATH here without it.
#
# Just `nix` today -- the only thing the remote build commands below
# actually invoke -- but a list (not a single hardcoded check) so a
# future remote step needing another tool can add itself here instead of
# growing a parallel check.
local remote_required_cmds=(nix)
local tooling_check_cmd="cd '${remote_repo_dir}' && . scripts/lib/nix-bootstrap.sh && ensure_nix_profile"
local cmd
for cmd in "${remote_required_cmds[@]}"; do
tooling_check_cmd="${tooling_check_cmd} && command -v ${cmd}"
done
if ssh "$ssh_target" "$tooling_check_cmd" >/dev/null 2>&1; then
echo "Build tooling already present on ${node}."
else
echo "==> Bootstrapping build tooling on ${node} (scripts/codex-setup.sh)..."
ssh "$ssh_target" "cd '${remote_repo_dir}' && bash scripts/codex-setup.sh"
# Only on this first-time bootstrap, not every run -- a node that
# already has tooling either already went through this once, or had
# it configured some other way, and re-running is harmless but
# pointless. Non-fatal: this only makes the node's own builds faster
# (substitute from nix-cache instead of building from source) and
# offloadable to it as a remote builder -- worth trying, not worth
# aborting the image build over if nix-cache happens to be down right
# now. Needs ensure_nix_profile first, same as the tooling_check_cmd
# above -- ssh's non-interactive command execution won't have picked
# up a freshly single-user-installed `nix` otherwise.
echo "==> Configuring ${node} as a nix-cache substituter/remote-builder client..."
if ! ssh "$ssh_target" "cd '${remote_repo_dir}' && . scripts/lib/nix-bootstrap.sh && ensure_nix_profile && bash scripts/proxmox/configure-nix-cache-client.sh"; then
echo "WARNING: configure-nix-cache-client.sh failed on ${node} -- continuing without it (${node} will build from source / against cache.nixos.org only)." >&2
fi
fi
}
# --- sync locally-managed host-keys/ to the node ---------------------------
# Gitignored (see .gitignore), so `git pull` above never carries it -- both
# build paths need it present as NIXOS_HOST_KEYS_DIR / --pre-format-files
# input on the node itself now that the build runs there. scp (not rsync,
# not already a dependency anywhere else in this repo) mirrors how this
# script already transfers the --image case below.
sync_remote_host_keys() {
echo
echo "==> Syncing host-keys/ to ${node}..."
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would copy ${repo_root}/host-keys/ to ${ssh_target}:${remote_repo_dir}/host-keys/"
return
fi
ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'"
scp -pr "${repo_root}/host-keys/." "${ssh_target}:${remote_repo_dir}/host-keys/"
}
# --- build (or reuse an image already on the node) ------------------------
echo
local_image=""
image_already_remote=0
if [[ -n "$image" ]]; then
[[ -f "$image" ]] || { echo "ERROR: --image '${image}' not found." >&2; exit 1; }
local_image="$image"
echo "Using provided image: ${local_image}"
elif [[ "$force_rebuild" -eq 1 ]]; then
echo "--force-rebuild: skipping the existing-image check on ${node}."
else
echo "==> Checking whether ${node} already has ${remote_path}..."
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would check: ssh ${ssh_target} -- test -f ${remote_path}"
elif ssh "$ssh_target" "test -f '${remote_path}'" 2>/dev/null; then
echo "Found it -- reusing, skipping build (use --force-rebuild to override)."
image_already_remote=1
else
echo "Not found -- will build."
fi
fi
if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
ensure_remote_repo
sync_remote_host_keys
# Relayed into the remote build below exactly as decided by the local
# nix_extra_opts call earlier in this script -- that decision (whether
# nix-cache is reachable) is made once, locally, same as it always has
# been; only *where* the resulting "${NIX_OPTS[@]}" gets used as a `nix
# build` flag moves to the node. NIX_EXTRA_OPTS is already a %q-quoted
# string built for exactly this eval-based reconstruction (see env.sh).
nix_opts_display=""
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
printf -v nix_opts_display '%q ' "${NIX_OPTS[@]}"
nix_opts_display=" ${nix_opts_display% }"
fi
if [[ "$type" == "lxc" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would build on ${node}: NIXOS_HOST_KEYS_DIR=\$(pwd)/host-keys nix build --impure \\"
echo "[dry-run] --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.tarball"
echo "[dry-run] would stage the result at ${remote_path}"
local_image="<built-tarball>"
else
echo "==> Building LXC tarball for ${flake_target} on ${node}..."
# Built as a single already-%q-quoted command string, not separate ssh
# argv elements -- ssh joins remote command args with plain spaces and
# hands the result to the remote shell to re-split, which would
# otherwise scatter NIX_EXTRA_OPTS (itself several space-separated,
# %q-quoted tokens) across the wrong positional parameters below.
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q' \
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
set -euo pipefail
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_pfx="$6"
declare -a NIX_OPTS=()
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
cd "$repo_dir"
# A single-user Nix install only gets sourced into login shells; this ssh
# session is neither, so `nix` wouldn't otherwise be on PATH here even
# right after a successful install.
. scripts/lib/nix-bootstrap.sh
ensure_nix_profile
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
".#nixosConfigurations.${target}.config.system.build.tarball" \
--out-link "result-${target}"
built="$(find "result-${target}/tarball" -maxdepth 1 -type f | head -1)"
if [[ -z "$built" ]]; then
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
exit 1
fi
$sudo_pfx mkdir -p "$dest_dir"
$sudo_pfx cp "$built" "${dest_dir}/${dest_name}"
echo "Built and staged: ${dest_dir}/${dest_name}"
REMOTE_SCRIPT
local_image="$remote_path"
echo "Built on ${node}: ${remote_path}"
fi
else
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would build on ${node}: nix build --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
echo "[dry-run] would run: ${sudo_display}./result-${flake_target} \\"
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \\"
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \\"
echo "[dry-run] --build-memory 2048"
echo "[dry-run] would stage the result at ${remote_path}"
local_image="<built-image>.raw"
else
echo "==> Building Disko image for ${flake_target} on ${node}..."
# See the LXC branch above for why this is one %q-quoted command
# string rather than separate ssh argv elements.
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q' \
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
set -euo pipefail
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_pfx="$6"
declare -a NIX_OPTS=()
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
cd "$repo_dir"
. scripts/lib/nix-bootstrap.sh
ensure_nix_profile
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
".#nixosConfigurations.${target}.config.system.build.diskoImagesScript" \
--out-link "result-${target}"
$sudo_pfx "./result-${target}" \
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
--build-memory 2048
built="$(find . -maxdepth 1 -name '*.raw' -newer "result-${target}" | head -1)"
if [[ -z "$built" ]]; then
echo "ERROR: no .raw image found in ${repo_dir} after build." >&2
exit 1
fi
$sudo_pfx mkdir -p "$dest_dir"
$sudo_pfx mv "$built" "${dest_dir}/${dest_name}"
echo "Built and staged: ${dest_dir}/${dest_name}"
REMOTE_SCRIPT
local_image="$remote_path"
echo "Built on ${node}: ${remote_path}"
fi
fi
fi
# --- upload -- only for an explicit --image; a build above stages its
# result directly at ${remote_path} on the node already, and reusing an
# image already on the node needs nothing transferred either. ------------
echo
if [[ -n "$image" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would upload: scp ${local_image} ${ssh_target}:${remote_path}"
else
echo "==> Uploading to ${node}:${remote_path}..."
ssh "$ssh_target" "mkdir -p ${remote_dir}"
scp "$local_image" "${ssh_target}:${remote_path}"
fi
fi
# --- create -----------------------------------------------------------------
echo
if [[ "$type" == "lxc" ]]; then
echo "==> Creating LXC container ${vmid} (${name})..."
local_disk_size="${disk_size:-$PROXMOX_DEFAULT_LXC_DISK_GB}"
# --memory doesn't touch swap -- it silently stays at Proxmox's own
# 512M default otherwise (confirmed live: --memory 2048 left swap at
# 512). Default to matching whatever --memory resolved to above.
local_swap="${swap:-$memory}"
# --unprivileged: read back from modules/platforms/lxc.nix's own
# proxmoxLXC.privileged (via flake_target_lxc_privileged) rather than
# hardcoded, since that's no longer the same for every lxc-* target --
# lxc-docker sets it true so the container's NFS mounts work at all (the
# kernel's NFS client can't mount from inside any unprivileged
# container's user namespace, no matter what AppArmor allows -- see that
# option's own comment). The NixOS config inside the image bakes in
# cgroup/capability/mount expectations matching whichever value it was
# built with, so this must stay in sync with it -- `pct create`'s own
# CLI default for this flag is privileged (unlike the web UI, which
# defaults its checkbox the other way), so leaving it unset would create
# a privileged container running a NixOS config that assumes
# unprivileged for every target except lxc-docker, a real mismatch.
privileged_eval="$(flake_target_lxc_privileged "$repo_root" "$flake_target")"
unprivileged_flag=1
[[ "$privileged_eval" == "true" ]] && unprivileged_flag=0
#
# --features nesting=1,keyctl=1: required for a modern (v247+) systemd
# guest to actually boot unprivileged -- confirmed live: without this,
# AppArmor denies the nested user namespaces and credential mounts
# systemd routinely uses (even plain getty units), and every getty
# crash-loops on a denied mount every ~3s (visible as garbage on the
# console) while core services like nsncd fail the same way.
#
# ...,mount=nfs;nfs4: without it AppArmor blanket-denies the `nfs`/
# `rpc_pipefs` mount syscalls any NFS client share needs -- confirmed
# live on lxc-docker: `mount: /var/lib/nfs/rpc_pipefs: permission
# denied`. The value's `;` (Proxmox's own multi-fstype separator for
# this one feature, per PVE::LXC's use of PVE::ParseUtils::split_list)
# must stay single-quoted here: create_cmd is sent to `remote()`, which
# hands the whole string to `ssh` as a single command for the *remote*
# shell to parse -- unquoted, that `;` would be read as a remote
# command separator and silently truncate this into two commands.
create_cmd="${sudo_prefix} pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged ${unprivileged_flag} --features '${PROXMOX_DEFAULT_LXC_FEATURES}' --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
remote "$create_cmd"
remote "${sudo_prefix} pct start ${vmid}"
else
echo "==> Creating VM ${vmid} (${name})..."
# pre-enrolled-keys=0 disables OVMF's Secure Boot key pre-enrollment --
# required, or systemd-boot (unsigned) can't be trusted by the firmware.
# --agent 1: wires up the virtio-serial channel QEMU exposes to the guest.
# modules/common/configuration.nix sets services.qemuGuest.enable = true
# on every host, so the guest-side qemu-ga daemon is already running --
# without this flag Proxmox never creates the channel it listens on, so
# `qm guest exec`/`qm agent` and the UI's IP-address display silently
# never work for any VM this script creates.
remote "${sudo_prefix} qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0 --agent enabled=1"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] ssh ${ssh_target} -- ${sudo_display}qm importdisk ${vmid} ${remote_path} ${storage}"
echo "[dry-run] (would parse the resulting disk identifier from that output)"
echo "[dry-run] ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
else
importdisk_output="$(ssh "$ssh_target" "${sudo_prefix} qm importdisk ${vmid} ${remote_path} ${storage}")"
echo "$importdisk_output"
disk_id="$(echo "$importdisk_output" | grep -oP "(?<=Successfully imported disk as ')[^']+" | sed 's/^unused[0-9]*://')"
if [[ -z "$disk_id" ]]; then
echo "ERROR: couldn't parse the imported disk identifier from qm importdisk's output above." >&2
echo "The VM shell (${vmid}) and imported disk both exist -- finish attaching it by hand:" >&2
echo " ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --scsi0 ${storage}:<disk-id-from-output-above>" >&2
echo " ssh ${ssh_target} -- ${sudo_display}qm set ${vmid} --boot order=scsi0" >&2
exit 1
fi
remote "${sudo_prefix} qm set ${vmid} --scsi0 ${disk_id}"
fi
remote "${sudo_prefix} qm set ${vmid} --boot order=scsi0"
remote "${sudo_prefix} qm start ${vmid}"
fi
echo
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] Nothing was built, uploaded, or created."
else
echo "Done. ${name} (VMID ${vmid}) should be booting on ${node}."
fi
-253
View File
@@ -1,253 +0,0 @@
#!/usr/bin/env bash
# recover-hosts.sh — Fix sops/SSH-key/GitHub-token issues on deployed NixOS hosts
# and trigger a Switch-nix rebuild on each.
#
# Run from the repo root on the workstation (nixos@nixos):
# bash scripts/recover-hosts.sh [<hostname> ...]
#
# With no args it discovers and checks every known hostname.
# With args it checks only those hostnames:
# bash scripts/recover-hosts.sh tor-relay
#
# Fixes applied automatically (then prompts before rebuilding):
# 1. SSH host key drift — live key no longer matches host-keys/<target>_ssh_host_ed25519_key
# Fix: scp the registered key back and restore it (needs sudo once per host).
# To push new keys proactively (before drift, e.g. right after
# sync-host-keys.sh --regenerate-all-keys), use instead:
# scripts/secrets/push-host-keys.sh --all
# 2. Stale/invalid GitHub access token — the rendered nix-github-token.conf has
# a token GitHub rejects (401), blocking any rebuild that fetches disko or
# other public GitHub flake inputs.
# Fix: empty the rendered file so nix makes unauthenticated requests instead.
# Public repos (disko, nixpkgs, etc.) work fine without auth. sops-nix
# re-renders the correct new token automatically after the first successful
# rebuild.
#
# Both fixes need one interactive sudo session per host. The script opens a
# single ssh -t per broken host so you enter the password once and all steps
# run in sequence.
set -euo pipefail
cd "$(dirname "$0")/.."
source scripts/env.sh 2>/dev/null || true
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5)
SSH_USER=nixos
# Known flake-target → ssh hostname map for all currently-defined hosts.
# Add new hosts here as they are deployed.
declare -A TARGET_HOST=(
[lxc-docker]=docker
[lxc-nix-cache]=nix-cache
[lxc-pxe-boot]=pxe-boot
[lxc-tor-relay]=tor-relay
[lxc-minimal]=nix-minimal
[proxmox-server]=server
[baremetal-gui]=nixos
)
# ── helpers ───────────────────────────────────────────────────────────────────
info() { echo " [✓] $*"; }
warn() { echo " [!] $*"; }
step() { echo "==> $*"; }
ssh_host_age() {
ssh-keyscan -t ed25519 "$1" 2>/dev/null \
| nix shell nixpkgs#ssh-to-age --command ssh-to-age 2>/dev/null \
| head -1 || true
}
registered_age() {
local keyfile="host-keys/${1}_ssh_host_ed25519_key.pub"
[ -f "$keyfile" ] || return 0
nix shell nixpkgs#ssh-to-age --command ssh-to-age < "$keyfile" 2>/dev/null \
| head -1 || true
}
github_token_valid() {
local host=$1
local raw token code
raw=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
"cat /run/secrets/rendered/nix-github-token.conf 2>/dev/null || true")
token=$(echo "$raw" | grep -oP '(?<=github\.com=)\S+' || true)
if [ -z "$token" ]; then
return 0 # no token = unauthenticated, works for public repos
fi
code=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $token" \
"https://api.github.com/repos/nix-community/disko" 2>/dev/null || echo 000)
[ "$code" = "200" ]
}
# ── discover hosts ────────────────────────────────────────────────────────────
if [ $# -gt 0 ]; then
HOSTNAMES=("$@")
else
HOSTNAMES=()
seen=()
for target in "${!TARGET_HOST[@]}"; do
h="${TARGET_HOST[$target]}"
# deduplicate (e.g. proxmox-server and lxc-server both map to "server")
if [[ ! " ${seen[*]:-} " =~ " $h " ]]; then
seen+=("$h")
if ssh "${SSH_OPTS[@]}" "$SSH_USER@$h" "true" 2>/dev/null; then
HOSTNAMES+=("$h")
fi
fi
done
fi
if [ ${#HOSTNAMES[@]} -eq 0 ]; then
echo "No reachable hosts found. Pass hostnames explicitly or check SSH."
exit 1
fi
echo ""
echo "Hosts to check: ${HOSTNAMES[*]}"
echo ""
# ── check phase ───────────────────────────────────────────────────────────────
NEEDS_FIX=()
for host in "${HOSTNAMES[@]}"; do
step "$host"
if ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" "true" 2>/dev/null; then
warn "SSH unreachable — clearing stale known_hosts entry"
ssh-keygen -R "$host" 2>/dev/null || true
continue
fi
flake_target=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
"cat /etc/flake-target 2>/dev/null || true")
echo " flake-target: ${flake_target:-unknown}"
host_broken=false
# SSH host key
if [ -n "$flake_target" ] && [ -f "host-keys/${flake_target}_ssh_host_ed25519_key.pub" ]; then
live=$(ssh_host_age "$host")
want=$(registered_age "$flake_target")
if [ "$live" = "$want" ]; then
info "SSH host key OK"
else
warn "SSH host key MISMATCH (live ≠ host-keys/) -- use push-host-keys.sh proactively next time"
echo " live: $live"
echo " registered: $want"
host_broken=true
fi
else
echo " [~] No host-keys/ entry for ${flake_target:-unknown} — skipping key check"
fi
# GitHub token
if github_token_valid "$host"; then
info "GitHub token OK"
else
warn "GitHub token invalid (rebuild will fail with 401)"
host_broken=true
fi
# sops-nix result
sops_result=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
"systemctl show sops-nix --property=Result --value 2>/dev/null || echo unknown")
if [ "$sops_result" = "success" ]; then
info "sops-nix: success"
else
warn "sops-nix: $sops_result"
fi
$host_broken && NEEDS_FIX+=("$host")
echo ""
done
# ── fix phase ─────────────────────────────────────────────────────────────────
if [ ${#NEEDS_FIX[@]} -eq 0 ]; then
echo "All hosts healthy — nothing to fix."
exit 0
fi
echo "Hosts needing fixes: ${NEEDS_FIX[*]}"
echo ""
echo "Each fix requires one sudo session per host. You will be prompted for"
echo "the nixos sudo password once per host; all steps run in that session."
echo ""
read -r -p "Proceed with fixes + Switch-nix on each broken host? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
echo ""
for host in "${NEEDS_FIX[@]}"; do
step "Fixing $host"
flake_target=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
"cat /etc/flake-target 2>/dev/null || true")
fix_script=""
# Fix 1: restore SSH host key
live=$(ssh_host_age "$host")
want=$(registered_age "${flake_target:-}")
if [ -n "$want" ] && [ "$live" != "$want" ]; then
echo " Uploading registered SSH host key (private + public)..."
scp -o StrictHostKeyChecking=no \
"host-keys/${flake_target}_ssh_host_ed25519_key" \
"$SSH_USER@$host:/tmp/recover_ed25519_key"
scp -o StrictHostKeyChecking=no \
"host-keys/${flake_target}_ssh_host_ed25519_key.pub" \
"$SSH_USER@$host:/tmp/recover_ed25519_key.pub"
fix_script+='
echo "[fix] Restoring SSH host key..."
install -m 0600 /tmp/recover_ed25519_key /etc/ssh/ssh_host_ed25519_key
install -m 0644 /tmp/recover_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
rm -f /tmp/recover_ed25519_key /tmp/recover_ed25519_key.pub
echo " Done."
'
ssh-keygen -R "$host" 2>/dev/null || true
fi
# Fix 2: clear invalid GitHub token
if ! github_token_valid "$host"; then
fix_script+='
echo "[fix] Clearing stale GitHub token (nix will use unauthenticated access)..."
echo "" > /run/secrets/rendered/nix-github-token.conf
systemctl restart nix-daemon 2>/dev/null || true
echo " Done."
'
fi
# Fix 3: rebuild
fix_script+='
echo "[fix] Running nixos-rebuild switch..."
nixos-rebuild switch \
--no-write-lock-file \
--refresh \
--flake "git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(cat /etc/flake-target)"
echo "[fix] Rebuild complete."
'
echo " Opening SSH session (enter sudo password when prompted)..."
if ssh -t -o StrictHostKeyChecking=no "$SSH_USER@$host" \
"sudo bash -s" <<< "$fix_script"; then
echo ""
info "$host fixed and rebuilt"
else
rc=$?
echo ""
warn "$host: rebuild exited with code $rc (may still have succeeded — check sops-nix below)"
fi
# Verify: re-check sops-nix result post-rebuild
sops_result_after=$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$host" \
"systemctl show sops-nix --property=Result --value 2>/dev/null || echo unknown" 2>/dev/null || echo "ssh-failed")
if [ "$sops_result_after" = "success" ]; then
info "$host sops-nix: success post-rebuild"
else
warn "$host sops-nix: $sops_result_after post-rebuild (may need another pass)"
fi
echo ""
done
echo "Recovery complete."
-150
View File
@@ -1,150 +0,0 @@
#!/usr/bin/env bash
# Backs up the local sops age key (the private key that decrypts
# secrets/*.yaml -- normally the one trusted as &admin) to an arbitrary
# destination path, e.g. a USB drive or other offline storage, so it can
# later be restored and handed to rotate-admin-key.sh if this machine's
# copy is ever lost, or to run either script from a different machine.
#
# Usage:
# scripts/secrets/backup-admin-key.sh <dest-path> [--key-file <path>] [--force] [--dry-run]
#
# Source key resolution matches sops/age's own default order:
# $SOPS_AGE_KEY (inline identity text) if set, else
# --key-file if given, else
# $SOPS_AGE_KEY_FILE if set, else
# ${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
sops_yaml="${repo_root}/.sops.yaml"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/sops-age.sh
source "${repo_root}/scripts/lib/sops-age.sh"
# Pin cwd for the same reason rotate-admin-key.sh does: age/sops calls
# below should never depend on wherever the caller's shell happened to be.
cd "$repo_root"
usage() {
cat <<EOF
Usage: $0 <dest-path> [--key-file <path>] [--force] [--dry-run]
<dest-path> Where to write the backup. Parent directories are
created as needed. Written with 0600 permissions.
--key-file <path> Read the key from here instead of the default
sops/age resolution (\$SOPS_AGE_KEY_FILE, then
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt).
Ignored if \$SOPS_AGE_KEY is set (that always wins,
same precedence sops/age itself uses).
--force Overwrite <dest-path> if it already exists.
--dry-run Print what would happen; write nothing.
EOF
}
dry_run=0
force=0
key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
dry_run=1
shift
;;
--force)
force=1
shift
;;
--key-file)
key_file="${2:?--key-file requires a path}"
shift 2
;;
-h | --help)
usage
exit 0
;;
--*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
args+=("$1")
shift
;;
esac
done
if [[ "${#args[@]}" -ne 1 ]]; then
usage >&2
exit 1
fi
dest="${args[0]}"
nix_extra_opts
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
echo "==> Source: \$SOPS_AGE_KEY (inline identity from the environment)."
src_content="$SOPS_AGE_KEY"
else
[[ -s "$key_file" ]] || {
echo "ERROR: no key found. \$SOPS_AGE_KEY is unset and ${key_file} doesn't exist or is empty." >&2
exit 1
}
echo "==> Source: ${key_file}"
src_content="$(cat "$key_file")"
fi
# Round-trip through a private scratch file (rather than trusting the
# source string as-is) so age-keygen -y validates it's a real identity
# before anything is written to <dest-path>.
scratch="$(mktemp)"
trap 'rm -f "$scratch"' EXIT
( umask 077; printf '%s\n' "$src_content" > "$scratch" )
src_pub="$(age_pubkey_from_identity_file "$scratch")" || {
echo "ERROR: source doesn't look like a valid age identity (age-keygen -y failed)." >&2
exit 1
}
echo " public key: ${src_pub}"
current_admin_pub="$(sops_yaml_admin_pubkey "$sops_yaml")"
if [[ -n "$current_admin_pub" && "$current_admin_pub" != "$src_pub" ]]; then
echo "NOTE: this key does not match .sops.yaml's current &admin entry (${current_admin_pub})."
echo " Backing it up anyway -- this script doesn't require it to be the admin key."
fi
if [[ -e "$dest" && "$force" -ne 1 ]]; then
echo "ERROR: ${dest} already exists. Pass --force to overwrite." >&2
exit 1
fi
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] would write $(wc -c <"$scratch" | tr -d ' ') bytes to ${dest} (mode 0600)"
[[ -e "$dest" ]] && echo "[dry-run] would overwrite existing file (--force given)"
echo "[dry-run] Nothing was written. Re-run without --dry-run to apply this."
exit 0
fi
mkdir -p "$(dirname "$dest")"
install -m 600 "$scratch" "$dest"
dest_pub="$(age_pubkey_from_identity_file "$dest")"
if [[ "$dest_pub" != "$src_pub" ]]; then
echo "ERROR: ${dest} was written but its public key doesn't match the source -- investigate before relying on this backup." >&2
exit 1
fi
cat <<EOF
Done. Backed up to: ${dest}
public key: ${dest_pub}
This is a private key -- store it somewhere offline/secure, not in this
repo or anywhere it'd get committed. Restore it with:
scripts/secrets/rotate-admin-key.sh ${dest}
EOF
-318
View File
@@ -1,318 +0,0 @@
#!/usr/bin/env bash
# Pushes newly-generated SSH host keys from host-keys/ to already-running
# NixOS hosts, so they can decrypt sops secrets after a nixos-rebuild
# following scripts/secrets/sync-host-keys.sh --regenerate-all-keys.
#
# Before pushing any key, verifies that .sops.yaml and secrets/*.yaml are
# committed and pushed to the remote -- hosts rebuild from the remote Gitea
# flake, so recipient changes must land there before any rebuild, not just
# before the key push.
#
# push-host-keys.sh --all [--dry-run] [--skip-git-check]
# push-host-keys.sh <target> [--dry-run] [--skip-git-check]
#
# --all Push to every reachable managed host. Default when no
# target is given.
# <target> Push to one flake target only (e.g. lxc-server).
# --dry-run Print what would be done; write nothing.
# --skip-git-check Skip the commit/push check. Use only when the remote
# already has the current .sops.yaml/secrets/*.yaml.
#
# SSH: connects as SSH_USER@<hostname> (default: nixos, the user with the
# admin authorized key), then installs files via sudo -S (reads the sudo
# password from stdin). The password is prompted once at startup and reused
# for every host -- no PTY or terminal required on the remote side.
# Hosts are reached at their bare hostname (relies on LAN DNS/mDNS).
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
keydir="${repo_root}/host-keys"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/nix-eval.sh
source "${repo_root}/scripts/lib/nix-eval.sh"
: "${SSH_USER:=nixos}"
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5)
dry_run=0
skip_git_check=0
sudo_password=""
usage() {
cat <<EOF
Usage: $0 [--all | <target>] [--dry-run] [--skip-git-check]
--all Push to every reachable managed host. Default when no
target is given.
<target> Push to one flake target only (e.g. lxc-server).
--dry-run Print what would be done; write nothing.
--skip-git-check Skip the check that .sops.yaml/secrets/*.yaml are
committed and pushed to the remote repo.
Environment:
SSH_USER SSH username (default: nixos).
SUDO_PASS Sudo password (skips the interactive prompt; useful
when calling from another script).
EOF
}
# Prompt for the sudo password once; store it for all _do_push calls.
# Accepts SUDO_PASS from the environment to allow non-interactive callers.
prompt_sudo_password() {
[[ "$dry_run" -eq 1 ]] && return
if [[ -n "${SUDO_PASS:-}" ]]; then
sudo_password="$SUDO_PASS"
return
fi
# read exits non-zero when stdin is not a terminal (e.g. CI, background
# agents). Catch that and give a clear message rather than a silent exit.
if ! read -r -s -p "sudo password for ${SSH_USER} on remote hosts: " sudo_password; then
echo >&2
echo "ERROR: stdin is not a terminal -- cannot prompt for sudo password." >&2
echo " Set SUDO_PASS=<password> in the environment and re-run." >&2
exit 1
fi
echo >&2
}
locally_managed_hosts() {
for f in "${keydir}"/*_ssh_host_ed25519_key.pub; do
[[ -e "$f" ]] || continue
basename "$f" _ssh_host_ed25519_key.pub
done
}
# --- git state check/fix --------------------------------------------------
# Hosts rebuild from the remote Gitea flake:
# nixos-rebuild switch --flake "git+https://<gitea>/nixos.git#<target>"
# so .sops.yaml (updated recipients) and secrets/*.yaml (re-encrypted DEKs)
# must be committed and pushed before any rebuild can succeed. This check
# catches the common case where --regenerate-all-keys was just run but the
# resulting diff hasn't been committed/pushed yet.
ensure_remote_current() {
[[ "$skip_git_check" -eq 1 ]] && return
cd "$repo_root"
local dirty_unstaged dirty_staged
dirty_unstaged="$(git diff --name-only -- .sops.yaml secrets/ 2>/dev/null || true)"
dirty_staged="$(git diff --cached --name-only -- .sops.yaml secrets/ 2>/dev/null || true)"
if [[ -n "$dirty_unstaged" || -n "$dirty_staged" ]]; then
echo "Uncommitted changes in sops-managed files:"
[[ -n "$dirty_unstaged" ]] && sed 's/^/ (unstaged) /' <<<"$dirty_unstaged"
[[ -n "$dirty_staged" ]] && sed 's/^/ (staged) /' <<<"$dirty_staged"
echo
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would prompt to commit .sops.yaml/secrets/ before continuing."
else
read -rp "Commit .sops.yaml + secrets/ now? [y/N]: " ans
if [[ "$ans" =~ ^[Yy]$ ]]; then
git add -- .sops.yaml secrets/
git commit -m "secrets: update recipients and re-encrypt for host key changes"
echo "Committed."
else
echo "Continuing with uncommitted changes -- the remote won't have the"
echo "updated recipients until you commit and push."
fi
fi
echo
fi
# Check if we're ahead of the remote tracking branch
local ahead
ahead="$(git rev-list --count '@{upstream}..HEAD' 2>/dev/null || echo "")"
if [[ -z "$ahead" ]]; then
echo "NOTE: no remote tracking branch found -- skipping push check."
echo " Ensure the remote has the current .sops.yaml/secrets/ before"
echo " triggering nixos-rebuild on any host."
echo
return
fi
if [[ "$ahead" -gt 0 ]]; then
echo "Local branch is ${ahead} commit(s) ahead of remote."
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would prompt to push before continuing."
else
read -rp "Push to remote now? [y/N]: " ans
if [[ "$ans" =~ ^[Yy]$ ]]; then
git push
echo "Pushed."
else
echo "Continuing without pushing -- remember to push before running"
echo "nixos-rebuild on any of these hosts."
fi
fi
echo
fi
}
# --- key installation (shared) -------------------------------------------
_do_push() {
local hostname="$1" target="$2"
local keyfile="${keydir}/${target}_ssh_host_ed25519_key"
local pubfile="${keyfile}.pub"
if [[ "$dry_run" -eq 1 ]]; then
echo " [dry-run] would scp host-keys/${target}_ssh_host_ed25519_key{,.pub} to /tmp/"
echo " [dry-run] would: sudo -S install -m 0600/0644 to /etc/ssh/ and rm /tmp copies"
return
fi
# Upload to /tmp (writable as nixos, no privilege needed)
scp -o StrictHostKeyChecking=no \
"$keyfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key"
scp -o StrictHostKeyChecking=no \
"$pubfile" "${SSH_USER}@${hostname}:/tmp/push_ed25519_key.pub"
# Install via sudo -S: the password is piped via herestring so no PTY is
# needed on either side. -p '' suppresses sudo's own prompt string.
ssh -o StrictHostKeyChecking=no "${SSH_USER}@${hostname}" \
"sudo -S -p '' bash -c '
install -m 0600 /tmp/push_ed25519_key /etc/ssh/ssh_host_ed25519_key
install -m 0644 /tmp/push_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub
rm -f /tmp/push_ed25519_key /tmp/push_ed25519_key.pub
echo \" [ok] host key installed\"
'" <<< "$sudo_password"
# Drop the stale known_hosts entry for this host (public key just changed)
ssh-keygen -R "$hostname" 2>/dev/null || true
echo " Done. Run nixos-rebuild switch on ${hostname} to activate."
}
# --- single named target --------------------------------------------------
push_target() {
local target="$1"
local keyfile="${keydir}/${target}_ssh_host_ed25519_key"
if [[ ! -f "$keyfile" ]]; then
echo "ERROR: host-keys/${target}_ssh_host_ed25519_key not found." >&2
echo " This target may not be locally managed (e.g. &${target} was" >&2
echo " registered from the host's real SSH key, not generated here)." >&2
exit 1
fi
local hostname
hostname="$(flake_target_hostname "$repo_root" "$target")"
if [[ -z "$hostname" ]]; then
echo "ERROR: cannot resolve hostname for '${target}' from the flake." >&2
exit 1
fi
echo "==> ${target} (→ ${hostname})"
if ! ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" true 2>/dev/null; then
echo " SKIP: ${SSH_USER}@${hostname} unreachable."
return
fi
# Sanity-check that /etc/flake-target on the host agrees
local live_target
live_target="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" \
"cat /etc/flake-target 2>/dev/null || true")"
if [[ -n "$live_target" && "$live_target" != "$target" ]]; then
echo " WARN: host reports /etc/flake-target='${live_target}', not '${target}'."
echo " Pushing the key you specified (${target}) anyway."
fi
_do_push "$hostname" "$target"
}
# --- all managed hosts ----------------------------------------------------
# For each unique hostname derived from managed targets, SSHes in and reads
# /etc/flake-target to determine which key to push -- handles the case where
# multiple targets share a hostname (e.g. lxc-server and proxmox-server both
# resolve to "server"; only one is actually running).
push_all() {
mapfile -t managed < <(locally_managed_hosts)
if [[ "${#managed[@]}" -eq 0 ]]; then
echo "No managed keys in host-keys/ -- nothing to push."
return
fi
echo "Pushing to all reachable managed hosts..."
echo
declare -A seen_hostnames=()
local t hostname
for t in "${managed[@]}"; do
hostname="$(flake_target_hostname "$repo_root" "$t" 2>/dev/null || true)"
[[ -z "$hostname" ]] && continue
[[ -n "${seen_hostnames[$hostname]+x}" ]] && continue
seen_hostnames["$hostname"]=1
echo "==> checking ${hostname}"
if ! ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" true 2>/dev/null; then
echo " SKIP: ${SSH_USER}@${hostname} unreachable."
continue
fi
# Ask the host which flake target it actually is
local live_target
live_target="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${hostname}" \
"cat /etc/flake-target 2>/dev/null || true")"
if [[ -z "$live_target" ]]; then
echo " SKIP: no /etc/flake-target on host -- can't determine which key to push."
continue
fi
local live_keyfile="${keydir}/${live_target}_ssh_host_ed25519_key"
if [[ ! -f "$live_keyfile" ]]; then
echo " SKIP: host is '${live_target}' but no host-keys/${live_target}_... (hand-registered key, not managed here)."
continue
fi
echo " target: ${live_target}"
_do_push "$hostname" "$live_target"
done
}
# --- main -----------------------------------------------------------------
mode="all"
target_arg=""
extra_args=()
for arg in "$@"; do
case "$arg" in
--dry-run) dry_run=1 ;;
--skip-git-check) skip_git_check=1 ;;
--all) mode="all" ;;
-h|--help) usage; exit 0 ;;
--*) echo "Unknown option: $arg" >&2; usage >&2; exit 1 ;;
*) extra_args+=("$arg") ;;
esac
done
if [[ "${#extra_args[@]}" -gt 1 ]]; then
echo "ERROR: specify at most one target (or --all)." >&2
usage >&2; exit 1
elif [[ "${#extra_args[@]}" -eq 1 ]]; then
mode="single"
target_arg="${extra_args[0]}"
fi
[[ "$dry_run" -eq 1 ]] && { echo "[dry-run] no changes will be made"; echo; }
nix_extra_opts
ensure_remote_current
prompt_sudo_password
if [[ "$mode" == "single" ]]; then
push_target "$target_arg"
else
push_all
fi
echo
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply."
else
echo "Key push complete. For each updated host, run nixos-rebuild switch to"
echo "apply the config and let sops-nix decrypt secrets with the new key."
fi
-193
View File
@@ -1,193 +0,0 @@
#!/usr/bin/env bash
# Rotates the &admin sops age key: decrypts with a backed-up copy of the
# key CURRENTLY trusted as &admin, replaces .sops.yaml's &admin entry with
# a new key already present in this environment, and re-encrypts every
# secrets/*.yaml for the new recipient set. After this runs, the old key
# can no longer decrypt anything -- this is a real, one-way handoff of
# trust, not a preview.
#
# This is the automation for the manual steps create-proxmox-resource.sh /
# sync-host-keys.sh print when they bootstrap a brand-new, not-yet-trusted
# age key on a machine that's never had admin access before:
#
# scripts/secrets/rotate-admin-key.sh /path/to/backed-up/admin/keys.txt
#
# The backup key's *public* key must match .sops.yaml's current &admin
# entry -- this script verifies that by deriving it, it doesn't just trust
# the filename or take it on faith. The new key defaults to wherever sops
# itself would already look ($SOPS_AGE_KEY_FILE, then the XDG default), so
# the common case is just pointing this at the restored backup.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
sops_yaml="${repo_root}/.sops.yaml"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/sops-age.sh
source "${repo_root}/scripts/lib/sops-age.sh"
# sops resolves .sops.yaml by walking up from the process's cwd, not from
# the target file's own path -- if this script were invoked from somewhere
# other than the repo root (or from inside another checkout/worktree that
# happens to have its own .sops.yaml), `sops updatekeys` would silently
# re-encrypt against the WRONG config's recipient list instead of this
# repo's. Pin cwd here so every sops/age call below is unambiguous
# regardless of where the caller's shell started out.
cd "$repo_root"
usage() {
cat <<EOF
Usage: $0 <path-to-backed-up-admin-key> [--new-key-file <path>] [--dry-run]
<path-to-backed-up-admin-key> age identity file for the key CURRENTLY
trusted as &admin. Only ever read -- never
copied or modified.
--new-key-file <path> age identity file for the key to promote
to &admin. Defaults to \$SOPS_AGE_KEY_FILE,
then
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt
(sops/age's own default resolution order).
--dry-run Print what would change; touches nothing
(.sops.yaml untouched, no sops updatekeys
calls).
EOF
}
dry_run=0
new_key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
dry_run=1
shift
;;
--new-key-file)
new_key_file="${2:?--new-key-file requires a path}"
shift 2
;;
-h | --help)
usage
exit 0
;;
--*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
args+=("$1")
shift
;;
esac
done
if [[ "${#args[@]}" -ne 1 ]]; then
usage >&2
exit 1
fi
backup_key="${args[0]}"
[[ -s "$backup_key" ]] || { echo "ERROR: backup key file not found or empty: ${backup_key}" >&2; exit 1; }
[[ -s "$new_key_file" ]] || { echo "ERROR: new key file not found or empty: ${new_key_file}" >&2; exit 1; }
nix_extra_opts
echo "==> Deriving public keys..."
old_pub="$(age_pubkey_from_identity_file "$backup_key")"
new_pub="$(age_pubkey_from_identity_file "$new_key_file")"
echo " backup (old admin) key: ${old_pub}"
echo " new admin key: ${new_pub}"
if [[ "$old_pub" == "$new_pub" ]]; then
echo "ERROR: backup key and new key are identical -- nothing to rotate." >&2
exit 1
fi
current_admin_pub="$(sops_yaml_admin_pubkey "$sops_yaml")"
if [[ -z "$current_admin_pub" ]]; then
echo "ERROR: couldn't find a '&admin age1...' line in ${sops_yaml}." >&2
exit 1
fi
if [[ "$current_admin_pub" != "$old_pub" ]]; then
echo "ERROR: ${backup_key} doesn't match the current &admin key in .sops.yaml." >&2
echo " .sops.yaml &admin: ${current_admin_pub}" >&2
echo " backup key pubkey: ${old_pub}" >&2
echo "Wrong backup file, or .sops.yaml has already moved on -- not touching anything." >&2
exit 1
fi
mapfile -t secrets_files < <(find "${repo_root}/secrets" -maxdepth 1 -name '*.yaml' | sort)
if [[ "${#secrets_files[@]}" -eq 0 ]]; then
echo "ERROR: no secrets/*.yaml files found under ${repo_root}/secrets." >&2
exit 1
fi
# sops_can_decrypt <key-file> <secrets-file>: used both to confirm the
# backup key still works before touching anything, and again after
# rotation to confirm the new key does too.
sops_can_decrypt() {
local key_file="$1" secrets_file="$2"
SOPS_AGE_KEY_FILE="$key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
"sops -d '${secrets_file}'" >/dev/null
}
echo "==> Confirming the backup key can actually decrypt..."
if ! sops_can_decrypt "$backup_key" "${secrets_files[0]}"; then
echo "ERROR: backup key failed to decrypt $(basename "${secrets_files[0]}") -- aborting." >&2
exit 1
fi
echo " OK: decrypted $(basename "${secrets_files[0]}")"
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] would replace .sops.yaml's &admin line:"
echo "[dry-run] - ${current_admin_pub}"
echo "[dry-run] + ${new_pub}"
echo "[dry-run] would then re-encrypt (sops updatekeys --yes) for the new recipient set:"
for f in "${secrets_files[@]}"; do
echo "[dry-run] secrets/$(basename "$f")"
done
echo
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
exit 0
fi
echo "==> Rotating .sops.yaml's &admin key..."
sed -i "s|^ - &admin age1[a-z0-9]*| - \&admin ${new_pub}|" "$sops_yaml"
grep -qF "$new_pub" "$sops_yaml" || {
echo "ERROR: sed edit didn't take -- .sops.yaml left unchanged, check it by hand." >&2
exit 1
}
echo " Updated."
echo "==> Re-encrypting secrets/*.yaml for the new recipient set..."
for f in "${secrets_files[@]}"; do
echo "==> $(basename "$f")"
sops_updatekeys "$f" "$backup_key"
done
echo "==> Verifying the new key can decrypt everything..."
for f in "${secrets_files[@]}"; do
if ! sops_can_decrypt "$new_key_file" "$f"; then
echo "ERROR: new key failed to decrypt $(basename "$f") after rotation -- investigate before committing." >&2
exit 1
fi
echo " OK: $(basename "$f")"
done
cat <<EOF
Done. .sops.yaml's &admin key is now:
${new_pub}
The old key (${old_pub}) can no longer decrypt any secrets/*.yaml
re-encrypted above.
Review the diff, then commit:
git add .sops.yaml secrets/*.yaml
git commit -m "Rotate sops admin age key"
EOF
-459
View File
@@ -1,459 +0,0 @@
#!/usr/bin/env bash
# Manages host-keys/ + .sops.yaml + secrets/*.yaml recipients together, so
# a flake target's SSH host key and its sops registration never drift out
# of sync with each other or with the flake itself.
#
# sync-host-keys.sh --all Generate/register every flake
# target missing a key.
# sync-host-keys.sh <target> Same, for just one target.
# sync-host-keys.sh --remove Interactively remove one
# locally-managed key.
# sync-host-keys.sh --regenerate-all-keys Remove and freshly regenerate
# every locally-managed key.
#
# "Generate/register" is idempotent and additive only: an existing
# host-keys/ file is never touched, and .sops.yaml only ever gains an
# anchor/alias it doesn't already have -- safe to re-run any time, e.g.
# right after adding a new host to flake.nix.
#
# --remove and --regenerate-all-keys only ever operate on anchors that have
# a corresponding host-keys/<name>_ssh_host_ed25519_key file. Anchors
# without one (&admin, and any anchor for an already-deployed host whose
# real /etc/ssh key was registered by hand, e.g. &docker/&server/&nix-cache
# today) are never listed, removed, or regenerated -- this tooling only
# ever touches keys it itself manages.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
sops_yaml="${repo_root}/.sops.yaml"
keydir="${repo_root}/host-keys"
editor="${repo_root}/scripts/lib/sync-host-keys-edit-sops.py"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/nix-eval.sh
source "${repo_root}/scripts/lib/nix-eval.sh"
# shellcheck source=../lib/ssh-host-keys.sh
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
# shellcheck source=../lib/sops-age.sh
source "${repo_root}/scripts/lib/sops-age.sh"
# shellcheck source=../lib/confirm.sh
source "${repo_root}/scripts/lib/confirm.sh"
mkdir -p "$keydir"
usage() {
cat <<EOF
Usage: $0 --all [--dry-run]
$0 <flake-target> [--dry-run]
$0 --remove [--dry-run]
$0 --regenerate-all-keys [--dry-run]
--all Generate + register a host key for every flake
target that's missing one.
<flake-target> Same, for just one target (e.g. lxc-server).
Reports if it already has one.
--remove Interactively pick one locally-managed key to
remove from .sops.yaml and host-keys/.
--regenerate-all-keys Remove every locally-managed key and generate
fresh replacements for every current flake
target. Destructive -- requires typed
confirmation.
--dry-run Combine with any of the above: print what would
change (host-keys/ files, .sops.yaml anchors and
key_groups, which secrets/*.yaml would be
re-encrypted) without touching anything. No keys
generated, no files written, no sops calls,
no prompts for confirmation.
EOF
}
# --- step 0: make sure we can actually decrypt anything at all -------------
#
# Registering a host means editing .sops.yaml and then running
# `sops updatekeys`, which has to decrypt each secrets file with an
# existing recipient's key before it can re-encrypt it for the new one.
# Check this before doing anything else, the same order sops/age itself
# resolves a usable key in: SOPS_AGE_KEY (inline), then SOPS_AGE_KEY_FILE,
# then the XDG default path.
ensure_admin_decrypt_key() {
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
echo "Using SOPS_AGE_KEY from the environment."
return
fi
local key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
if [[ -s "$key_file" ]]; then
echo "Found existing sops age key at ${key_file}."
return
fi
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] No sops age decryption key found (checked \$SOPS_AGE_KEY, \$SOPS_AGE_KEY_FILE, ${key_file})."
echo "[dry-run] Would generate a new one here -- continuing the dry run without one; any"
echo "[dry-run] 'would re-encrypt' output below couldn't actually run for real yet."
return
fi
echo "No sops age decryption key found (checked \$SOPS_AGE_KEY, \$SOPS_AGE_KEY_FILE, ${key_file})."
echo "Generating a new one at ${key_file}..."
mkdir -p "$(dirname "$key_file")"
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -o '${key_file}'" 2>&1 | grep -v "^Public key:" || true
local new_pub
new_pub="$(age_pubkey_from_identity_file "$key_file")"
cat <<EOF
A brand-new age key was just generated -- it cannot decrypt anything that
already exists in secrets/*.yaml, since nothing was ever encrypted for it.
That trust can't be bootstrapped automatically (nobody can decrypt a file
for a recipient that didn't exist when it was last encrypted).
To actually use this key:
1. Have someone who currently CAN decrypt replace the &admin entry in
.sops.yaml with this public key:
${new_pub}
2. They re-encrypt every secrets/*.yaml:
sops updatekeys --yes secrets/common.yaml
sops updatekeys --yes secrets/nix-cache.yaml
sops updatekeys --yes secrets/server.yaml
3. Re-run this script.
Exiting without making any other changes.
EOF
exit 1
}
discover_targets() {
# installer is the one nixosConfigurations target that doesn't import
# sops-nix at all (see CLAUDE.md's "Security Notes" -- hardcoded login
# password instead) -- config.sops.secrets doesn't exist for it.
list_flake_targets "$repo_root" | grep -v '^installer$'
}
locally_managed_hosts() {
for f in "$keydir"/*_ssh_host_ed25519_key.pub; do
[[ -e "$f" ]] || continue
basename "$f" _ssh_host_ed25519_key.pub
done
}
add_keys_json="[]"
add_aliases_json="[]"
dry_run=0
queue_host_sync() {
local host="$1"
local keyfile="${keydir}/${host}_ssh_host_ed25519_key"
local has_local_key=0 has_anchor=0
[[ -f "$keyfile" ]] && has_local_key=1
grep -qE "^ - &${host} age1" "$sops_yaml" && has_anchor=1
if [[ "$has_local_key" -eq 0 && "$has_anchor" -eq 1 ]]; then
echo "SKIP ${host}: .sops.yaml already has an &${host} anchor, but"
echo " host-keys/${host}_ssh_host_ed25519_key is missing locally."
echo " Not generating a replacement -- it wouldn't match whatever's"
echo " already registered (and possibly deployed). Remove the"
echo " &${host} line from .sops.yaml first if you really want a"
echo " fresh key, then re-run."
return 1
fi
if [[ "$has_local_key" -eq 0 ]]; then
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] ${host}: would generate host key"
else
echo "==> ${host}: generating host key"
generate_host_ed25519_key "$host" "$keyfile"
fi
else
echo "==> ${host}: host key already present"
fi
if [[ "$has_anchor" -eq 0 ]]; then
local age_pub
if [[ "$dry_run" -eq 1 ]]; then
age_pub="dry-run-placeholder-not-a-real-key"
else
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
fi
add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \
'. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")"
fi
echo "==> ${host}: checking which secrets files it references"
local basenames
mapfile -t basenames < <(
nix eval --json --no-use-registries --no-accept-flake-config \
"${repo_root}#nixosConfigurations.${host}.config.sops.secrets" \
--apply 'builtins.mapAttrs (n: v: baseNameOf v.sopsFile)' \
| jq -r '[.[]] | unique | .[]'
)
local basename
for basename in "${basenames[@]}"; do
add_aliases_json="$(jq --arg host "$host" --arg basename "$basename" \
'. + [{host: $host, basename: $basename}]' <<<"$add_aliases_json")"
done
}
# In dry-run, this runs the exact same edit logic (so idempotency/what's-
# actually-new is determined for real, not guessed) but against a scratch
# copy of .sops.yaml that's discarded afterward -- the real file is never
# opened for writing, and `sops updatekeys` never runs.
apply_edit_plan() {
local plan="$1"
local target="$sops_yaml"
local tmpfile=""
if [[ "$dry_run" -eq 1 ]]; then
tmpfile="$(mktemp)"
cp "$sops_yaml" "$tmpfile"
target="$tmpfile"
fi
local result
result="$(echo "$plan" | nix-shell "${NIX_OPTS[@]}" -p python3 --run "python3 '${editor}' '${target}'")"
[[ -n "$tmpfile" ]] && rm -f "$tmpfile"
local added removed changed
added="$(jq -r '.added_keys[]?' <<<"$result")"
removed="$(jq -r '.removed_keys[]?' <<<"$result")"
changed="$(jq -r '.changed_secrets_files[]?' <<<"$result")"
if [[ -z "$added" && -z "$removed" && -z "$changed" ]]; then
echo "Nothing changed in .sops.yaml."
return
fi
local prefix=""
[[ "$dry_run" -eq 1 ]] && prefix="[dry-run] would "
[[ -n "$added" ]] && echo "${prefix}Add .sops.yaml anchors: $(tr '\n' ' ' <<<"$added")"
[[ -n "$removed" ]] && echo "${prefix}Remove .sops.yaml anchors: $(tr '\n' ' ' <<<"$removed")"
if [[ -n "$changed" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would re-encrypt:"
while IFS= read -r basename; do
[[ -z "$basename" ]] && continue
echo " secrets/${basename}"
done <<<"$changed"
else
echo "Re-encrypting affected secrets files..."
while IFS= read -r basename; do
[[ -z "$basename" ]] && continue
echo "==> secrets/${basename}"
sops_updatekeys "${repo_root}/secrets/${basename}"
done <<<"$changed"
fi
fi
}
flush_additions() {
if [[ "$add_keys_json" == "[]" && "$add_aliases_json" == "[]" ]]; then
echo "Nothing to do -- every requested target already has a fully registered host key."
return
fi
echo
echo "Applying .sops.yaml edits..."
local plan
plan="$(jq -n --argjson add_keys "$add_keys_json" --argjson add_aliases "$add_aliases_json" \
'{add_keys: $add_keys, add_aliases: $add_aliases}')"
apply_edit_plan "$plan"
echo
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
else
echo "Done. Review the .sops.yaml / secrets/*.yaml diff, then commit and push --"
echo "the flake build the installer uses has to see the new recipient(s) before"
echo "any of these hosts can decrypt their secrets on first boot."
fi
}
cmd_all() {
echo "Discovering flake targets..."
local targets
mapfile -t targets < <(discover_targets)
local host
for host in "${targets[@]}"; do
queue_host_sync "$host" || true
done
flush_additions
}
cmd_target() {
local host="$1"
local targets
mapfile -t targets < <(discover_targets)
if ! printf '%s\n' "${targets[@]}" | grep -qxF "$host"; then
echo "ERROR: '${host}' is not a current nixosConfigurations target." >&2
echo "Current targets:" >&2
printf ' %s\n' "${targets[@]}" >&2
exit 1
fi
queue_host_sync "$host" || exit 1
flush_additions
}
cmd_remove() {
local hosts
mapfile -t hosts < <(locally_managed_hosts)
if [[ "${#hosts[@]}" -eq 0 ]]; then
echo "No locally-managed keys in host-keys/ -- nothing to remove."
return
fi
echo "Locally-managed keys:"
local i=1 host
for host in "${hosts[@]}"; do
local registered="not registered in .sops.yaml"
grep -qE "^ - &${host} age1" "$sops_yaml" && registered="registered in .sops.yaml"
printf ' %d) %s (%s)\n' "$i" "$host" "$registered"
i=$((i + 1))
done
local choice
read -rp "Remove which one? (number, or blank to cancel): " choice
if [[ -z "$choice" ]]; then
echo "Cancelled."
return
fi
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#hosts[@]} )); then
echo "ERROR: invalid selection." >&2
exit 1
fi
local target="${hosts[$((choice - 1))]}"
if [[ "$dry_run" -ne 1 ]]; then
read -rp "Really remove '${target}'? Its host-keys/ files will be deleted and it will lose access to every secrets file it can currently decrypt. (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Cancelled."
return
fi
fi
local plan
plan="$(jq -n --arg host "$target" \
'{remove_keys: [$host], remove_aliases_for_hosts: [$host]}')"
apply_edit_plan "$plan"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would delete host-keys/${target}_ssh_host_ed25519_key(.pub)."
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
else
rm -f "${keydir}/${target}_ssh_host_ed25519_key" "${keydir}/${target}_ssh_host_ed25519_key.pub"
echo "Removed host-keys/${target}_ssh_host_ed25519_key(.pub)."
echo
echo "Review the diff, then commit and push."
fi
}
cmd_regenerate_all() {
local hosts
mapfile -t hosts < <(locally_managed_hosts)
if [[ "${#hosts[@]}" -eq 0 ]]; then
echo "No locally-managed keys in host-keys/ -- nothing to regenerate."
return
fi
echo "This will remove and freshly regenerate ALL locally-managed keys:"
printf ' %s\n' "${hosts[@]}"
echo
echo "After regenerating, each host needs its new key before it can decrypt secrets:"
echo " • Already running: push the key before rebuilding:"
echo " scripts/secrets/push-host-keys.sh --all"
echo " • Not yet deployed: rebuild the install image with the new keys baked in"
echo " (see docs/auto-installer.md)."
if [[ "$dry_run" -ne 1 ]]; then
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
echo "Cancelled."
return
fi
fi
echo
local hosts_json
hosts_json="$(printf '%s\n' "${hosts[@]}" | jq -R . | jq -s .)"
local plan
plan="$(jq -n --argjson hosts "$hosts_json" \
'{remove_keys: $hosts, remove_aliases_for_hosts: $hosts}')"
apply_edit_plan "$plan"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would delete ${#hosts[@]} host-keys/ file pair(s)."
echo "[dry-run] would then generate fresh replacements for the same hosts"
echo "[dry-run] (not simulated further here -- run without --dry-run, or"
echo "[dry-run] preview a specific target with: $0 <target> --dry-run)."
echo
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
return
fi
echo "Removing existing keys..."
local host
for host in "${hosts[@]}"; do
rm -f "${keydir}/${host}_ssh_host_ed25519_key" "${keydir}/${host}_ssh_host_ed25519_key.pub"
done
echo "Removed ${#hosts[@]} host-keys/ file pair(s)."
echo
echo "Regenerating fresh keys for every current flake target..."
cmd_all
echo
echo "Next steps:"
echo " 1. Commit and push .sops.yaml + secrets/ so the remote flake is current."
echo " 2. Push the new host key to each already-running managed host:"
echo " scripts/secrets/push-host-keys.sh --all"
echo " (this also prompts to commit/push if step 1 wasn't done yet)"
echo " 3. Run nixos-rebuild switch on each updated host."
echo " 4. For hosts not yet deployed, rebuild the install image (see"
echo " docs/auto-installer.md)."
}
main() {
local args=()
local arg
for arg in "$@"; do
if [[ "$arg" == "--dry-run" ]]; then
dry_run=1
else
args+=("$arg")
fi
done
set -- "${args[@]+"${args[@]}"}"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] no changes will be made"
echo
fi
nix_extra_opts
ensure_admin_decrypt_key
case "${1:-}" in
--all)
cmd_all
;;
--remove)
cmd_remove
;;
--regenerate-all-keys)
cmd_regenerate_all
;;
-h | --help | "")
usage
;;
--*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
cmd_target "$1"
;;
esac
}
main "$@"
-110
View File
@@ -1,110 +0,0 @@
#!/usr/bin/env bash
# Detects and fixes drift between the ed25519 SSH host key nix-cache is
# actually serving right now and vars.nixCacheHostKey (variables.nix) --
# the value modules/nix-cache/remote-builder-client.nix bakes into every
# client's declarative programs.ssh.knownHosts, and
# scripts/proxmox/configure-nix-cache-client.sh hardcodes as its own
# default for non-NixOS clients.
#
# This value has no automatic source of truth: nix-cache's host key is
# generated once (first boot / container recreate) and never touches this
# repo again unless someone remembers to update it by hand afterwards. It
# drifted silently once already -- confirmed live: variables.nix recorded
# a key that no longer matched what nix-cache actually presented, which
# would fail every real client's SSH host-key verification for
# distributed builds without ever producing an obvious error pointing
# back here (a client just sees "Host key verification failed" against
# *some* key, with no hint that the trusted value itself was stale).
#
# codex-maintenance.sh runs this in --check mode on every invocation so
# that drift surfaces as a warning instead of a future debugging session.
#
# Usage:
# scripts/secrets/sync-nix-cache-host-key.sh [--check] [--dry-run] [--host <name>]
#
# --check Only report drift (exit 1 if found, 2 if nix-cache is
# unreachable); never writes. For CI/maintenance use.
# --dry-run Show what would change; never writes.
# --host Override the hostname to scan (default: variables.nix's
# nixCacheHost / env.sh's NIX_CACHE_HOST).
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
variables_nix="${repo_root}/variables.nix"
client_script="${repo_root}/scripts/proxmox/configure-nix-cache-client.sh"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
check_only=0
dry_run=0
host="${NIX_CACHE_HOST}"
while [[ $# -gt 0 ]]; do
case "$1" in
--check) check_only=1; shift ;;
--dry-run) dry_run=1; shift ;;
--host)
host="${2:?--host requires a hostname}"
shift 2
;;
-h|--help)
sed -n '2,23p' "$0"
exit 0
;;
*)
echo "ERROR: unknown argument: $1" >&2
exit 1
;;
esac
done
current_value="$(grep -oE 'nixCacheHostKey = "[^"]+"' "$variables_nix" | sed -E 's/nixCacheHostKey = "(.*)"/\1/')"
if [[ -z "$current_value" ]]; then
echo "ERROR: couldn't find nixCacheHostKey in $variables_nix" >&2
exit 1
fi
current_type_blob="$(awk '{print $1, $2}' <<<"$current_value")"
current_label="$(awk '{print $3}' <<<"$current_value")"
echo "Scanning ${host} for its current ed25519 SSH host key..."
nix_extra_opts
scanned="$(nix-shell "${NIX_OPTS[@]}" -p openssh --run "ssh-keyscan -t ed25519 -T 5 '${host}'" 2>/dev/null | grep -v '^#' | head -1 || true)"
if [[ -z "$scanned" ]]; then
echo "ERROR: couldn't reach ${host} (or got no ed25519 host key back) via ssh-keyscan." >&2
exit 2
fi
scanned_type_blob="$(awk '{print $2, $3}' <<<"$scanned")"
if [[ "$current_type_blob" == "$scanned_type_blob" ]]; then
echo "Up to date: ${host}'s host key matches variables.nix's nixCacheHostKey."
exit 0
fi
echo "DRIFT DETECTED:"
echo " variables.nix has: $current_type_blob"
echo " ${host} is now: $scanned_type_blob"
if [[ "$check_only" -eq 1 ]]; then
echo
echo "Run 'scripts/secrets/sync-nix-cache-host-key.sh' (no flags) to fix." >&2
exit 1
fi
new_value="${scanned_type_blob} ${current_label}"
if [[ "$dry_run" -eq 1 ]]; then
echo "(--dry-run: would update variables.nix and ${client_script##*/} to:)"
echo " $new_value"
exit 0
fi
sed -i "s|nixCacheHostKey = \"[^\"]*\"|nixCacheHostKey = \"${new_value}\"|" "$variables_nix"
sed -i "s|NIX_CACHE_HOST_KEY:=[^}]*}|NIX_CACHE_HOST_KEY:=${new_value}}|" "$client_script"
echo "Updated variables.nix and ${client_script##*/} to:"
echo " $new_value"
echo
echo "This only takes effect on already-deployed NixOS clients after their"
echo "next rebuild (programs.ssh.knownHosts is declarative). Review with"
echo "'git diff', then run 'bash scripts/codex-maintenance.sh' before committing."
+45 -225
View File
@@ -1,252 +1,72 @@
root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str] root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str]
nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,type:str] nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,type:str]
nix-github-token: ENC[AES256_GCM,data:k1vYz7SqVhzpWa6jTL6NUD8lKOCpHCgTm+HT4IcnbzbSTUZP/bJUYw==,iv:UqAULZnr/4+VcioUDfTwvOSuwM8K9JgGhiApvYQPyoc=,tag:1LKHXhWAO/AHPDIZFBb04A==,type:str] nix-github-token: ENC[AES256_GCM,data:OfNRGJg16Ede6EilWUetCs9za+xk5/Lsa3SpVajsqz8PMdA1xQNeCWdX7ZAMdijHClpBhU6ETFGsXvt41O9aORS951uijeGSW7/NH35/bnPISrKdYeBx/+xEiqwH,iv:QGU3v7xOy89uzRTCb1U9ICyJ8XYIpXrUsDt12aL3g2Y=,tag:Bde2wcWNv8H4WLxSEUAodg==,type:str]
sops: sops:
age: age:
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBibGF2a2FRcTRFTk9rQ0NN YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmaFVBeVp3Zk80TG1OU045
TTFoRlBWbGdSMzVVRHYvVFFvVm05ZDZBb1VrCkFoM2dTMEJDMHRKd240OEExWnUv b204QzFYL1psVm1xdlBVYmVIWFpKVnJWbUdvCnJRVGVUbElPYVczeEdkSDU4WGYz
RHV2S0taWnl0dS80YVIvTjBpVEk5a0kKLS0tIGF2TkNQdlVJTTNOSnI4cEZCRll3 bkliK0dGcitjZnRYUXJQVVpTY2gva0kKLS0tIHBxMDBNYWJrSHAvNlZDWERmbVQ3
NE9aSWxVVTlyV2hPQ284cU94Y2hVNEkKhUFgr345o91orzqCzkKdh6dAXlPfwqTJ VDFscTRjdGpLUnk5VVdyTjY5MFNPSW8KX5Or/zoixFulaX8ljY4ke3jdhc3Jaw95
su5kATsokLzcyvT/gcOKsDBzO219CJ0cSVmpgVJtImSCJDwUS3A6Ug== 4BqrlILGapcOsmWTDkDZBtS81oNvlRnH+xaM2UJSe+nIplNcC+Cl2g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGc3cwNzk1d2xDajFqQTNH YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1ZUZ0bXRGVFMzZzg1TzhE
QlhoS0ZqWnJ6b1NoU3EvMlFsTG1oYkVrYTI0CjNEekFrV285aVc2Vk5pc0VHNDBO N3kwYjdEV0tmcVdacVJHZUNXRHo1aHhXczFBCm1WcW51K3NSK25uMUFNb3NRbE9J
MndVZHgrbE10TXVOL0I2Y1hKWEJCcm8KLS0tIHRoa2JtNHVLU1YySmlXMnpsY3lh UE00VWNIclhVZHdPUmlNOXI1aGlhck0KLS0tIFphckVmRXZrTmI5K3BUL3NlMTAv
R0NTR2szNVhvVy81bHRjLzJSTWNWMEkK26wIFqhVk1fYV79odbslVaFxoDLykRNN OG9RTHNEQXVMcXVWZXZnN2NYZ3hraUUKEXrloIgmADuRpa2YN2GIRYT+YN60pxga
ZiOkWewH/JOuIXR260UWAGq4bKXfnUs78mAchh9R2I0F4p/CjGMdQA== bLeVDtBYU+KA+2F8S46o43ZmRTE2MXWlMhnZ35uNR4hgE3auKlIziA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVRTE5TUNYWUpxbWdIenB0 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZdXBMY2t6U0VBWjRyWUV2
WVdlMXFWK01vMndHTDhRdFppRC9MY09xeFZVCkhEL21Pd1pzcjlKQld5a0JvMjlu WS9oS0tWUnBQMWhPc3VsTVVKSnZSemxvU0FZCmFWbzFEZEZrMzJxTmpJKy9HY2F5
QnhXNDlETmV0QXJ6ZWhzdGROZ25MQjAKLS0tIDQyclo1ZWtkM3VNbjJ1dHhTZXk4 RHBjcUNYZ1R5ZHUxVmhPYjJRQkZyMU0KLS0tIGcvZ2hPQURmanJLRjRaNEJtTml3
OWRVdXZEZ2pEWXJGQmNaMm1aT2l0aWsK43Oc54CGekbbBkcnctXnUMyqo8ygmBL0 dElTa2hZck1rSG1HcDBVSjZQNjhzT0EKgUylDuQBZkW1Z6AB/EsVbxcippw5XOeY
BMC6FtUVYMkP++Y7KFR6sudMemf9f+FbsCVRqBd9x+c2YzzgyH8ogw== e+NDtqFixYR1u57+fKVl5dY281kspYIQDCUx1lBZiH7iwUPZWNtEfA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEaEtNL243bFF0am9HSkZp YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHYWtBYmU2Qk0vQ2dUQjUy
anh1Ukl0bTJVUGJXUzM2NFo3Ry8yeTBVUUIwCktPVkpKNXBOYjRTMWNlay9Ia3Nj RGs1b1BzVm82Z210MGcrbnhiS0pCVnJzbkFVCnhpZUxSVmZTd0VSM0ZvZ01tM2hP
bURMbFk4WUw3NVhUb25mZmtSU1ZRbHcKLS0tIHdkVEovUVpxMk5aVGRPMWdpaUdj QlJ4NUQ0VjBBSXJGR0VScE5zNXhjVjgKLS0tIFZyNVF2a2VPdzhyZ1MzN3dYa0xx
NzdISWtmYTFBb1FkT3hTeWFnaFJWOGcKdcLL3/fAgDME0Xsx7lS4q1boG8t7no3T U0tqTWZNSDdHU3Z5UHNBbGR6aFZ1MDQK7z0OC0yazekxpLmiKFuqWZm1rWMZqpXb
BZ6kdHnWe7fdzoTrCsaxWZPRom1hs10FZ4H+VI+CiEKki0N2XDMB4Q== FGnZFMDdEwB5wTeiy4LCXS70zyr5eVT7KPeK25EG6pXQ4qqKZ12CyA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQTkRYelcvY1VEcThELzVo YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqSG5NemtzUXlhelpvblFl
a2E5QVNnME5VMU56SDduVlZKQUdhaTlKY25rCllGRk5GZlJMb1c4M0o0dkREZFFt TldjUGVtZFFXWGM5ei9EN3V5N2ZLY0VRV0RJCnFNN1lVNmhWckFWbUVJd0grR2Ja
MW9BcDU2MkxWeHVESUtqcHpWMDRzLzQKLS0tIFZSU2J0RXoyQUxlYVNUa2t5ZVhp MmdYazZLUVFpelUyMFhta3dGSEc2Mm8KLS0tIHl4TlhhVFVOcmkxaHpKVWlrdEJx
bEVlNURjeGRvdXkrS0N1eDllV3lJTGsKc/4cEEhEJwmLSxUDFzAMxD+7TWcOKNjD VlBQazd5MG1yc3E0TzQrd3FmWG8wd00K8+cAm4oWVraERb8i8EurAnsR/L085uPR
+Kcz2+sX8sTXLBLCGABR1XQdy1aejVku4atujnDOE6BzYjjfkRfc9A== zzIZEYW6hHxf9Ah9vMzM+3KIsnnS0Gr5QT8ATzpd8huXCqPNiveDFQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQdDdQUEh6ejg3UG1LMi8w
bTJacGZQY3dUVEtJY3NGeWpxYWRITkZpN2swClVmb2F5N0phYzRXVG1RY1NsUm52
OWVLY3FJU0JXcGFVZDJGdVJ0dVhBN0UKLS0tIEl2b3JjV2JaS0crMnRpZElGdDZS
MDFyelRvcUsyUDdtai9IYnpHK3g2L2cKlvZloUKACDy9OyRI7ruPUjYqFENXzcVI
amFHFTbEGKVWIPwDjEwsMT+S4bHBkNWyny5dD5CrBpk7fiyQ+5U8+Q==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGa3RDdTdFK2VxTGYvYm1R YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTZnZIa2xXM3RIYk1tbitC
RlRkRFZqTWVXSWRMd2ZjckV4bXZSWnVXYmc4CnFTNnRQSFJORWdlTGRUajNjZTNL NlRTeXFib3JDdXdhR01ZdUl6Z09vWmllRGxBCnpPalo4K0JhRlNpQ2d0b2RNcjUx
SkhjdVhFSGJHZ2ZvRnhvaHFhVXQzek0KLS0tIDRXUTZTU0Rlb2crTWd6VDV5VVcx K3FRaXdMVHZud2FKRkw4UDREL1NrMDgKLS0tIE5CZDBHRXNscG9JK1hvWlZPVjJy
Q2x1TnAxem45RXordHlqYy9CRzBzTlEKvKncXgXO1rJcHRXJE//7YbnwqCx+472D d0d2VnE4bXd6MGNPVzVUdFdocDYzcHcKW2Hs5oBO5ooc/uqWV/TOjq1xsxFR204s
XXby5thGhzTSTgcNcakyK20fzNlOYr3D/iRA4Ytcd0zHcGlj4qVaQQ== GRtFUOg9tfglk3TcQbgZV0/pYlBJYzgb7iZD0qVe4XYj68q1A1bPwQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
- enc: | lastmodified: "2026-07-19T02:30:40Z"
-----BEGIN AGE ENCRYPTED FILE----- mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str]
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnMVZiZUNob2pqdTA0NlJ1
UTdGdlNEVW5YOHZaYlZFZy9YaGVoV3FoR2hzCjdwc2F3bkVaUmg3dTZ6b1F2Ymwy
QkNvb1NEYnhLUkgxUnFYb0R4U0puZ2cKLS0tIFdrbktMWTRSQS9DT3doY2RaODJL
Wi9YazhEZzZFVXZpS1k3SE5wRWlLdzgKnlng++3X0NvOvl3qES0J166pXGM46dy8
38/RE0rjc+92+JnOXsgBUfvmS0td+oR5T4lxGpdngksv6gyPaU71Pg==
-----END AGE ENCRYPTED FILE-----
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLRmpiOTdKYmVJOEJoRFRl
bm1UcjhXeWQxdWhWMm0vQXgxWEg1VTREekJJCjFJYnFkZFFlRUM1MHo4djBjWC9h
YlBlUVl4R3BOelNqY1lPNVBINHFtRTAKLS0tIFk5QU9qWW1jRW90SmdXVU1naUJk
R1V2eGhrci9CYzdkQnRYTnAybVY4L2cKw3LFTr8m8OMmlVysBZbiMKKxaA7Nt8F4
CJEgkcPyQGOFnNen7UAX1ksJrm+3gV7ZL91VxzVvCdd6zScHlG0PFg==
-----END AGE ENCRYPTED FILE-----
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5Y1ROSGlLcFczdjJNazFE
TUZJVmg2RldsT0pQdlZrRmNNL2ZibXNoZVNFCmZ0TXN6ZTJ6SVluZDlIVjJGSm8v
N1d4T2VHZEdiSVZ4ckpCY1pRQlFDcjgKLS0tIDAySFJGVC9kM1hVbEZqUUdqSHhv
YUFUeGlCcU9GMHAyZVBnWlRDSm4zUU0Kw6c8zMdxGwpNlukkw9ECT2aXJW98qNh3
wjUXDzmXNk9f8LNvj67na/JvsqXR4yxXFsEQkphAm6DvLkZ+5BfJiA==
-----END AGE ENCRYPTED FILE-----
recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzQ0trMEVBcm9RQzFXUUlF
ejBOYkt4ekFWcmxub2FETElidzI1SXZCZ0JJCldEczV6TGhzazdiOWFiWjVObU9N
NWdDOWpHWFRydHo0UjdzdHVYZ2pIeUkKLS0tIGN0RHBTbEUzMDVnK1Z3UGVhZStW
ZXJiQmhKYUtSdzRDd0JGK1h3VExoc0kKM+tX68bgbAgdi1Gk3MeW2YXwmmszGEeR
WCrKI5hkBU6ZaAMF6LU9idvdUqFCdJ4l8up5WSQuO+10a0pvYGj/Kg==
-----END AGE ENCRYPTED FILE-----
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEYVRwdE5vVjBKczFBY3pj
Q0R0ZDZkUU43N3Rpb3NCTWg1czFrMWtrWEhBCjlONWhjWWZISy9oUS9pRFFlNXlZ
Ri8rbEMzKzV3R1NvN2orT2o4ejNPYlUKLS0tIFVDSXZaaGxMSWRpTmphYmRuMXpH
US9qYS9MZThjbkppcXNQUFdzZ1hDUWMKa9UH9GgZt8UD+TWcR3jnkNsVsfOlWAFw
cgh/gddnKu8lTa+9g6P3gi85/JhNqt3Fl4mmzk1aU0RjOIadzPWs4w==
-----END AGE ENCRYPTED FILE-----
recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1SXpOLytMRGtzSUR6aE9S
NWpIZ3RhRElIN3dTKzNrZWoxeDVTK1Y5eUdJCjF1cVJTWnFKT2RGYkNOQjRCUm9P
V1pPSlhQaDFhWXBJV3FMSlp3N3FXVzgKLS0tIC9PQkRrZ1g4YWMyaW9zbGJ4V0lw
VDJneWs3NTJoem5oWEZFVkd5YlplQ0UKFXMBfbrVMyydpCHRaqmkYOS/+CufsDGN
69Whneu9oXKGnbrFN+ikh+2O48VLWr9DSiWb0QcIagkNlxXcV3Vd+Q==
-----END AGE ENCRYPTED FILE-----
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEL0d5NytCUzNyV0k5c3VH
Tk9FK3FrcmdGVDJmN0lMTTVoaThJbUUybVhjCmVjNXVrdGp5SXJ2czVPa095cU5j
YUo0RXNXQ0dBQmlVOFlGZ3dsQTJ3MUUKLS0tIE4welZDbDZ5OVQwNk9reGozSG9P
Rkdiazg1ZjBmZVlORWtGVEREM0F3em8KFqIg8N82g3E8irYf8Zw0OtLWVH7PzLmh
bQkotIzeemREJjA6Q9mIxojG1mD4DcexIBSkBAhIH6vM8Nksa5H/xg==
-----END AGE ENCRYPTED FILE-----
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkdjNHQlpCd1RJWFg5WkVY
NDU2TFA3YnlKekRKNkUzcmYrem95SG1uWXlrCkFpSThsdjNDSzh0eFpESVNEalJY
QjVlVkhQeTB1SXVpT0JvNWV3VXJFVFUKLS0tIG5SbWp1bDhDdFlvWXEvMUhxcy9R
KzBuczlWZDQ0cTFFb1dyVEpidjR5OXMKUXPeEpeoxb5egqBBI8auM4MJzZ3RbMrG
0L9Y24zY+1i+QIM7WIlanYsNyhrRMHy1mXBjRHs/ka7ibyO9HUSaBw==
-----END AGE ENCRYPTED FILE-----
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuMHVoK2VEYUZnNkc2VnFN
ZVRxTzh5Rk0zQjRzT3ZvcG1FWnBGL1k3Zm5BCm16amZ5Yk1PT3dBRmJEMzhBdlRK
bG5hMFdDS1owZ3lsMkJhSmZSc0F4S28KLS0tIFVhaGFCd3dUaThFYUZYT3hGaEJD
OE92TnowVjN1OTl0UVE5SjdtNFVaMmMKGcM0fE5+iL6Oz9pHCCMlpOwwQruAG2Ur
SXu4gqz4vQNZlmY5o3hQi9zmZV4OOlKhwmInc+5vE2ypeL1/zNBX8A==
-----END AGE ENCRYPTED FILE-----
recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBobTM5QWgyem5uUlBybWVv
RE1jUGtlYXdZOHhmMFlIekpvVWNxV0J1L0RJCjhsTEVBY1V4WnhGUWNmSVJNNFor
OVI0bjJwd09NMkJkU214bnRFQVcvdzgKLS0tIFpIaWhZVWh2TGFNaG1aMWYzcDFF
NFdMeGdBTkxoWTgyeUdxL1RqTkNSdEUKb2tuVoaqRY+B+KgsbfCnWwi299aVSH/N
4GLnmC97DoIIcMF3NJbHXPyUTCCrJ142j3rXGrpATkO3IaeMTfeN1g==
-----END AGE ENCRYPTED FILE-----
recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpS2ZqR09PSEpaN1FYSXpm
akl0QzAwbnUrNXBFL2RaaythUU1Td3FicWtNCkFtMG9Ka05PSUpCR0hiNnpBVyth
TXNLa3Z2L041SEl0bjRzR2dRWGZNVDAKLS0tIDcrV255cVFRTXJ0bVcrcVRFWVdZ
WGdVNHNIL09tVS8zTSs4VHp4dXdoUFEKkXqeKKwQ8fp4AvIy5Mzf/hD9IO3NgAw3
70TH4YjCEiBpd3Q7/+iyB4yBYCayJ8U+KjC1Q8aTLQEqQKyiEcheoA==
-----END AGE ENCRYPTED FILE-----
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6b2hSbm02NjhPb2s3WDQ3
VkFuMVg5eVdoTmtnbjl0L05EN0hqVThXY0RZCk1STXZveTZSSURkNnpDTVU0L3c3
UkJNclhLcEdaMXFDaEdLZEVjL2w5c1UKLS0tIGNnME5mVWk3aU1LelJGb1UrV0xS
UHl0Z0hWYlp3b3MvN0FETndicCtpd1kKOMn31Y0XKogmJpm3CrIoqEIrwKqjZxE1
2N8LX8N7FJNQW3LaoyTok2hbe7KjhASlXVapDM8aozNouKnGv5h6Kw==
-----END AGE ENCRYPTED FILE-----
recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvTGpYNDdYbU5qWkRJRVRu
cmt2MEs5bTJUV0k2QjA3VW91eDQ2ODVUWVRrClZORzJSVmh0eFNyQmtDNjMxMGZs
L1grblE1V2hPcXBFMHRIOERJL2pBZ0kKLS0tIFNLbkZRK2Z2Y3N2Y2hTSUgzcW5m
WXpCZEdoelo0SXNKcnNuMmg4cDAzeDgKtGn1ZDRCbrTdmzCn9st/61rKOvEZqCtG
0brTsCk21iWLzKrMsg/B+kNiPB9/bAhessFJ/TDqZPn9/DWZ0x97Dg==
-----END AGE ENCRYPTED FILE-----
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyQ3Z6SkNyVW1kNWdHemZi
R3owRUViRVBFMFhXZjYrcWhzUnFjZzVkb1c0ClhZNWp5NTVpcitkSmlkMWoyL0kw
ZkExZ21Nb1E3TUtvT3BNdXEycTYwL2sKLS0tIE5MaEFyMEJnV1FiWUZxYWFsNkQw
czNCdjRZZEZIOWxXeFR5Q1kxQkNYK3cKLq/eGWojEiqYwcwvnKGxHLHFhvCGLqlP
rVu2LqLqmEkrblRas5JJ4cQg9wd9XQuxdTOT5GSFUtZrX+P7XTbhNA==
-----END AGE ENCRYPTED FILE-----
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuUS9MK0hsbytBWTk3bVdJ
L0xiT2xhUG5UU1dtOXBlMVg4VFVZTDJkZVJrCk9leDBtUkN2bnBUeE9BNnY1NnlF
TVVHdUowY2pBZXBIcnNUTG1aYllwUDgKLS0tIFdpRnVZRGxmK2JYeVJMUEtMQWxT
cnBIanpxYmQ1U0U1UXdhUS8zdXRkTVUKrEoNKzY3DXyu547GmbSsXVmIDvIoNCi9
hqVSCXG1r4t/tKfVuNyUsBDzfboiRgH+5gNk+h4hxmbI2w472nvPXg==
-----END AGE ENCRYPTED FILE-----
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZR1F4Q1JBa2pscXVKTUli
eFdDZWdsYlY4cmQ1ZFBreXJDOW9LaW45ZWx3CnA1MWJYSC9ucW5OT3VWQ0w1ajR6
eUN5eE4xUVFxdGNEakpQYVNuaWR5eEUKLS0tIG53RDIwZDVDWFd3ZjIwd2laK3JQ
a0hRZERlalBWTXZoUm5lYmlYR2dGdFkKIbB5cuiUOGGmZx5QxuZbYeUjNORT4pKa
ASvNnGv9aM5uJebWLhhO0Kak3dM+OxP6+tt1/MNNP1LxUqFPJuJdeA==
-----END AGE ENCRYPTED FILE-----
recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxb254bVQ0OXQ3RDNZMWU3
aUhLQWZYa1VobDdCbHZyb1JFbGRBNHBQUkVRCmR4ei9oTEhQSkhUK29PemdRbVNn
elQvN0UvUldnRmRNZ04xbDlCL202WlEKLS0tIFdiVS9maFYrODMrSU9aMFlXRU9U
a1BXUWozUEF3YlU4RHBhTytEVktTNGMK8KiRVfHfgWojDSS5k5g+NuIIFZSucBOc
PoZNWFmDyMmL0IDvTYWS8lgzgppgjSXkecIGtTGmiiXpTxot05jLAA==
-----END AGE ENCRYPTED FILE-----
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMU1ZFRVpld1ptM2lnYm9u
QmhSTE95ZUM1cVJUV09sdVhTMGVtRnRpb0JJCnFucml1RGtxdVJCOE0xUXNXeVdt
VnVBYXFhVUpGamhCbWtySnhGeEtKNU0KLS0tIEp4OGRGYXR3WHhNZHgzU3hNS3E2
RVpSMForUGZRS1dUR0hacnBQQ0dLRW8KK77iAvB2e75eGPC587lCBic6HaKypnnn
HAbjBkl7ZwyKWpyR0pOTNauHWh2D8WDGjc2ciGaI7j0cbNeTdzCyAg==
-----END AGE ENCRYPTED FILE-----
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTTUd4VWF0bXZQbGFKSFg1
K1ZsYUNlMHpSNG1WTlRxMUJmZ2I1aVNHYVY0Cm9FWGZDYnhhdXNCZUxJZmlZdGh2
ZG5FWkJJUitVMDc5L0Qza0JqQzBsN3MKLS0tIEYxanNSakVud3c4NHRyZlNnNTVp
ZVo5TUVXcFYyeUk1eXZ6VldWa2l3b1UKCuXfXW12E0ybEmADPBsuno9lUH+yZC30
buTHSyG2WUpV2J8Om0uxUrnbGXKLUhdjUut2OijAi/GQP63CWMNlMA==
-----END AGE ENCRYPTED FILE-----
recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhbjAzd1lOM3FaVGxIUE5Y
ZENGZlZLNVZsaTg4U3MxQU9PMTNhTjRVY1NjClVVWUNUaWY1c05udUJVOG8vOGJv
dXFlZUxxQzlCaGgwYWp5TmtBbFpiNzgKLS0tIHJSMDNMRG5ONzd0d3hoQ0FLOU93
TW5HWnBRTXJwN2VjTllqTVRBcHBiTUkKcI7du8jo9LWLtwnsDFyec+fn4w6zWyaw
r7m05cm4sKv6xDPMZrGNlwRziA327qn8/7/xB+Tl62+eECaeIqLUUQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArSG5nejgrdDBBMnhzYVpo
eENwVzA2dnlPZS9DaXhBa25EdzArS0VIbURjCmhiYUNMOXRHLy9xaEJZZExRanpl
dGhiZzlyN2d5NStOdmZMWnBhbURQNU0KLS0tIDhDNFNleWNsK2VITHRvRVZKb2xs
NjFkYzJKdUFFS2dUeG4reW9NWlkvZXMKkUob0p2OZXPgjLJ3MUto1tiJTUL6iyFw
uZZKNG4C2phTb9Ry4+wvKb5PakAS8cmoBFRcVz0p6EBsl/yxbwcEMQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
lastmodified: "2026-07-23T21:15:41Z"
mac: ENC[AES256_GCM,data:qFhnPra6IE3wyKQ4WKweON0S0YtD5I0adGZVfA0m6BVilN6bX5oC/1j5NK2oHrsz920hSl0SOF8LrpqOrUyGjSRkPsN4kq8qr9bJcrX4URiktP0oRden5LLt6hf+ZRP7WmRXFqixPkPHJnZIoAvkNnTFce7cDq5NEAHkKUEKG7k=,iv:nyblUDGeu3TUfFivYylOn3C/HITj99qiPI2+mh8AGh4=,tag:FrtRzSCylC4wlIoqZdfx7w==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.13.2 version: 3.13.1
-52
View File
@@ -1,52 +0,0 @@
wifi-password: ENC[AES256_GCM,data:SZQPtU6PYHbf9o83wq3KTupx,iv:FxO68Pn/+N58r/OPLfkAMYPFpP8TYxszMniFd/01E38=,tag:jwxaY6zDEcO5r9OWSfvUyw==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoVjNIOHJseTl6WDBzMDds
RGJqdTlyaWV1UUY2V1cvUkN6dUxTam1yUWtrCjBwQ25kZ2RVTTRwenlzQmZaM2hR
Ums4Z1NueFEzcXk1YmFjNHhVZTNHZUEKLS0tIDl2aVJlNk5jeG1wT3FMTnNvNU51
bVhFd3NTbWJLdDZzUDB6bkJnYkpUUWcKnz6lOojAEcROmGNLXGc+DWfaRLWFMS6g
V2Kx+Ouhojx5anoE0ZkFs+7iVEvCX+oe5LOeqzxoXUFEMBldu3AKhQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhR3lVVldmZnJTWEFpZDVn
c29IVGttK1pvdXlaWVRIU2kwaGNIejJLc0hFCmsybnRia0oyRUZTcHg2V1BCeHg2
RUszSVN5dDlqVEhybFpBME1ramNKWWcKLS0tIHkyMDV1Z0V6ZzFyMlN4dDhjNVBp
Ylo3OXBIM2VOUGc0eTdJNGtYUFhLRmcKy55/uTFdjQflrDfGU8o0B6i+eF9gmaoN
TR47KGtDk3PNE7M+H8YLTEYGEypoP5Rtiwy778HaOQkLlturEJ3qhg==
-----END AGE ENCRYPTED FILE-----
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzZFZjZVI3QkVjQ0FjWlAx
VDhCOG5lcVUvQkhkdFRPZDY3aUpDRWppWUN3CmEzQVBBVFhzNVN0VVRTdXFVS2wz
cXJiTE1MN0krY0FvZ3crL0kwWWtRREUKLS0tIGtkWXdMVC9MRVpJM3ZVK3M4SElZ
OWwzbDMyd2c3WllxZUtGTHRBQ3FsbDAKkFlJVvJxaqc41fG2NXEkf2ZWmwxPqvrt
zqr1xc5kn846G+VF97U4Ei3Kpe6TgSYc3evmZDsV6ajINM0Xi+3JKA==
-----END AGE ENCRYPTED FILE-----
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOZ0NJN1MyekRaNk56V2t0
enNBUVpVYnZjVm1nSnBhTithcXdKVE9VclRvCkZkbDVFNnVnSzhWWmdPQURqdFpC
MTJHcVJmSFBpRDVmbExGNVJuQWQzZTgKLS0tIEhjYVRkb0FWWnd0QWFCQUZLSnp4
RVpVWU9kNlZjajVxNk13V1k1UUNzV2MKVglPgF8juRhUfN4/MI6E9Z/yONCjUkzO
dCH6rY3eZh3EQm2F0XJpfsXSRw8bT4fEm5LCqAtTjdMLrgEWoPh4EQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUQVJhUjBFUURMUEtWNzV1
YVJrYnFFY3JyUnJtV2ZadGxBOWkzY28zK2tRCnJRck9UV0RjYmxUcEUreGVyaEd3
aU5td2ltRUZ0UjZqRm43YXlYSDV3SGsKLS0tIDArcTZZTXI4ZXJWZzdBL0dsN3Vk
U3lXWnRTaVJIVzl3NWw0dE1VMWEwbXMKUEgFevlDaV2GwzHfoFDjts6DXIQd76tF
RekC7ZRIzd3b0/6+oDv02Now7Fj+r5NuBrmwu7H4PYvF1gYtZ4eIPw==
-----END AGE ENCRYPTED FILE-----
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
lastmodified: "2026-07-22T01:15:21Z"
mac: ENC[AES256_GCM,data:dC/oIqMUHkOh3AocOwP7Gc6XGH3L+nTqJfhFNts1DNbRXsopNIxVBtIz2pEhwnWSQrqPisDLmPHFBwRpGVn01u8w8IU1FKbAKC0J2nJXF8ozpInbjzDOmehqPWZG7yaKoq8cwAnp5XOk+IVO4l6tPxLxkExU5fT2ALuMq+sgOko=,iv:jaVyArpf6zMCFa6J9X1aQMGrmFq+W2CPZdWO6vVW68c=,tag:S+qF8/FkgHc4uW0e4ICmSQ==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.2
+19 -38
View File
@@ -1,53 +1,34 @@
beszel-token: ENC[AES256_GCM,data:meuzUP/6wCssJDVTgbC0XwiLZPMGyDl55HEIiON9xOXCD9k6,iv:TDqWcp+8Mxd8wN09r5otQRQXq3XTeQphaTWxvvuLTAs=,tag:cRPZQGlwB/dTguBAheWPQg==,type:str] beszel-token: ENC[AES256_GCM,data:meuzUP/6wCssJDVTgbC0XwiLZPMGyDl55HEIiON9xOXCD9k6,iv:TDqWcp+8Mxd8wN09r5otQRQXq3XTeQphaTWxvvuLTAs=,tag:cRPZQGlwB/dTguBAheWPQg==,type:str]
cache-priv-key: ENC[AES256_GCM,data:6vQKIf7eS0WNL2Eptoi4VWr18SRMZfN/H/aFUUtXdMYQY5LLyBp2EHRKqZcGFuh1nZhUdAxUztq/CVXx+QFxKW+ElHxCxUSp0QqI1fdSkBkKZb8hlit5SoX9JtLzZGg0HBNM3nJu,iv:0J+xmrPJhInHhFR/c41ACjuTfaIoMkQFSfbL2KkgFa8=,tag:f4s9Szs5oprVVRSyXaX48A==,type:str]
sops: sops:
age: age:
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMWjdNblFJdUpyMGFrSWJm YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbHd1Y1dpa2ZiQ2E1bXRP
a21ESTZTK05YNk5CWFlKNlhIdXhIUFhQNVR3CnZZK1N2N3VKVWs5ZGxZaUhCb1VP SnkrWHI0V2U1eGFKY0RZamZQQjluS0hGeENZCmduUFQyd1Q5RkZGMGFoenJhNmZh
dlJaNXM2alNiUG42NlFqeVV3WDdtbnMKLS0tIFpvTlBZU2RpNUtoNVoxeUdKU2Zt VmZ2OStNaFpJejFxQ2JUZXFpNThaMncKLS0tIEhJdGdQeXEzb25Nbk5YZktCWVUz
QjFydW1xeDd6N2xBU2lhRmtuREhkNDQK49/mcHVCj9tBtGe1ZnK14YWHCtgzBBvV NUJDUWRoTkd5R2pFblZjS0NSbVR5dkkKDYeW+zRpha04/CasFM91K6v1PpkNGHRu
giDA8BzThifXjWtL8zF60kibKFVRGrsRZT38tRuv5QKzu6Z5Cbc+2g== qAoKs9KSg9VxS7ya8RuLmylKRdpPkupm/8SXIJvQuCXp5LWmNJ4zkA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkZmtLL3pzbmNBNkkzRDhF YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4UktiUUdmd092bnZMdFMz
MFZrS3FlZ3I0ZlNJbVN4aUpxYkQ5VkRSYlRjCnUxM0tqRFJDMis0TDd6cUthN0ZE Q05xVlhnNzhPZUY4TjF5bjRCbklacTdJd1RBCnJ4bEVMaWRLZExIUFVEUDBXTnlV
eTBrSDd4a1BzNHZqZVJIc3V4NzBoeGsKLS0tIE9ENFBWUUtadC9vQ1UyRmFLWG9B MDU4WUk5VlJjdlhVUVpzZXlnWHhqTjQKLS0tIE9iclhGVElPcU9OLzF0bEVjMjVp
UUZLeThnUzBNRSttemtuaTN1NWlkUVkKbDeId9JVSvjszcA0TT4L9C4IB5xLCuJK YzlQTGFYZEhLdTk3N3ZramVjVGRXREUKjOjqO/jX5iEKN43WiJ8qS/1mDfxdj8uO
ESqfwJe7QoTBpYA3RMmtIcYK3oYX9KKGrdUU0J+TV0Flr6wqHBHbOQ== K7Yyc/Yj0qMvGwLNzphjmbT29P7dXk4Ht4TedqCd+8DfhpOgLVIaSA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4OVJvSTVkcDdMQW9HdXcz YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjaXpvQU9yRUc3ektPMmU5
bmVOVjQvYU81MExSZ3NtUzZTNHNRb1VjNmljCkZ6bDN1OGx4NkhGVENOTjQxWFVy Q0xLWU9mYXVOeTVBNGNsZzJPa1lkVlRUZHlvClhoSVV6YjB1Q3pTTzVvNFlNYkU4
QWNNbENkMVpOTS90dzFWalVtczZ1YU0KLS0tIFZSQVNkUGE0TERyQ1BPTjVQUEdN Y2RrbFk5SUFKSDVjeFU0Wlc2aGFUMFUKLS0tIDlVeXM4WWRUakg4Zk5mcjdJL3VC
Y2tFOXlkRHhGTmNWVDZWRnQ5dTNTM1EKNEZZqO6mmj3GnJJ0/8UrisHODEKPjyah S1k5eFBpVnREQlZwYllpeTNhWW5GS1EKi2sMwyJJ0D8acjCZmxlcwdU1sglBuxR7
aAi5rMnEDAniqyouTP1Mq3GOn67KoESqZX4qY3nHvJC7QBIDii3sBg== 2SEMsctdGC+5E3ilPXvPpZ5RONZHbXxn6kQRBlBv6AJERpGDzsfgfA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
- enc: | lastmodified: "2026-07-19T02:30:40Z"
-----BEGIN AGE ENCRYPTED FILE----- mac: ENC[AES256_GCM,data:7+FeT6aeCGn+JFBXbPO0qP4BJ1nHPSennewv1kWkG+hOTIqs1ymuswUK1Hyfi6Z9h2umFX9HvK+o3qtmYvk6k7BUNe6w6QUHTNwm6lmNqrb7sgAE3iFVI2p9m14NGhgoTfnXx1M4JIZ1iuNYhCukpENI4+svIe+r7x5YeE5Evac=,iv:4AKAPI6upyAvHBr8BLWX7R/NupmJdcXdqiN8e0ZQ3ls=,tag:TkpfpCaLhn+Mx5cQZZuMdA==,type:str]
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5aW8rZG5MenF5RmFWdUgx
dk1oV3BpYmNGUnJHTkhOVjVVM0k1T2hlQkdjCm9xYXNkVWkrTWloWEtRcmRuUUkr
NUEyZ0ZFeFZ0M2dHUTNYdzFVQ2pDS2MKLS0tIGNnaXcxUUo0cGE4T28vdGJPT1d5
SnliakxSZjBNQlpjSlNJelVWVUJORDgKWVuK6VYN5BySdRLHLlU0Sk6PPAsz5lVS
Vw96AbKrmczhkiYUWGacde0KjBJb/7tyfXeckV1iqj1/6vT03dDNlA==
-----END AGE ENCRYPTED FILE-----
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCckVtTWtCSWpxSGFEbWI5
MkVkeTRYN1MzMEk2L1RQemRvM1RSaEVpakVNCjBsbm5qcDFlRXRCQXNkQTd2Snlk
WW51Sm1xYjJldmJ0R09WR0hHbWk3NWMKLS0tIGlWYk4zNElVQk9qcGJUUUptMVZV
aStXT2lVNXlwRG54SmhxV21tb2M3NzAK2nlgUrlarGERUClDvytMXwPO5e+zaIlW
T68ePI4e1vh2qj5xpjXoK5Pzv6oUcjhB0P7Ij2lA8bRdFLWfHPAqvA==
-----END AGE ENCRYPTED FILE-----
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
lastmodified: "2026-07-19T23:30:21Z"
mac: ENC[AES256_GCM,data:kLGE2xawQT7mx+sfw68hmGk5nCEGiEjZrqTEl9B1dtQmTrMwmoVr/1RISi4LfJrwxy31mDgff4lcIL4wIJuM373uk3X8j4RNyYQNTfKEkORT6r8NHeepNs267O77pKGd7OmcM4MT/BqOnB8ELS7Wlf2ect7CAlvUUVyc8icxgZE=,iv:EYLDsHYHZ1XOQXafOTqHHWpk/OBNq/R6IJnOBYV33E4=,tag:thxrCPC5oGvDjhK7Dz87YA==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.13.1 version: 3.13.1
+11 -38
View File
@@ -3,49 +3,22 @@ sops:
age: age:
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxT3dteUY4T0VxaUljbFQ3 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaFZURjAxMGRJZEJ5MW1x
UjBSSjZCdWpmTGpDS1BabjV4R0lyOE9haEZrCjlmSXllWTB0L0E5N1MrTk1IODR2 ZjVwWEQrQlkwNmRibVNiL2RpTTFLeUVDQ3hRCkVIamVnZkM1MnlueloxMHVFQnBF
bXZuamN3Yjk0THdka1dBZE5uS05DRkEKLS0tIDBkTmtpU1JoNmN0cHd0WGVseERt RjV2bnUrZUo4WGZJTmR4Y0xITkxRUkUKLS0tIGlJdVQ5MFBubVhxRUVMWW0wSGpP
eEcrOXdFaUNBQ3g2aytYM3RwM1dBd00KiSRrzv6eRMdIzZaUlC6JmsZV6rYJgrre UGdKNUNPYW9nek1UZ0tWbXd3QVNUNDgKIHOiKelITQdH5R4Nc3WF7mzz15D1f9on
vbI1KHtNZKetx81+MmElIKog6kMtTPtI7MVpMwCCPO8zAU5Uh7jSoQ== VaTdr5qkf8LNNvPI0fxsXA9is5cqeg+KbDRHtUumEhNp6Zrf8zWBkw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmWUMyVGpua25ZZ1FRZ21p YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N0hvV01naytDSWVwK1B1
a1lxdS85d2JzOXBLVC92MjlNbTZOOVgvSlhNCm12WmtIb3dQanNkanBCU2t4VTd2 MXM1ZkdpaVc2Q3FPV2VBcC83WmcvSjdGUGgwClJGSXZ6YW5JeFlValNJbjVhK29u
TGFidEpSR254Qm5jTVF0WnprcjJWVncKLS0tIGwzRWZkY25rZXE4Q2VETU93Mno3 bUFqN2dRQTI4ZkwyeXNWYk5JeWVJRXcKLS0tIDlMMkNBUnNUSTJwVVFmc2dlcEZS
aGN6MmcvVkRGZkJQN3V1ak90T3BMVFEKLcEdqKQ3SStSdH5vjQ6RH+oA10OLGXad VGQ1VHR2dXB0M3RsalppVWxiUUROM0UKZM/4QDTam3LDTzjnDs41Ije50R7Q7GC4
TZE0h435sg/tq/Rff+KWngczFRuq7Yg0KYph8bNAKy6uf+RceZ8Ngg== IZbUZjs72rBzY8IkJDbN9JidadEc4NAtMOJwXiJbpZGiCBNfc8+SXw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSMG12OTVwdFhnTkFrNmZl
UDFLcmJtL2FNejJnWlZYd093MlZmWG04dmdJCkhGLzNGcVQyVVp6SGdISTF0ejhv
OVRoQXNlSWZCWDdDMTM0YTFJYlRQd2cKLS0tIHJxVDVscUxEdTV1MGhNVlpWVS8v
aWhSNDh1NEZHYzRHeXZNOFRuRzBjazAK2RMK5ylm4uM55fXgrRrY+GeN86ym9wC9
JNDcNyxE6H/sZGBZJbFLWKDj+VM9X86j5TFFpnjSrKi2CPOzmzgmKw==
-----END AGE ENCRYPTED FILE-----
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLRHdnSkFZc0NYYnQvd0Mz
a3dmTHdLN3Z6UGh3SkpBMGhtSHRJTC9SblJnCmFuOTJGSE5nOWIwQWRNV010RkMw
VnBJVXdtVm1tL2d2bXFWWnBtamVRVk0KLS0tIDNHUndvTnd2Z2VSb3dxVUFUcTcz
ODVDRzBBclhPZjJDYVkwWktMUENSek0KnTFdNMa8L5uPLvl2aJLvaZEUTbGwL+va
wLnWhf/bm8edMeyfWcpS2uHMSDpozWfqObrrP23LCrDBf/B7EG+TWw==
-----END AGE ENCRYPTED FILE-----
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyeTJMS1A0eXJnQVcvaVEy
VEttdGdMcVIzNlIvclZESkJoTHBZeW1GUVFBCmhmRlBLN3lLdW9TS1JKOW83Yis4
dVBGNjdPZXdGYllzN0VyRGFVRFFCMU0KLS0tIEZqN1ljQ0s3TDJYWEEwWk1YVmFs
R1l1LzdHUDVvUHQwY1NtZUUxajZkZUkKZv1ejN+LKZqFqDRak5ktFaR3kMPCQeFl
q8/DHLFx+1IFChxMGkgstGGmkZdxG4khpYJJXAVNyQsyIW9bjJiqZQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
lastmodified: "2026-07-19T02:30:40Z" lastmodified: "2026-07-19T02:30:40Z"
mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str] mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
-25
View File
@@ -1,25 +0,0 @@
beszel-token: ENC[AES256_GCM,data:gjbT3uROiVKQOJaUeafTxjVknQO1Tvbyx/Pl2bTad7DezByX,iv:3ikf7OaT2omO8yd6G6UwYbaRBSzyvbn+NghxAe5bcgI=,tag:Zuc2EP8rUtdDhr5CzSW2Pw==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJL25EUUZack1FMzlnMmdk
Z1hnejZRNnMwVHpmWkFNdGcyeHVsNnRsSGtZCk9OdDhhcnR1WW9ZMEZ1OUVYbm1n
RmZRVy8wb1J3emJBK3Rrd1d4U1dYUDAKLS0tIGJaaElvSk1sOTBOM0lKck16OUtu
NnRZb3U0ZndmaHBZTm8zczhWdE1oaEUKkf6fLomAHoKPhuM4e9q96YmmH+h4VrEj
2x0rnwBwOoRzYWutB2MVtlsphAZmZ/PK0tEecT2MM0XXayVG/33qdg==
-----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpeEJvaHl4akc1TFdEVGV2
czg3OVprU3p4ejNpTktXZEpneDgrOEhrZGlZCmJwT1dhSkZneHE5UmR0WTd5UENq
VHJEWG1EekJLY2pRZldtVGtxTHlGaGMKLS0tIEYrWHE0WTgyUlIwdktmNzNIS3FW
ZVRvT1dHa1Vzc2RSakVISzdMTlpnVGsKeT+edn4+LUkVtpRUNd/gKX3H1HG2bvNo
c8iI6qr/l6oxfP85OrKYFDU9IGvDMxSSdbixHtojPEb5OKVurV0WPQ==
-----END AGE ENCRYPTED FILE-----
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
lastmodified: "2026-07-23T23:32:57Z"
mac: ENC[AES256_GCM,data:l9a/yNRoxY1hvSkLuR4N7deeKue/1JPlSvZvJfCSNbQ21p1qR433BbSDYvfW+kXQXS8GVcfgXSd9ywNzgVvkA5lR1++uYsZBLbYxJ+s3TKWs6/yECAZ0eM1KBA0BEm7cLSsHTOwd+2WspvmCYir++FDO9XRuS3guiMnQBglDf/E=,iv:eE6GVqexQNSiLYfmTTUdUx5AO//wyjSIsr96xAX1pcI=,tag:CNsoyDZYLUt5Seu7W5wJrw==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.2
+5 -143
View File
@@ -12,22 +12,8 @@
nfsServerHost = "server"; # NFS export source hostname nfsServerHost = "server"; # NFS export source hostname
dockerHost = "docker"; # docker-compose stack host dockerHost = "docker"; # docker-compose stack host
# Raspberry Pi's own Tailscale hostname (not fronted by `server` — it
# exports its own NFS share directly). Resolved as
# "${raspberryPiHost}.${tailnetDomain}" in modules/raspi/mount-data.nix.
raspberryPiHost = "raspberrypi";
remoteBuilderUser = "nixremote"; # remote builder SSH user remoteBuilderUser = "nixremote"; # remote builder SSH user
# nix-cache's own SSH host public key (not a secret — the private half
# never leaves the host). Wired into every client's
# programs.ssh.knownHosts by modules/nix-cache/remote-builder-client.nix
# so distributed builds don't hit "Host key verification failed" on a
# fresh client that has never manually ssh'd to nix-cache before. Update
# this if nix-cache's host key is ever rotated or the host is rebuilt
# from scratch.
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuHUxGNH6ei3BZD+EfZs3l4X8uJNcjQiOsM/G4yo4O/ lxc-nix-cache";
# Public keys authorized to SSH in as remoteBuilderUser on the nix-cache # Public keys authorized to SSH in as remoteBuilderUser on the nix-cache
# host (modules/nix-cache/server.nix) — one per client host that's allowed # host (modules/nix-cache/server.nix) — one per client host that's allowed
# to use it as a distributed builder. # to use it as a distributed builder.
@@ -38,145 +24,21 @@
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIGtOWOCS+ImHc7NehguoyD7PbonGosKMZqc9+QR3v/h root@nixos" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIGtOWOCS+ImHc7NehguoyD7PbonGosKMZqc9+QR3v/h root@nixos"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxXTQxFnArK5HXG7czeoybZebCGfxpUdusJkPn+BCSp root@server" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxXTQxFnArK5HXG7czeoybZebCGfxpUdusJkPn+BCSp root@server"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJEj26SL/emsVjW2YhRucJVp2kTz8WgcEQgjBEBLRikk root@claude"
]; ];
# Admin SSH public key, authorized on the primary user of every host and # Admin SSH public key, authorized on the primary user of every host and
# the installer image's nixos/root users. # the installer image's nixos/root users.
adminSshKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream"; adminSshKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream";
# Prestaged wifi SSID for the gui host's NetworkManager profile
# (modules/networking/wifi.nix). The password is not here -- it's
# sops-encrypted in secrets/gui.yaml (wifi-password) instead, since this
# file isn't a secret store.
wifiSsid = "nbn-fttp-net-5G";
# Bare-metal gui host's two disks for a ZFS RAID0 (striped) root pool
# (modules/disko/baremetal.nix). Only used transiently at disko-format
# time (partitioning); the resulting fileSystems/zpool import reference
# by-partlabel/by-id paths afterward regardless, same as
# modules/disko/proxmox.nix's own plain "/dev/sda".
guiRootDisk1 = "/dev/sda";
guiRootDisk2 = "/dev/sdb";
# System # System
timeZone = "Australia/Brisbane"; timeZone = "Australia/Brisbane";
primaryUser = "nixos"; # main interactive user on every host
# Main interactive user on every host. Every module that grants this user
# a group, a home directory, or tmpfiles ownership should reference
# vars.primaryUser rather than the literal "nixos", so renaming it is a
# one-line change.
primaryUser = "nixos";
# Storage # Storage
storageRoot = "/tank"; # ZFS pool root on `server` storageRoot = "/tank"; # ZFS pool root on `server`
# NFS datasets exported from `storageRoot` on `nfsServerHost` and mounted # Well-known ports for LAN services referenced from multiple hosts
# by client hosts. `subpath` is relative to `storageRoot` — combined with beszelHubPort = 8090;
# it to build both the export line in modules/build-types/server.nix and pveWebPort = 8006;
# the "<nfsServerHost>:<storageRoot>/<subpath>" device string each client pbsWebPort = 8007;
# mount uses in modules/docker/mount-data.nix. `mountpoint` is the
# absolute local path clients mount it at, referenced by that same file's
# fileSystems attribute name plus every other place that needs to know
# where the share lives locally (modules/build-types/docker.nix's
# tmpfiles rules, modules/traefik/rotate-logs.nix's log path). Renaming a
# dataset or moving where it's mounted only needs changing it here — the
# export and every client reference follow automatically.
nfsShares = {
dockerConfig = {
subpath = "docker/config";
mountpoint = "/mnt/docker/config";
};
dockerDatabases = {
subpath = "docker/databases";
mountpoint = "/mnt/docker/databases";
};
dockerVolumes = {
subpath = "docker/volumes";
mountpoint = "/mnt/docker/volumes";
};
nextcloudData = {
subpath = "docker/nextcloud-data";
mountpoint = "/mnt/nextcloud-data";
};
raspiVolumes = {
subpath = "raspi/volumes";
mountpoint = "/mnt/raspi-backup";
};
};
# The Raspberry Pi's own NFS export — not under storageRoot/nfsServerHost,
# served directly by the Pi itself over Tailscale (see raspberryPiHost
# above) and mounted at raspiMountpoint by modules/raspi/mount-data.nix.
raspiNfsPath = "/home/raspi/raspi";
raspiMountpoint = "/mnt/raspi";
# Every literal port referenced from modules/ or hosts/, grouped by the
# service/host that opens or connects to it — kept as separate entries
# even where two happen to share a number today (e.g. nixCacheHttp and
# pxeBootHttp are both 80) so changing one service's port can never
# silently change an unrelated one.
ports = {
# nix-cache's nginx reverse proxy in front of nix-serve
# (modules/nix-cache/server.nix).
nixCacheHttp = 80;
# pxe-boot's nginx asset server, also used to build pxeBaseUrl
# (modules/build-types/pxe-boot.nix).
pxeBootHttp = 80;
# pxe-boot's atftpd TFTP server — UDP, not TCP
# (modules/build-types/pxe-boot.nix).
pxeBootTftp = 69;
# `server`'s NFS exports need both the portmapper (rpcbind) and the
# NFS data port itself opened (modules/build-types/server.nix).
nfsRpcbind = 111;
nfsd = 2049;
# Opened on the docker host's firewall for the Traefik-fronted
# container stack (docker-compose config lives in the separate
# /home/debian/docker repo, not here): 80/443 are Traefik's own
# HTTP/HTTPS listeners; 8080 is an additional exposed service whose
# exact backend isn't declared in this repo (modules/build-types/docker.nix).
dockerHttp = 80;
dockerHttps = 443;
dockerExtra = 8080;
# Beszel monitoring hub, reachable at
# http://<dockerHost>.<homeDomain>:<beszelHub> from every agent
# (modules/beszel/enable-agent.nix, hosts/nixos/home.nix).
beszelHub = 8090;
# Proxmox VE and Proxmox Backup Server web UIs, opened as desktop
# shortcuts on the gui build type (hosts/nixos/home.nix).
pveWeb = 8006;
pbsWeb = 8007;
# 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
# build (modules/disko/proxmox.nix, config.system.build.diskoImagesScript
# — see docs/proxmox-images.md). Root fills whatever's left after the ESP
# and swap partitions within this total.
proxmoxImageSize = "20G";
# nix-cache's Nix store garbage collection retention
# (modules/nix-cache/server.nix).
nixCacheGcMaxAge = "30d";
# Traefik access log rotation, watched on the docker host at
# nfsShares.dockerVolumes.mountpoint (modules/traefik/rotate-logs.nix).
traefikLogRotate = {
maxSize = "100M"; # rotate once a log file exceeds this size
keep = 20; # number of rotated logs to retain before deleting the oldest
};
} }