Compare commits

..
Author SHA1 Message Date
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
159 changed files with 1036 additions and 9907 deletions

No files matched your search

Submodule .claude/worktrees/scripts-dedup deleted from e578443914.
-29
View File
@@ -1,29 +0,0 @@
name: Check NixOS configurations
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
eval-hosts:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19
# Scoped to files changed since the PR base / previous push -- see
# scripts/codex-maintenance.sh. CI never passes --full-check: that
# full sweep is for local/manual use, since it's slow enough to time
# out this runner.
- name: Run maintenance checks (secrets, fmt, lint, eval -- changed files only)
env:
MAINT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: bash scripts/codex-maintenance.sh
-32
View File
@@ -1,32 +0,0 @@
name: Update flake.lock
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
jobs:
update-flake-lock:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19
- name: Update and commit flake.lock
run: |
set -euo pipefail
nix --extra-experimental-features 'nix-command flakes' flake update
if git diff --quiet -- flake.lock; then
echo "No flake.lock changes detected"
exit 0
fi
git config user.name "gitea-actions"
git config user.email "gitea-actions@nix-cache.local"
git add flake.lock
git commit -m "chore: update flake.lock"
git push
-11
View File
@@ -1,11 +0,0 @@
#!/usr/bin/env bash
# Blocks commits containing secrets. Installed via:
# git config core.hooksPath .githooks
# (scripts/codex-setup.sh does this automatically in Codex sessions.)
set -euo pipefail
if command -v gitleaks >/dev/null 2>&1; then
gitleaks protect --staged -v
else
nix-shell -p gitleaks --run "gitleaks protect --staged -v"
fi
-29
View File
@@ -1,29 +0,0 @@
name: Check NixOS configurations
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
eval-hosts:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19
# Scoped to files changed since the PR base / previous push -- see
# scripts/codex-maintenance.sh. CI never passes --full-check: that
# full sweep is for local/manual use, since it's slow enough to time
# out this runner.
- name: Run maintenance checks (secrets, fmt, lint, eval -- changed files only)
env:
MAINT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: bash scripts/codex-maintenance.sh
-37
View File
@@ -1,37 +0,0 @@
name: Update flake.lock
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-flake-lock:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19
- name: Update flake.lock
run: |
nix --extra-experimental-features 'nix-command flakes' flake update
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
add-paths: flake.lock
branch: chore/update-flake-lock
title: "chore: update flake.lock"
commit-message: "chore: update flake.lock"
body: |
This is an automated update of `flake.lock` generated by the scheduled workflow.
It updates pinned flake inputs so dependency updates can be reviewed and merged via PR.
+3 -17
View File
@@ -3,23 +3,9 @@
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
.direnv
# Python bytecode cache (scripts/lib/*.py)
__pycache__/
*.pyc
# Locally-generated SSH host keys staged for transfer to a new machine
# during install (see scripts/prepare-host-key.sh) — never commit these.
host-keys/
# Temporary Milestone 1 audit checklist (remove-sensetive-info-refactor.md)
# - working notes only, never committed, deleted once every row is rotated.
secrets-inventory.md
auto-installer/flake.lock
auto-installer/result
auto-installer/nixos-auto.iso
-93
View File
@@ -1,93 +0,0 @@
keys:
- &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- &proxmox-minimal age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n
- &lxc-gui age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
- &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:
# Shared across every currently-deployed host: root/nixos password hash,
# GitHub access token. Same value on every host today, so every live host's
# key can decrypt it (matches current risk profile — narrow further in
# Milestone 4 if hosts should diverge).
- path_regex: secrets/common\.yaml$
key_groups:
- age:
- *admin
- *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-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$
key_groups:
- age:
- *admin
- *linode-nix-cache
- *lxc-nix-cache
- *proxmox-nix-cache
- path_regex: secrets/server\.yaml$
key_groups:
- age:
- *admin
- *linode-server
- *lxc-server
- *proxmox-server
- 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
-50
View File
@@ -1,50 +0,0 @@
# AGENTS.md
## Repo purpose
This repository contains flake-based NixOS configurations for Wayne's LAN
servers and workstation.
The flake exposes NixOS configurations named `<platform>-<buildtype>`
(platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`,
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`), generated from `modules/platforms/*`
and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
every combination is built — `pxe-boot` has no `linode` variant. See
`README.md` for the full current target list; treat `flake.nix` as the
source of truth since this list can drift.
Do not deploy, switch, reboot, repartition, format disks, or run destructive
install commands from this repository unless explicitly asked.
## Safety rules
- Never run `nixos-rebuild switch`, `boot`, `test`, `nixos-install`, `parted`,
`mkfs`, `mkswap`, `swapon`, `mount`, or destructive disk commands in Codex.
- Validation work should be limited to evaluation, linting, formatting checks,
and `nix build --dry-run --no-link`.
- Do not add secrets, tokens, private keys, password hashes, or live credentials
to the repo.
- Treat `flake.nix`, Home Manager config, and Nix config files as public.
- If you find committed tokens or hashes, flag them immediately and recommend
rotation/removal.
## Expected commands
Use these commands when validating changes:
```bash
bash scripts/codex-setup.sh
bash scripts/codex-maintenance.sh
```
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:
```bash
nix eval .#nixosConfigurations.<host>.config.system.build.toplevel.drvPath --raw
```
-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`).
-467
View File
@@ -1,467 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repo purpose
Flake-based NixOS configuration for Wayne's LAN servers and workstation. There is
no application code here — changes are Nix module edits that affect real
machines when deployed.
## Safety rules (read before touching anything)
- **Never** run `nixos-rebuild switch|boot|test`, `nixos-install`, `parted`,
`mkfs`, `mkswap`, `swapon`, `mount`, or any other destructive disk/deploy
command from an agent session, even if asked indirectly. Deployment is done
manually by the operator on the target host.
- Validation is limited to evaluation, linting, formatting checks, and
`nix build --dry-run --no-link`.
- Do not add secrets, tokens, private keys, or new password hashes to the repo.
- This repo currently contains **committed password hashes** in
`modules/installer/common.nix` (the auto-installer's own root/nixos login —
a deliberate, documented choice, see `docs/auto-installer.md`, not
accidental tech debt) and **SSH public keys** in `variables.nix`
(`vars.adminSshKey`, `vars.remoteBuilderAuthorizedKeys`) plus a couple of
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 — used only by the auto-installer's own
environment for pre-seeding non-LXC host keys before first boot (see
`docs/auto-installer.md`). Never commit its contents; if `git status`
ever shows it as trackable, something is wrong. All deployed hosts use
clan vars (`vars/per-machine/<target>/openssh/`, committed and
sops-encrypted) for their SSH host keys — those ARE tracked by git and
belong in the repo.
### Two Proxmox nodes: `pve1.sweet.home` (production) and `pve-test.sweet.home` (sandbox)
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
```bash
# One-time environment bootstrap (installs Nix if missing, prints hosts)
bash scripts/codex-setup.sh
# Changed-files-only validation: secret grep (whole repo), nixpkgs-fmt --check
# 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
# Full sweep: nixpkgs-fmt --check/statix over the whole tree, eval every host
# and package. Slow (minutes) -- CI never runs this; use it locally before a
# 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
nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
# Evaluate a single host without building (fast sanity check)
nix eval .#nixosConfigurations.<host>.config.system.build.toplevel.drvPath --raw
# Dry-run build a single host
nix build --dry-run --no-link .#nixosConfigurations.<host>.config.system.build.toplevel
```
Formatting/lint tools (`nixpkgs-fmt`, `statix`) are not installed locally; the
maintenance script pulls them via `nix run github:NixOS/nixpkgs/nixos-25.11#<tool>`.
There is no test suite — "correctness" here means the flake evaluates and
`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`). Stores keys as clan vars
(`vars/per-machine/<target>/openssh/`, committed and sops-encrypted) for
all flake targets. The primary tool for provisioning a new host's
secrets access — see "Creating a new machine" in
`docs/auto-installer.md`.
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a
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
`flake.nix` is the single entry point. It generates one
`nixosConfigurations.<platform>-<buildtype>` attribute per target via the
`mkTarget` function, composed from:
```
nixosSystem {
modules = [
disko.nixosModules.disko
sops-nix.nixosModules.sops
./modules/common/configuration.nix
./modules/platforms/${platform}.nix # what it runs on
./modules/build-types/${buildType}.nix # what it's for
hostPath # hosts/<name>/host.nix — per-machine identity
home-manager.nixosModules.home-manager { ... }
] ++ (client-only modules, for every buildType except "nix-cache" itself)
}
```
Platforms: `linode`, `proxmox`, `lxc`, `baremetal`. Build types: `minimal`,
`nix-cache`, `server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`,
`tor-relay`. Not every combination is built — e.g. `pxe-boot` has no `linode`
variant (PXE/DHCP/TFTP need LAN L2 adjacency a Linode VPS doesn't have),
`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`,
`docs/flake-lock-automation.md`, and the CI eval workflows
(`.github/workflows/check-nixos.yml`, `.gitea/workflows/check-nixos.yml`) list
hosts by hand (or, for the CI workflows, evaluate the flake dynamically) and
can drift from it, so re-check them against `flake.nix` when adding or
removing a host.
### Composition pattern
- `hosts/<name>/host.nix` — per-machine identity **only**: hostname, hostId,
per-machine secrets, `system.stateVersion`. These files carry no `imports`
of their own beyond narrow parameterized helpers (see
`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.
- `modules/platforms/{linode,proxmox,lxc,baremetal}.nix` — platform-specific
config: boot method, guest tooling, and the hardware config, imported
directly by the platform module itself — **not** wired in from
`flake.nix`. VM platforms use `../hardware-configuration/vm/{proxmox,linode}.nix`;
`baremetal.nix` uses `../hardware-configuration/baremetal.nix` (adapted
from a real `nixos-generate-config` run on the actual hardware, not a
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:
minimal/server/docker/gui/pxe-boot/nix-cache/tailscale-exit-node/tor-relay.
- `modules/common/configuration.nix` — base NixOS config imported by every
host: locale, users, nix settings, git.
- `modules/common/home.nix` / `hosts/nixos/home.nix` — Home Manager config for
the `nixos` user; the `nixos` workstation (`gui` build type) has its own,
other hosts share `modules/common/home.nix`.
- `modules/disko/proxmox.nix` — declarative disk layout (GPT: ESP + swap +
ext4 root) via disko, used by all Proxmox-VM hosts (`proxmox-*`, not
`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/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
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
`nixremote` signing/SSH keys fit together).
- `modules/beszel/host-token.nix` — parameterized helper module
(`{ name, sopsFile }`) that wires a host's beszel-agent sops secret/template
and `environmentFile`; used by `hosts/server/host.nix` and
`hosts/nix-cache/host.nix` to avoid duplicating that boilerplate.
- `modules/tailscale/`, `modules/docker/`, `modules/networking/`,
`modules/traefik/`, `modules/tor/`, `modules/services/*` — single-purpose,
single-host
feature modules (e.g. `docker/enable-service.nix`,
`services/zfs/enable-service.nix`). Grep `modules/build-types/*.nix` for
each build type's `imports` list to see which modules apply where.
New host = new `hosts/<name>/host.nix` + a matching
`mkTarget { platform; buildType; hostPath; }` entry added to `flake.nix`'s
`generatedTargets`, composed from existing `modules/*` pieces rather than
duplicating config.
### Other docs worth reading before touching these areas
- `docs/nix-cache.md` — nix-cache binary cache/remote-builder design and key
handling.
- `docs/pxe-boot.md` — the `pxe-boot` host's iPXE/TFTP/HTTP boot chain and
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
(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`.
+1 -172
View File
@@ -1,173 +1,2 @@
# NixOS LAN Configurations
# nixos
Flake-based NixOS configuration repository for Wayne's LAN servers and
workstation.
## Hosts
Targets are named `<platform>-<buildtype>`, generated from two orthogonal
pieces composed in `flake.nix`:
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`, `baremetal`
- **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`,
`gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
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,
`tor-relay` currently only exists as `lxc-tor-relay`, and `baremetal`
currently only exists as `baremetal-gui` (the real gui-host hardware). The
full list:
| Target | Purpose |
| --- | --- |
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS |
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox — previously the flat `nix-minimal` target |
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder — previously the flat `nix-cache` target |
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host — previously the flat `server` target |
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack — previously the flat `docker` target |
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation — 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 — 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
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
fixed regardless of which platform it's built for — see
`flake-target-refactor-spec.md` for the full rationale. Every deployed host
stamps its own active target name into `/etc/flake-target` at build time, so
`nixos-rebuild switch --flake .#$(cat /etc/flake-target)` always picks up the
right one even after a platform migration changes the flake attribute name.
List hosts with:
```bash
nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
```
## Layout
| Path | Purpose |
| --- | --- |
| `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` |
| `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) |
| `modules/platforms/` | Platform-specific config: virtualisation guest tools, boot method, hardware config (`linode.nix`, `proxmox.nix`, `lxc.nix`, `baremetal.nix`) |
| `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/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` |
| `host-keys/` | Gitignored; only used by the auto-installer environment for pre-seeding SSH host keys before first boot — see `docs/auto-installer.md`. All deployed hosts use clan vars (`vars/per-machine/<target>/openssh/`) instead |
| `vars/per-machine/` | Clan vars: committed, sops-encrypted SSH host keys for all deployed hosts; read by `create-proxmox-resource.sh` at deploy time |
| `docs/` | Operational notes for cache, builders, lock updates, boot services, the auto-installer, and Proxmox image builds |
| `scripts/` | Codex setup, validation, host-key, release-bump, and Proxmox resource helpers |
## Validation
Safe validation commands for Codex and local review:
```bash
bash scripts/codex-setup.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:
```bash
nix eval .#nixosConfigurations.<host>.config.system.build.toplevel.drvPath --raw
```
Use `nix build --dry-run --no-link` when build planning is needed. Do not run
deployment, install, disk formatting, mount, or reboot commands from automated
review sessions.
## Operations
- Host rebuilds should consume the committed `flake.lock`.
- Routine dependency updates should happen through the flake lock automation
described in `docs/flake-lock-automation.md`.
- `nix-cache` serves substitutes over HTTP and can act as a remote builder for
client hosts.
- `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
Do not commit tokens, private keys, live credentials, or new password hashes
as plaintext. Secrets are managed with [sops-nix](https://github.com/Mic92/sops-nix):
encrypted files live under `secrets/`, recipients (per-host age keys derived
from each host's existing SSH host key, plus an admin key) are declared in
`.sops.yaml`. To add or edit a secret:
```bash
nix-shell -p sops --run "sops secrets/<file>.yaml"
```
then reference it from a module via `config.sops.secrets."<name>".path`
(or `sops.templates` for values that need to be embedded in a rendered
config file, e.g. `nix.conf`'s `access-tokens`). Never write a secret value
directly into a tracked `.nix` file. A pre-commit hook (`.githooks/`,
enabled via `git config core.hooksPath .githooks`, done automatically by
`scripts/codex-setup.sh`) runs `gitleaks protect --staged` to catch mistakes
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. All deployed hosts use clan vars
(`vars/per-machine/<target>/openssh/`, committed and sops-encrypted) for
their SSH host keys.
This repository's git *history* still contains secrets committed before this
migration (see `remove-sensetive-info-refactor.md`) — those are being
scrubbed and rotated separately; don't treat the repo as safe to make public
until that's finished.
+24
View File
@@ -0,0 +1,24 @@
{ config, pkgs, lib, ... }:
let
mySwitchCmd = ''
sudo nixos-rebuild switch \
--no-write-lock-file \
--refresh \
--flake git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(hostname)
'';
myTestCmd = ''
sudo nixos-rebuild test \
--no-write-lock-file \
--refresh \
--flake git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(hostname)
'';
in {
programs.bash = {
enable = true;
shellAliases = {
"Switch-nix" = mySwitchCmd;
"Test-nix" = myTestCmd;
};
};
}
+76
View File
@@ -0,0 +1,76 @@
{ config, lib, pkgs, ... }:
{
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
];
# Use the GRUB 2 boot loader.
boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
# Set your time zone.
time.timeZone = "Australia/Brisbane";
# Enable QEMU agent
services.qemuGuest.enable = true;
# Enable docker-compose
environment.systemPackages = with pkgs; [
vim
btop
git
gcr
];
#Set root password
users.users.root = {
hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
};
# Define a user account. Don't forget to set a password with ‘passwd’.
users.users.nixos = {
isNormalUser = true;
extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
packages = with pkgs; [
tree
];
hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream"
];
};
# Enable the OpenSSH daemon.
services.openssh.enable = true;
#Enable flakes
nix.settings.experimental-features = "nix-command flakes";
nix.settings = {
substituters = [
"http://nix-cache"
# "https://cache.nixos.org/"
];
trusted-public-keys = [
"cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
];
};
programs.git = {
enable = true;
package = pkgs.git;
config = {
credential.helper = "store";
};
};
}
@@ -5,26 +5,22 @@
{
imports =
[
(modulesPath + "/profiles/qemu-guest.nix")
[ (modulesPath + "/profiles/qemu-guest.nix")
];
boot = {
initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
initrd.kernelModules = [ ];
kernelModules = [ "kvm-amd" ];
extraModulePackages = [ ];
};
# boot.loader.grub.device = "/dev/sda2"; # or "nodev" for efi only
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ ];
boot.extraModulePackages = [ ];
# fileSystems."/" =
# { device = "/dev/disk/by-label/nixos";
# fsType = "ext4";
# };
fileSystems."/" =
{ device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
# swapDevices =
# [ { device = "/dev/disk/by-label/swap"; }
# ];
swapDevices =
[ { device = "/dev/disk/by-label/swap"; }
];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
+66
View File
@@ -0,0 +1,66 @@
{ config, pkgs, lib, ... }:
let
remote = "root@proxmox-ip:/var/lib/vz/template/iso";
localMount = "${config.home.homeDirectory}/proxmox-iso";
in {
imports = [
./aliases.nix
];
home.username = "nixos"; # your actual username
home.homeDirectory = "/home/nixos";
home.stateVersion = "25.05"; # match your NixOS stateVersion
programs.home-manager.enable = true; # mandatory to activate HM
programs.bash.enable = true;
home.file = {
".config/nix/nix.conf".text = ''
access-tokens = github.com=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
'';
};
# Optional: packages
home.packages = with pkgs; [
git
vim
tmux
nano
sshfs
];
# Optional: set environment vars
home.sessionVariables = {
EDITOR = "nano";
};
systemd.user.services.mount-proxmox-iso = {
Unit = {
Description = "Mount Proxmox ISO dir via SSHFS";
After = [ "network-online.target" ];
Wants = [ "network-online.target" ];
};
Service = {
Type = "simple";
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${localMount}";
ExecStart = "${pkgs.sshfs}/bin/sshfs -o IdentityFile=${config.home.homeDirectory}/.ssh/id_ed25519,allow_other,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 root@proxmox-ip:/var/lib/vz/template/iso ${localMount}";
ExecStop = "${pkgs.fuse3}/bin/fusermount3 -u ${localMount}";
Restart = "on-failure";
};
Install = {
WantedBy = [ "default.target" ];
};
};
# Optional: enable bash (or zsh, fish...)
# programs.bash.enable = true;
# Optional: manage dotfiles via symlinks
# home.file = {
# ".tmux.conf".source = ./dotfiles/tmux.conf;
# ".config/nvim/init.vim".source = ./dotfiles/init.vim;
# };
}
-311
View File
@@ -1,311 +0,0 @@
# Auto-installer
This flake builds a self-contained NixOS installer environment that can
install any host exposed by its own `nixosConfigurations`. It was migrated
from a formerly-separate `nix-auto-installer` repo — everything it did now
lives here.
The installer provides a small NixOS install environment (ISO, or the same
image netbooted via PXE) with SSH access, Git support, and an interactive
installation script.
Logging in as any user (root or `nixos`) runs
`/etc/nixos-installer/installer/auto-install.sh` (the same file as
`scripts/installer/auto-install.sh` in this repo — see "Installer process"
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 —
see "LXC hosts" immediately below for why those are different.**
## LXC hosts
`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
interactive menu deliberately excludes them. Don't try to select one there;
`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
running on — it's designed to protect exactly this case, so it just fails.
`modules/platforms/lxc.nix` imports nixpkgs' own
`virtualisation/proxmox-lxc.nix` module, which gives every `lxc-*` host a
`config.system.build.tarball` output — a complete, directly Proxmox-importable
container image, no install step at all:
```sh
nix build .#nixosConfigurations.lxc-minimal.config.system.build.tarball
```
This is a plain rootfs tarball, not a `vzdump` backup archive — restoring it
with `pct restore` fails ("archive contains no configuration file"), since
that command expects backup-archive metadata this tarball doesn't have. Use
it as a CT *template* instead: drop it under Proxmox's template storage
(conventionally `/var/lib/vz/template/cache/` for the `local` storage, or
the GUI's "Create CT" → upload-as-template flow) and create a container
from it, supplying all config on the command line since a template has none
of its own:
```sh
pct create <vmid> local:vztmpl/<file>.tar.xz \
--unprivileged 1 --features nesting=1,keyctl=1 \
--rootfs local-lvm:8 --hostname <name> --cores 2 --memory 2048 --swap 2048 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp
pct start <vmid>
```
Every one of those extra flags is load-bearing, confirmed by actually
booting one:
- `--unprivileged 1` — `modules/platforms/lxc.nix` sets
`proxmoxLXC.privileged = false`, so the image assumes it's running
unprivileged. `pct create`'s own CLI default for this flag is
privileged (unlike the web UI, whose checkbox defaults the other way)
— omit it and you get a privileged container running a NixOS config
that assumes unprivileged, a real mismatch.
- `--features nesting=1,keyctl=1` — required for a modern (v247+)
systemd guest to boot unprivileged at all. Without it, AppArmor denies
the nested user namespaces and credential mounts systemd routinely
uses (even plain getty units) — every getty crash-loops on a denied
`/run/credentials/*` mount every ~3s (this is what garbage on the
console turns out to be) while core services like `nsncd` fail the
same way, and the system never finishes activating.
- `--swap 2048` — `--memory` doesn't touch swap; it silently stays at
Proxmox's own 512M default otherwise. Match it to `--memory` unless
you deliberately want otherwise.
First boot runs `boot.postBootCommands` (registers the Nix store DB and
system profile) — there's no separate activation step to run yourself.
`scripts/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
- `modules/installer/common.nix` — shared by every installer target: SSH
access, users, the generated `/etc/auto-install.sh` script, and the
`programs.bash.loginShellInit` hook that runs it on login.
- `modules/installer/iso.nix` — ISO/netboot-specific: imports the stock
`installation-cd-minimal.nix` module plus `common.nix`. Also used, paired
with `netboot-minimal.nix`, to build the PXE netboot variant (see
`docs/pxe-boot.md`).
- `modules/installer/host-keys.nix` — optionally bakes pre-generated SSH
host keys into the image; see "Host keys" below.
- `scripts/secrets/sync-host-keys.sh` — admin-workstation tool that generates,
registers, and (via `--remove`/`--regenerate-all-keys`) retires host
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:
```nix
nixosConfigurations.installer # ISO/netboot installer image
packages.x86_64-linux.iso # installer ISO/netboot image
packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
```
```sh
nix build .#iso
nix build .#pxe
```
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
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`.
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
`sops-nix` derives each host's decryption key from its own
`/etc/ssh/ssh_host_ed25519_key`, generated at **activation** time — before
systemd would otherwise generate one on first boot. Without pre-seeding this
key, secrets (including the root/nixos login password) fail to decrypt on a
genuinely fresh install.
Generated host keys live in `host-keys/` at the repo root (`ssh_host_ed25519_key`
+ `.pub` pairs per hostname). This directory is **gitignored on purpose** —
private key material must never be committed — which also means flakes can't
see it through a normal relative path. `modules/installer/host-keys.nix`
reads it through `builtins.getEnv`, which Nix silently returns as an empty
string under normal (non-`--impure`) evaluation, so the module is a no-op —
safe by default, including in CI — unless explicitly opted into:
```sh
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build .#iso --impure
```
When built this way, every key currently in `host-keys/` is baked into the
image at `/etc/host-keys/<hostname>_ssh_host_ed25519_key(.pub)`, and
`auto-install.sh` automatically installs whichever one matches the flake
target selected at install time — no manual per-host scp step needed.
**Trade-off, accepted deliberately for this LAN-only setup:** baking keys in
means every key present in `host-keys/` at build time becomes readable by
anyone who can reach the built image — including, for the PXE variant, anyone
who can reach the `pxe-boot` host's unauthenticated HTTP server. This is
considered acceptable here because `pxe-boot` sits behind LAN-only network
infrastructure, not the open internet. If that ever changes, reconsider this
default.
`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`
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
Disk partitioning is handled by Disko — the installer has no hardcoded
`parted`/`mkfs`/`mkswap`/`mount` commands, and `auto-install.sh` runs
`disko --mode destroy,format,mount` unconditionally, no branching on whether
the target has a Disko config. Every host reachable through this menu has
one:
- `proxmox-*` (`modules/disko/proxmox.nix`): a real GPT partition table
(ESP + swap + root) on `/dev/sda`.
- `linode-*` (`modules/disko/linode.nix`): Linode provisions and sizes
`/dev/sda`/`/dev/sdb` itself as whole, unpartitioned block devices before
the OS ever boots, so this declares them with `destroy = false` (skips
disko's wipe stage for these disks entirely — see the option's own docs)
and a bare `filesystem`/`swap` content type with no partition table, and
the format step it does run only calls `mkfs`/`mkswap` if `blkid` shows
the device isn't already formatted — a re-run against an
already-provisioned Linode disk is a no-op, not a wipe.
`lxc-*` is the only category without one — it's excluded from this menu
entirely (see "LXC hosts" above), so it never reaches this code path.
## Installer process
`scripts/installer/auto-install.sh` is a real, version-controlled shell
script — not an inline Nix string. It sources `scripts/env.sh` for
`LAN_DOMAIN` itself (same as every other script in `scripts/`), so it
behaves identically whether it's run straight from a git checkout (e.g.
manually, from a stock NixOS ISO that isn't this repo's own installer
image) or from inside the built installer image. That's also why it's
baked in at `/etc/nixos-installer/installer/auto-install.sh` rather than a
flat `/etc/auto-install.sh` — `modules/installer/common.nix` bakes
`scripts/env.sh` in alongside it at `/etc/nixos-installer/env.sh`,
preserving the same relative layout (`installer/auto-install.sh` ->
`../env.sh`) the checked-out repo has, so the script's own
`source ".../env.sh"` line resolves correctly in both places without any
Nix-level templating.
Once running, it:
1. Queries `nixosConfigurations` from this flake over the network (`git+https://<lanDomain>/beatzaplenty/nixos.git`) — this happens at *install* time, not build time, so a generic installer image always sees whatever hosts are currently committed, without needing a rebuild.
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).
4. Runs `disko --mode destroy,format,mount` (see "Storage" above — every host reachable through this menu has a Disko config, so this is unconditional).
5. Installs the target's SSH host key from `/etc/host-keys` or `/root/host-keys` (see "Host keys" above).
6. Runs `nixos-install --flake <url>#<choice> --no-root-password`.
7. Cleans up and reboots.
## Creating a new machine
Do this instead of jumping straight to a plain install whenever the target
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
GitHub token behind sops-nix for all of them).
1. **Add the flake target** — `hosts/<name>/host.nix` plus the matching
`mkTarget { ... }` entry in `flake.nix`'s `generatedTargets` (see
"Composition pattern" in `CLAUDE.md`). No secrets involved yet, so this
is safe to commit on its own if you want a clean history.
2. **On your admin workstation, generate and register its host key:**
```sh
./scripts/secrets/sync-host-keys.sh <flake-target>
```
This generates `host-keys/<flake-target>_ssh_host_ed25519_key(.pub)`,
adds it as a new `.sops.yaml` anchor, works out which `secrets/*.yaml`
files this specific host actually references (from its own
`config.sops.secrets`, not guessed), adds it to each one's
`key_groups`, and re-encrypts them with `sops updatekeys` — no manual
YAML editing. Safe to re-run; it only fills in what's missing.
Doing this for every host that needs one at once — after adding several
new targets, or just to catch up any that were missed — is
`./scripts/secrets/sync-host-keys.sh --all`. See `scripts/secrets/sync-host-keys.sh --help`
for its other modes (`--remove`, `--regenerate-all-keys`).
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
regardless of the next step.
4. **Build the installer image with keys baked in** (or reuse an already-serving `pxe-boot` host, which does this automatically once redeployed):
```sh
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build .#iso --impure
```
5. **Boot it on the target machine**, log in, select the new host's flake
target from the menu, confirm. `auto-install.sh` finds the baked-in key,
runs Disko + `nixos-install`, and reboots.
6. **Verify after reboot:**
```sh
ssh <new-host> ls /run/secrets/
```
If that's empty or login fails, the host's age key most likely wasn't in
`.sops.yaml` (or wasn't re-encrypted into the secrets file it needs) when
`nixos-install` ran — fix `.sops.yaml`/`secrets/*.yaml`, push, then re-run
`nixos-install --flake .#<hostname> --no-root-password` from a rescue
environment against the existing `/mnt`, or just redo the install.
## Safety
This installer is destructive: `disko --mode destroy,format,mount` erases
any disk defined by the selected host's Disko configuration. Always verify
the selected host profile and target machine before confirming.
-83
View File
@@ -1,83 +0,0 @@
# flake.lock automation
This repository uses CI workflows to keep `flake.lock` up to date on a schedule
and to verify that declared NixOS hosts still evaluate after dependency updates.
## What this automation does
- A scheduled workflow runs `nix flake update` once per week.
- 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.
- A separate CI workflow runs `scripts/codex-maintenance.sh` before merge.
Its default mode scopes eval to the hosts/packages a change can affect,
determined from a git diff against the PR base — but a `flake.lock` change
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.
## Why hosts should stop using `--upgrade-all`
`flake.lock` is the source of truth for pinned dependency versions in a flake-based workflow. Normal host rebuilds should consume the committed lock file instead of upgrading dependencies ad-hoc on each machine.
Recommended rebuild command:
```bash
sudo nixos-rebuild switch --flake git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(cat /etc/flake-target)
```
Flake attribute names are `<platform>-<buildtype>` (e.g. `proxmox-docker`)
and no longer match `hostname`, since a host's hostname stays fixed while
the platform backing it can change. Each `nixosConfiguration` stamps its own
active target name into `/etc/flake-target` at build time, which is what the
command above reads.
Using the committed lock file keeps all hosts aligned and makes updates auditable through CI and code review.
Codex and automated review sessions must not run rebuilds. Limit checks to
evaluation, linting, formatting, and dry-run builds.
## Command differences
- `nix flake update`
- Updates flake input pins in `flake.lock`.
- Should be run in CI or in a dedicated update PR workflow.
- `nixos-rebuild --upgrade`
- Primarily for channel-based workflows; not the normal path for flake-pinned deployments.
- `nixos-rebuild --upgrade-all`
- Aggressively updates package sources and bypasses coordinated lock-file updates.
- Avoid for routine flake-based host rebuilds.
## nix-cache and remote builder fit
With `nix-cache` acting as a binary cache and remote builder, lock-file updates become safer and more reproducible:
- CI verifies host evaluations against the updated lock file.
- Builds can be performed once on the remote builder.
- Built artifacts can be served via `nix-cache` to other hosts, reducing rebuild time and drift.
## Token and secret handling
Do **not** commit access tokens into `flake.nix`, `flake.lock`, or any other tracked file.
If private source access is needed:
- configure tokens locally in `~/.config/nix/nix.conf` or equivalent machine-local config, or
- provide tokens through CI secrets/environment variables.
## GitHub Actions setup notes
- Ensure `GITHUB_TOKEN` has permission to create branches and pull requests (workflow sets `contents: write` and `pull-requests: write`).
- The update workflow uses `peter-evans/create-pull-request` with branch `chore/update-flake-lock`.
- The evaluation workflow runs on pull requests, pushes to `main`, and manual dispatch.
## Gitea Actions runner setup notes
- Ensure the runner image includes Git and can execute the Nix installer action.
- For direct push mode, grant workflow push permission to the repository.
- The workflow sets commit identity to:
- `user.name = gitea-actions`
- `user.email = gitea-actions@nix-cache.local`
- Commits are only created when `flake.lock` actually changes.
-91
View File
@@ -1,91 +0,0 @@
# nix-cache architecture
This repository configures `nix-cache` as a **binary cache server** and a **remote builder** for other hosts.
## Important design notes
- This is **not** a shared `/nix/store` setup.
- Every machine still keeps and uses its own local `/nix/store`.
- Clients prefer `http://nix-cache` for substitutes and keep `https://cache.nixos.org/` as fallback.
- Clients can offload builds to `nix-cache` through SSH (`nix.distributedBuilds`).
- 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`.
## Binary cache signing key
`modules/nix-cache/client.nix` hardcodes every client's trust in one
specific public key (`cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4=`).
That means whichever host is currently playing the `nix-cache` role has to
use that *exact* keypair — not a freshly generated one — or no client will
accept substitutes from it (they'd just silently fall back to building
from source). So unlike most per-host secrets, this one can't be
self-generated on first boot; it's managed via sops-nix like every other
secret in this repo, sourced from `secrets/nix-cache.yaml`'s
`cache-priv-key` entry (`modules/nix-cache/server.nix`).
**Adding or rotating the value:**
```bash
nix-shell -p sops --run 'sops secrets/nix-cache.yaml'
```
Add (or replace) a `cache-priv-key` entry with the private key file's exact
contents. If you don't have it yet, generate a keypair once:
```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
Each client authenticates as `nixremote` using its **own default root SSH
identity** (`/root/.ssh/id_ed25519`) — not a separately-named or shared
keypair. If a client doesn't have one yet:
```bash
sudo ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
```
Then add its `.pub` contents as a new entry in `vars.remoteBuilderAuthorizedKeys`
(`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
sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache nix-store --version
```
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
After deployment:
```bash
curl http://nix-cache/nix-cache-info
nix store ping --store http://nix-cache
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
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 path-info -r nixpkgs#hello
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.
-124
View File
@@ -1,124 +0,0 @@
# pxe-boot
The `pxe-boot` host serves HTTP boot assets for iPXE clients — including
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
- Hostname: `pxe-boot`
- Web service: nginx on TCP port 80
- PXE root: `/srv/pxe`
- HTTP root for scripts and images: `/srv/pxe/http`
- TFTP root for first-stage bootloaders: `/srv/pxe/tftp`
- iPXE entry script: `/srv/pxe/http/boot.ipxe`
- Generated iPXE menu: `/srv/pxe/http/menu.ipxe`
- SystemRescue iPXE script: `/srv/pxe/http/systemrescue.ipxe`
- TFTP fallback script: `/srv/pxe/tftp/autoexec.ipxe`
- Boot binaries copied from the Nix `ipxe` package:
- `/srv/pxe/tftp/ipxe.efi`
- `/srv/pxe/tftp/undionly.kpxe`
## Directory Layout
The host creates these directories with systemd tmpfiles:
```text
/srv/pxe
/srv/pxe/http
/srv/pxe/http/images
/srv/pxe/http/auto-installer
/srv/pxe/http/nixos-minimal
/srv/pxe/http/systemrescue
/srv/pxe/http/ubuntu
/srv/pxe/http/rescue
/srv/pxe/tftp
```
Mount shared image storage under `/srv/pxe/http`, preferably
`/srv/pxe/http/images` unless a menu entry expects files in a specific
directory such as `/srv/pxe/http/auto-installer`.
The HTTP iPXE chain is:
```text
undionly.kpxe or ipxe.efi
-> autoexec.ipxe from the TFTP root, when iPXE requests it
-> http://192.168.2.247/boot.ipxe
-> http://192.168.2.247/menu.ipxe
```
The generated menu currently exposes entries for:
- NixOS Auto-Installer
- NixOS Minimal
- SystemRescue environment
- iPXE shell
- Reboot
Both NixOS entries chain-load a `netboot.ipxe` staged into their own
directory (`/srv/pxe/http/auto-installer/netboot.ipxe` and
`/srv/pxe/http/nixos-minimal/netboot.ipxe`), each nixpkgs' own generated
netboot iPXE script (correct `init=`/`initrd=` kernel parameters included)
rather than a hand-rolled boot line — that script in turn expects its
kernel/initrd siblings in the same directory. Each directory's three files
(`bzImage`, `initrd`, `netboot.ipxe`) are built from source and staged
automatically by `modules/pxe-boot/stage-installer-artifacts.nix` via
`systemd.tmpfiles.rules` — 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:
```text
/srv/pxe/http/images/systemrescue.iso
```
The `stage-systemrescue.service` oneshot extracts that ISO into:
```text
/srv/pxe/http/systemrescue
```
The rescue menu entry then chains `http://192.168.2.247/systemrescue.ipxe`,
which loads the SystemRescue kernel and initramfs from the extracted tree and
uses `archiso_http_srv` to fetch the squashfs payload over HTTP.
## Validation
Safe evaluation check:
```bash
nix eval .#nixosConfigurations.proxmox-pxe-boot.config.system.build.toplevel.drvPath --raw
```
After deployment by an operator, basic service checks are:
```bash
curl http://pxe-boot/boot.ipxe
curl http://pxe-boot/menu.ipxe
curl http://pxe-boot/systemrescue.ipxe
curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/vmlinuz
curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/sysresccd.img
```
During a successful BIOS chainload, TFTP should deliver `undionly.kpxe` once,
then nginx should log requests for `/boot.ipxe` and `/menu.ipxe`. Repeated TFTP
downloads of `undionly.kpxe` indicate the iPXE stage is still not reaching the
HTTP chain.
Generated
+30 -225
View File
@@ -1,95 +1,17 @@
{
"nodes": {
"clan-core": {
"inputs": {
"data-mesher": "data-mesher",
"disko": [
"disko"
],
"flake-parts": "flake-parts",
"nix-darwin": "nix-darwin",
"nix-select": "nix-select",
"nixpkgs": [
"nixpkgs"
],
"sops-nix": [
"sops-nix"
],
"systems": "systems",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1783497933,
"narHash": "sha256-TxmwEews6URFPqOWEHNychtXbFDgLZjbOfEXtvtOm6U=",
"rev": "3dc0221ca09033599fe98055e9bbc81bdf32732a",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/3dc0221ca09033599fe98055e9bbc81bdf32732a.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/clan-core/archive/26.05.tar.gz"
}
},
"data-mesher": {
"inputs": {
"flake-parts": [
"clan-core",
"flake-parts"
],
"nixpkgs": [
"clan-core",
"nixpkgs"
],
"treefmt-nix": [
"clan-core",
"treefmt-nix"
]
},
"locked": {
"lastModified": 1778718524,
"narHash": "sha256-pXLoI6Ax0EnUK6r34UM1vibVC7CfTu6j72R2692ZzPs=",
"rev": "12c552ad547d87254f33f33bddd1a2cdbeac754d",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/12c552ad547d87254f33f33bddd1a2cdbeac754d.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/data-mesher/archive/main.tar.gz"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1781152676,
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
"owner": "nix-community",
"repo": "disko",
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"flake-compat": {
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-compat_2": {
@@ -108,30 +30,9 @@
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
@@ -152,17 +53,16 @@
"flake-utils": "flake-utils"
},
"locked": {
"lastModified": 1715533576,
"narHash": "sha256-fT4ppWeCJ0uR300EH3i7kmgRZnAVxrH+XtK09jQWihk=",
"lastModified": 1696331477,
"narHash": "sha256-YkbRa/1wQWdWkVJ01JvV+75KIdM37UErqKgTf0L54Fk=",
"owner": "gytis-ivaskevicius",
"repo": "flake-utils-plus",
"rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f",
"rev": "bfc53579db89de750b25b0c5e7af299e0c06d7d3",
"type": "github"
},
"original": {
"owner": "gytis-ivaskevicius",
"repo": "flake-utils-plus",
"rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f",
"type": "github"
}
},
@@ -173,54 +73,19 @@
]
},
"locked": {
"lastModified": 1784350909,
"narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=",
"lastModified": 1765860045,
"narHash": "sha256-7Lxp/PfOy4h3QIDtmWG/EgycaswqRSkDX4DGtet14NE=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3",
"rev": "09de9577d47d8bffb11c449b6a3d24e32ac16c99",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-26.05",
"repo": "home-manager",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1779036909,
"narHash": "sha256-zXcwYQGCT6pzinK+1dBB2ekTVtfxGZAapb3Evdcu4fY=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "56c666e108467d87d13508936aade6d567f2a501",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-select": {
"locked": {
"lastModified": 1763303120,
"narHash": "sha256-yxcNOha7Cfv2nhVpz9ZXSNKk0R7wt4AiBklJ8D24rVg=",
"rev": "3d1e3860bef36857a01a2ddecba7cdb0a14c35a9",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/3d1e3860bef36857a01a2ddecba7cdb0a14c35a9.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz"
}
},
"nixos-conf-editor": {
"inputs": {
"flake-compat": "flake-compat",
@@ -228,11 +93,11 @@
"snowfall-lib": "snowfall-lib"
},
"locked": {
"lastModified": 1771149335,
"narHash": "sha256-YPUIwyumbQOE2DUY8NIsHIUTGUQnDVhnTVUZMZDRwi4=",
"lastModified": 1707338010,
"narHash": "sha256-UtLeZzDdk96sLTRcWsGrkWCslNUDytrGe0VmOTB/iig=",
"owner": "snowfallorg",
"repo": "nixos-conf-editor",
"rev": "9f8b4519a2e0e8919b69b7572bc26dab54274a6f",
"rev": "27b5e92f580f794c690093503869aab242f075ab",
"type": "github"
},
"original": {
@@ -243,11 +108,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1771008912,
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
"lastModified": 1698318101,
"narHash": "sha256-gUihHt3yPD7bVqg+k/UVHgngyaJ3DMEBchbymBMvK1E=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
"rev": "63678e9f3d3afecfeafa0acead6239cdb447574c",
"type": "github"
},
"original": {
@@ -259,28 +124,25 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1784432872,
"narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=",
"lastModified": 1765779637,
"narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
"rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-26.05",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"clan-core": "clan-core",
"disko": "disko",
"home-manager": "home-manager",
"nixos-conf-editor": "nixos-conf-editor",
"nixpkgs": "nixpkgs_2",
"sops-nix": "sops-nix"
"nixpkgs": "nixpkgs_2"
}
},
"snowfall-lib": {
@@ -293,11 +155,11 @@
]
},
"locked": {
"lastModified": 1765361626,
"narHash": "sha256-kX0Dp/kYSRbQ+yd9e3lmmUWdNbipufvKfL2IzbrSpnY=",
"lastModified": 1696432959,
"narHash": "sha256-oJQZv2MYyJaVyVJY5IeevzqpGvMGKu5pZcCCJvb+xjc=",
"owner": "snowfallorg",
"repo": "lib",
"rev": "c566ad8b7352c30ec3763435de7c8f1c46ebb357",
"rev": "92803a029b5314d4436a8d9311d8707b71d9f0b6",
"type": "github"
},
"original": {
@@ -306,43 +168,7 @@
"type": "github"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1783174389,
"narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1774449309,
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
"owner": "nix-systems",
"repo": "default",
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
"type": "github"
},
"original": {
"owner": "nix-systems",
"ref": "future-26.11",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@@ -356,29 +182,8 @@
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
}
+88 -224
View File
@@ -1,239 +1,103 @@
{
description = "LAN NixOS configs";
nixConfig = {
access-tokens = [
"github.com=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"
];
};
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
nixos-conf-editor.url = "github:snowfallorg/nixos-conf-editor";
home-manager = {
url = "github:nix-community/home-manager/release-26.05";
url = "github:nix-community/home-manager/release-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
clan-core = {
url = "https://git.clan.lol/clan/clan-core/archive/26.05.tar.gz";
# Deduplicate modules: clan-core bundles its own disko and sops-nix
# (both imported by nixosModules.clanCore). Without follows, we'd get
# two different versions of each, and disko's _module.args.diskoLib
# unique option would conflict. With follows, clan-core uses the same
# store paths as us, so NixOS deduplicates the imports.
inputs = {
nixpkgs.follows = "nixpkgs";
disko.follows = "disko";
sops-nix.follows = "sops-nix";
};
};
};
outputs = { self, nixpkgs, nixos-conf-editor, home-manager, sops-nix, ... } @ inputs:
outputs = { self, nixpkgs, nixos-conf-editor, home-manager, ... } @ inputs:
let system = "x86_64-linux"; in {
nixosConfigurations = {
# automatically use each host folder by name
nixos = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/nixos/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./hosts/nixos/home.nix;
}
];
let
system = "x86_64-linux";
inherit (nixpkgs) lib;
pkgs = nixpkgs.legacyPackages.${system};
vars = import ./variables.nix;
# Generates a nixosConfiguration from a platform (what it runs on) and
# a build type (what it's for), plus the per-identity host.nix that
# carries the bits that must stay fixed regardless of platform
# (hostName, hostId, per-machine secrets). Every build type except
# nix-cache itself consumes the nix-cache substituter and remote
# builder.
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix }:
let
flakeTarget = "${platform}-${buildType}";
in
nixpkgs.lib.nixosSystem {
inherit system;
modules = [
inputs.disko.nixosModules.disko
sops-nix.nixosModules.sops
inputs.clan-core.nixosModules.clanCore
{
# Required clan settings. directory is the flake root (where
# vars/ and sops/ directories live); machine.name is the flake
# target name (matches what clan vars generate uses as the key
# under vars/per-machine/). enableRecommendedDefaults = false
# is mandatory: without it, clan unconditionally enables
# networking.useNetworkd, adds packages, and tweaks nix settings
# -- none of which belong here.
clan.core = {
settings.directory = self;
settings.machine.name = flakeTarget;
enableRecommendedDefaults = false;
};
}
./modules/clan/ssh-host-key.nix
./modules/common/configuration.nix
./modules/platforms/${platform}.nix
./modules/build-types/${buildType}.nix
hostPath
{ environment.etc."flake-target".text = flakeTarget; }
home-manager.nixosModules.home-manager
{
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
extraSpecialArgs = { inherit vars; };
users.nixos = import homeFile;
};
}
] ++ lib.optionals (buildType != "nix-cache") [
./modules/nix-cache/client.nix
./modules/nix-cache/remote-builder-client.nix
];
# flakeTarget is passed via specialArgs (not read back from
# config.environment.etc."flake-target" above) specifically so
# modules/platforms/lxc.nix can use it to select its own host key
# file without a same-option circular dependency (a module
# contributing to environment.etc can't read the merged
# environment.etc it's itself contributing to).
specialArgs = { inherit inputs vars netbootSystem netbootMinimalSystem flakeTarget; };
};
# Generated platform x build-type matrix. pxe-boot has no linode
# variant (PXE/DHCP/TFTP need LAN L2 adjacency, which a Linode VPS
# doesn't have).
generatedTargets = {
linode-minimal = mkTarget { platform = "linode"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
proxmox-minimal = mkTarget { platform = "proxmox"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
lxc-minimal = mkTarget { platform = "lxc"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
linode-nix-cache = mkTarget { platform = "linode"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
proxmox-nix-cache = mkTarget { platform = "proxmox"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
lxc-nix-cache = mkTarget { platform = "lxc"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
linode-server = mkTarget { platform = "linode"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
proxmox-server = mkTarget { platform = "proxmox"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
lxc-server = mkTarget { platform = "lxc"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
linode-docker = mkTarget { platform = "linode"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
proxmox-docker = mkTarget { platform = "proxmox"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
lxc-docker = mkTarget { platform = "lxc"; buildType = "docker"; hostPath = ./hosts/docker/host.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; };
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; };
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
# flake): a self-contained NixOS installer that boots, discovers this
# flake's own nixosConfigurations over the network, and runs
# nixos-install against whichever one the operator picks. These are
# deliberately not part of the platform x build-type matrix above —
# they're throwaway boot media, not persistent hosts, so they skip
# disko/sops-nix/home-manager and just need `vars`.
installerTargets = {
installer = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./modules/installer/iso.nix ];
specialArgs = { inherit vars; };
};
};
# Same installer environment, built as netboot (kernel + initrd +
# 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 {
inherit system;
modules = [
./modules/installer/common.nix
({ modulesPath, ... }: {
imports = [
(modulesPath + "/installer/netboot/netboot-minimal.nix")
];
})
{ networking.hostName = "auto-installer"; }
];
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
{
nixosConfigurations = generatedTargets // installerTargets;
# Buildable auto-installer artifacts (`nix build .#<name>`). No `lxc`
# variant (installer-boots-as-an-LXC-container) or `all` bundle
# anymore — lxc-* and proxmox-* hosts deploy via their own tarball/
# disk-image outputs instead (see docs/auto-installer.md and
# docs/proxmox-images.md), which left the installer's own LXC form
# with no real use case: it's excluded from the install menu (same
# bind-mount problem as any LXC nixos-install target) and nothing
# else needed booting the installer itself as a container.
packages.${system} = {
iso = installerTargets.installer.config.system.build.isoImage;
pxe = pkgs.linkFarm "pxe" [
{ name = "netboot.ipxe"; path = netbootSystem.config.system.build.netbootIpxeScript; }
{ name = "initrd"; path = netbootSystem.config.system.build.netbootRamdisk; }
{ name = "kernel"; path = netbootSystem.config.system.build.kernel; }
];
# Vanilla NixOS minimal netboot bundle — see netbootMinimalSystem
# above. Staged onto the pxe-boot host alongside packages.pxe by
# modules/pxe-boot/stage-installer-artifacts.nix.
pxe-minimal = pkgs.linkFarm "pxe-minimal" [
{ 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; }
];
specialArgs = { inherit inputs; };
};
docker = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/docker/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
];
};
kuma = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/kuma/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
];
};
server = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/server/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
];
};
nix-cache = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/nix-cache/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
];
};
nix-minimal = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/nix-minimal/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
];
};
};
};
}
+154
View File
@@ -0,0 +1,154 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ config, lib, pkgs, inputs, ... }:
let
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
docker
pytz
]);
in
{
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
networking.hostName = "docker"; # Define your hostname.
virtualisation.docker.enable = true;
# Enable docker-compose
environment.systemPackages = with pkgs; [
docker-compose
docker_25
];
# Mount docker persistent data
fileSystems."/mnt/docker-persistent-data" = {
device = "/dev/disk/by-label/docker-data";
fsType = "ext4";
options = [ "defaults" "nofail" "noatime" ];
};
# Create nextcloud cron scheduled task
systemd.services.nextcloud = {
description = "Nextcloud scheduled task";
script = ''docker-compose -f ~/docker/nextcloud/docker-compose.yml exec -u 33 webapp php ./cron.php'';
serviceConfig = {
Type = "oneshot";
User = "nixos";
};
path = with pkgs; [ docker docker-compose ];
};
systemd.timers.nextcloud = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*:0/5";
Persistent = true;
};
};
# create update task
systemd.services.update-containers = {
description = "Update Docker Compose Containers";
after = [ "docker.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
User = "nixos";
WorkingDirectory = "/home/nixos/docker";
ExecStart = "${pythonEnv}/bin/python3 /home/nixos/docker/update-containers.py";
StandardOutput = "journal";
StandardError = "journal";
};
path = [ pkgs.docker pkgs.docker-compose ]; # Ensures docker CLI is available in $PATH
};
systemd.timers.update-containers = {
description = "Weekly + Reboot container update";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5min"; # Run 5 minutes after boot
OnUnitActiveSec = "1w"; # Run every week after last run
Persistent = true; # Catch up if system was off
};
};
# Create Gotify Docker monitoring script task
systemd.services.docker-health-to-gotify = {
description = "Alert Gotify when Docker containers become unhealthy";
after = [ "docker.service" ];
serviceConfig = {
Type = "oneshot";
# Run as root so it can read /etc/secrets and access docker socket
# User = "root";
#EnvironmentFile = "-/etc/secrets/docker-health-alert.env";
ExecStart = "${pkgs.bash}/bin/bash /mnt/docker-persistent-data/docker/gotify/docker-health-to-gotify.sh";
StandardOutput = "journal";
StandardError = "journal";
};
path = with pkgs; [ docker curl coreutils gnused ];
};
systemd.timers.docker-health-to-gotify = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "1min";
AccuracySec = "15s";
Persistent = true;
};
};
#Add sym links to data on users home folder
system.userActivationScripts.createDockerSymlink.text = ''
ln -sf /mnt/docker-persistent-data/docker /home/nixos/docker
'';
system.userActivationScripts.createSetupSymlink.text = ''
ln -sf /mnt/docker-persistent-data/setup /home/nixos/setup
'';
users.users.nixos.extraGroups = [ "docker" ];
services.openssh.settings.PermitRootLogin = "yes";
# Open ports in the firewall.
networking.firewall.allowedTCPPorts = [ 80 8080 443 ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.sta>
system.stateVersion = "25.05"; # Did you read the comment?
}
-11
View File
@@ -1,11 +0,0 @@
_:
{
networking.hostName = "docker";
networking.hostId = "007f0200";
boot.zfs.forceImportRoot = false;
# Preserved from the pre-refactor `docker` target — stateVersion must never
# be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+77
View File
@@ -0,0 +1,77 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ config, lib, pkgs, inputs, ... }:
let
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
docker
pytz
]);
in
{
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
networking.hostName = "kuma"; # Define your hostname.
virtualisation.docker.enable = true;
# Enable docker-compose
environment.systemPackages = with pkgs; [
docker-compose
docker_25
];
# Mount docker persistent data
# fileSystems."/mnt/docker-persistent-data" = {
# device = "/dev/disk/by-label/docker-data";
# fsType = "ext4";
# options = [ "defaults" "nofail" "noatime" ];
# };
#Add sym links to data on users home folder
# system.userActivationScripts.createDockerSymlink.text = ''
# ln -sf /mnt/docker-persistent-data/docker /home/nixos/docker
# '';
# system.userActivationScripts.createSetupSymlink.text = ''
# ln -sf /mnt/docker-persistent-data/setup /home/nixos/setup
# '';
users.users.nixos.extraGroups = [ "docker" ];
services.openssh.settings.PermitRootLogin = "yes";
# Open ports in the firewall.
networking.firewall.allowedTCPPorts = [ 80 8080 443 ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.sta>
system.stateVersion = "25.05"; # Did you read the comment?
}
+56
View File
@@ -0,0 +1,56 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ config, lib, pkgs, inputs, ... }:
{
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
networking.hostName = "nix-cache"; # Define your hostname.
services.nix-serve.enable = true;
services.nix-serve.secretKeyFile = "/etc/nix/cache-priv.pem";
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."cache.local" = {
locations."/".proxyPass = "http://${config.services.nix-serve.bindAddress}:${toString config.services.nix-serve.port}";
};
};
networking.firewall.allowedTCPPorts = [ config.services.nginx.defaultHTTPListenPort ];
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ 80 8080 443 ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.sta>
system.stateVersion = "25.05"; # Did you read the comment?
}
-21
View File
@@ -1,21 +0,0 @@
{ vars, ... }:
{
imports = [
(import ../../modules/beszel/host-token.nix {
name = "nix-cache";
sopsFile = ../../secrets/nix-cache.yaml;
})
];
networking.hostName = vars.nixCacheHost;
services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
};
# Preserved from the pre-refactor `nix-cache` target — stateVersion must
# never be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+76
View File
@@ -0,0 +1,76 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ config, lib, pkgs, inputs, ... }:
{
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
networking.hostName = "nix-minimal"; # Define your hostname.
environment.systemPackages = with pkgs; [
sshfs
fuse3 # needed for modern sshfs
];
# Enable FUSE (if not already)
boot.extraModprobeConfig = ''
options fuse user_allow_other
'';
# Allow mounting with user permissions (optional, see note)
# security.wrappers.sshfs = {
# source = "${pkgs.sshfs}/bin/sshfs";
# owner = "nixos";
# group = "users";
# permissions = "4755";
# fileSystems."/mnt/proxmox-iso" = {
# device = "root@pve:/var/lib/vz/template/iso";
# fsType = "fuse.sshfs";
# options = [
# "IdentityFile=/home/nixos/.ssh/id_ed25519.pub" # or your SSH key path
# "allow_other"
# "reconnect"
# "ServerAliveInterval=15"
# "ServerAliveCountMax=3"
# "StrictHostKeyChecking=no" # only if you're OK with this
# ];
# };
# };
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ 80 8080 443 ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.sta>
system.stateVersion = "25.05"; # Did you read the comment?
}
-12
View File
@@ -1,12 +0,0 @@
_:
{
# Preserves the hostname of the existing, already-deployed machine
# (previously the flat `nix-minimal` target) — the flake attribute name
# changed, the real machine's hostname did not.
networking.hostName = "nix-minimal";
# Preserved from the pre-refactor `nix-minimal` target — stateVersion must
# never be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+133
View File
@@ -0,0 +1,133 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).
{ config, pkgs, lib, inputs, ... }:
let
nixosConfEditor = builtins.getFlake "github:snowfallorg/nixos-conf-editor";
in {
#{
environment.systemPackages = with pkgs; [
inputs.nixos-conf-editor.packages.${pkgs.system}.nixos-conf-editor
nodejs
appimage-run
seahorse
vscode
p7zip
popsicle # balena-etcher
shotcut
gimp
pdfarranger
terminator
libreoffice-qt
transmission_4-qt
];
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
# Bootloader.
boot.loader.grub.useOSProber = true;
networking.hostName = "nixos"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Select internationalisation properties.
i18n.defaultLocale = "en_AU.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "en_AU.UTF-8";
LC_IDENTIFICATION = "en_AU.UTF-8";
LC_MEASUREMENT = "en_AU.UTF-8";
LC_MONETARY = "en_AU.UTF-8";
LC_NAME = "en_AU.UTF-8";
LC_NUMERIC = "en_AU.UTF-8";
LC_PAPER = "en_AU.UTF-8";
LC_TELEPHONE = "en_AU.UTF-8";
LC_TIME = "en_AU.UTF-8";
};
# Enable the X11 windowing system.
services.xserver.enable = true;
# Enable the Cinnamon Desktop Environment.
services.xserver.displayManager.lightdm.enable = true;
services.xserver.desktopManager.cinnamon.enable = true;
# services.xserver.desktopManager.xfce.enable = true;
# Configure keymap in X11
services.xserver.xkb = {
layout = "au";
variant = "";
};
# Enable CUPS to print documents.
services.printing.enable = true;
# Enable sound with pipewire.
# services.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
#jack.enable = true;
# use the example session manager (no others are packaged yet so this is enabled by default,
# no need to redefine it in your config for now)
#media-session.enable = true;
};
# Enable touchpad support (enabled default in most desktopManager).
# services.xserver.libinput.enable = true;
users.users.nixos.extraGroups = [ "networkmanager" ]; # Enable ‘sudo’ for the user.
# Install firefox.
programs.firefox.enable = true;
system.stateVersion = "25.05"; # Did you read the comment?
nix.settings.experimental-features = "nix-command flakes";
services.xrdp.enable = true;
services.xrdp.defaultWindowManager = "cinnamon-session";
services.xrdp.openFirewall = true;
nixpkgs.config.allowUnfree = true;
services.gnome.gnome-keyring.enable = true;
# security.pam.services.lightdm.enableGnomeKeyring = true;
# services.gnome.gnome-keyring.enable = true;
security.pam.services.login.enableGnomeKeyring = true;
services.xserver.displayManager.sessionCommands = ''
eval $(gnome-keyring-daemon --start --components=secrets,ssh)
export SSH_AUTH_SOCK
'';
# systemd.services.nextcloud-appimage = {
# enable = true;
# Unit = {
# Description = "Nextcloud AppImage client";
# After = [ "graphical-session.target" ];
# Wants = [ "graphical-session.target" ]; # optional but helpful
# };
# Service = {
# ExecStart = "/run/current-system/sw/bin/appimage-run /home/nixos/Applications/Nextcloud.AppImage --background";
# Restart = "on-failure";
# # You can add RestartSec = "5s"; if you like
# };
# Install = {
# WantedBy = [ "default.target" ];
# };
# };
}
+63 -70
View File
@@ -1,86 +1,79 @@
{ config, pkgs, lib, vars, ... }:
{ config, pkgs, lib, ... }:
{
imports = [
../../modules/common/aliases.nix
imports = [
../../common/aliases.nix
];
home = {
username = vars.primaryUser;
homeDirectory = "/home/${vars.primaryUser}";
stateVersion = "25.05"; # match your NixOS stateVersion
home.username = "nixos"; # your actual username
home.homeDirectory = "/home/nixos";
home.stateVersion = "25.05"; # match your NixOS stateVersion
# Optional: packages
packages = with pkgs; [
git
vim
tmux
nextcloud-client
# vscode
chromium
claude-code
fish
sops
];
programs.home-manager.enable = true; # mandatory to activate HM
# Optional: set environment vars
sessionVariables = {
EDITOR = "vim";
SOPS_AGE_KEY_FILE = "~/.config/sops/age/keys.txt";
};
# Optional: packages
home.packages = with pkgs; [
git
vim
tmux
nextcloud-client
# vscode
chromium
];
file = {
".local/share/applications/proxmox-chromium-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox (Chromium)
Exec=chromium --app=https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --window-size=1920,1080 --window-position=0,0
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false
Categories=Hypervisor;
StartupWMClass=PVE
'';
".local/share/applications/pbs-chromium-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox Backup Server (Chromium)
Exec=chromium --app=https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --window-size=1920,1080 --window-position=0,0
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
'';
".local/share/applications/proxmox-firefox-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox (Firefox)
Exec=firefox --new-instance https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --profile ProxmoxWebApp --window-size=1920,1080 --class ProxmoxWebApp
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false
Categories=Hypervisor;
StartupWMClass=PVE
'';
".local/share/applications/pbs-firefox-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox Backup Server (Firefox)
Exec=firefox --new-window https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --profile PbsWebApp --window-size=1920,1080 --class PbsWebApp
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
StartupWMClass=PBS
'';
};
# Optional: set environment vars
home.sessionVariables = {
EDITOR = "vim";
};
programs.home-manager.enable = true; # mandatory to activate HM
# Optional: enable bash (or zsh, fish...)
programs.bash.enable = true;
services.nextcloud-client = {
services.nextcloud-client = {
enable = true;
# Optionally start in background directly
startInBackground = true;
};
}
home.file = {
".local/share/applications/proxmox-chromium-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox (Chromium)
Exec=chromium --app=https://pve.sweet.home:8006 --window-size=1920,1080 --window-position=0,0
Icon=/home/nixos/.local/share/icons/proxmox.png
Terminal=false
Categories=Hypervisor;
StartupWMClass=PVE
'';
".local/share/applications/pbs-chromium-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox Backup Server (Chromium)
Exec=chromium --app=https://192.168.2.108:8007 --window-size=1920,1080 --window-position=0,0
Icon=/home/nixos/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
'';
".local/share/applications/proxmox-firefox-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox (Firefox)
Exec=firefox --new-instance https://pve.sweet.home:8006 --profile ProxmoxWebApp --window-size=1920,1080 --class ProxmoxWebApp
Icon=/home/nixos/.local/share/icons/proxmox.png
Terminal=false
Categories=Hypervisor;
StartupWMClass=PVE
'';
".local/share/applications/pbs-firefox-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox Backup Server (Firefox)
Exec=firefox --new-window https://192.168.2.108:8007 --profile PbsWebApp --window-size=1920,1080 --class PbsWebApp
Icon=/home/nixos/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
StartupWMClass=PBS
'';
};
}
-18
View File
@@ -1,18 +0,0 @@
_:
{
imports = [
../../modules/networking/wifi.nix
];
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
# be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
-9
View File
@@ -1,9 +0,0 @@
_:
{
networking.hostName = "pxe-boot";
# Preserved from the pre-refactor `pxe-boot` target — stateVersion must
# never be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+147
View File
@@ -0,0 +1,147 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ config, lib, pkgs, inputs,... }:
{
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
networking.hostName = "server"; # Define your hostname.
# Pick only one of the below networking options.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Mount server data
fileSystems."/srv" = {
device = "/dev/disk/by-label/server-data";
fsType = "ext4";
options = [ "defaults" ];
};
fileSystems."/backup" = {
device = "/dev/disk/by-label/backup-data";
fsType = "ext4";
options = [ "defaults" ];
};
#Add sym links to data on users home folder
system.userActivationScripts.createDockerSymlink.text = ''
ln -sf /srv/scripts /home/nixos/scripts
'';
#system.userActivationScripts.createSetupSymlink.text = ''
# ln -sf /mnt/docker-persistent-data/setup /home/nixos/setup
#'';
services.nfs.server = {
enable = true;
exports = ''
/srv/ 192.168.2.0/24(rw,sync,no_subtree_check)
/backup 192.168.2.0/24(rw,sync,no_subtree_check)
'';
};
security.sudo = {
enable = true;
extraRules = [
{
users = [ "nixos" ];
commands = [
{
command = "/run/current-system/sw/bin/rsync";
options = [ "NOPASSWD" ];
}
];
}
];
};
systemd.services.backup = {
description = "Backup data";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
User = "root";
# WorkingDirectory = "/home/nixos/scripts";
ExecStart = "${pkgs.bash}/bin/bash -e /srv/scripts/rsync.sh";
StandardOutput = "journal";
StandardError = "journal";
};
path = with pkgs; [ bash rsync openssh coreutils ];
};
systemd.timers.backup = {
description = "Daily backup";
wantedBy = [ "timers.target" ];
timerConfig = {
OnUnitActiveSec = "1d"; # Run every day after last run
Persistent = true; # Catch up if system was off
};
};
networking.wireguard.enable = true;
networking.wireguard.interfaces.wg0 = {
ips = [ "10.100.0.254/24" ];
listenPort = 51820;
privateKeyFile = "/etc/wireguard/server.key";
peers = [
{
publicKey = "cA30PTE/2gMbIhtAXaRPJkQeDc7/UWoA6eK3K+Xpsww=";
allowedIPs = [ "10.100.0.2/32" ];
}
];
};
# Allow WireGuard UDP port in firewall
networking.firewall.allowedUDPPorts = [ 51820 ];
# Enable IPv4 forwarding
boot.kernel.sysctl = {
"net.ipv4.ip_forward" = 1;
};
# NAT: lets LAN devices reply to the Pi without needing routes on the LAN router
networking.nat = {
enable = true;
externalInterface = "enp0s18";
internalInterfaces = [ "wg0" ];
};
networking.enableIPv4Forwarding = true;
services.openssh.settings.PermitRootLogin = "yes";
# Open ports in the firewall.
networking.firewall.allowedTCPPorts = [ 2049 ];
# networking.firewall.allowedUDPPorts = [ 111 2049 20048 ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.sta>
system.stateVersion = "25.05"; # Did you read the comment?
}
-24
View File
@@ -1,24 +0,0 @@
{ vars, ... }:
{
imports = [
(import ../../modules/beszel/host-token.nix {
name = "server";
sopsFile = ../../secrets/server.yaml;
})
];
networking.hostName = vars.nfsServerHost;
networking.hostId = "6689f93e";
services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
LOG_LEVEL = "debug";
};
# Preserved from the pre-refactor `server` target — stateVersion must never
# be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
-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";
}
-18
View File
@@ -1,18 +0,0 @@
{ vars, ... }:
{
services.beszel.agent.enable = true;
services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}";
};
# The upstream module runs beszel-agent under DynamicUser with
# ProtectSystem = "strict" and no StateDirectory, so /var/lib/beszel-agent
# (where the agent persists its hub-pairing fingerprint, per
# https://github.com/henrygd/beszel/discussions/1542) isn't writable --
# every restart silently fails to save it and regenerates a fresh one in
# memory, permanently desyncing from whatever the hub has on record after
# the very first successful pairing. Give it real persistent storage.
systemd.services.beszel-agent.serviceConfig.StateDirectory = "beszel-agent";
}
-11
View File
@@ -1,11 +0,0 @@
{ name, sopsFile }:
{ config, ... }:
{
sops.secrets."beszel-token".sopsFile = sopsFile;
sops.templates."${name}-beszel.env".content = ''
TOKEN=${config.sops.placeholder."beszel-token"}
'';
services.beszel.agent.environmentFile = config.sops.templates."${name}-beszel.env".path;
}
-6
View File
@@ -1,6 +0,0 @@
_:
{
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
}
-45
View File
@@ -1,45 +0,0 @@
{ pkgs, vars, ... }:
{
# Pins the Docker Engine version, carried forward from the pre-refactor
# `docker` target's inline pkgs overlay.
nixpkgs.overlays = [
(final: prev: {
docker = prev.docker_29;
docker_cli = prev.docker_29;
})
];
imports = [
../docker/mount-data.nix
../docker/enable-service.nix
../docker/nextcloud-cron-job.nix
../docker/docker-health-to-gotify.nix
../tailscale/enable-service.nix
../traefik/rotate-logs.nix
../raspi/mount-data.nix
../services/enable-rpcbind.nix
];
environment.systemPackages = with pkgs; [
nfs-utils
];
boot.supportedFilesystems = [ "nfs" ];
systemd.tmpfiles.rules = [
"L+ /home/${vars.primaryUser}/docker - - - - ${vars.nfsShares.dockerConfig.mountpoint}"
"d /mnt/docker 0755 ${vars.primaryUser} users -"
"d ${vars.nfsShares.raspiVolumes.mountpoint} 0755 ${vars.primaryUser} users -"
];
users.users.${vars.primaryUser}.extraGroups = [ "docker" ];
services.openssh.settings.PermitRootLogin = "yes";
networking.firewall.allowedTCPPorts = [
vars.ports.dockerHttp
vars.ports.dockerExtra
vars.ports.dockerHttps
vars.ports.beszelHub
];
}
-73
View File
@@ -1,73 +0,0 @@
{ config, pkgs, lib, inputs, vars, ... }:
{
environment.systemPackages = with pkgs; [
inputs.nixos-conf-editor.packages.${pkgs.stdenv.hostPlatform.system}.nixos-conf-editor
nodejs
appimage-run
seahorse
vscode
p7zip
popsicle # balena-etcher
shotcut
gimp
pdfarranger
terminator
libreoffice-qt
transmission_4-qt
];
boot.loader.grub.useOSProber = true;
programs.direnv.enable = true;
services = {
xserver = {
enable = true;
displayManager = {
lightdm.enable = true;
sessionCommands = ''
eval $(gnome-keyring-daemon --start --components=secrets,ssh)
export SSH_AUTH_SOCK
'';
};
desktopManager.cinnamon.enable = true;
xkb = {
layout = "au";
variant = "";
};
};
printing.enable = true;
pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
xrdp = {
enable = true;
defaultWindowManager = "cinnamon-session";
openFirewall = true;
};
gnome.gnome-keyring.enable = true;
};
security = {
rtkit.enable = true;
pam.services.login.enableGnomeKeyring = true;
};
# 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
# that won't exist there.
users.users.${vars.primaryUser}.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
programs.firefox.enable = true;
nixpkgs.config.allowUnfree = true;
}
-16
View File
@@ -1,16 +0,0 @@
{ lib, pkgs, config, vars, ... }:
{
networking.networkmanager.enable = true;
# 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
# that won't exist there.
users.users.${vars.primaryUser}.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
environment.systemPackages = with pkgs; [
inetutils
mtr
sysstat
];
}
-8
View File
@@ -1,8 +0,0 @@
{ ... }:
{
imports = [
../nix-cache/server.nix
../beszel/enable-agent.nix
];
}
-165
View File
@@ -1,165 +0,0 @@
{ config, lib, pkgs, inputs, vars, ... }:
let
pxeRoot = "/srv/pxe";
httpRoot = "${pxeRoot}/http";
tftpRoot = "${pxeRoot}/tftp";
pxeBaseUrl = "http://${vars.pxeServerIp}";
bootIpxe = pkgs.writeText "boot.ipxe" ''
#!ipxe
dhcp
echo Booting from PXE server...
chain ${pxeBaseUrl}/menu.ipxe
'';
autoexecIpxe = pkgs.writeText "autoexec.ipxe" ''
#!ipxe
dhcp
chain ${pxeBaseUrl}/boot.ipxe
'';
systemRescueIpxe = pkgs.writeText "systemrescue.ipxe" ''
#!ipxe
set base ${pxeBaseUrl}
kernel ''${base}/systemrescue/sysresccd/boot/x86_64/vmlinuz initrd=sysresccd.img archisobasedir=sysresccd archiso_http_srv=''${base}/systemrescue/ ip=dhcp checksum
initrd ''${base}/systemrescue/sysresccd/boot/x86_64/sysresccd.img sysresccd.img
boot
'';
stageSystemRescue = pkgs.writeShellScript "stage-systemrescue" ''
set -eu
iso="${httpRoot}/images/systemrescue.iso"
staged="${httpRoot}/systemrescue"
tmp="${httpRoot}/.systemrescue.tmp"
previous="${httpRoot}/.systemrescue.previous"
if [ ! -e "$iso" ]; then
echo "SystemRescue ISO not found at $iso; skipping staging."
exit 0
fi
rm -rf "$tmp"
mkdir -p "$tmp"
${pkgs.libarchive}/bin/bsdtar -C "$tmp" -xf "$iso"
test -f "$tmp/sysresccd/boot/x86_64/vmlinuz"
test -f "$tmp/sysresccd/boot/x86_64/sysresccd.img"
chmod -R a+rX "$tmp"
rm -rf "$previous"
if [ -e "$staged" ]; then
mv "$staged" "$previous"
fi
mv "$tmp" "$staged"
rm -rf "$previous"
'';
menuIpxe = pkgs.writeText "menu.ipxe" ''
#!ipxe
set base ${pxeBaseUrl}
menu PXE Boot Menu
item auto-installer NixOS Auto-Installer
item nixos-minimal NixOS Minimal
item rescue Rescue Environment
item shell iPXE Shell
item reboot Reboot
choose target && goto ''${target}
:auto-installer
chain ''${base}/auto-installer/netboot.ipxe
:nixos-minimal
chain ''${base}/nixos-minimal/netboot.ipxe
:rescue
chain ''${base}/systemrescue.ipxe
:shell
shell
:reboot
reboot
'';
in
{
imports = [
../pxe-boot/stage-installer-artifacts.nix
];
environment.systemPackages = with pkgs; [
ipxe
];
services = {
nginx = {
enable = true;
virtualHosts."pxe-boot" = {
default = true;
root = httpRoot;
locations."/" = {
extraConfig = ''
autoindex on;
'';
};
};
};
# TFTP is only used to deliver the initial iPXE bootloader. After iPXE
# starts, all further assets are fetched via nginx over HTTP.
atftpd = {
enable = true;
root = tftpRoot;
extraOptions = [
"--verbose=5"
];
};
openssh.settings.PermitRootLogin = "yes";
};
systemd.tmpfiles.rules = [
"d ${pxeRoot} 0755 root root -"
"d ${httpRoot} 0755 root root -"
"d ${httpRoot}/images 0755 root root -"
"d ${httpRoot}/auto-installer 0755 root root -"
"d ${httpRoot}/nixos-minimal 0755 root root -"
"d ${httpRoot}/systemrescue 0755 root root -"
"d ${httpRoot}/ubuntu 0755 root root -"
"d ${httpRoot}/rescue 0755 root root -"
"d ${tftpRoot} 0755 root root -"
"C+ ${httpRoot}/boot.ipxe 0644 root root - ${bootIpxe}"
"C+ ${httpRoot}/menu.ipxe 0644 root root - ${menuIpxe}"
"C+ ${httpRoot}/systemrescue.ipxe 0644 root root - ${systemRescueIpxe}"
"C+ ${tftpRoot}/autoexec.ipxe 0644 root root - ${autoexecIpxe}"
"C+ ${tftpRoot}/ipxe.efi 0644 root root - ${pkgs.ipxe}/ipxe.efi"
"C+ ${tftpRoot}/undionly.kpxe 0644 root root - ${pkgs.ipxe}/undionly.kpxe"
];
systemd.services.stage-systemrescue = {
description = "Stage SystemRescue ISO contents for HTTP PXE boot";
after = [
"local-fs.target"
"systemd-tmpfiles-setup.service"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = stageSystemRescue;
};
};
networking.firewall.allowedTCPPorts = [ vars.ports.pxeBootHttp ];
networking.firewall.allowedUDPPorts = [ vars.ports.pxeBootTftp ];
}
-90
View File
@@ -1,90 +0,0 @@
{ vars, lib, pkgs, ... }:
let
poolName = lib.removePrefix "/" vars.storageRoot;
# For each NFS share subpath, generate every ancestor path so ZFS datasets
# are created parent-first. e.g. "docker/config" → ["docker" "docker/config"]
ancestors = path:
let parts = lib.splitString "/" path;
in lib.imap1 (i: _: lib.concatStringsSep "/" (lib.take i parts)) parts;
poolDatasets = lib.unique (
lib.concatMap (share: ancestors share.subpath) (lib.attrValues vars.nfsShares)
);
in
{
imports = [
../beszel/enable-agent.nix
../services/zfs/enable-service.nix
];
boot.zfs.extraPools = [ poolName ];
# On a fresh image deploy the data disk (scsi1) starts blank — no pool
# exists yet, so zfs-import-tank.service would spin for 60 s and fail.
# This service runs first: if the pool is already present it exits instantly;
# otherwise it creates it (with all required datasets) so the standard
# import service finds it ready on the very first boot.
systemd.services."zfs-init-${poolName}" = {
description = "Initialize '${poolName}' ZFS pool on first boot if not present";
wantedBy = [ "zfs-import-${poolName}.service" ];
before = [ "zfs-import-${poolName}.service" ];
after = [ "systemd-udev-settle.service" ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [ pkgs.zfs_unstable ];
script = ''
# Already imported — nothing to do.
if zpool list "${poolName}" >/dev/null 2>&1; then
exit 0
fi
# 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 = {
after = [ "zfs-mount.service" ];
requires = [ "zfs-mount.service" ];
};
services.nfs.server = {
enable = true;
exports = ''
${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath} ${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}/${vars.nfsShares.dockerDatabases.subpath} ${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}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
'';
};
networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd ];
}
-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
];
}
-30
View File
@@ -1,30 +0,0 @@
{ pkgs, ... }: {
# Defines the SSH host key as a clan vars generator so that:
# - `clan vars generate <target>` creates and encrypts the key pair
# - The private key lives at vars/per-machine/<target>/openssh/ssh_host_ed25519_key/secret
# (sops binary-encrypted, admin-key-only; decrypted by the build script)
# - The public key lives at vars/per-machine/<target>/openssh/ssh_host_ed25519_key.pub/value
# (plaintext; used by sync-host-keys.sh to derive the sops age fingerprint)
#
# neededFor = "activation" means clan's deployment tool would upload this
# before running nixos-rebuild/nixos-install (for VM/baremetal via
# nixos-anywhere). For lxc-* hosts, the build script bakes it into the
# tarball directly via NIXOS_HOST_KEYS_DIR -- the neededFor value here
# simply ensures it is NOT mapped to sops.secrets (which would try to
# decrypt it at runtime as a regular service secret, which is wrong: the
# SSH host key reaches the container via the tarball, not sops).
clan.core.vars.generators.openssh = {
files."ssh_host_ed25519_key" = {
secret = true;
neededFor = "activation";
};
files."ssh_host_ed25519_key.pub" = {
secret = false;
neededFor = "activation";
};
runtimeInputs = [ pkgs.openssh ];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out/ssh_host_ed25519_key"
'';
};
}
-50
View File
@@ -1,50 +0,0 @@
{ config, pkgs, lib, vars, ... }:
let
# Flake attribute names are now <platform>-<buildtype> (e.g. proxmox-docker)
# and no longer match networking.hostName, since a host's hostname stays
# fixed while the platform backing it can change. Each nixosConfiguration
# stamps its own active target name into /etc/flake-target at build time.
mySwitchCmd = ''
sudo nixos-rebuild switch \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
'';
myTestCmd = ''
sudo nixos-rebuild test \
--no-write-lock-file \
--refresh \
--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
{
programs.bash = {
enable = true;
shellAliases = {
"Switch-nix" = mySwitchCmd;
"Test-nix" = myTestCmd;
};
initExtra = buildImageFn;
};
}
-114
View File
@@ -1,114 +0,0 @@
{ config, lib, pkgs, vars, ... }:
{
imports =
[
# Include the results of the hardware scan.
# ./hardware-configuration.nix
./set-locale.nix
];
# Use the GRUB 2 boot loader.
# boot.loader.grub.enable = true;
#boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
# Recommended over the true default (bypasses ZFS's own import safeguards)
# per the option's own docs; matches hosts/docker/host.nix and
# modules/services/zfs/enable-service.nix, which already set this
# explicitly. Harmless no-op on hosts that don't use ZFS at all.
boot.zfs.forceImportRoot = false;
# Set your time zone.
time.timeZone = vars.timeZone;
# Enable QEMU agent
services.qemuGuest.enable = true;
# Enable docker-compose
environment.systemPackages = with pkgs; [
vim
btop
git
gcr
jq
];
# Secrets shared by every host, decrypted at activation via each host's
# existing SSH host key (sops-nix derives the age key from
# /etc/ssh/ssh_host_ed25519_key automatically — see modules/common/README
# or docs/ for the sops workflow). hashedPassword/hashedPasswordFile need
# neededForUsers so they're available before the normal secret-activation
# step, since user creation happens very early in boot.
sops = {
defaultSopsFile = ../../secrets/common.yaml;
secrets = {
"root-hashedPassword".neededForUsers = true;
"nixos-hashedPassword".neededForUsers = true;
"nix-github-token" = { };
};
# nix.conf doesn't support a *File-style option for access-tokens, so the
# token is rendered into a runtime-only file (never touches the Nix store)
# and pulled in via nix.conf's native !include directive.
templates."nix-github-token.conf".content = ''
access-tokens = github.com=${config.sops.placeholder."nix-github-token"}
'';
};
nix.extraOptions = ''
!include ${config.sops.templates."nix-github-token.conf".path}
'';
users = {
# With mutableUsers = false, update-users-groups.pl enforces hashedPasswordFile
# on every activation regardless of whether the account already exists in
# /etc/shadow. The default (true) only applies hashedPasswordFile to newly-
# created accounts — which means a freshly-built proxmox disk image (where
# activation runs without a usable sops key, so both accounts land in shadow
# with ‘!’) will never have its passwords fixed by subsequent boots.
mutableUsers = false;
users.root = {
hashedPasswordFile = config.sops.secrets."root-hashedPassword".path;
};
users.${vars.primaryUser} = {
isNormalUser = true;
extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
packages = with pkgs; [
tree
];
hashedPasswordFile = config.sops.secrets."nixos-hashedPassword".path;
openssh.authorizedKeys.keys = [
vars.adminSshKey
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGygkCljN6uKpdJbHTOQtn8ZnH+wKXDLAwrDFbLrE/65 nixos@nixos"
];
};
};
# Enable the OpenSSH daemon.
services.openssh.enable = true;
#Enable flakes
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = true;
};
programs.git = {
enable = true;
package = pkgs.git;
config = {
credential.helper = "store";
};
};
}
-69
View File
@@ -1,69 +0,0 @@
{ config, pkgs, lib, vars, ... }:
let
remote = "root@proxmox-ip:/var/lib/vz/template/iso";
localMount = "${config.home.homeDirectory}/proxmox-iso";
in
{
imports = [
./aliases.nix
];
home = {
username = vars.primaryUser;
homeDirectory = "/home/${vars.primaryUser}";
stateVersion = "25.11"; # match your NixOS stateVersion
# Optional: packages
packages = with pkgs; [
git
vim
tmux
nano
sshfs
];
# Optional: set environment vars
sessionVariables = {
EDITOR = "nano";
};
};
programs.home-manager.enable = true; # mandatory to activate HM
programs.bash.enable = true;
# GitHub access-tokens setting used to live here in plaintext; it's now
# rendered system-wide from a sops-nix secret via nix.extraOptions in
# modules/common/configuration.nix instead (covers the daemon for every
# user, not just this one).
# systemd.user.services.mount-proxmox-iso = {
# Unit = {
# Description = "Mount Proxmox ISO dir via SSHFS";
# After = [ "network-online.target" ];
# Wants = [ "network-online.target" ];
# };
# Service = {
# Type = "simple";
# ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${localMount}";
# ExecStart = "${pkgs.sshfs}/bin/sshfs -o IdentityFile=${config.home.homeDirectory}/.ssh/id_ed25519,allow_other,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 root@proxmox-ip:/var/lib/vz/template/iso ${localMount}";
# ExecStop = "${pkgs.fuse3}/bin/fusermount3 -u ${localMount}";
# Restart = "on-failure";
# };
# Install = {
# WantedBy = [ "default.target" ];
# };
# };
# Optional: enable bash (or zsh, fish...)
# programs.bash.enable = true;
# Optional: manage dotfiles via symlinks
# home.file = {
# ".tmux.conf".source = ./dotfiles/tmux.conf;
# ".config/nvim/init.vim".source = ./dotfiles/init.vim;
# };
}
-18
View File
@@ -1,18 +0,0 @@
_:
{
i18n.defaultLocale = "en_AU.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "en_AU.UTF-8";
LC_IDENTIFICATION = "en_AU.UTF-8";
LC_MEASUREMENT = "en_AU.UTF-8";
LC_MONETARY = "en_AU.UTF-8";
LC_NAME = "en_AU.UTF-8";
LC_NUMERIC = "en_AU.UTF-8";
LC_PAPER = "en_AU.UTF-8";
LC_TELEPHONE = "en_AU.UTF-8";
LC_TIME = "en_AU.UTF-8";
};
}
-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";
};
};
};
}
-70
View File
@@ -1,70 +0,0 @@
{ config, vars, ... }:
{
disko.devices = {
disk.main = {
type = "disk";
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 = {
type = "gpt";
partitions = {
esp = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
extraArgs = [
"-F"
"32"
"-n"
"boot"
];
};
};
swap = {
size = "8G";
content = {
type = "swap";
randomEncryption = false;
# label = "swap";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
extraArgs = [
"-L"
"nixos"
];
};
};
};
};
};
};
}
@@ -1,28 +0,0 @@
{ pkgs, vars, ... }:
{
systemd.services.docker-health-to-gotify = {
description = "Alert Gotify when Docker containers become unhealthy";
after = [ "docker.service" ];
serviceConfig = {
Type = "oneshot";
# Run as root so it can read /etc/secrets and access docker socket
# User = "root";
#EnvironmentFile = "-/etc/secrets/docker-health-alert.env";
ExecStart = "${pkgs.bash}/bin/bash /home/${vars.primaryUser}/docker/monitoring/gotify/docker-health-to-gotify.sh";
StandardOutput = "journal";
StandardError = "journal";
};
path = with pkgs; [ docker curl coreutils gnused ];
};
systemd.timers.docker-health-to-gotify = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "1min";
AccuracySec = "15s";
Persistent = true;
};
};
}
-23
View File
@@ -1,23 +0,0 @@
{ pkgs, ... }:
{
# virtualisation.docker.enable = true;
virtualisation.docker = {
enable = true;
package = pkgs.docker;
# listenOptions = [
# "unix:///var/run/docker.sock"
# "tcp://0.0.0.0:2375"
#];
# daemon.settings = {
# metrics-addr = "0.0.0.0:9323";
# experimental = true;
# };
};
environment.systemPackages = with pkgs; [
docker-compose
docker-buildx
];
}
-84
View File
@@ -1,84 +0,0 @@
{ 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 = {
${vars.nfsShares.dockerConfig.mountpoint} = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.dockerDatabases.mountpoint} = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.dockerVolumes.mountpoint} = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.nextcloudData.mountpoint} = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.raspiVolumes.mountpoint} = {
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
};
}
-22
View File
@@ -1,22 +0,0 @@
{ pkgs, vars, ... }:
{
# Create nextcloud cron scheduled task
systemd.services.nextcloud = {
description = "Nextcloud scheduled task";
script = ''${pkgs.bash}/bin/bash ~/docker/services-up.sh --profile nextcloud exec -u 33 nextcloud-webapp php ./cron.php'';
serviceConfig = {
Type = "oneshot";
User = vars.primaryUser;
};
path = with pkgs; [ docker docker-compose ];
};
systemd.timers.nextcloud = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*:0/5";
Persistent = true;
};
};
}
@@ -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;
}
@@ -1,43 +0,0 @@
# Do not modify this file! It was generated by ‘nixos-generate-config’
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[
(modulesPath + "/profiles/qemu-guest.nix")
];
boot = {
initrd.availableKernelModules = [ "virtio_pci" "virtio_scsi" "ahci" "sd_mod" ];
initrd.kernelModules = [ ];
kernelModules = [ ];
extraModulePackages = [ ];
# Enable LISH
kernelParams = [ "console=ttyS0,19200n8" ];
loader = {
grub = {
device = "/dev/sda";
extraConfig = ''
serial --speed=19200 --unit=0 --word=8 --parity=no --stop=1;
terminal_input serial;
terminal_output serial;
'';
forceInstall = true;
# device = "nodev";
};
timeout = 10;
};
};
# fileSystems."/" and swapDevices are now owned by disko
# (../disko/linode.nix, imported from ../platforms/linode.nix) — same
# /dev/sda root + /dev/sdb swap layout, declared there instead so disko's
# (idempotent, non-destructive — see that file) format/mount scripts stay
# in sync with what NixOS actually mounts.
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
-121
View File
@@ -1,121 +0,0 @@
{ pkgs, lib, vars, ... }:
{
imports = [
./host-keys.nix
];
networking.useDHCP = lib.mkDefault true;
# Recommended over the true default (bypasses ZFS's own import safeguards)
# per the option's own docs. This installer environment has no ZFS pools
# of its own to import, so this is a no-op here — just silences the
# eval-time warning, matching modules/common/configuration.nix.
boot.zfs.forceImportRoot = false;
time.timeZone = vars.timeZone;
# Without this, the installer only ever sees cache.nixos.org, which
# doesn't carry sops-install-secrets (it's built straight from the
# sops-nix flake's own Go source, not part of nixpkgs) — every install
# would otherwise compile it from scratch, which is what ran an 8GB LXC
# container's disk out of space. Push a built copy to nix-cache once
# (from a machine with real disk headroom) and every future install,
# of any type, fetches instead of rebuilding.
nix.settings = {
substituters = [
"http://nix-cache"
"https://cache.nixos.org/"
];
trusted-public-keys = [
"cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
];
};
environment = {
systemPackages = with pkgs; [
git
curl
jq
parted
e2fsprogs
btrfs-progs
util-linux
disko
];
# Auto-install script, kept as a real, version-controlled shell file at
# scripts/installer/auto-install.sh rather than an inline Nix string.
# It sources scripts/env.sh itself (for LAN_DOMAIN, same as every other
# script in this repo) rather than relying on Nix-level templating, so
# it behaves identically whether it's run straight from a git checkout
# or from here -- baking scripts/env.sh in alongside it at a matching
# 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" = {
source = ../../scripts/installer/auto-install.sh;
mode = "0755";
};
};
};
programs.git.enable = true;
# Run the installer on first login. Previously this copied an /etc file
# into the nixos user's ~/.bash_profile via an activation script that
# got dropped in a refactor (and only ever worked for that one user
# anyway) — loginShellInit is NixOS's native hook for this, applies to
# any user's login shell (root included), and needs no home-directory
# file-copying/chown.
programs.bash.loginShellInit = ''
if [ -n "$PS1" ] && [ ! -e "$HOME/.auto_install_ran" ]; then
sudo /etc/nixos-installer/installer/auto-install.sh
touch "$HOME/.auto_install_ran"
fi
'';
services.openssh.enable = true;
services.openssh.settings = {
PermitRootLogin = "yes";
PasswordAuthentication = true;
};
# nixpkgs' own installer profile (profiles/installation-device.nix, pulled
# in via installation-cd-minimal.nix) sets initialHashedPassword = "" for
# both users — its own passwordless-login convention for install media.
# That's a second, non-null password option alongside our hashedPassword
# below, which NixOS warns about as ambiguous precedence. Force it null
# rather than adopting passwordless login: this image now also boots over
# LAN PXE with PasswordAuthentication enabled, so passwordless root SSH
# would be reachable by anyone on the LAN, not just local console.
users.users.root = {
hashedPassword =
"$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
initialHashedPassword = lib.mkForce null;
};
users.users.${vars.primaryUser} = {
isNormalUser = true;
extraGroups = [
"wheel"
];
shell = pkgs.bashInteractive;
hashedPassword =
"$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
initialHashedPassword = lib.mkForce null;
openssh.authorizedKeys.keys = [
vars.adminSshKey
];
};
system.stateVersion = "26.05";
}
-37
View File
@@ -1,37 +0,0 @@
{ lib, ... }:
let
# host-keys/ is gitignored (private key material must never be committed),
# which means flakes' git-filtered source tree can never see it via a
# normal relative path — referencing it at all requires stepping outside
# pure evaluation. builtins.getEnv is neutered to "" under normal
# `nix build`/`nix eval` (no error, just empty), so this whole module is a
# silent no-op unless the operator explicitly opts in with --impure and
# the env var set — safe by default, including in CI.
#
# NIXOS_HOST_KEYS_DIR=$(pwd)/host-keys nix build .#iso --impure
#
# See docs/auto-installer.md.
hostKeysDirStr = builtins.getEnv "NIXOS_HOST_KEYS_DIR";
hasHostKeysDir = hostKeysDirStr != "" && builtins.pathExists hostKeysDirStr;
hostKeysDir = /. + hostKeysDirStr;
keyFileNames =
if hasHostKeysDir
then
lib.filter
(name: lib.hasSuffix "_ssh_host_ed25519_key" name || lib.hasSuffix "_ssh_host_ed25519_key.pub" name)
(lib.attrNames (builtins.readDir hostKeysDir))
else [ ];
in
{
environment.etc = lib.listToAttrs (map
(name: {
name = "host-keys/${name}";
value = {
source = hostKeysDir + "/${name}";
mode = "0400";
};
})
keyFileNames);
}
-8
View File
@@ -1,8 +0,0 @@
{ modulesPath, ... }:
{
imports = [
"${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix"
./common.nix
];
}
@@ -1,12 +0,0 @@
{ config, lib, ... }:
{
options.myNetworking.ipForwarding.enable = lib.mkEnableOption "Enable IPv4 and IPv6 forwarding";
config = lib.mkIf config.myNetworking.ipForwarding.enable {
boot.kernel.sysctl = {
"net.ipv4.ip_forward" = 1;
"net.ipv6.conf.all.forwarding" = 1;
};
};
}
-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";
};
};
};
}
-15
View File
@@ -1,15 +0,0 @@
{ vars, ... }:
{
nix.settings = {
substituters = [
"http://${vars.nixCacheHost}.${vars.homeDomain}"
"https://cache.nixos.org/"
];
trusted-public-keys = [
"cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
];
auto-optimise-store = true;
};
}
@@ -1,40 +0,0 @@
{ pkgs, vars, ... }:
{
# Authenticate as nixremote using the client host's own default root SSH
# identity (/root/.ssh/id_ed25519) rather than a separately-named key --
# matches vars.remoteBuilderAuthorizedKeys, which already authorizes
# each host's own default key (one entry per host, not a shared
# 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 = {
distributedBuilds = true;
buildMachines = [
{
hostName = "${vars.nixCacheHost}.${vars.homeDomain}";
sshUser = vars.remoteBuilderUser;
sshKey = "/root/.ssh/id_ed25519";
inherit (pkgs.stdenv.hostPlatform) system;
maxJobs = 4;
speedFactor = 2;
supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ];
}
];
settings = {
builders-use-substitutes = true;
max-jobs = "auto";
};
};
}
-61
View File
@@ -1,61 +0,0 @@
{ config, pkgs, vars, ... }:
{
# nix-serve's signing key has to be the *same* key on every host that
# ever plays the nix-cache role -- modules/nix-cache/client.nix hardcodes
# every client's trust in one specific public key ("cache.local-1:..."),
# so a freshly self-generated key here wouldn't be trusted by anyone.
# Managed via sops-nix like every other secret in this repo instead of
# the old manual `nix-store --generate-binary-cache-key` step -- see
# "Binary cache signing key" in docs/nix-cache.md for how to add/rotate
# the value in secrets/nix-cache.yaml.
sops.secrets."cache-priv-key".sopsFile = ../../secrets/nix-cache.yaml;
services = {
nix-serve = {
enable = true;
secretKeyFile = config.sops.secrets."cache-priv-key".path;
};
nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."${vars.nixCacheHost}.${vars.homeDomain}" = {
locations."/" = {
proxyPass = "http://${config.services.nix-serve.bindAddress}:${toString config.services.nix-serve.port}";
};
};
};
openssh.enable = true;
};
networking.firewall.allowedTCPPorts = [ vars.ports.nixCacheHttp ];
users.groups.${vars.remoteBuilderUser} = { };
users.users.${vars.remoteBuilderUser} = {
isSystemUser = true;
group = vars.remoteBuilderUser;
createHome = true;
home = "/var/lib/nixremote";
shell = pkgs.bashInteractive;
# Client public keys allowed to use this host as a remote builder —
# single source of truth is vars.remoteBuilderAuthorizedKeys (safe to
# commit public keys only).
openssh.authorizedKeys.keys = vars.remoteBuilderAuthorizedKeys;
};
nix.settings = {
trusted-users = [ "root" vars.remoteBuilderUser ];
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = true;
builders-use-substitutes = true;
};
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than ${vars.nixCacheGcMaxAge}";
};
}
-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;
}
-14
View File
@@ -1,14 +0,0 @@
{ ... }:
{
imports = [
../hardware-configuration/vm/linode.nix
../disko/linode.nix
];
networking = {
usePredictableInterfaceNames = false;
useDHCP = false;
interfaces.eth0.useDHCP = true;
};
}
-251
View File
@@ -1,251 +0,0 @@
{ config, lib, modulesPath, flakeTarget, ... }:
let
# Bakes this exact flake target's pre-generated SSH host key straight
# into /etc/ssh/ -- mirrors modules/installer/host-keys.nix's
# builtins.getEnv pattern (impure and empty under normal `nix
# build`/`nix eval`, so this is a no-op unless explicitly opted into
# with NIXOS_HOST_KEYS_DIR=... --impure), but places the key directly
# rather than staging it under /etc/host-keys/ for a later manual copy
# -- this is the whole system for a `lxc-*` host, built straight to a
# pct-restorable tarball with no install step, so there's no later copy
# step to stage for.
#
# Without this, config.system.build.tarball's built-in system just
# generates a fresh host key at first boot like any other host would --
# but sops-nix derives its decryption key from *this* file, and
# .sops.yaml only trusts whatever key scripts/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
# /sbin/init directly, no bootloader/initrd involved — and Proxmox has its
# own container hostname/network provisioning outside Nix. nixpkgs' own
# virtualisation/proxmox-lxc.nix module already handles all of this
# correctly (boot.isContainer, loader.initScript, systemd-networkd) and,
# critically, provides config.system.build.tarball — a directly
# `pct restore`-able container image, no nixos-install/bind-mount needed
# (nixos-install refuses to touch the filesystem it's currently running
# on, which is exactly what bind-mounting / onto /mnt for an installer
# LXC container does).
imports = [
(modulesPath + "/virtualisation/proxmox-lxc.nix")
];
proxmoxLXC = {
# host.nix declares each host's real hostname (networking.hostName);
# keep that instead of letting Proxmox's ambient container config win.
manageHostName = true;
# Unprivileged by default -- matches how these containers are actually
# created (scripts/proxmox/create-proxmox-resource.sh reads this value
# 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 = {
grub.enable = false;
systemd-boot.enable = false;
};
# NetworkManager depends on a running udevd to enumerate/classify devices,
# which boot.isContainer disables (see nixpkgs' container-config.nix) —
# that's what broke DHCP-hostname registration in Pi-hole. The imported
# proxmox-lxc.nix module already switches networking to systemd-networkd
# for the same reason; it just doesn't disable NetworkManager itself,
# which modules/common/configuration.nix enables for every host.
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: activation scripts that bracket the etc step, with explicit deps
# to enforce the correct ordering. Without deps the topological sort places
# preserveSshHostKey AFTER etc (confirmed live on a deployed lxc-tor-relay:
# position 7 vs etc's position 5) -- the key is already gone by the time it
# tries to save it. The etc/setupSecrets entries ADD to existing deps
# (types.listOf concatenates across module definitions).
system.activationScripts = {
# Saves the live key to /run before etc can delete it.
preserveSshHostKey = ''
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
fi
'';
# Reinstalls the key after etc runs if it was removed as "obsolete".
# The resulting file is not registered in environment.etc for either
# generation, so subsequent rebuilds leave it alone permanently.
restoreSshHostKey = {
deps = [ "etc" ];
text = ''
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
fi
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
'';
};
# Force etc to wait until the key is saved, and sops to wait until the
# key is restored. Without these the topological sort breaks the chain.
etc = { deps = [ "preserveSshHostKey" ]; };
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
};
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
# 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. In this config sops-nix
# does NOT generate its own boot-time service (confirmed live: no
# sops-nix.service in systemctl list-unit-files on a deployed
# lxc-tor-relay container); /run/secrets is a tmpfs cleared on every
# reboot, so secrets must be reinstalled on each non-first boot by
# nixos-lxc-sops-reinstall (below).
#
# A systemd service, not boot.postBootCommands: tried that first (it's
# a 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
'';
};
# Reinstalls sops secrets on every non-first boot. /run/secrets is a
# tmpfs that is cleared on each reboot; without this service, secrets
# are permanently absent after the first boot and every service that
# reads from /run/secrets fails on start.
#
# wantedBy/before network.target: switch-to-configuration test requires
# D-Bus to restart systemd targets after running activation scripts. D-Bus
# is available once basic.target completes (the default After=basic.target
# that DefaultDependencies would otherwise add). Placing the service before
# network.target ensures secrets are ready before any network-dependent
# service (including beszel-agent and nix-serve) starts, while running late
# enough that D-Bus is already up.
#
# ConditionPathExists=... skips this service on the genuine first boot
# (the marker doesn't exist yet); nixos-lxc-first-boot-activate handles
# that case. On every subsequent boot the condition passes and secrets
# are reinstalled before user services start.
#
# SuccessExitStatus=11: switch-to-configuration exits 11 when it cannot
# acquire the activation lock (another switch is already in progress).
# During a nixos-rebuild switch the activation already installs secrets, so
# treating the lock-held case as success is correct.
systemd.services.nixos-lxc-sops-reinstall = {
description = "Reinstall sops secrets on each non-first boot (LXC, /run is tmpfs)";
wantedBy = [ "network.target" ];
before = [ "network.target" ];
unitConfig.ConditionPathExists = "/var/lib/nixos-lxc-first-boot-activated";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
SuccessExitStatus = "11";
};
script = ''
/run/current-system/bin/switch-to-configuration test
'';
};
}
-81
View File
@@ -1,81 +0,0 @@
{ lib, flakeTarget, ... }:
let
# Bakes this exact flake target's pre-generated SSH host key straight
# into /etc/ssh/ -- mirrors lxc.nix's builtins.getEnv pattern (impure
# and empty under normal `nix build`/`nix eval`, so this is a no-op
# unless explicitly opted into with NIXOS_HOST_KEYS_DIR=... --impure).
#
# Unlike --pre-format-files (which places files on the QEMU builder VM's
# rootfs, not the target disk), embedding via environment.etc here means
# nixos-install's own activation step installs the key onto the target
# disk. sshd-keygen then finds it already present and skips generation,
# so the disk image boots with the clan-registered key and sops can
# decrypt on first boot.
#
# Without this, nixos-install's sshd-keygen activation generates a fresh
# key (unregistered in .sops.yaml), sops decryption fails permanently,
# and password hashes are never applied -- confirmed live: passwords
# stayed '!' even with mutableUsers = false because hashedPasswordFile
# pointed to a path that sops never wrote.
hostKeysDirStr = builtins.getEnv "NIXOS_HOST_KEYS_DIR";
hasHostKeysDir = hostKeysDirStr != "" && builtins.pathExists hostKeysDirStr;
hostKeysDir = /. + hostKeysDirStr;
privKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key";
pubKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key.pub";
hasKeyForThisTarget =
hasHostKeysDir
&& builtins.pathExists privKeyFile
&& builtins.pathExists pubKeyFile;
in
{
imports = [
../hardware-configuration/vm/proxmox.nix
../boot/efi.nix
../disko/proxmox.nix
];
environment.etc = lib.mkIf hasKeyForThisTarget {
"ssh/ssh_host_ed25519_key" = {
source = privKeyFile;
mode = "0600";
};
"ssh/ssh_host_ed25519_key.pub" = {
source = pubKeyFile;
mode = "0644";
};
};
# NixOS's etc activation removes any /etc file that was in the previous
# generation's environment.etc but is absent from the current one. Since
# the SSH key is only in environment.etc during the --impure build (when
# NIXOS_HOST_KEYS_DIR is set), normal rebuilds would remove it as
# "obsolete". These scripts mirror lxc.nix's approach: save the live key
# before etc runs, restore it after. Without the explicit deps, the
# topological sort places preserveSshHostKey after etc (confirmed live on
# lxc-tor-relay: position 7 vs etc's position 5), so the key is gone
# before it can be saved.
system.activationScripts = {
preserveSshHostKey = ''
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
fi
'';
restoreSshHostKey = {
deps = [ "etc" ];
text = ''
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
fi
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
'';
};
etc = { deps = [ "preserveSshHostKey" ]; };
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
};
}
@@ -1,32 +0,0 @@
{ netbootSystem, netbootMinimalSystem, ... }:
let
# config.system.build.kernel and .netbootRamdisk are directories, not the
# files themselves — nixpkgs' own system.build.kexecTree does the same
# ${...}/<file> dereference for the same reason.
mkStageRules = { dirName, system }:
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
{
# 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-minimal`), and stages both where menu.ipxe's
# :auto-installer / :nixos-minimal entries expect them, so the pxe-boot
# host is self-contained — no manual operator step to populate
# /srv/pxe/http after deploy.
systemd.tmpfiles.rules =
mkStageRules { dirName = "auto-installer"; system = netbootSystem; }
++ mkStageRules { dirName = "nixos-minimal"; system = netbootMinimalSystem; };
}
-32
View File
@@ -1,32 +0,0 @@
{ config, lib, vars, ... }:
{
fileSystems.${vars.raspiMountpoint} = {
device = "${vars.raspberryPiHost}.${vars.tailnetDomain}:${vars.raspiNfsPath}";
fsType = "nfs4";
options = [
"nofail"
"_netdev"
"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
"x-systemd.automount"
# Unmount after 10 min idle
"x-systemd.idle-timeout=600"
# Give the Pi/Tailscale a little time to appear
"x-systemd.device-timeout=10s"
];
};
}
-5
View File
@@ -1,5 +0,0 @@
_:
{
services.rpcbind.enable = true;
}
@@ -1,12 +0,0 @@
{ pkgs, boot, ... }:
{
boot.postBootCommands = ''
echo "=== STARTING ZPOOL IMPORT ==="
${pkgs.zfs_unstable}/bin/zpool import -a -N -d /dev/disk/by-path
${pkgs.zfs_unstable}/bin/zpool status
${pkgs.zfs_unstable}/bin/zfs mount -a
${pkgs.zfs_unstable}/bin/zfs list
echo "=== ZPOOL IMPORT COMPLETE ==="
'';
}
-21
View File
@@ -1,21 +0,0 @@
{ pkgs, ... }:
{
boot = {
supportedFilesystems = [ "zfs" ];
zfs = {
forceImportRoot = false;
package = pkgs.zfs_unstable;
devNodes = "/dev/disk/by-id";
};
};
services.zfs = {
autoScrub.enable = true;
autoSnapshot.enable = true;
trim.enable = true;
};
#systemd.services.zfs-import-cache.enable = true;
systemd.services.zfs-mount.enable = true;
}
-5
View File
@@ -1,5 +0,0 @@
_:
{
services.tailscale.enable = true;
}
-35
View File
@@ -1,35 +0,0 @@
{ pkgs, ... }:
{
imports = [ ./enable-service.nix ];
services.tailscale = {
# Enables the sysctl forwarding settings subnet routers need;
# without this, --advertise-routes has no effect.
useRoutingFeatures = "server";
# Lets peers reach this node directly over the tailscale UDP port
# instead of relaying through DERP.
openFirewall = true;
};
# Tailscale recommends these ethtool flags on the uplink interface to get
# full UDP GRO throughput on subnet routers (https://tailscale.com/s/ethtool-config-udp-gro).
# The interface is derived from the default route so it works regardless of
# what the NIC is named on a given host.
systemd.services.tailscale-udp-gro = {
description = "Enable UDP GRO forwarding on uplink for Tailscale subnet router";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.ethtool pkgs.iproute2 ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "tailscale-udp-gro" ''
NETDEV=$(ip -o route get 8.8.8.8 | cut -f 5 -d " ")
ethtool -K "$NETDEV" rx-udp-gro-forwarding on rx-gro-list off
'';
};
};
}
-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 ];
}
-20
View File
@@ -1,20 +0,0 @@
{ vars, ... }:
{
services.logrotate = {
enable = true;
settings = {
"${vars.nfsShares.dockerVolumes.mountpoint}/traefik-data/logs/*.log" = {
daily = true;
size = vars.traefikLogRotate.maxSize;
rotate = vars.traefikLogRotate.keep;
compress = true;
missingok = true;
notifempty = true;
copytruncate = 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
@@ -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."
-321
View File
@@ -1,321 +0,0 @@
#!/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
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/nix-bootstrap.sh
source "${script_dir}/lib/nix-bootstrap.sh"
# shellcheck source=lib/nix-eval.sh
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)"
cd "$repo_root"
full_check=false
dry_run=false
usage() {
cat <<'EOF'
Usage: scripts/codex-maintenance.sh [--full-check] [--dry-run]
--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
if ! command -v nix >/dev/null 2>&1; then
echo "ERROR: nix is not available in PATH. Run bash scripts/codex-setup.sh first." >&2
exit 127
fi
echo "Checking for obvious committed secrets..."
if grep -RInE 'github_pat_|ghp_|access-tokens|hashedPassword[[:space:]]*=' \
--exclude-dir=.git \
--exclude=flake.lock \
.; then
echo
echo "WARNING: Potential secrets or password hashes found. Review before committing."
else
echo "No obvious token patterns found."
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 "Checking Nix formatting with nixpkgs-fmt..."
if $full_check; then
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 "Running statix lint..."
if $full_check; then
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 "Checking nix-cache host key for drift..."
if bash "${script_dir}/secrets/sync-nix-cache-host-key.sh" --check; then
:
else
drift_status=$?
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 [[ ${#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 "Running dry-run builds for the active scope (up to ${NIX_PARALLEL_JOBS} at a time). This will not create result symlinks."
declare -a host_build_jobs=()
for host in "${hosts[@]:-}"; do
host_build_jobs+=("Dry-run build: ${host}${NIX_PARALLEL_SEP}.#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
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
echo
echo "Maintenance checks complete."
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/nix-bootstrap.sh
source "${script_dir}/lib/nix-bootstrap.sh"
# shellcheck source=lib/nix-eval.sh
source "${script_dir}/lib/nix-eval.sh"
install_nix_if_missing() {
if command -v nix >/dev/null 2>&1; then
return
fi
echo "Nix not found. Installing Nix..."
if [ "$(id -u)" -eq 0 ]; then
echo "Running as root; preparing nixbld users for container/Codex environment..."
if ! getent group nixbld >/dev/null; then
groupadd -r nixbld
fi
for i in $(seq 1 10); do
if ! id "nixbld$i" >/dev/null 2>&1; then
useradd \
-r \
-g nixbld \
-G nixbld \
-d /var/empty \
-s /usr/sbin/nologin \
"nixbld$i" || true
fi
done
mkdir -p /etc/nix
cat > /etc/nix/nix.conf <<'EOF'
experimental-features = nix-command flakes
accept-flake-config = false
warn-dirty = false
build-users-group = nixbld
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
else
sh <(curl -L https://nixos.org/nix/install) --no-daemon
fi
ensure_nix_profile
}
install_nix_if_missing
ensure_nix_profile
mkdir -p "$HOME/.config/nix"
cat > "$HOME/.config/nix/nix.conf" <<'EOF'
experimental-features = nix-command flakes
accept-flake-config = false
warn-dirty = false
build-users-group =
EOF
echo "Nix version:"
nix --version
echo "Enabling tracked git hooks (pre-commit secret scan)..."
git config core.hooksPath .githooks
echo "Installing jq if unavailable..."
if ! command -v jq >/dev/null 2>&1; then
nix profile install nixpkgs#jq
fi
echo "Available NixOS hosts:"
list_flake_targets .
echo "Codex setup complete. Run bash scripts/codex-maintenance.sh to validate changes."
-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
-106
View File
@@ -1,106 +0,0 @@
#!/usr/bin/env bash
# Clan vars helpers: manage SSH host keys stored as clan vars (sops-encrypted
# binary files under vars/per-machine/<target>/openssh/) instead of the
# gitignored host-keys/ directory.
#
# Layout (per clan's convention):
# vars/per-machine/<target>/openssh/ssh_host_ed25519_key/secret -- sops binary (admin-encrypted)
# vars/per-machine/<target>/openssh/ssh_host_ed25519_key.pub/value -- plaintext SSH pubkey
#
# Sourced by create-proxmox-resource.sh and sync-host-keys.sh.
# Depends on sops-age.sh and ssh-host-keys.sh being sourced first (for
# sops_yaml_admin_pubkey, ssh_pubkey_to_age, and NIX_OPTS).
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
declare -a NIX_OPTS=()
fi
# clan_ssh_key_exists <target> <repo_root>
# Returns 0 if clan vars hold a SSH host key for <target>, 1 otherwise.
clan_ssh_key_exists() {
local target="$1" repo_root="$2"
[[ -f "${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key/secret" ]]
}
# clan_ssh_pubkey_path <target> <repo_root>
# Prints the path to the plaintext SSH public key value file.
clan_ssh_pubkey_path() {
local target="$1" repo_root="$2"
echo "${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key.pub/value"
}
# clan_decrypt_ssh_key <target> <repo_root> <dest_dir>
# Decrypts the sops-encrypted SSH host private key for <target> into <dest_dir>,
# naming it <target>_ssh_host_ed25519_key (to match NIXOS_HOST_KEYS_DIR
# conventions that lxc.nix and the disko build already expect). Also copies
# the plaintext public key. The caller is responsible for protecting and
# cleaning up <dest_dir>.
clan_decrypt_ssh_key() {
local target="$1" repo_root="$2" dest_dir="$3"
local secret="${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key/secret"
local pubval="${repo_root}/vars/per-machine/${target}/openssh/ssh_host_ed25519_key.pub/value"
local dest_priv="${dest_dir}/${target}_ssh_host_ed25519_key"
local dest_pub="${dest_dir}/${target}_ssh_host_ed25519_key.pub"
nix-shell "${NIX_OPTS[@]}" -p sops --run \
"sops -d --output-type binary '${secret}'" > "$dest_priv"
chmod 0600 "$dest_priv"
cp "$pubval" "$dest_pub"
}
# clan_generate_ssh_key <target> <repo_root>
# Generates a new SSH host key pair and stores it in clan vars format:
# - private key: sops binary-encrypted for the admin age key
# - public key: plaintext value file
# Idempotent: if the secret already exists, prints a note and returns 0.
# Requires sops_yaml_admin_pubkey (from sops-age.sh) to be available.
clan_generate_ssh_key() {
local target="$1" repo_root="$2"
local var_base="${repo_root}/vars/per-machine/${target}/openssh"
local secret_dir="${var_base}/ssh_host_ed25519_key"
local pubval_dir="${var_base}/ssh_host_ed25519_key.pub"
if [[ -f "${secret_dir}/secret" ]]; then
echo "Clan SSH host key for ${target} already exists -- skipping generation."
return 0
fi
# Resolve admin age public key from .sops.yaml
local admin_pubkey
admin_pubkey="$(sops_yaml_admin_pubkey "${repo_root}/.sops.yaml")"
if [[ -z "$admin_pubkey" ]]; then
echo "ERROR: Could not find &admin age key in ${repo_root}/.sops.yaml" >&2
return 1
fi
# Generate the SSH key pair in a secure temp directory
local tmpdir
tmpdir="$(mktemp -d)"
local priv_tmp="${tmpdir}/ssh_host_ed25519_key"
# shellcheck disable=SC2064
trap "rm -rf '${tmpdir}'" RETURN
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
"ssh-keygen -t ed25519 -N '' -C '${target}' -f '${priv_tmp}'" >/dev/null
# Create a minimal sops config that uses only the admin age key -- this
# prevents sops from merging in ALL recipients from .sops.yaml (which
# would unnecessarily encrypt for every host's key, not just admin).
local sops_cfg="${tmpdir}/sops-config.json"
printf '{"creation_rules":[{"key_groups":[{"age":["%s"]}]}]}\n' \
"$admin_pubkey" > "$sops_cfg"
# Encrypt the private key in sops binary format (admin-only recipient)
mkdir -p "$secret_dir" "$pubval_dir"
nix-shell "${NIX_OPTS[@]}" -p sops --run \
"sops -e --config '${sops_cfg}' --input-type binary '${priv_tmp}'" \
> "${secret_dir}/secret"
# Store the public key as a plaintext value file
cp "${priv_tmp}.pub" "${pubval_dir}/value"
echo "Generated and stored clan SSH host key for ${target}."
echo " Private key: ${secret_dir}/secret (sops binary, admin-key encrypted)"
echo " Public key: ${pubval_dir}/value"
}
-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()
-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}."
Loaded 100 of 159 files, more files were not shown because too many files have changed in this diff. Show more