Introduces clan-core pinned to its 26.05 release alongside the existing nixpkgs
26.05 input. No host configuration is changed — this is a pure dependency
addition so Phase 2 (per-host vars/secret management migration) has the input
available without a separate flake.lock bump.
clan-core.inputs.nixpkgs.follows = "nixpkgs" keeps a single nixpkgs closure.
sops-nix remains as a flake input; vars layers on top of it rather than
replacing it (clan's sops storage backend still needs sops-nix).
All hosts evaluate cleanly (codex-maintenance.sh --full-check equivalent
triggered by the flake.nix change).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three ordering-related fixes to the Proxmox provisioning flow:
1. prepare-host-key.sh: make idempotent -- if the key already exists, print
a note and exit 0 instead of erroring. The caller (create-proxmox-resource.sh)
already guards standalone calls, but the script itself should be safe to
run directly on a host that was already keyed.
2. create-proxmox-resource.sh: after sync-host-keys.sh updates .sops.yaml /
secrets/, detect uncommitted changes and block with a prompt until the
operator confirms they've committed and pushed. The PVE node's git pull
only picks up committed+pushed state; without this gate, a new host's sops
recipient is missing from the secrets files the image build uses, so the
host can't decrypt secrets on first boot.
3. create-proxmox-resource.sh: add an explicit existence check for the host
key in both the LXC and VM remote build heredocs, before it's passed as
--pre-format-files / NIXOS_HOST_KEYS_DIR input. Gives a clear error
pointing at sync-host-keys.sh instead of a raw `cp: cannot stat` from
disko deep in the build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The disko images script does `cd "$TMPDIR"` before parsing its arguments,
so relative paths passed to --pre-format-files resolve against the temp
dir instead of the repo root. Use $(pwd) to capture the absolute repo
path before the disko script changes directory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complete the rename so all identifiers match networking.hostName:
- flake.nix: attribute names and buildType strings
(linode/proxmox/lxc-tailscale-subnet-router → *-tailscale-router)
- modules/build-types/tailscale-subnet-router.nix → tailscale-router.nix
- .sops.yaml: anchor and alias names (age keys unchanged, no re-encrypt needed)
- host-keys/: local gitignored key files renamed (not committed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If host-keys/<target>_ssh_host_ed25519_key doesn't exist, run
prepare-host-key.sh to generate it before sync-host-keys.sh runs.
Prevents sync-host-keys.sh from hitting its SKIP/exit-1 path (anchor
in .sops.yaml but no local key) and the downstream disko build failure
when --pre-format-files can't find the key file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The directory name was tailscale-subnet-router but networking.hostName
was already tailscale-router, causing a mismatch that confused scripts
comparing directory paths against actual hostnames. Flake attribute
names (linode/proxmox/lxc-tailscale-subnet-router) and .sops.yaml
anchors are unchanged — they describe the build type, not the hostname.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
systemd-resolved only uses LLMNR for single-label hostnames, never DNS —
same issue mount-data.nix already documented and fixed for NFS by switching
to server.sweet.home. Change the substituter URL, SSH knownHosts, and
remote-builder hostName from bare "nix-cache" to "nix-cache.sweet.home",
and update nginx's virtualHost to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
read exits non-zero when stdin is not a terminal (set -e killed the
script silently). Catch that and emit a clear error pointing to the
SUDO_PASS environment variable rather than crashing with no output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/dev/tty exists as a device node even without a controlling terminal,
so -r/-w tests pass but opening it fails. Plain 'read -r -s' from stdin
is enough: works interactively from a real terminal, and from a non-tty
context the caller should set SUDO_PASS in the environment instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Environments without a controlling terminal (containers, CI agents)
don't have /dev/tty. Try it first for the sudo password prompt, fall
back to plain stdin so the script works in both contexts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of ssh -t (requires PTY on both sides), prompt for the sudo
password once at startup and pipe it to each remote invocation via
sudo -S. This works from any context -- interactive terminal, background
agent, or script -- with no PTY needed on either end.
Also accepts SUDO_PASS from the environment for non-interactive callers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ssh -t won't allocate a PTY when its own stdin is redirected (by a
heredoc). Replaced the heredoc-fed 'sudo bash -s' with commands passed
as an argument string so stdin stays free and -t can properly allocate
a PTY for the sudo password prompt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New script: scripts/secrets/push-host-keys.sh
- Pushes newly-generated SSH host keys from host-keys/ to already-running
NixOS hosts after sync-host-keys.sh --regenerate-all-keys.
- Before pushing any key, checks that .sops.yaml and secrets/*.yaml are
committed and pushed to the remote Gitea flake (hosts rebuild from there,
so recipient changes must land first); offers to auto-commit/push if not.
- Reads /etc/flake-target from each host to confirm which key to install,
handling the case where multiple flake targets share a hostname.
- Deduplicates by hostname in --all mode; skips hand-registered targets
that have no host-keys/ entry.
- --dry-run, --skip-git-check, SSH_USER override (default: nixos).
sync-host-keys.sh --regenerate-all-keys:
- Updated pre-confirmation warning to distinguish already-running hosts
(need push-host-keys.sh) from not-yet-deployed hosts (need installer
image rebuild).
- Added next-steps block after regeneration completes pointing to
push-host-keys.sh --all.
recover-hosts.sh:
- Header and SSH host key mismatch warn now cross-reference
push-host-keys.sh as the proactive (pre-drift) alternative.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The host was always intended as a LAN subnet router (--advertise-routes),
not a full exit node (--advertise-exit-node). Rename every trace of
"exit-node" to "subnet-router" and remove the --advertise-exit-node flag
from extraSetFlags; the operator supplies --advertise-routes at first
tailscale up and Tailscale persists it in state across reboots.
Routing sysctls (useRoutingFeatures = "server"), openFirewall, and
trustedInterfaces = ["tailscale0"] are still required for subnet routing
to work, so the module is kept — just correctly named.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TF2dsuKZAiyZWQ1D7CuHJm
- Add hosts/tor-relay/host.nix import of host-token.nix so the agent
gets its TOKEN from a sops-managed environment file
- Add secrets/tor-relay.yaml (encrypted beszel token for this host)
- Add creation_rules entry for secrets/tor-relay.yaml in .sops.yaml
- Update &lxc-tor-relay age key to the host's actual current key
(old key was from a prior LXC incarnation; new key extracted from
Switch-nix output: age1gl5ujmhd2pe37...)
- Re-encrypt secrets/common.yaml via sops updatekeys to swap in the
new key, so the host can decrypt its password hash on next boot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NixOS's etc activation removes files that were in a previous generation's
environment.etc but absent from the current one -- even real copies, not
only symlinks. LXC tarballs bake the host key into environment.etc (via
NIXOS_HOST_KEYS_DIR), but every subsequent nixos-rebuild switch lacks that
env var, so the key is removed as "obsolete". sops-nix derives its age
decryption key from /etc/ssh/ssh_host_ed25519_key, so deletion cascades
into "Error getting data key: 0 successful groups required, got 0" for
every sops secret on the host.
Fix: two activation scripts bracket the etc step.
preserveSshHostKey (no deps, runs before etc): copies the live key to
/run (tmpfs) before etc can delete it.
restoreSshHostKey (deps=[etc], runs after etc): reinstalls via `install`
if etc removed the key. The resulting file is not tracked in either
generation's environment.etc, so subsequent rebuilds leave it alone.
scripts/recover-hosts.sh: restore both private and public key files (not
just the private key), use install(1) for atomic mode setting, and add a
post-rebuild sops-nix verification step to confirm success.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zT1L6hmsq6i1evAEH7dmi
A fresh proxmox-server deploy has a blank scsi1 disk, so
zfs-import-tank.service spun 60 s then failed with no pool found.
Add zfs-init-tank.service that runs before the import: exits immediately
if the pool already exists, imports it if it exists but isn't imported
yet, or creates it on /dev/disk/by-id/scsi-*drive-scsi1 (Proxmox's
virtio-scsi naming for the second disk) with all required NFS datasets
if the disk is blank.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The post-build cp/mv into /var/lib/vz/template/cache (LXC) and
/var/lib/vz/import (VM) are Proxmox-owned root directories -- they need
sudo_pfx just like pct/qm/pvesh do. nix build writes to the nix store
as the SSH user, but staging into /var/lib/vz/ requires root.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The script was root-only and hard-coded /etc/nix/nix.conf and
/etc/ssh/ssh_known_hosts, making it always fail (non-fatally) when
called as a non-root SSH user from create-proxmox-resource.sh.
Add dual-mode detection based on EUID:
- root (multi-user/daemon): existing behavior unchanged -- writes
/etc/nix/nix.conf, /etc/ssh/ssh_known_hosts, restarts nix-daemon
- non-root (single-user): writes ~/.config/nix/nix.conf and
~/.ssh/known_hosts, creates the config file if missing, skips the
daemon restart (single-user has no daemon), defaults REMOTE_BUILDER_KEY
to ~/.ssh/id_ed25519 instead of /root/.ssh/id_ed25519
create-proxmox-resource.sh already calls the script without sudo (as the
SSH user), so no change is needed there -- the script now handles both
cases on its own.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nix was installed as root on pve1 (the codex-setup.sh root path, meant
for container/Codex environments), making nix build require sudo there.
After cleaning up the root install and reinstalling as the SSH user
(wayne), nix is owned by that user and runs directly without sudo.
create-proxmox-resource.sh: drop sudo_pfx from nix build in both
remote scripts. The SSH user owns the store after reinstall; nix build
goes through the nix daemon-or-store directly. sudo stays on pct/qm/pvesh
(cluster IPC) and the disko image-writer script (writes to disk).
codex-setup.sh: add build-users-group = (empty) to the user nix.conf
written by the non-root install path. Guards against a stale
/etc/nix/nix.conf from a prior root install (which sets
build-users-group = nixbld) silently breaking single-user builds.
Manual cleanup required once on each Proxmox node that had root's nix:
sudo rm -rf /nix /etc/nix
sudo rm -f /etc/profile.d/nix.sh /etc/profile.d/nix-daemon.sh
for i in $(seq 1 10); do sudo userdel nixbld$i 2>/dev/null||true; done
sudo groupdel nixbld 2>/dev/null || true
After that, the next create-proxmox-resource.sh run auto-reinstalls
nix as the SSH user via codex-setup.sh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nix on pve1 was installed as root (single-user), so wayne can't access
/nix/var/nix/db/big-lock without root -- nix build genuinely needs sudo
there. The previous fix to drop sudo_pfx was wrong.
The real fix is node config: add nix to wayne's NOPASSWD rules in
sudoers on pve1 (see below). pct/qm/pvesh already have NOPASSWD and
work fine in non-interactive SSH heredocs; nix was just missing from
that list.
On pve1 as root:
echo 'wayne ALL=(root) NOPASSWD: ALL' | tee /etc/sudoers.d/wayne-nopasswd
chmod 440 /etc/sudoers.d/wayne-nopasswd
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nix build runs through the nix daemon and doesn't need root; the
tooling-check step already confirms the SSH user can run nix directly
(ensure_nix_profile + command -v nix). sudo without a TTY blocks
non-interactive SSH heredoc sessions with "a terminal is required".
Keep sudo on pct/qm/pvesh (cluster IPC) and the disko image-writer
script (writes to block devices) -- those actually require root.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-user Nix installations are owned by root. When PROXMOX_SSH_USER
is not root, prefix the remote nix build command with sudo_prefix, same
as the Proxmox tool invocations. Passes sudo_prefix as an extra arg to
both the LXC tarball and VM disko image build heredocs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/root/nixos was only correct when PROXMOX_SSH_USER=root. Now that it
defaults to wayne, use /home/${PROXMOX_SSH_USER}/nixos so git clone
goes somewhere the SSH user can actually write to.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PROXMOX_SSH_USER was changed from root to wayne, but all remote pvesh/qm/pct
invocations assumed root. When run as a non-root user these commands fail with
ipcc_send_rec errors because they can't reach the pve-cluster IPC socket.
Adds a global sudo_prefix (empty when PROXMOX_SSH_USER=root, "sudo" otherwise)
and applies it to every remote Proxmox command in the script, including the
duplicate-host heredoc check, pvesh nextid, vmid existence checks, resource
destruction, and all create/start commands. Removes the now-redundant local
sudo_prefix definition that was previously only in the VM image build branch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>