Compare commits

..
147 Commits
Author SHA1 Message Date
beatzaplenty 80bffb58c9 Update hosts/docker/configuration.nix 2026-04-27 12:09:31 +00:00
beatzaplenty f8acce2614 Update hosts/docker/configuration.nix 2026-04-27 12:05:04 +00:00
beatzaplenty daadd97f84 Update hosts/docker/configuration.nix 2026-04-27 12:00:36 +00:00
beatzaplenty a3be101ce3 Update hosts/docker/configuration.nix 2026-04-27 11:54:57 +00:00
beatzaplenty 4bf32f23ab Update hosts/docker/configuration.nix 2026-04-14 23:29:01 +00:00
beatzaplenty f14aa9755b Update hosts/docker/configuration.nix 2026-04-14 23:15:55 +00:00
beatzaplenty 8f5f975d83 Update hosts/docker/configuration.nix 2026-04-14 23:05:01 +00:00
beatzaplenty 7f7b03384f Update hosts/docker/configuration.nix 2026-04-13 21:58:10 +00:00
beatzaplenty 6e138c6f7f Update hosts/docker/configuration.nix 2026-04-02 00:07:32 +00:00
beatzaplenty e6a72813e4 Update hosts/docker/configuration.nix 2026-04-01 23:38:07 +00:00
beatzaplenty 350a2d2bfb Update common/configuration.nix 2026-03-31 21:16:59 +00:00
beatzaplenty cb1ddc2109 Update common/configuration.nix 2026-03-31 21:11:00 +00:00
beatzaplenty 376d53de40 Update hosts/server/configuration.nix 2026-03-27 19:52:58 +00:00
beatzaplenty fd592ba5de Update hosts/server/configuration.nix 2026-03-27 19:48:50 +00:00
beatzaplenty 4b37d36212 Update hosts/docker/configuration.nix 2026-02-25 00:20:50 +00:00
beatzaplenty 0a10efefa6 Update hosts/docker/configuration.nix 2026-02-25 00:17:39 +00:00
beatzaplenty 5e559efc6f Update hosts/docker/configuration.nix 2026-02-25 00:16:37 +00:00
beatzaplenty 149f56ce10 Update common/home.nix 2026-02-17 16:00:33 +00:00
beatzaplenty c6f6441907 Update flake.nix 2026-02-17 15:59:35 +00:00
beatzaplenty cc956d3038 Update hosts/docker/configuration.nix 2026-02-17 15:57:26 +00:00
beatzaplenty d409b5b718 Update hosts/docker/configuration.nix 2026-02-17 15:56:47 +00:00
beatzaplenty 6ef87a3226 Update hosts/docker/configuration.nix 2026-02-17 15:53:06 +00:00
beatzaplenty 9a190a28d6 Update hosts/docker/configuration.nix 2026-02-17 15:52:05 +00:00
beatzaplenty e7a215cd15 Update hosts/docker/configuration.nix 2026-02-17 15:50:06 +00:00
beatzaplenty 651f4e61c5 Update hosts/docker/configuration.nix 2026-02-17 15:40:04 +00:00
beatzaplenty ffad8bd6b8 Update hosts/docker/configuration.nix 2026-02-17 11:44:58 +00:00
beatzaplenty 01aaf10e2d Update hosts/docker/configuration.nix 2026-02-16 09:22:16 +00:00
beatzaplenty 8db2a2db86 Update hosts/docker/configuration.nix 2026-02-16 09:14:46 +00:00
beatzaplenty 3389c9549a Update flake.nix 2026-02-09 10:12:06 +00:00
beatzaplenty 7ab8cf15ca Update hosts/nix-cache/configuration.nix 2026-02-09 10:05:23 +00:00
beatzaplenty 56a25ab5d7 Update hosts/server/configuration.nix 2026-02-09 10:00:53 +00:00
beatzaplenty 76649ad698 modified: hosts/docker/configuration.nix 2025-12-20 13:06:57 +10:00
beatzaplenty 487d8bc474 modified: hosts/server/configuration.nix 2025-12-18 12:24:04 +10:00
beatzaplenty 529535cffd modified: hosts/server/configuration.nix 2025-12-18 12:22:59 +10:00
beatzaplenty aff4cf1c16 modified: hosts/server/configuration.nix 2025-12-18 12:16:06 +10:00
beatzaplenty 3db1205a8d modified: hosts/server/configuration.nix 2025-12-18 12:06:39 +10:00
beatzaplenty bc5be541c3 modified: hosts/server/configuration.nix 2025-12-18 10:51:11 +10:00
beatzaplenty 38c55c3de2 modified: hosts/kuma/configuration.nix 2025-12-17 14:57:34 +10:00
beatzaplenty 25b8ebcad7 modified: flake.nix
new file:   hosts/kuma/configuration.nix
2025-12-17 14:34:23 +10:00
beatzaplenty d08951d46a modified: flake.nix 2025-12-17 14:31:09 +10:00
beatzaplenty 9eba317de0 modified: flake.nix 2025-12-17 14:30:15 +10:00
beatzaplenty a9bb49a7e7 Update hosts/docker/configuration.nix 2025-12-16 12:01:55 +00:00
beatzaplenty 46c71eb619 Update hosts/docker/configuration.nix 2025-12-16 11:56:53 +00:00
beatzaplenty 8918af8d6e Update hosts/docker/configuration.nix 2025-12-16 11:53:51 +00:00
beatzaplenty 48447f7a47 Update hosts/docker/configuration.nix 2025-12-16 11:52:41 +00:00
beatzaplenty 943c5324ca Update flake.nix 2025-12-16 11:42:35 +00:00
beatzaplenty a8f417b4f9 Update flake.nix 2025-12-16 11:41:38 +00:00
beatzaplenty ff75079327 Update flake.lock 2025-12-16 11:34:53 +00:00
beatzaplenty 6f79daefc4 Update flake.nix 2025-12-16 10:56:47 +00:00
beatzaplenty 66b6bc0c2a Add flake.lock 2025-12-16 10:51:45 +00:00
beatzaplenty b952075a1e Update flake.nix 2025-12-16 10:25:19 +00:00
beatzaplenty 49c72848e6 modified: hosts/docker/configuration.nix 2025-07-23 18:23:25 +10:00
beatzaplenty c19919a868 modified: hosts/docker/configuration.nix 2025-07-23 18:09:12 +10:00
beatzaplenty 4c4ed6416d modified: hosts/nixos/home.nix 2025-07-22 17:21:30 +10:00
beatzaplenty a457d04a0a modified: hosts/nixos/home.nix 2025-07-22 17:19:13 +10:00
beatzaplenty 019ab8c2cc modified: hosts/nixos/home.nix 2025-07-22 17:16:42 +10:00
beatzaplenty cb0efa12ce modified: hosts/nixos/home.nix 2025-07-22 17:15:33 +10:00
beatzaplenty bb454d5a97 modified: hosts/nixos/home.nix 2025-07-22 17:13:40 +10:00
beatzaplenty af4021b29f modified: hosts/nixos/home.nix 2025-07-22 17:11:46 +10:00
beatzaplenty a7a6951115 modified: hosts/nixos/home.nix 2025-07-22 17:07:16 +10:00
beatzaplenty 00bab4b69a modified: hosts/nixos/home.nix 2025-07-22 16:55:51 +10:00
beatzaplenty 6c304304f0 modified: hosts/nixos/home.nix 2025-07-22 16:47:21 +10:00
beatzaplenty 60942af93f modified: hosts/nixos/home.nix 2025-07-22 16:44:34 +10:00
beatzaplenty f4dd65c66e modified: hosts/nixos/home.nix 2025-07-22 16:37:38 +10:00
beatzaplenty 3fbc290fd5 modified: hosts/nixos/home.nix 2025-07-22 16:29:29 +10:00
beatzaplenty 1b6779f89c modified: hosts/nixos/home.nix 2025-07-22 16:22:14 +10:00
beatzaplenty dc933fc69b modified: hosts/nixos/home.nix 2025-07-22 16:12:08 +10:00
beatzaplenty 4d322a94ac modified: hosts/nixos/home.nix 2025-07-22 16:10:58 +10:00
beatzaplenty 17ecbc617d modified: hosts/nixos/home.nix 2025-07-22 16:09:06 +10:00
beatzaplenty c892c70860 modified: hosts/nixos/home.nix 2025-07-22 16:04:27 +10:00
beatzaplenty 376f0b98dd modified: hosts/nixos/home.nix 2025-07-22 15:57:39 +10:00
beatzaplenty 728129a4b2 modified: hosts/nixos/home.nix 2025-07-22 15:55:05 +10:00
beatzaplenty fe8649b09a modified: hosts/nixos/home.nix 2025-07-22 15:50:48 +10:00
beatzaplenty 64183b0ab9 modified: hosts/nixos/home.nix 2025-07-22 15:33:23 +10:00
beatzaplenty 8c7a7815a7 modified: hosts/nixos/home.nix 2025-07-22 15:18:27 +10:00
beatzaplenty 0dc6c5099f modified: flake.nix
modified:   hosts/nixos/configuration.nix
2025-07-22 14:29:45 +10:00
beatzaplenty 236385e480 modified: hosts/nixos/configuration.nix 2025-07-22 14:22:49 +10:00
beatzaplenty ddf0133da5 modified: hosts/nixos/home.nix 2025-07-22 14:21:34 +10:00
beatzaplenty f4a849bbac modified: flake.nix
modified:   hosts/nixos/home.nix
2025-07-22 14:15:15 +10:00
beatzaplenty 0c2344098b modified: flake.nix 2025-07-22 14:10:44 +10:00
beatzaplenty ee44a2dafd modified: flake.nix 2025-07-22 14:09:08 +10:00
beatzaplenty ff014f6658 modified: flake.nix 2025-07-22 14:05:43 +10:00
beatzaplenty c8dc59750b modified: hosts/nixos/home.nix 2025-07-22 14:02:14 +10:00
beatzaplenty 35f2c457dd modified: flake.nix
modified:   hosts/nixos/home.nix
2025-07-22 13:59:42 +10:00
beatzaplenty 5a60d574d8 modified: flake.nix 2025-07-22 13:47:56 +10:00
beatzaplenty 08f6dd7347 modified: flake.nix 2025-07-22 13:37:24 +10:00
beatzaplenty 0228bdf429 modified: flake.nix 2025-07-22 13:33:45 +10:00
beatzaplenty 1d99b54374 modified: flake.nix 2025-07-22 13:31:05 +10:00
beatzaplenty dd32d23249 modified: common/home.nix
modified:   hosts/nix-minimal/configuration.nix
2025-07-21 16:16:47 +10:00
beatzaplenty 47927a91f1 deleted: auto-installer/flake.nix
deleted:    auto-installer/installer.nix
	modified:   hosts/nix-minimal/configuration.nix
	modified:   prepare.sh
2025-07-21 13:39:03 +10:00
beatzaplenty 954d89f101 Added minimal build for iso testing and building
modified:   flake.nix
	new file:   hosts/nix-minimal/configuration.nix
2025-07-21 12:13:07 +10:00
beatzaplenty 5bb1b0c6a1 modified: auto-installer/installer.nix 2025-07-21 12:08:08 +10:00
beatzaplenty 706e74c46d modified: auto-installer/installer.nix 2025-07-21 10:33:21 +10:00
beatzaplenty 094b6c34f0 modified: auto-installer/installer.nix 2025-07-21 10:18:29 +10:00
beatzaplenty aeb2cde784 modified: auto-installer/installer.nix 2025-07-21 10:11:02 +10:00
beatzaplenty f1520fbd75 modified: auto-installer/installer.nix 2025-07-21 09:58:34 +10:00
beatzaplenty 0321f764aa modified: auto-installer/installer.nix 2025-07-21 09:54:23 +10:00
beatzaplenty e0a1d1b5dc modified: auto-installer/installer.nix 2025-07-21 09:36:26 +10:00
beatzaplenty cfea52cf7d modified: auto-installer/installer.nix 2025-07-21 09:27:04 +10:00
beatzaplenty 94736a53ef modified: auto-installer/installer.nix 2025-07-21 09:19:07 +10:00
beatzaplenty 1960c4cd8c modified: auto-installer/installer.nix 2025-07-21 08:51:03 +10:00
beatzaplenty b66f5dd800 modified: auto-installer/installer.nix 2025-07-21 08:38:46 +10:00
beatzaplenty 3d2938d896 modified: auto-installer/installer.nix 2025-07-21 02:11:34 +10:00
beatzaplenty 903ac787b1 Merge branch 'main' of https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos 2025-07-21 01:59:28 +10:00
beatzaplenty 34cf3a330a modified: auto-installer/installer.nix 2025-07-21 01:58:06 +10:00
beatzaplenty c8c5d21e5a Merge branch 'main' of https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos 2025-07-21 01:48:57 +10:00
beatzaplenty ec82189d47 modified: .gitignore 2025-07-21 01:47:56 +10:00
beatzaplenty ebf8173845 modified: auto-installer/installer.nix 2025-07-21 01:47:12 +10:00
beatzaplenty 360d0d4f46 modified: auto-installer/installer.nix 2025-07-21 01:32:06 +10:00
beatzaplenty fb73fe70cd modified: .gitignore 2025-07-21 00:53:34 +10:00
beatzaplenty a0699349c7 modified: auto-installer/installer.nix 2025-07-21 00:52:51 +10:00
beatzaplenty 2e6f19e597 modified: auto-installer/installer.nix 2025-07-21 00:27:38 +10:00
beatzaplenty d5f6a154ca modified: auto-installer/installer.nix 2025-07-21 00:26:47 +10:00
beatzaplenty afda3b0a5d modified: auto-installer/installer.nix 2025-07-21 00:25:39 +10:00
beatzaplenty 4c4b3d47f3 modified: auto-installer/installer.nix 2025-07-21 00:24:06 +10:00
beatzaplenty 7413340c75 modified: auto-installer/installer.nix 2025-07-21 00:21:09 +10:00
beatzaplenty b0dd745a03 modified: .gitignore 2025-07-21 00:14:59 +10:00
beatzaplenty be3012a65a modified: .gitignore 2025-07-21 00:13:55 +10:00
beatzaplenty 55dd99e155 new file: auto-installer/flake.nix
new file:   auto-installer/installer.nix
2025-07-21 00:10:48 +10:00
beatzaplenty 845ab268ce modified: hosts/nixos/configuration.nix 2025-07-20 22:54:08 +10:00
beatzaplenty 1df53af9bf modified: hosts/nixos/configuration.nix 2025-07-20 22:53:23 +10:00
beatzaplenty 427155bcb0 modified: flake.nix 2025-07-20 22:52:44 +10:00
beatzaplenty e8ee5981ac modified: flake.nix 2025-07-20 22:45:37 +10:00
beatzaplenty 6d701af3c7 modified: flake.nix 2025-07-20 22:44:31 +10:00
beatzaplenty d6f9fe97f5 added winapps flake to nixos pc
modified:   flake.nix
2025-07-20 22:40:14 +10:00
beatzaplenty d450750818 modified: hosts/nixos/configuration.nix 2025-07-20 22:27:41 +10:00
beatzaplenty 1c5dac74ca modified: hosts/nixos/configuration.nix 2025-07-20 22:25:46 +10:00
beatzaplenty d4f5e0b22d added software
modified:   hosts/nixos/configuration.nix
2025-07-20 22:24:44 +10:00
beatzaplenty b0964fcb80 modified: hosts/nixos/configuration.nix 2025-07-20 22:11:59 +10:00
beatzaplenty a285756040 modified: hosts/nixos/configuration.nix 2025-07-20 22:07:25 +10:00
beatzaplenty 20d38c571c modified: hosts/nixos/configuration.nix 2025-07-20 21:59:56 +10:00
beatzaplenty e22dcd6d02 modified: hosts/nixos/configuration.nix 2025-07-20 21:55:22 +10:00
beatzaplenty a2bc222fc8 modified: hosts/nixos/configuration.nix 2025-07-20 21:50:57 +10:00
beatzaplenty 198ba7a198 modified: hosts/nixos/configuration.nix 2025-07-20 21:50:10 +10:00
beatzaplenty cc6704830e modified: hosts/nixos/configuration.nix 2025-07-20 21:48:47 +10:00
beatzaplenty 072f1690e6 enabled keyring
modified:   hosts/nixos/configuration.nix
2025-07-20 21:47:47 +10:00
beatzaplenty 5ca3254788 move vscode to config from home manager
modified:   hosts/nixos/configuration.nix
	modified:   hosts/nixos/home.nix
2025-07-20 21:40:22 +10:00
beatzaplenty 398dc5161a enable allow unfree software
modified:   hosts/nixos/configuration.nix
2025-07-20 21:27:27 +10:00
beatzaplenty 798c3c10d5 added vscode
modified:   hosts/nixos/home.nix
2025-07-20 21:25:36 +10:00
beatzaplenty af2f4263cc moved aliases to common file and imported to all home.nix variants
new file:   common/aliases.nix
	modified:   common/home.nix
	modified:   hosts/nixos/home.nix
2025-07-20 21:15:31 +10:00
beatzaplenty fa76fb0f13 modified: common/home.nix 2025-07-16 21:11:07 +10:00
beatzaplenty d6c1e48667 modified: hosts/nix-cache/configuration.nix 2025-07-16 20:34:45 +10:00
beatzaplenty aa065f21e3 modified: common/configuration.nix 2025-07-16 20:31:41 +10:00
beatzaplenty 61f5059492 modified: common/configuration.nix
modified:   hosts/nix-cache/configuration.nix
2025-07-16 20:24:23 +10:00
beatzaplenty bd6f0ab825 modified: common/home.nix 2025-07-16 20:06:55 +10:00
beatzaplenty 0ab5a56493 modified: flake.nix 2025-07-16 20:03:18 +10:00
beatzaplenty c9ab34040d modified: flake.nix 2025-07-16 19:57:42 +10:00
183 changed files with 1098 additions and 14529 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 -19
View File
@@ -3,25 +3,9 @@
result result
result-* result-*
# Disko's proxmox-* image-builder writes the finished .raw disk image
# directly into the current directory, not into a result-* symlink (see
# docs/proxmox-images.md, scripts/create-proxmox-resource.sh) — several GB
# each, never meant to be committed.
*.raw
# Ignore automatically generated direnv output # Ignore automatically generated direnv output
.direnv .direnv
# Python bytecode cache (scripts/lib/*.py) auto-installer/flake.lock
__pycache__/ auto-installer/result
*.pyc auto-installer/nixos-auto.iso
# 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
.claude/worktrees/
.claude/settings.local.json
-201
View File
@@ -1,201 +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-tailscale-router age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
- &lxc-docker age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
- &lxc-minimal age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
- &lxc-nix-cache age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
- &lxc-pxe-boot age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
- &lxc-tailscale-router age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
- &lxc-tor-relay age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
- &proxmox-docker age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
- &proxmox-gui age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
- &proxmox-nix-cache age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
- &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
- &proxmox-ha-server-1 age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6
- &proxmox-ha-server-2 age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r
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-tailscale-router
- *lxc-docker
- *lxc-minimal
- *lxc-nix-cache
- *lxc-pxe-boot
- *lxc-tailscale-router
- *lxc-tor-relay
- *proxmox-docker
- *proxmox-gui
- *proxmox-nix-cache
- *proxmox-pxe-boot
- *proxmox-tailscale-router
- *proxmox-ha-server-1
- *proxmox-ha-server-2
- path_regex: secrets/nix-cache\.yaml$
key_groups:
- age:
- *admin
- *linode-nix-cache
- *lxc-nix-cache
- *proxmox-nix-cache
- path_regex: secrets/tor-relay\.yaml$
key_groups:
- age:
- *admin
- *lxc-tor-relay
- path_regex: secrets/tailscale-router\.yaml$
key_groups:
- age:
- *admin
- *linode-tailscale-router
- *lxc-tailscale-router
- *proxmox-tailscale-router
# HA file server per-node secrets (beszel-token).
# proxmox-ha-server-1 / proxmox-ha-server-2 keys are added automatically
# by scripts/secrets/sync-host-keys.sh once the hosts are provisioned;
# until then only the admin key can decrypt these files.
- path_regex: secrets/ha-server-1\.yaml$
key_groups:
- age:
- *admin
- *proxmox-ha-server-1
# proxmox-ha-server-1 added by sync-host-keys.sh
- path_regex: secrets/ha-server-2\.yaml$
key_groups:
- age:
- *admin
- *proxmox-ha-server-2
# proxmox-ha-server-2 added by sync-host-keys.sh
# Shared HA cluster corosync authkey (binary sops file).
# Encrypted for both HA nodes so either can decrypt on boot.
# Both host keys added by sync-host-keys.sh; admin key allows initial creation.
- path_regex: secrets/ha-corosync-authkey$
key_groups:
- age:
- *admin
- *proxmox-ha-server-1
- *proxmox-ha-server-2
# proxmox-ha-server-1 added by sync-host-keys.sh
# proxmox-ha-server-2 added by sync-host-keys.sh
# gui-host-specific secrets (currently: wifi-password, see
# 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
# IPA host keytabs (binary sops files).
# Each keytab is encrypted for all platform variants of that host so any
# deployed variant can decrypt it at boot. Run
# scripts/ipa/create-nixos-ipa-host-account.sh <hostname> to enroll a new
# host and produce the keytab; this section is updated by that script.
- path_regex: secrets/nix-cache\.keytab$
key_groups:
- age:
- *admin
- *linode-nix-cache
- *lxc-nix-cache
- *proxmox-nix-cache
- path_regex: secrets/tailscale-router\.keytab$
key_groups:
- age:
- *admin
- *linode-tailscale-router
- *lxc-tailscale-router
- *proxmox-tailscale-router
- path_regex: secrets/pxe-boot\.keytab$
key_groups:
- age:
- *admin
- *lxc-pxe-boot
- *proxmox-pxe-boot
# nixos = the workstation (hosts/nixos/host.nix). All gui platform variants
# share the hostname "nixos" and must be able to decrypt at boot.
- path_regex: secrets/nixos\.keytab$
key_groups:
- age:
- *admin
- *baremetal-gui
- *lxc-gui
- *proxmox-gui
- *linode-gui
- path_regex: secrets/docker\.keytab$
key_groups:
- age:
- *admin
- *linode-docker
- *lxc-docker
- *proxmox-docker
- path_regex: secrets/tor-relay\.keytab$
key_groups:
- age:
- *admin
- *lxc-tor-relay
- path_regex: secrets/nix-minimal\.keytab$
key_groups:
- age:
- *admin
- *lxc-minimal
- *proxmox-minimal
- *linode-minimal
# Host keytab for ha-server-1 FreeIPA enrollment (binary sops file).
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
- path_regex: secrets/ha-server-1\.keytab$
key_groups:
- age:
- *admin
- *proxmox-ha-server-1
# proxmox-ha-server-1 added by sync-host-keys.sh
# Host keytab for ha-server-2 FreeIPA enrollment (binary sops file).
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
- path_regex: secrets/ha-server-2\.keytab$
key_groups:
- age:
- *admin
- *proxmox-ha-server-2
# proxmox-ha-server-2 added by sync-host-keys.sh
-52
View File
@@ -1,52 +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`, `baremetal`; build types: `minimal`,
`nix-cache`, `docker`, `gui`, `pxe-boot`, `tailscale-router`,
`tor-relay`, `ha-server`), 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, `ha-server` only
exists on `proxmox`, and `tor-relay` only exists on `lxc`. 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
```
-556
View File
@@ -1,556 +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`, `vars.beszelHubKey`). 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/secrets/push-host-keys.sh [--all | <target>] [--dry-run]
[--skip-git-check]` — pushes newly-generated SSH host keys from
`host-keys/` to already-running NixOS hosts, so they can decrypt sops
secrets after a rebuild following `sync-host-keys.sh
--regenerate-all-keys`. Verifies that `.sops.yaml` and `secrets/*.yaml`
are committed and pushed to the remote first (hosts rebuild from the
remote Gitea flake, so recipient changes must land there before any key
push).
### `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/clone-pve1-to-pve-test.sh <vmid> [--new-vmid <id>]
[--mode snapshot|suspend|stop] [--dry-run]` — ad-hoc clone of a single
VM or CT from pve1 (production) to pve-test (sandbox) via vzdump +
qmrestore/pct restore. Streams the archive directly between nodes (no
local staging copy). Always restores with `--unique 1` (fresh MAC
addresses) since the original is still running on the LAN. Cleans up
the vzdump archive from both nodes after a successful restore. The
script's own default is pve1 → pve-test, matching CLAUDE.md's policy
(unlike `create-proxmox-resource.sh`, which defaults to production for
the operator's own unqualified use).
- `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/ha/`
HA cluster lifecycle and operational scripts. All mutate real cluster state
when run for real — always run against pve-test first unless the operator
explicitly targets pve1.
- `scripts/ha/deploy.sh [--skip-*] [--destroy] [--dry-run]` — full
lifecycle manager: phases through bridge creation, key sync, VM creation
(via `create-proxmox-resource.sh`), NIC/disk attachment, and cluster
initialisation. `--destroy` tears it back down. Safe to rerun
idempotently; each phase can be individually skipped.
- `scripts/ha/cluster-init.sh` — one-time cluster bootstrap run **as root
on ha-server-1** after both VMs are booted. Generates/distributes the
Corosync authkey, initialises DRBD metadata, creates XFS on `/dev/drbd0`,
configures LIO iSCSI, and registers all Pacemaker resources (DRBD → XFS
→ iSCSI → NFS → VIPs).
- `scripts/ha/health.sh` — read-only cluster health snapshot: SSH
reachability, quorum, DRBD state, Pacemaker resources, and VIP port
reachability. Safe to run from the workstation at any time.
- `scripts/ha/failover.sh [--to node1|node2] [--force] [--timeout <s>]
[--dry-run]` — graceful failover by putting the active node into
Pacemaker standby and waiting for resources to appear on the target.
- `scripts/ha/acceptance-tests.sh` — T1–T7 acceptance tests (failover,
NFS/iSCSI connectivity, DRBD sync, etc.) that must all pass before the
cluster is considered production-ready.
- `scripts/ha/resize-data-disk.sh --size +NNg [--force] [--dry-run]` —
online data-disk resize: `qm resize` on both VMs, guest block-device
rescan, `drbdadm resize`, `xfs_growfs`. No downtime required.
- `scripts/ha/cluster-enable-stonith.sh` — enables the `fence_pve_ssh`
STONITH resource after the fence SSH key is deployed to both nodes and
authorised on the Proxmox host. Run once after `cluster-init.sh`.
- `scripts/ha/fence-pve-ssh.py` — Python STONITH fence agent for Pacemaker.
Deploy to `/etc/pacemaker/fence_pve_ssh` on both HA nodes (`chmod +x`).
SSHes to the Proxmox host and runs `qm stop/start <vmid>`.
### `scripts/ipa/`
- `scripts/ipa/create-nixos-ipa-host-account.sh [options] <hostname>` —
adds a NixOS host to the FreeIPA domain and produces a sops-encrypted
keytab at `secrets/<hostname>.keytab`, ready for `modules/ipa/client.nix`.
Replaces three error-prone manual steps: `ipa host-add`, `ipa-getkeytab`
(run on the DC, SCP'd back), and `sops encrypt` in the correct location
(must be at `secrets/<hostname>.keytab` for the creation rule to match).
### `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.
- `nix-parallel.sh` — `run_nix_parallel`: fans out independent `nix eval`/
`nix build --dry-run` calls across up to `NIX_PARALLEL_JOBS` processes,
capped by available memory (~1 GB/job) rather than raw `nproc` to avoid
OOM on constrained CI runners. Used by `codex-maintenance.sh`.
- `clan-vars.sh` — helpers for reading/writing SSH host keys stored as clan
vars (`vars/per-machine/<target>/openssh/`, sops-encrypted) instead of
the gitignored `host-keys/` directory. Sourced by
`create-proxmox-resource.sh` and `sync-host-keys.sh`; depends on
`sops-age.sh` and `ssh-host-keys.sh` being sourced first.
- `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/recover-hosts.sh [<hostname> ...]` — fixes sops/SSH-key/GitHub-token
issues on deployed NixOS hosts and triggers a `Switch-nix` rebuild on each.
With no args discovers every known hostname; with args checks only those.
Fixes applied automatically (prompts before rebuilding): SSH host key drift
(restores the registered key) and stale GitHub access tokens (empties the
rendered `nix-github-token.conf` so Nix falls back to unauthenticated requests
until sops-nix re-renders the correct token after the next successful rebuild).
- `scripts/gc-hosts.sh [--dry-run]` — runs `nix-collect-garbage -d` on all live
NixOS hosts (workstation first, then pve1, then all Proxmox guests). Excludes
`nix-cache` (gc-ing the shared binary cache evicts store paths other hosts
depend on). Uses passwordless sudo where available; falls back to user-level gc.
- `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`, `docker`, `gui`, `pxe-boot`, `tailscale-router`, `tor-relay`,
`ha-server`. 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` only exists as `lxc-tor-relay`, `ha-server` only exists as
`proxmox-ha-server-{1,2}`, and `baremetal` 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 — 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/docker/gui/pxe-boot/nix-cache/tailscale-router/tor-relay/ha-server.
- `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
`ha-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/ha/` — HA cluster NixOS modules: `cluster-config.nix` (DRBD,
Corosync, Pacemaker, firewall rules, cluster-wide NFS/iSCSI port
authorisation — shared by both ha-server nodes), `pacemaker-stack.nix`
(Pacemaker + Corosync service enablement), and supporting modules. See
`docs/ha.md` for the cluster operational guide.
- `modules/ipa/client.nix` — FreeIPA client enrollment: sssd, Kerberos keytab,
and IPA host registration; imported by every real host via
`modules/common/configuration.nix`.
- `modules/beszel/enable-agent.nix` — enables beszel-agent, sets `HUB_URL`,
fixes the upstream `StateDirectory` bug, and wires the universal
`beszel-token` sops secret (from `secrets/common.yaml`) into the agent's
`environmentFile`; see `docs/beszel.md` for the full setup guide.
- `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`.
- `docs/ha.md` — HA file-server cluster: DRBD + XFS + LIO iSCSI + NFS managed
by Corosync + Pacemaker; network topology; lifecycle scripts in `scripts/ha/`.
+1 -170
View File
@@ -1,171 +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`, `docker`, `gui`,
`pxe-boot`, `tailscale-router`, `tor-relay`, `ha-server`
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` and `ha-server` currently only exist on `lxc`/`proxmox`, 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-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-router` / `proxmox-tailscale-router` / `lxc-tailscale-router` | Tailscale subnet router + MagicDNS forwarder for the LAN |
| `lxc-tor-relay` | Tor middle relay |
| `proxmox-ha-server-1` / `proxmox-ha-server-2` | HA file-server cluster nodes — DRBD + XFS + iSCSI + NFS, managed by Corosync + Pacemaker |
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. 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 the
sops-nix migration — those are being scrubbed and rotated separately; don't
treat the repo as safe to make public until that's finished.
-25
View File
@@ -1,25 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIESDCCArCgAwIBAgIBATANBgkqhkiG9w0BAQsFADA1MRMwEQYDVQQKDApTV0VF
VC5IT01FMR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjYwNzI2
MjExMzQxWhcNNDYwNzI2MjExMzQxWjA1MRMwEQYDVQQKDApTV0VFVC5IT01FMR4w
HAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggGiMA0GCSqGSIb3DQEBAQUA
A4IBjwAwggGKAoIBgQCzljYktbHdMGVJ6Wq0XQJuHLN6dkCSOgtoIzQtriPQkkNI
uo28LwobaiQQ8sX4kGRH/BTKnH8QlId/jug4Uc+sDHnABYu++AiOhPbBX8gCpRQ0
hebBjZiktHSBUEJR31siWOVdBoKBDJEoxehx7XUXvcxIJcaRN+LHYjO86nJN55HB
VwFU2JcYDk98c+144dFJxXdr++MjWe4Z/oVVU8JHIOtNtKhVhvij6oOSWxcYoJO/
S80LRj1vx/o6o/3G6bYug7PjY7JjZk/Oj61whijZkcsoO1MXSYI6UywJZGflv+ZB
7HyufdYAsK3WhE8O2FX3/kq64Ol83HNtoR8Dt68rTg1xpW6K45jS6iDPKueYGkb0
oSx7e++90VAW2PDhj6QQ3JJ4O5VQwrrecekJzUrAean0FOEbmgyi4PsEp1Vk6LDQ
SsIn1x0euyxVivQMlzNX2XrZL3urn1BNPqAdntXQMkR0Wl8sbUiJPe0kxG52CGXs
6yfNEXbPmVGcC0TBdGECAwEAAaNjMGEwHQYDVR0OBBYEFLh5QbI1UWMH0WR4z8bG
lhrOX3X5MB8GA1UdIwQYMBaAFLh5QbI1UWMH0WR4z8bGlhrOX3X5MA8GA1UdEwEB
/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgHGMA0GCSqGSIb3DQEBCwUAA4IBgQCVodVN
owwo53OQe02QhtEbIur2PL7zIfvhvCTRD4J8gwpbMIqT7JQK0tV6Mvsg2L8yTb2O
KjrWeLKHGWaZZlhGSPTbkMFdb/Ls8M9FSnkc2bwcdWW3Z1lOiCjBYYqwLCG6JhvB
5SXVwWNJwXeasL2m7oFTSwhsqPpARJ2t25u2N35o+tqIoCjijKwkmEOT66N9EAbu
2VQjtYZWPkBtP4YCe0Ey6u4oy7sy8ThNAjOylZok+J4JW7QEFjK4Q/emhA4aQq5H
gg9qgMuG+5oi6D1g2Wy+fMTRBaukJtLYZbBpQMQhMYWg44uPp/2bbNPTID/nV1KB
GcPyHaskcVxPdYWxAPMwk3AeJXWyOq7atAPTF5sbk0kQQf2m+vyOqcli5CxRMUgV
rcyi9l6+dZW4U+38Q0ET5M3OuxNI4hA7kVY2cfTakXWNqh97+TIHnstblDhAxECK
6ZLMJQYUy7LqJTX84H27CBWLexEMjXwdr5HCV88Fj6mAK0fRufnIw5FeneA=
-----END CERTIFICATE-----
+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 = imports =
[ [ (modulesPath + "/profiles/qemu-guest.nix")
(modulesPath + "/profiles/qemu-guest.nix")
]; ];
boot = { boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ]; boot.initrd.kernelModules = [ ];
initrd.kernelModules = [ ]; boot.kernelModules = [ ];
kernelModules = [ "kvm-amd" ]; boot.extraModulePackages = [ ];
extraModulePackages = [ ];
};
# boot.loader.grub.device = "/dev/sda2"; # or "nodev" for efi only
# fileSystems."/" = fileSystems."/" =
# { device = "/dev/disk/by-label/nixos"; { device = "/dev/disk/by-label/nixos";
# fsType = "ext4"; fsType = "ext4";
# }; };
# swapDevices = swapDevices =
# [ { device = "/dev/disk/by-label/swap"; } [ { device = "/dev/disk/by-label/swap"; }
# ]; ];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking # 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 # (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.11"; # 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;
# };
}
-313
View File
@@ -1,313 +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-router`, `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 # auto-installer netboot bundle (kernel + initrd + ipxe script)
packages.x86_64-linux.pxe-minimal # vanilla NixOS minimal netboot bundle (no installer wiring)
```
```sh
nix build .#iso
nix build .#pxe
nix build .#pxe-minimal
```
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.
-104
View File
@@ -1,104 +0,0 @@
# Beszel agent
[Beszel](https://github.com/henrygd/beszel) is the monitoring dashboard used
in this LAN. The hub runs as a Docker container on `docker.sweet.home` (port
`vars.ports.beszelHub`, 8090). Each monitored NixOS host runs a
`beszel-agent` that connects back to the hub.
---
## How it works
Everything is handled by a single module:
**`modules/beszel/enable-agent.nix`** — imported by a build type. It:
- Enables `beszel-agent`
- Sets `HUB_URL` to `docker.sweet.home:8090`
- Sets `KEY` from `vars.beszelHubKey` (`variables.nix`) — the hub's SSH
public key, shared by every agent. Update `beszelHubKey` if the docker
host is ever rebuilt and the hub generates a new keypair.
- Reads the universal `beszel-token` from `secrets/common.yaml` via sops
and passes it to the agent as `TOKEN` in an env file
- Fixes an upstream bug where the agent couldn't persist its hub-pairing
fingerprint across restarts (adds a real `StateDirectory`)
A host file needs no beszel configuration at all — just import the module
in the build type and add the system in the hub UI.
---
## Adding beszel to a new build type
Add `../beszel/enable-agent.nix` to the `imports` list in
`modules/build-types/<type>.nix`:
```nix
imports = [
../beszel/enable-agent.nix
# ... other imports
];
```
That's the only change required. The host file needs nothing.
---
## Adding a new system to the hub
1. Rebuild and deploy the host with its build type importing `enable-agent.nix`.
2. Open the beszel hub (`http://docker.sweet.home:8090`).
3. Go to **Systems → Add system**, enter the host's IP and the default port
(45876). The agent will connect and the system will appear as active.
---
## One-time setup: add the token to `secrets/common.yaml`
The universal token is stored once in the common secrets file, shared by all
agents. Only needed once, not per-host:
```sh
sops secrets/common.yaml
```
Add:
```yaml
beszel-token: <token from the beszel hub Settings → Keys>
```
`secrets/common.yaml` is already a sops recipient for every host via their
SSH host keys, so no additional sops recipient setup is needed.
---
## Optional: monitoring extra filesystems
To report disk usage for a mount beyond the root filesystem, add
`EXTRA_FILESYSTEMS` in the host file:
```nix
services.beszel.agent.environment = {
EXTRA_FILESYSTEMS = "/mnt/data"; # colon-separated for multiple paths
};
```
---
## Optional: monitoring Docker containers
`enable-agent.nix` has a commented-out line for Docker monitoring:
```nix
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
```
Uncomment it if the host runs docker-socket-proxy and you want per-container
stats. Hosts without Docker should leave it commented out.
---
## If the hub key changes
If the docker host is ever rebuilt and beszel generates a new SSH keypair,
update `beszelHubKey` in `variables.nix` and rebuild all beszel-enabled hosts.
The new key is visible in the beszel hub under **Settings → Keys**.
-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.
-160
View File
@@ -1,160 +0,0 @@
# HA File-Server Cluster
Two `proxmox-ha-server-{1,2}` VMs form an active/passive file-server cluster:
DRBD replicates a block device between nodes; Corosync + Pacemaker manage
failover; XFS, LIO iSCSI, and NFS are brought up as a collocated resource
group on whichever node holds the DRBD Primary role.
NixOS modules: `modules/ha/`. Lifecycle scripts: `scripts/ha/`.
Cluster-wide constants: `variables.nix` (`haServer*` vars).
---
## Network layout
Three subnets — all internal to pve1 (`vmbr0`/`vmbr1`/`vmbr2`):
| Subnet | VLAN | CIDR | Bridge | Purpose |
|---|---|---|---|---|
| LAN | 2 | `192.168.2.0/24` | `vmbr0` | Management, LAN NFS |
| Cluster | 10 | `192.168.10.224/29` | `vmbr1` | Corosync ring0 + DRBD replication |
| Storage-client | 20 | `192.168.20.0/24` | `vmbr2` | NFS + iSCSI for docker/swarm |
Each HA VM has three NICs: `ens18` (LAN/vmbr0), `ens19` (cluster/vmbr1),
`ens20` (storage-client/vmbr2). See `docs/ip-addressing.md` for all IPs.
Corosync ring0 uses the cluster NIC; ring1 (backup heartbeat) uses the LAN
NIC. DRBD replicates over the cluster NIC. No storage traffic crosses the LAN.
---
## Pacemaker resources
All resources run collocated on whichever node is Primary, in this order:
```
ms-drbd0 (promotable DRBD clone)
→ xfs-data (XFS mount on /dev/drbd0 → /srv/ha-data)
→ iscsi-target (targetctl)
→ nfs-server (nfs-server.service)
→ vip-lan (192.168.2.229/24 on vmbr0 — NFS for LAN clients)
→ vip-storage (192.168.20.229/24 on vmbr2 — NFS + iSCSI for VLAN 20)
```
`vip-lan` serves pxe-boot and other LAN-only NFS clients.
`vip-storage` serves docker and any future swarm nodes; iSCSI is available on
VLAN 20 but NFS is preferred for multi-host volume sharing.
---
## DRBD fencing
`fencing resource-only` with `crm-fence-peer.sh`/`crm-unfence-peer.sh`
wrappers (`modules/ha/cluster-config.nix`). The DRBD kernel module invokes
these via the User Mode Helper with a minimal PATH; the wrappers prepend
`/run/current-system/sw/bin` before exec-ing the real handlers so Pacemaker
tools (`cibadmin`, `crm_mon`, etc.) are found.
STONITH is initially disabled (`stonith-enabled: false`,
`no-quorum-policy: ignore`). Enable it once the `fence_pve_ssh` fence agent
(`scripts/ha/fence-pve-ssh.py`) is deployed and authorised:
```bash
scripts/ha/cluster-enable-stonith.sh # run as root on ha-server-1
```
---
## Deploying the cluster from scratch
Use `scripts/ha/deploy.sh` — it orchestrates all phases:
```bash
# Against pve-test (safe — Claude's default target):
scripts/ha/deploy.sh --node "$PVE_TEST_HOST" [--dry-run]
# Against pve1 (production — requires explicit operator go-ahead):
scripts/ha/deploy.sh --node "$PVE1_HOST"
```
Phases (each skippable with `--skip-<phase>`):
1. `ensure-bridge` — creates `vmbr1`/`vmbr2` on the Proxmox node if absent
2. `sync-keys` — generates SSH host keys for both nodes; registers sops recipients
3. `create-vms` — builds disk images, creates VMs via `create-proxmox-resource.sh`
4. `add-hardware` — attaches storage NIC and DRBD data disk to each VM
5. `init-cluster` — runs `scripts/ha/cluster-init.sh` on ha-server-1
`--destroy` runs the teardown sequence.
---
## Day-to-day operations
```bash
# Read-only health check (safe from workstation):
scripts/ha/health.sh
# Graceful failover (prompts for confirmation):
scripts/ha/failover.sh [--to node1|node2]
# Online data-disk growth (no downtime):
scripts/ha/resize-data-disk.sh --size +20G
# Acceptance tests (run after any significant change):
scripts/ha/acceptance-tests.sh
```
---
## Adding FreeIPA host accounts
IPA host registration is automated:
```bash
scripts/ipa/create-nixos-ipa-host-account.sh <hostname>
```
This runs `ipa host-add`, fetches a keytab from the domain controller, and
writes a sops-encrypted `secrets/<hostname>.keytab` in one step. The module
`modules/ipa/client.nix` (imported by every host via
`modules/common/configuration.nix`) consumes the keytab via sops-nix.
---
## Storage layout
```
/srv/ha-data/
docker/
config/ NFS → docker:/mnt/docker/config
databases/ NFS → docker:/mnt/docker/databases
volumes/ NFS → docker:/mnt/docker/volumes
nextcloud-data/ NFS → docker:/mnt/docker/nextcloud-data
proxmox/
iso/ NFS → pve1 ISO storage
lxc/ NFS → pve1 CT template storage
pxe-boot/
images/ NFS → pxe-boot:/srv/pxe/http/images (PXE assets)
raspi/
volumes/ NFS → raspi NFS mounts
iscsi-lun.img iSCSI fileio backstore (VLAN 20 only, not in active use)
```
All shares are defined in `variables.nix` (`vars.nfsShares.*`). The NFS
export list lives in `modules/ha/nfs-exports.nix`.
---
## Key variables
| Variable | Description |
|---|---|
| `vars.haServer1Ip` / `vars.haServer2Ip` | LAN management IPs |
| `vars.haServer1StorageIp` / `vars.haServer2StorageIp` | Cluster NIC IPs (DRBD/Corosync ring0) |
| `vars.haServerLanVip` | Pacemaker `vip-lan` — NFS for LAN (192.168.2.229) |
| `vars.haServerVip` | Pacemaker `vip-storage` — NFS + iSCSI for VLAN 20 (192.168.20.229) |
| `vars.haLanNfsFqdn` | FQDN of `vip-lan`: `ha-vip-lan.sweet.home` |
| `vars.haStorageRoot` | XFS mount point: `/srv/ha-data` |
| `vars.haServerDrbdDisk` | Block device for DRBD backing store |
| `vars.haStorageCidr` | Cluster subnet CIDR (`192.168.10.224/29`) |
| `vars.haClientCidr` | Storage-client subnet CIDR (`192.168.20.0/24`) |
-186
View File
@@ -1,186 +0,0 @@
# IP Addressing Scheme
## Subnets
| Subnet | VLAN | CIDR | Purpose | Routed? |
|---|---|---|---|---|
| LAN | 2 (native/untagged) | `192.168.2.0/24` | General LAN — clients and infrastructure | Yes (gateway .254) |
| Cluster | 10 | `192.168.10.224/29` | HA file server DRBD replication + Corosync heartbeat | No — internal `vmbr1` only, no uplink |
| Storage client | 20 | `192.168.20.0/24` | HA file server NFS (and iSCSI if needed) — docker and swarm nodes mount from VIP here | No — internal `vmbr2` only, no uplink |
The cluster and storage-client subnets never leave pve1. `vmbr1` and `vmbr2` are Proxmox Linux
bridges with no physical port attached; traffic between guests on each bridge stays in-kernel.
VLAN IDs match the third octet of each subnet (VLAN 2 → 192.168.**2**.x, VLAN 10 → 192.168.**10**.x,
VLAN 20 → 192.168.**20**.x). The host octet is consistent across all subnets — e.g. ha-node1
is always `.228`: `192.168.2.228` (LAN), `192.168.10.228` (cluster), `192.168.20.228` (storage client).
**Protocol separation** (enforced by firewall on HA nodes):
- NFS (ports 111, 2049, 20048): both subnets, each restricted to its own CIDR
- VLAN 2 only → `vip-lan` (192.168.2.229) — pxe-boot and other LAN clients
- VLAN 20 only → `vip-storage` (192.168.20.229) — docker, future swarm nodes
- iSCSI (port 3260): VLAN 20 only — available but not in active use; NFS is preferred
for multi-host access (shared volumes across a Docker Swarm require a shared filesystem,
not per-host block devices)
---
## DNS Zones
FreeIPA (domain-controller.sweet.home) is authoritative for all zones. Three
zones correspond to the three subnets — one per VLAN. All zones are internal
only; no external delegation.
### sweet.home — VLAN 2 (192.168.2.x)
General LAN zone. All infrastructure hostnames live here.
| Hostname | A record | Notes |
|---|---|---|
| `domain-controller.sweet.home` | `192.168.2.253` | FreeIPA / KDC / DNS |
| `ha-vip-lan.sweet.home` | `192.168.2.229` | Pacemaker `vip-lan` — NFS for LAN clients |
| `ha-server-1.sweet.home` | `192.168.2.228` | HA node 1 management NIC |
| `ha-server-2.sweet.home` | `192.168.2.227` | HA node 2 management NIC |
| `server.sweet.home` | `192.168.2.226` | Current ZFS/NFS server (retiring) |
| `docker.sweet.home` | `192.168.2.225` | Docker/Traefik host |
| `nix-cache.sweet.home` | `192.168.2.224` | Nix binary cache + remote builder |
| `pxe-boot.sweet.home` | `192.168.2.223` | PXE / TFTP / HTTP netboot |
| `tailscale-router.sweet.home` | `192.168.2.222` | Tailscale exit node |
| `tor-relay.sweet.home` | `192.168.2.221` | Tor relay |
| `pdm.sweet.home` | `192.168.2.220` | Proxmox Deploy Manager |
| `nixos.sweet.home` | `192.168.2.39` | Bare-metal workstation (DHCP) |
| `pve1.sweet.home` | `192.168.2.245` | Proxmox VE hypervisor |
| `pbs.sweet.home` | `192.168.2.244` | Proxmox Backup Server |
PTR records exist for all static hosts. The workstation (`nixos.sweet.home`) is
DHCP-assigned; its PTR is omitted.
### cluster.home — VLAN 10 (192.168.10.x)
Internal only — Corosync ring0 heartbeat and DRBD replication between HA nodes.
No VIP exists on this subnet (DRBD/Corosync endpoints are static per-node IPs).
| Hostname | A record | Notes |
|---|---|---|
| `ha-server-1.cluster.home` | `192.168.10.228` | HA node 1 cluster NIC (ens19 / vmbr1) |
| `ha-server-2.cluster.home` | `192.168.10.227` | HA node 2 cluster NIC (ens19 / vmbr1) |
PTR records exist for both. DNS here is for debugging convenience — DRBD and
Corosync use the IPs from the NixOS config directly, not DNS.
### storage.home — VLAN 20 (192.168.20.x)
Internal only — NFS (and iSCSI) client access to the HA storage VIP. NFS clients
mount from **`nfs.storage.home`** (the Pacemaker floating VIP) so mounts survive
failover transparently without reconfiguration.
| Hostname | A record | Notes |
|---|---|---|
| `nfs.storage.home` | `192.168.20.229` | Pacemaker `vip-storage` — NFS + iSCSI VIP |
| `ha-server-1.storage.home` | `192.168.20.228` | HA node 1 storage-client NIC (ens20 / vmbr2) |
| `ha-server-2.storage.home` | `192.168.20.227` | HA node 2 storage-client NIC (ens20 / vmbr2) |
| `docker.storage.home` | `192.168.20.225` | Docker host storage-client NIC (eth1 / vmbr2) |
| `server.storage.home` | `192.168.20.226` | server VM storage-client NIC (decommissioned — remove DNS record after VM is destroyed) |
PTR records exist for all five. Remove `server.storage.home`, `server.sweet.home`,
and their PTRs from FreeIPA DNS once the server VM is destroyed.
---
## LAN — 192.168.2.0/24
### Address map
| Range | Purpose |
|---|---|
| .1–.9 | Reserved, never assign |
| .10–.59 | Client DHCP pool (router-assigned) |
| .60–.219 | Unallocated buffer |
| .220–.229 | Virtual nodes (VMs / LXC containers) |
| .230–.239 | Expansion buffer (reserved, unallocated) |
| .240–.249 | Physical nodes (bare-metal hosts) |
| .250–.253 | Network services |
| .254 | Router / gateway |
### Network services (.250–.253)
| IP | Hostname | Role |
|---|---|---|
| `192.168.2.254` | router | Gateway (TP-Link) |
| `192.168.2.253` | domain-controller | FreeIPA — authoritative DNS for `sweet.home`, Kerberos, LDAP |
| `192.168.2.250`–`.252` | — | Reserved for future network services |
### Physical nodes (.240–.249)
| IP | Hostname | Role |
|---|---|---|
| `192.168.2.245` | pve1 | Proxmox VE hypervisor |
| `192.168.2.244` | pbs | Proxmox Backup Server |
| `192.168.2.243` | nixos | Bare-metal workstation (`baremetal-gui`) |
| `192.168.2.246`–`.249` | — | Reserved — second Proxmox node and associated services |
| `192.168.2.240`–`.242` | — | Reserved |
pve1 sits mid-range deliberately so a second Proxmox node can slot in on either side.
### Virtual nodes (.220–.229)
All VMs and LXC containers run on pve1.
| IP | Hostname | Role | Status |
|---|---|---|---|
| `192.168.2.229` | ha-vip-lan | HA file server LAN floating VIP (Pacemaker `vip-lan`) — LAN iSCSI + NFS | Active |
| `192.168.2.228` | ha-node1 | HA file server node 1 — management NIC | Active |
| `192.168.2.227` | ha-node2 | HA file server node 2 — management NIC | Active |
| `192.168.2.226` | server | Former NFS/ZFS file server — decommissioned | Removed from flake |
| `192.168.2.225` | docker | Docker / Traefik stack | Active |
| `192.168.2.224` | nix-cache | Nix binary cache + remote builder | Active |
| `192.168.2.223` | pxe-boot | PXE / TFTP / HTTP netboot server | Active |
| `192.168.2.222` | tailscale-router | Tailscale exit node / router | Active |
| `192.168.2.221` | tor-relay | Tor relay | Active |
| `192.168.2.220` | pdm | Proxmox Deploy Manager | Active |
### Client DHCP pool (.10–.59)
Assigned by the router. DNS option points to `192.168.2.253` (domain-controller).
Devices in this range: phones, laptops, IoT, Canon printer, any non-infrastructure host.
No static reservations for infrastructure hosts — all infra uses static IP configuration
on the guest itself (not DHCP reservations), so IPs survive VM recreation regardless of
MAC address churn.
---
## Cluster network — VLAN 10 — 192.168.10.224/29
Internal to pve1 only. Proxmox bridge `vmbr1`, no physical NIC attached.
| IP | Hostname | Interface role |
|---|---|---|
| `192.168.10.228` | ha-node1 | DRBD replication + Corosync ring0 (primary heartbeat) |
| `192.168.10.227` | ha-node2 | DRBD replication + Corosync ring0 (primary heartbeat) |
| — | no gateway | Isolated — not routed to LAN or internet |
Corosync ring1 (backup heartbeat only) uses the LAN IPs (`192.168.2.228` / `192.168.2.227`)
over `vmbr0` — no additional bridge needed, and DRBD traffic never crosses ring1.
---
## Storage-client network — VLAN 20 — 192.168.20.0/24
Internal to pve1 only. Proxmox bridge `vmbr2`, no physical NIC attached.
| IP | Hostname | Interface / role |
|---|---|---|
| `192.168.20.229` | ha-vip-storage | Pacemaker floating VIP — NFS + iSCSI endpoint |
| `192.168.20.228` | ha-node1 | Storage-client NIC (ens20 / vmbr2) |
| `192.168.20.227` | ha-node2 | Storage-client NIC (ens20 / vmbr2) |
| `192.168.20.226` | server | Storage-client NIC (ens19 / vmbr2) — decommissioned |
| `192.168.20.225` | docker | Storage-client NIC (eth1 / vmbr2) — NFS client |
| — | no gateway | Isolated — not routed to LAN or internet |
NFS clients mount from `192.168.20.229` (surviving failover transparently via the VIP).
Firewall on each HA node restricts NFS and iSCSI ports to `192.168.20.0/24` — LAN hosts
cannot reach either service on this VIP. The `vip-storage` endpoint is not reachable
from the workstation directly (internal bridge only); health checks proxy through the
active HA node.
-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.
-214
View File
@@ -1,214 +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`
- Debian Minimal iPXE script: `/srv/pxe/http/debian.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 -> /mnt/pxe-images (symlink to NFS share)
/srv/pxe/http/auto-installer
/srv/pxe/http/nixos-minimal
/srv/pxe/http/debian
/srv/pxe/http/systemrescue
/srv/pxe/http/ubuntu
/srv/pxe/http/rescue
/srv/pxe/tftp
```
`/srv/pxe/http/images` is a symlink to `/mnt/pxe-images`, which is an NFS
mount of `server.sweet.home:/tank/pxe-boot/images`
(`modules/pxe-boot/mount-pxe-images.nix`). Place large images there (ISOs,
disk images) rather than on the pxe-boot host's own root disk. For an LXC
pxe-boot container the mount uses NFSv3+nolock with `nofail` (eager,
non-blocking on server unavailability); for a Proxmox VM it uses NFSv4.2
with `x-systemd.automount` (lazy, triggered on first access).
When running as `lxc-pxe-boot`, the Proxmox container must have
`features: nesting=1,mount=nfs` (at minimum) in its Proxmox config. `nesting=1`
is required by systemd 260+ for credential isolation (user namespace creation
and internal move-mounts); without it, AppArmor denies both, and every
systemd service that uses `PrivateUsers`, `PrivateDevices`, or credential
passing fails on boot. `mount=nfs` allows the NFSv3 mount. Both are set
automatically by `scripts/proxmox/create-proxmox-resource.sh` (via
`PROXMOX_DEFAULT_LXC_FEATURES` in `scripts/env.sh` which defaults to
`nesting=1,keyctl=1,mount=nfs;nfs4`). If you ever change these features
manually via `pct set`, be sure to include both — `pct set` replaces the
entire features string, it does not append to it.
The HTTP iPXE chain is:
```text
undionly.kpxe or ipxe.efi
-> autoexec.ipxe from the TFTP root, when iPXE requests it
-> http://192.168.2.223/boot.ipxe
-> http://192.168.2.223/menu.ipxe
```
The generated menu currently exposes entries for:
- NixOS Auto-Installer
- NixOS Minimal
- Debian Minimal
- FreeIPA Server (Rocky Linux 9)
- 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 Debian Minimal entry chains `http://<pxeServerIp>/debian.ipxe`, which loads
the Debian bookworm netboot kernel and initrd from `/srv/pxe/http/debian/`. The
`fetch-debian-netboot.service` oneshot downloads these files from
`deb.debian.org` on first boot (idempotent — skips if files are already
present):
```text
/srv/pxe/http/debian/linux (Debian bookworm netboot kernel)
/srv/pxe/http/debian/initrd.gz (Debian bookworm netboot initrd)
```
The service requires outbound internet access on the pxe-boot host. To
re-download (e.g. after a Debian point release), delete the files and restart
the service:
```bash
rm /srv/pxe/http/debian/linux /srv/pxe/http/debian/initrd.gz
systemctl restart fetch-debian-netboot.service
```
To update to a different Debian release, change `debianRelease` in
`modules/build-types/pxe-boot.nix` and redeploy.
The **FreeIPA Server (Rocky Linux 9)** entry chains
`http://<pxeServerIp>/rocky-freeipa.ipxe`, which boots the Rocky Linux 9
Anaconda installer with a Kickstart file (`rocky-freeipa.ks`) hosted on the
same server. The `fetch-rocky-pxeboot.service` oneshot downloads the pxeboot
kernel and initrd from the Rocky Linux mirror on first boot (idempotent):
```text
/srv/pxe/http/rocky/vmlinuz (Rocky Linux 9 Anaconda pxeboot kernel)
/srv/pxe/http/rocky/initrd.img (Rocky Linux 9 Anaconda pxeboot initrd)
```
The Kickstart file is generated from the NixOS module and staged at
`/srv/pxe/http/rocky-freeipa.ks`. It performs a fully unattended install:
1. Installs Rocky Linux 9 with `ipa-server` + `ipa-server-dns` packages
2. Configures static IP `192.168.2.138`, hostname `domain-controller.sweet.home`
3. Creates user `wayne` with the `adminSshKey` from `variables.nix`
4. Generates random IPA passwords and writes them to `/root/ipa-credentials.txt`
5. Creates a `freeipa-first-boot.service` oneshot that runs `ipa-server-install`
on first reboot (~20 minutes)
After the install completes:
- SSH in as `wayne@domain-controller` using the admin key
- Monitor FreeIPA install progress: `sudo tail -f /root/freeipa-install.log`
- Retrieve credentials: `sudo cat /root/ipa-credentials.txt` (save to password manager)
- Configure Pi-hole: `server=/sweet.home/192.168.2.138` in dnsmasq
To refresh the pxeboot files (e.g. after a Rocky point release):
```bash
rm /srv/pxe/http/rocky/vmlinuz /srv/pxe/http/rocky/initrd.img
systemctl restart fetch-rocky-pxeboot.service
```
To update to a different Rocky release, change `rockyRelease` in
`modules/build-types/pxe-boot.nix` and redeploy.
The SystemRescue entry expects the source ISO at:
```text
/srv/pxe/http/images/systemrescue.iso
```
Since `/srv/pxe/http/images` is the NFS-backed symlink, place the ISO on the
NFS share at `server.sweet.home:/tank/pxe-boot/images/systemrescue.iso`.
The `stage-systemrescue.service` oneshot extracts that ISO into:
```text
/srv/pxe/http/systemrescue
```
The rescue menu entry then chains `http://192.168.2.223/systemrescue.ipxe`,
which loads the SystemRescue kernel and initramfs from the extracted tree and
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/debian.ipxe
curl -I http://pxe-boot/debian/linux
curl -I http://pxe-boot/debian/initrd.gz
curl http://pxe-boot/rocky-freeipa.ipxe
curl http://pxe-boot/rocky-freeipa.ks
curl -I http://pxe-boot/rocky/vmlinuz
curl -I http://pxe-boot/rocky/initrd.img
curl http://pxe-boot/systemrescue.ipxe
curl -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": { "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": { "flake-compat": {
"locked": { "locked": {
"lastModified": 1767039857, "lastModified": 1696426674,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra", "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"repo": "flake-compat", "revCount": 57,
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "tarball",
"type": "github" "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
}, },
"original": { "original": {
"owner": "edolstra", "type": "tarball",
"repo": "flake-compat", "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
"type": "github"
} }
}, },
"flake-compat_2": { "flake-compat_2": {
@@ -108,30 +30,9 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems_2" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1694529238, "lastModified": 1694529238,
@@ -152,17 +53,16 @@
"flake-utils": "flake-utils" "flake-utils": "flake-utils"
}, },
"locked": { "locked": {
"lastModified": 1715533576, "lastModified": 1696331477,
"narHash": "sha256-fT4ppWeCJ0uR300EH3i7kmgRZnAVxrH+XtK09jQWihk=", "narHash": "sha256-YkbRa/1wQWdWkVJ01JvV+75KIdM37UErqKgTf0L54Fk=",
"owner": "gytis-ivaskevicius", "owner": "gytis-ivaskevicius",
"repo": "flake-utils-plus", "repo": "flake-utils-plus",
"rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f", "rev": "bfc53579db89de750b25b0c5e7af299e0c06d7d3",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "gytis-ivaskevicius", "owner": "gytis-ivaskevicius",
"repo": "flake-utils-plus", "repo": "flake-utils-plus",
"rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f",
"type": "github" "type": "github"
} }
}, },
@@ -173,54 +73,19 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1785119570, "lastModified": 1765860045,
"narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=", "narHash": "sha256-7Lxp/PfOy4h3QIDtmWG/EgycaswqRSkDX4DGtet14NE=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d", "rev": "09de9577d47d8bffb11c449b6a3d24e32ac16c99",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "nix-community",
"ref": "release-26.05",
"repo": "home-manager", "repo": "home-manager",
"type": "github" "type": "github"
} }
}, },
"nix-darwin": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1779036909,
"narHash": "sha256-zXcwYQGCT6pzinK+1dBB2ekTVtfxGZAapb3Evdcu4fY=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "56c666e108467d87d13508936aade6d567f2a501",
"type": "github"
},
"original": {
"owner": "nix-darwin",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-select": {
"locked": {
"lastModified": 1763303120,
"narHash": "sha256-yxcNOha7Cfv2nhVpz9ZXSNKk0R7wt4AiBklJ8D24rVg=",
"rev": "3d1e3860bef36857a01a2ddecba7cdb0a14c35a9",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/nix-select/archive/3d1e3860bef36857a01a2ddecba7cdb0a14c35a9.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz"
}
},
"nixos-conf-editor": { "nixos-conf-editor": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
@@ -228,11 +93,11 @@
"snowfall-lib": "snowfall-lib" "snowfall-lib": "snowfall-lib"
}, },
"locked": { "locked": {
"lastModified": 1771149335, "lastModified": 1707338010,
"narHash": "sha256-YPUIwyumbQOE2DUY8NIsHIUTGUQnDVhnTVUZMZDRwi4=", "narHash": "sha256-UtLeZzDdk96sLTRcWsGrkWCslNUDytrGe0VmOTB/iig=",
"owner": "snowfallorg", "owner": "snowfallorg",
"repo": "nixos-conf-editor", "repo": "nixos-conf-editor",
"rev": "9f8b4519a2e0e8919b69b7572bc26dab54274a6f", "rev": "27b5e92f580f794c690093503869aab242f075ab",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -243,11 +108,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1771008912, "lastModified": 1698318101,
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", "narHash": "sha256-gUihHt3yPD7bVqg+k/UVHgngyaJ3DMEBchbymBMvK1E=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a82ccc39b39b621151d6732718e3e250109076fa", "rev": "63678e9f3d3afecfeafa0acead6239cdb447574c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -259,28 +124,25 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1785133411, "lastModified": 1765779637,
"narHash": "sha256-Yjv0WEg39KRYS0rBdTbu6Fc/or/ihAKk13W9sQ6VWd0=", "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2f5a153c270b70cb0f8c11f46d96d6d3bc39f4e3", "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-26.05", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"clan-core": "clan-core",
"disko": "disko",
"home-manager": "home-manager", "home-manager": "home-manager",
"nixos-conf-editor": "nixos-conf-editor", "nixos-conf-editor": "nixos-conf-editor",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs_2"
"sops-nix": "sops-nix"
} }
}, },
"snowfall-lib": { "snowfall-lib": {
@@ -293,11 +155,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1765361626, "lastModified": 1696432959,
"narHash": "sha256-kX0Dp/kYSRbQ+yd9e3lmmUWdNbipufvKfL2IzbrSpnY=", "narHash": "sha256-oJQZv2MYyJaVyVJY5IeevzqpGvMGKu5pZcCCJvb+xjc=",
"owner": "snowfallorg", "owner": "snowfallorg",
"repo": "lib", "repo": "lib",
"rev": "c566ad8b7352c30ec3763435de7c8f1c46ebb357", "rev": "92803a029b5314d4436a8d9311d8707b71d9f0b6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -306,43 +168,7 @@
"type": "github" "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": { "systems": {
"locked": {
"lastModified": 1774449309,
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
"owner": "nix-systems",
"repo": "default",
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
"type": "github"
},
"original": {
"owner": "nix-systems",
"ref": "future-26.11",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@@ -356,29 +182,8 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1780220602,
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",
"version": 7 "version": 7
} }
+88 -224
View File
@@ -1,239 +1,103 @@
{ {
description = "LAN NixOS configs"; description = "LAN NixOS configs";
nixConfig = {
access-tokens = [
"github.com=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"
];
};
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixos-conf-editor.url = "github:snowfallorg/nixos-conf-editor"; nixos-conf-editor.url = "github:snowfallorg/nixos-conf-editor";
home-manager = { home-manager = {
url = "github:nix-community/home-manager/release-26.05"; url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs"; 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 {
let nixosConfigurations = {
system = "x86_64-linux"; # automatically use each host folder by name
inherit (nixpkgs) lib; nixos = nixpkgs.lib.nixosSystem {
pkgs = nixpkgs.legacyPackages.${system}; inherit system;
vars = import ./variables.nix; modules = [
./hosts/nixos/configuration.nix
# Generates a nixosConfiguration from a platform (what it runs on) and ./common/hardware-configuration.nix
# a build type (what it's for), plus the per-identity host.nix that home-manager.nixosModules.home-manager {
# carries the bits that must stay fixed regardless of platform home-manager.useGlobalPkgs = true;
# (hostName, hostId, per-machine secrets). Every build type except home-manager.useUserPackages = true;
# nix-cache itself consumes the nix-cache substituter and remote home-manager.users.nixos = import ./hosts/nixos/home.nix;
# builder.
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix, nameSuffix ? "" }: }
let ];
flakeTarget = "${platform}-${buildType}${nameSuffix}";
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-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; }; specialArgs = { inherit inputs; };
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; }; docker = nixpkgs.lib.nixosSystem {
baremetal-gui = mkTarget { platform = "baremetal"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; }; inherit system;
modules = [
proxmox-pxe-boot = mkTarget { platform = "proxmox"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; }; ./hosts/docker/configuration.nix
lxc-pxe-boot = mkTarget { platform = "lxc"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; }; ./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
linode-tailscale-router = mkTarget { platform = "linode"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; }; home-manager.useGlobalPkgs = true;
proxmox-tailscale-router = mkTarget { platform = "proxmox"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; }; home-manager.useUserPackages = true;
lxc-tailscale-router = mkTarget { platform = "lxc"; buildType = "tailscale-router"; hostPath = ./hosts/tailscale-router/host.nix; }; home-manager.users.nixos = import ./common/home.nix;
}
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; }; ];
};
proxmox-ha-server-1 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-1/host.nix; nameSuffix = "-1"; }; kuma = nixpkgs.lib.nixosSystem {
proxmox-ha-server-2 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-2/host.nix; nameSuffix = "-2"; }; inherit system;
}; modules = [
./hosts/kuma/configuration.nix
# Auto-install environments (migrated from the former nix-auto-installer ./common/hardware-configuration.nix
# flake): a self-contained NixOS installer that boots, discovers this home-manager.nixosModules.home-manager {
# flake's own nixosConfigurations over the network, and runs home-manager.useGlobalPkgs = true;
# nixos-install against whichever one the operator picks. These are home-manager.useUserPackages = true;
# deliberately not part of the platform x build-type matrix above — home-manager.users.nixos = import ./common/home.nix;
# 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 { server = nixpkgs.lib.nixosSystem {
inherit system; inherit system;
modules = [ ./modules/installer/iso.nix ]; modules = [
specialArgs = { inherit vars; }; ./hosts/server/configuration.nix
}; ./common/hardware-configuration.nix
}; home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
# Same installer environment, built as netboot (kernel + initrd + home-manager.useUserPackages = true;
# iPXE script) instead of an ISO — this is what packages.pxe bundles. home-manager.users.nixos = import ./common/home.nix;
# }
# 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 nix-cache = nixpkgs.lib.nixosSystem {
# every boot waiting for a device that can never exist on a netboot inherit system;
# client ("A start job is running for /dev/disk/by-label/nixos-minimal-..."). modules = [
# Both installation-cd-base.nix and netboot.nix set fileSystems."/" via ./hosts/nix-cache/configuration.nix
# the identical lib.mkImageMediaOverride (mkOverride 60) priority -- ./common/hardware-configuration.nix
# genuinely conflicting root-filesystem strategies (ISO-by-label vs. home-manager.nixosModules.home-manager {
# netboot-tmpfs) at the same priority, and the ISO one was winning. home-manager.useGlobalPkgs = true;
# netboot-minimal.nix's own chain (netboot-base.nix) already imports home-manager.useUserPackages = true;
# profiles/installation-device.nix independently, so common.nix's home-manager.users.nixos = import ./common/home.nix;
# 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 nix-minimal = nixpkgs.lib.nixosSystem {
# (nixos-system-auto-installer-*) matches its iPXE menu entry — inherit system;
# see modules/build-types/pxe-boot.nix's :auto-installer item — and modules = [
# its staged directory, /srv/pxe/http/auto-installer. ./hosts/nix-minimal/configuration.nix
netbootSystem = nixpkgs.lib.nixosSystem { ./common/hardware-configuration.nix
inherit system; home-manager.nixosModules.home-manager {
modules = [ home-manager.useGlobalPkgs = true;
./modules/installer/common.nix home-manager.useUserPackages = true;
({ modulesPath, ... }: { home-manager.users.nixos = import ./common/home.nix;
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; }
];
}; };
}; };
} }
+209
View File
@@ -0,0 +1,209 @@
# 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;
virtualisation.docker = {
enable = true;
# listenOptions = [
# "unix:///var/run/docker.sock"
# "tcp://0.0.0.0:2375"
#];
# daemon.settings = {
# metrics-addr = "0.0.0.0:9323";
# experimental = true;
# };
};
services.tailscale.enable = true;
# Enable docker-compose
environment.systemPackages = with pkgs; [
docker-compose
docker_25
nfs-utils
];
services.logrotate = {
enable = true;
settings = {
"/mnt/docker-persistent-data/docker/core/traefik/data/logs/*.log" = {
daily = true;
size = "100M";
rotate = 20;
compress = true;
missingok = true;
notifempty = true;
copytruncate = true;
};
};
};
boot.supportedFilesystems = [ "nfs" ];
services.rpcbind.enable = true;
# Mount docker persistent data
fileSystems."/mnt/docker-persistent-data" = {
device = "/dev/disk/by-label/docker-data";
fsType = "ext4";
options = [ "defaults" "nofail" "noatime" ];
};
fileSystems."/mnt/raspi" = {
device = "raspberrypi.tail13f623.ts.net:/home/raspi/raspi";
fsType = "nfs4";
options = [
"nofail"
"_netdev"
"noatime"
# 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"
# Explicitly use NFSv4.2 if supported
"nfsvers=4.2"
];
};
# 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 = "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 9323 ];
# 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?
}
-25
View File
@@ -1,25 +0,0 @@
{ vars, ... }:
{
networking = {
hostName = "docker";
hostId = "007f0200";
useDHCP = false;
interfaces = {
${vars.vmLanInterface}.ipv4.addresses = [{ address = vars.dockerIp; prefixLength = vars.lanPrefixLength; }];
${vars.lxcStorageInterface}.ipv4.addresses = [{ address = vars.dockerStorageIp; prefixLength = vars.haClientPrefixLength; }];
};
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
nameservers = [ vars.domainControllerIp ];
};
boot.zfs.forceImportRoot = false;
# Only advertise the LAN interface to IPA DNS. Without this, SSSD registers
# every Docker bridge (172.x.x.x) as an A record for docker.sweet.home —
# the default dyndns.interface = "*" catches them all.
security.ipa.dyndns.interface = vars.lxcLanInterface; # eth0
# Preserved from the pre-refactor `docker` target — stateVersion must never
# be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
-17
View File
@@ -1,17 +0,0 @@
{ vars, ... }:
{
networking = {
hostName = vars.haServer1Host;
hostId = "3a4b5c6d";
useDHCP = false;
interfaces = {
${vars.vmLanInterface}.ipv4.addresses = [{ address = vars.haServer1Ip; prefixLength = vars.lanPrefixLength; }];
${vars.vmStorageInterface}.ipv4.addresses = [{ address = vars.haServer1StorageIp; prefixLength = vars.haStoragePrefixLength; }];
${vars.vmStorageClientInterface}.ipv4.addresses = [{ address = vars.haServer1ClientIp; prefixLength = vars.haClientPrefixLength; }];
};
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
nameservers = [ vars.domainControllerIp ];
};
system.stateVersion = "26.05";
}
-17
View File
@@ -1,17 +0,0 @@
{ vars, ... }:
{
networking = {
hostName = vars.haServer2Host;
hostId = "7e8f9a0b";
useDHCP = false;
interfaces = {
${vars.vmLanInterface}.ipv4.addresses = [{ address = vars.haServer2Ip; prefixLength = vars.lanPrefixLength; }];
${vars.vmStorageInterface}.ipv4.addresses = [{ address = vars.haServer2StorageIp; prefixLength = vars.haStoragePrefixLength; }];
${vars.vmStorageClientInterface}.ipv4.addresses = [{ address = vars.haServer2ClientIp; prefixLength = vars.haClientPrefixLength; }];
};
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
nameservers = [ vars.domainControllerIp ];
};
system.stateVersion = "26.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?
}
+60
View File
@@ -0,0 +1,60 @@
# 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}";
};
};
services.prometheus.exporters.node = {
enable = true;
openFirewall = true;
};
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?
}
-18
View File
@@ -1,18 +0,0 @@
{ vars, ... }:
{
networking = {
hostName = vars.nixCacheHost;
useDHCP = false;
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
address = vars.nixCacheIp;
prefixLength = vars.lanPrefixLength;
}];
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
nameservers = [ vars.domainControllerIp ];
};
# 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 = [ imports = [
../../modules/common/aliases.nix ../../common/aliases.nix
]; ];
home = { home.username = "nixos"; # your actual username
username = vars.primaryUser; home.homeDirectory = "/home/nixos";
homeDirectory = "/home/${vars.primaryUser}"; home.stateVersion = "25.05"; # match your NixOS stateVersion
stateVersion = "25.05"; # match your NixOS stateVersion
# Optional: packages programs.home-manager.enable = true; # mandatory to activate HM
packages = with pkgs; [
git
vim
tmux
nextcloud-client
# vscode
chromium
claude-code
fish
sops
];
# Optional: set environment vars # Optional: packages
sessionVariables = { home.packages = with pkgs; [
EDITOR = "nano"; git
SOPS_AGE_KEY_FILE = "${config.home.homeDirectory}/.config/sops/age/keys.txt"; vim
}; tmux
nextcloud-client
# vscode
chromium
];
file = { # Optional: set environment vars
".local/share/applications/proxmox-chromium-app.desktop".text = '' home.sessionVariables = {
[Desktop Entry] EDITOR = "vim";
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
'';
};
}; };
programs.home-manager.enable = true; # mandatory to activate HM
# Optional: enable bash (or zsh, fish...) # Optional: enable bash (or zsh, fish...)
programs.bash.enable = true; programs.bash.enable = true;
services.nextcloud-client = { services.nextcloud-client = {
enable = true; enable = true;
# Optionally start in background directly # Optionally start in background directly
startInBackground = true; 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";
}
-20
View File
@@ -1,20 +0,0 @@
{ vars, ... }:
{
networking = {
hostName = "pxe-boot";
useDHCP = false;
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
address = vars.pxeServerIp;
prefixLength = vars.lanPrefixLength;
}];
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
nameservers = [ vars.domainControllerIp ];
};
services.beszel.agent.environment = {
# KEY = "";
};
# Preserved from the pre-refactor `pxe-boot` target — stateVersion must
# never be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+150
View File
@@ -0,0 +1,150 @@
# 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" ];
};
services.prometheus.exporters.node = {
enable = true;
openFirewall = 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?
}
-19
View File
@@ -1,19 +0,0 @@
{ vars, ... }:
{
networking = {
hostName = "tailscale-router";
useDHCP = false;
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
address = vars.tailscaleRouterIp;
prefixLength = vars.lanPrefixLength;
}];
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
nameservers = [ vars.domainControllerIp ];
};
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
# for pool-import safety, and this host does neither.
system.stateVersion = "26.05";
}
-21
View File
@@ -1,21 +0,0 @@
{ vars, ... }:
{
networking = {
hostName = "tor-relay";
useDHCP = false;
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
address = vars.torRelayIp;
prefixLength = vars.lanPrefixLength;
}];
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
nameservers = [ vars.domainControllerIp ];
};
# No networking.hostId: only ZFS-touching hosts need one for pool-import
# safety, and this host does neither.
# 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";
}
-31
View File
@@ -1,31 +0,0 @@
{ config, vars, ... }:
{
# Universal token shared by all beszel agents. Add to secrets/common.yaml:
# sops secrets/common.yaml
# beszel-token: <value from the beszel hub UI>
sops.secrets."beszel-token" = { };
sops.templates."beszel.env".content = ''
TOKEN=${config.sops.placeholder."beszel-token"}
'';
services.beszel.agent = {
enable = true;
environmentFile = config.sops.templates."beszel.env".path;
environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}";
KEY = vars.beszelHubKey;
};
};
# 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";
}
-6
View File
@@ -1,6 +0,0 @@
_:
{
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
}
-44
View File
@@ -1,44 +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
../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
];
}
-150
View File
@@ -1,150 +0,0 @@
{ config, pkgs, lib, inputs, vars, ... }:
{
imports = [
../docker/enable-service.nix
];
nixpkgs.overlays = [
(final: prev: {
docker = prev.docker_29;
docker_cli = prev.docker_29;
})
];
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;
# GUI-specific Home Manager additions for the IPA primary user, extending
# the baseline in modules/ipa/client.nix with desktop apps and services
# that only make sense on a graphical workstation.
home-manager.users.${vars.ipaUser} = { pkgs, ... }: {
home = {
packages = with pkgs; [
git
vim
nextcloud-client
chromium
claude-code
fish
sops
];
sessionVariables = {
EDITOR = "nano";
SOPS_AGE_KEY_FILE = "/home/${vars.ipaUser}/.config/sops/age/keys.txt";
};
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=/home/${vars.ipaUser}/.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=/home/${vars.ipaUser}/.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=/home/${vars.ipaUser}/.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=/home/${vars.ipaUser}/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
StartupWMClass=PBS
'';
};
};
services.nextcloud-client = {
enable = true;
startInBackground = true;
};
};
}
-54
View File
@@ -1,54 +0,0 @@
# HA file server build type: DRBD + XFS + LIO iSCSI + NFS, managed by
# Corosync + Pacemaker. Both ha-server-1 and ha-server-2 use this type.
#
# NFS start/stop:
# services.nfs.server.enable = true configures /etc/exports, wires up
# rpcbind, and loads kernel modules — but nfs-server.service.wantedBy is
# force-cleared so systemd does NOT auto-start it at boot. Pacemaker's
# ha-group resource group (configured by scripts/ha/cluster-init.sh)
# starts and stops nfs-server as part of the failover sequence after the
# XFS mount and iSCSI target are brought up on the new Active node.
#
# Beszel agent:
# Enabled here via enable-agent.nix. The agent KEY (used to pair with
# the Beszel hub) is not set yet — add it to hosts/ha-server-{1,2}/host.nix
# under services.beszel.agent.environment.KEY once the hub accepts the
# new agents, following the pattern in hosts/server/host.nix.
{ lib, pkgs, vars, ... }:
let
# Generates /etc/exports lines for all nfsShares data entries.
# LAN (VLAN 2): NFS via vip-lan (192.168.2.229) for pxe-boot and other LAN clients.
# Storage-client (VLAN 20): NFS via vip-storage (192.168.20.229) for docker and
# future swarm nodes; firewall restricts these ports to haClientCidr only.
mkNfsExports = storageRoot:
lib.concatMapStrings
(share:
" ${storageRoot}/${share.subpath} ${vars.lanCidr}${vars.nfsShares.options}\n" +
" ${storageRoot}/${share.subpath} ${vars.haClientCidr}${vars.nfsShares.options}\n")
(lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares));
in
{
imports = [
../ha/pacemaker-stack.nix
../ha/iscsi-target.nix
../ha/cluster-config.nix
../beszel/enable-agent.nix
];
# xfsprogs: mkfs.xfs/xfs_info needed by cluster-init.sh.
# openiscsi: iscsiadm needed by acceptance-tests.sh T4 (iSCSI discovery check).
environment.systemPackages = [ pkgs.xfsprogs pkgs.openiscsi ];
services.nfs.server = {
enable = true;
exports = mkNfsExports vars.haStorageRoot;
};
# Pacemaker controls nfs-server — prevent systemd from starting it at boot
# on both nodes (only the Active node should be serving NFS).
systemd.services.nfs-server.wantedBy = lib.mkForce [ ];
# Same reason as server.nix: exports use standard auth, not Kerberos.
systemd.services.rpc-svcgssd.enable = false;
}
-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
];
}
-451
View File
@@ -1,451 +0,0 @@
{ config, lib, pkgs, inputs, vars, ... }:
let
pxeRoot = "/srv/pxe";
httpRoot = "${pxeRoot}/http";
tftpRoot = "${pxeRoot}/tftp";
pxeBaseUrl = "http://${vars.pxeServerIp}";
# Base network address extracted from lanCidr (e.g. "192.168.2.0" from
# "192.168.2.0/24") — used by dnsmasq's proxy DHCP range directive.
lanBaseAddr = lib.head (lib.splitString "/" vars.lanCidr);
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
'';
debianRelease = "bookworm";
debianMirror = "https://deb.debian.org/debian";
debianNetbootBase = "${debianMirror}/dists/${debianRelease}/main/installer-amd64/current/images/netboot/debian-installer/amd64";
rockyRelease = "9";
rockyArch = "x86_64";
rockyMirror = "https://dl.rockylinux.org/pub/rocky/${rockyRelease}";
rockyPxebootBase = "${rockyMirror}/BaseOS/${rockyArch}/os/images/pxeboot";
debianIpxe = pkgs.writeText "debian.ipxe" ''
#!ipxe
set base ${pxeBaseUrl}
kernel ''${base}/debian/linux
initrd ''${base}/debian/initrd.gz
boot
'';
fetchDebianNetboot = pkgs.writeShellScript "fetch-debian-netboot" ''
set -eu
dir="${httpRoot}/debian"
mirror="${debianNetbootBase}"
if [ -f "$dir/linux" ] && [ -f "$dir/initrd.gz" ]; then
echo "Debian ${debianRelease} netboot files already present; skipping download."
exit 0
fi
echo "Downloading Debian ${debianRelease} netboot kernel and initrd from $mirror ..."
${pkgs.curl}/bin/curl -fsSL -o "$dir/linux.tmp" "$mirror/linux"
${pkgs.curl}/bin/curl -fsSL -o "$dir/initrd.gz.tmp" "$mirror/initrd.gz"
mv "$dir/linux.tmp" "$dir/linux"
mv "$dir/initrd.gz.tmp" "$dir/initrd.gz"
echo "Debian ${debianRelease} netboot files staged."
'';
# Rocky Linux 9 iPXE script — boots vmlinuz+initrd.img from the staged
# /rocky/ directory and hands Anaconda the hosted Kickstart URL.
# net.ifnames=0 biosdevname=0 ensures the NIC is eth0 in both the
# installer and the installed system (matches the Kickstart NM config).
rockyFreeIpaIpxe = pkgs.writeText "rocky-freeipa.ipxe" ''
#!ipxe
set base ${pxeBaseUrl}
kernel ''${base}/rocky/vmlinuz inst.ks=''${base}/rocky-freeipa.ks inst.repo=${rockyMirror}/BaseOS/${rockyArch}/os/ net.ifnames=0 biosdevname=0 ip=dhcp quiet
initrd ''${base}/rocky/initrd.img
boot
'';
# Kickstart file for ${vars.ipaServer}.
# Installs Rocky Linux 9, sets a static IP, creates ${vars.ipaUser} with
# the admin SSH key, then on first reboot runs ipa-server-install via a
# systemd oneshot service. Passwords are generated at %post time, written
# to /root/ipa-credentials.txt (chmod 600), and read back by the
# first-boot script — never hardcoded here or in the repo.
rockyFreeIpaKs = pkgs.writeText "rocky-freeipa.ks" ''
#version=RHEL9
# Unattended Rocky Linux 9 + FreeIPA install
# Target: ${vars.ipaServer} ${vars.domainControllerIp}
url --url=${rockyMirror}/BaseOS/${rockyArch}/os/
repo --name=appstream --baseurl=${rockyMirror}/AppStream/${rockyArch}/os/
lang en_US.UTF-8
keyboard us
timezone UTC --utc
# DHCP during install; static IP configured in %post via NM config file
network --bootproto=dhcp --device=link --activate
network --hostname=${vars.ipaServer}
selinux --enforcing
firewall --enabled --service=ssh
rootpw --lock
user --name=${vars.ipaUser} --groups=wheel --shell=/bin/bash
sshkey --username=${vars.ipaUser} "${vars.adminSshKey}"
zerombr
clearpart --all --initlabel --drives=sda
# Keep net.ifnames=0 biosdevname=0 in the installed GRUB so the NIC
# stays eth0 after reboot (matches the NM connection file below).
bootloader --location=mbr --boot-drive=sda --append="net.ifnames=0 biosdevname=0"
part /boot --fstype=xfs --size=1024 --ondisk=sda
part swap --fstype=swap --size=2048 --ondisk=sda
part / --fstype=xfs --grow --size=1 --ondisk=sda --asprimary
%packages
@^minimal-environment
ipa-server
ipa-server-dns
%end
reboot
%post --log=/root/ks-post.log
set -euo pipefail
# -- Static IP: write NM connection file directly (NM not running in chroot) --
mkdir -p /etc/NetworkManager/system-connections
cat > /etc/NetworkManager/system-connections/eth0.nmconnection << 'NMCONN'
[connection]
id=eth0
type=ethernet
interface-name=eth0
autoconnect=true
[ethernet]
[ipv4]
method=manual
addresses=${vars.domainControllerIp}/${toString vars.lanPrefixLength}
gateway=${vars.lanGateway}
dns=${vars.domainControllerIp};
dns-search=${vars.homeDomain};
[ipv6]
method=auto
NMCONN
chmod 600 /etc/NetworkManager/system-connections/eth0.nmconnection
# -- /etc/hosts: FQDN must resolve to the real IP (not loopback) for IPA --
sed -i '/domain-controller/d' /etc/hosts
echo '${vars.domainControllerIp} ${vars.ipaServer} domain-controller' >> /etc/hosts
# -- Generate IPA passwords and store securely --
DM_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)
ADMIN_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)
printf 'Directory Manager: %s\nIPA Admin: %s\n' "$DM_PASS" "$ADMIN_PASS" \
> /root/ipa-credentials.txt
chmod 600 /root/ipa-credentials.txt
# -- First-boot script: reads passwords back, runs ipa-server-install --
cat > /usr/local/sbin/freeipa-first-boot.sh << 'FIRSTBOOT'
#!/bin/bash
set -euo pipefail
exec >> /root/freeipa-install.log 2>&1
echo "=== FreeIPA first-boot install started at $(date) ==="
DM_PASS=$(grep '^Directory Manager:' /root/ipa-credentials.txt | awk '{print $NF}')
ADMIN_PASS=$(grep '^IPA Admin:' /root/ipa-credentials.txt | awk '{print $NF}')
ipa-server-install \
--realm=${lib.strings.toUpper vars.homeDomain} \
--domain=${vars.homeDomain} \
--hostname=${vars.ipaServer} \
--ds-password="$DM_PASS" \
--admin-password="$ADMIN_PASS" \
--setup-dns \
--forwarder=${vars.domainControllerIp} \
--no-dnssec-validation \
--no-ntp \
--unattended
echo "=== FreeIPA install complete at $(date) ==="
echo "Credentials: /root/ipa-credentials.txt (save to password manager)"
echo "CA backup: /root/cacert.p12 (encrypted with Directory Manager password)"
systemctl disable freeipa-first-boot.service
FIRSTBOOT
chmod 700 /usr/local/sbin/freeipa-first-boot.sh
# -- Systemd oneshot service: runs freeipa-first-boot.sh on first real boot --
cat > /etc/systemd/system/freeipa-first-boot.service << 'UNIT'
[Unit]
Description=FreeIPA first-boot installation
After=network-online.target
Wants=network-online.target
ConditionPathExists=/root/ipa-credentials.txt
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/freeipa-first-boot.sh
TimeoutStartSec=1800
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
UNIT
mkdir -p /etc/systemd/system/multi-user.target.wants
ln -sf /etc/systemd/system/freeipa-first-boot.service \
/etc/systemd/system/multi-user.target.wants/freeipa-first-boot.service
echo "Kickstart %post complete. FreeIPA installs on first reboot (~20 min)."
%end
'';
fetchRockyPxeboot = pkgs.writeShellScript "fetch-rocky-pxeboot" ''
set -eu
dir="${httpRoot}/rocky"
base="${rockyPxebootBase}"
if [ -f "$dir/vmlinuz" ] && [ -f "$dir/initrd.img" ]; then
echo "Rocky Linux ${rockyRelease} pxeboot files already present; skipping download."
exit 0
fi
echo "Downloading Rocky Linux ${rockyRelease} pxeboot kernel and initrd from $base ..."
${pkgs.curl}/bin/curl -fsSL -o "$dir/vmlinuz.tmp" "$base/vmlinuz"
${pkgs.curl}/bin/curl -fsSL -o "$dir/initrd.img.tmp" "$base/initrd.img"
mv "$dir/vmlinuz.tmp" "$dir/vmlinuz"
mv "$dir/initrd.img.tmp" "$dir/initrd.img"
echo "Rocky Linux ${rockyRelease} pxeboot files staged."
'';
systemRescueIpxe = pkgs.writeText "systemrescue.ipxe" ''
#!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 debian Debian Minimal
item rocky-freeipa FreeIPA Server (Rocky Linux 9)
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
:debian
chain ''${base}/debian.ipxe
:rocky-freeipa
chain ''${base}/rocky-freeipa.ipxe
:rescue
chain ''${base}/systemrescue.ipxe
:shell
shell
:reboot
reboot
'';
in
{
imports = [
../pxe-boot/stage-installer-artifacts.nix
../pxe-boot/mount-pxe-images.nix
../beszel/enable-agent.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 -"
"L+ ${httpRoot}/images - - - - ${vars.nfsShares.pxebootImages.mountpoint}"
"d ${httpRoot}/auto-installer 0755 root root -"
"d ${httpRoot}/nixos-minimal 0755 root root -"
"d ${httpRoot}/systemrescue 0755 root root -"
"d ${httpRoot}/debian 0755 root root -"
"d ${httpRoot}/ubuntu 0755 root root -"
"d ${httpRoot}/rescue 0755 root root -"
"d ${httpRoot}/rocky 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}/debian.ipxe 0644 root root - ${debianIpxe}"
"C+ ${httpRoot}/rocky-freeipa.ipxe 0644 root root - ${rockyFreeIpaIpxe}"
"C+ ${httpRoot}/rocky-freeipa.ks 0644 root root - ${rockyFreeIpaKs}"
"C+ ${httpRoot}/systemrescue.ipxe 0644 root root - ${systemRescueIpxe}"
"C+ ${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"
];
services = {
fetch-debian-netboot = {
description = "Download Debian ${debianRelease} netboot kernel and initrd for HTTP PXE boot";
after = [
"local-fs.target"
"systemd-tmpfiles-setup.service"
"network-online.target"
];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = fetchDebianNetboot;
RemainAfterExit = true;
};
};
fetch-rocky-pxeboot = {
description = "Download Rocky Linux ${rockyRelease} pxeboot kernel and initrd for HTTP PXE boot";
after = [
"local-fs.target"
"systemd-tmpfiles-setup.service"
"network-online.target"
];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = fetchRockyPxeboot;
RemainAfterExit = true;
};
};
stage-systemrescue = {
description = "Stage SystemRescue ISO contents for HTTP PXE boot";
after = [
"local-fs.target"
"systemd-tmpfiles-setup.service"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = stageSystemRescue;
};
};
};
};
services.dnsmasq = {
enable = true;
settings = {
# Disable DNS listener — only proxy DHCP is needed here.
# Without this dnsmasq tries to bind port 53 which systemd-resolved
# already owns, causing startup failure.
port = 0;
dhcp-range = [ "${lanBaseAddr},proxy" ];
dhcp-match = [
"set:ipxe,175"
"set:efi64,option:client-arch,7"
"set:efi64,option:client-arch,9"
];
dhcp-userclass = "set:ipxe,iPXE";
dhcp-boot = [
"tag:ipxe,tag:efi64,http://${vars.pxeServerIp}/boot.ipxe"
"tag:ipxe,http://${vars.pxeServerIp}/boot.ipxe"
"tag:efi64,ipxe.efi,,${vars.pxeServerIp}"
"undionly.kpxe,,${vars.pxeServerIp}"
];
};
};
networking.firewall.allowedTCPPorts = [ vars.ports.pxeBootHttp ];
networking.firewall.allowedUDPPorts = [ vars.ports.pxeBootTftp vars.ports.dhcp ];
}
-44
View File
@@ -1,44 +0,0 @@
{ vars, ... }:
{
imports = [
../tailscale/subnet-router.nix
../tailscale/ts-dns-forwarder.nix
../beszel/enable-agent.nix
];
# "server", not "both": this build type advertises LAN subnet routes but
# doesn't use another tailscale exit node itself, so it doesn't need the
# "client"-side loose reverse-path filtering that "both" would also enable.
# Deliberately kept explicit here (not just relying on subnet-router.nix's
# own setting) so the intent is clear at the build-type level.
services.tailscale.useRoutingFeatures = "server";
# Advertise the LAN subnet so Tailscale peers can route back to LAN machines.
# Must also be approved in the Tailscale admin console (Machines → Edit route settings).
services.tailscale.extraUpFlags = [ "--advertise-routes=${vars.lanCidr}" ];
networking.firewall = {
# Forwarded subnet-router traffic arrives on tailscale0 already
# tailscale-authenticated -- the firewall's normal per-port allow-list
# would otherwise drop it. Standard NixOS/Tailscale subnet-router guidance.
trustedInterfaces = [ "tailscale0" ];
# SNAT LAN traffic going into Tailscale so the remote peer sees it as
# coming from this router's Tailscale IP rather than a raw LAN IP.
# Without this, Tailscale drops forwarded packets whose source is not a
# recognised Tailscale address.
#
# We target POSTROUTING directly (always-existing built-in chain) rather
# than nixos-nat-post: extraCommands runs after the old nixos-nat-post is
# deleted but before the new one is created, so -A nixos-nat-post silently
# fails. The -C check makes the rule idempotent across firewall reloads.
extraCommands = ''
iptables -t nat -C POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE
'';
extraStopCommands = ''
iptables -t nat -D POSTROUTING -s ${vars.lanCidr} -o tailscale0 -j MASQUERADE 2>/dev/null || true
'';
};
}
-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"
'';
};
}
-7
View File
@@ -1,7 +0,0 @@
_:
{
# Switch-nix, Test-nix, and buildImage are defined system-wide in
# modules/common/configuration.nix so all users (including IPA accounts)
# get them. Add any Home-Manager-only per-user shell config here.
}
-121
View File
@@ -1,121 +0,0 @@
{ config, lib, pkgs, vars, ... }:
let
switchCmd = ''
sudo nixos-rebuild switch \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.giteaDomain}/${vars.giteaRepoPath}.git#$(cat /etc/flake-target)
'';
testCmd = ''
sudo nixos-rebuild test \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.giteaDomain}/${vars.giteaRepoPath}.git#$(cat /etc/flake-target)
'';
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
{
imports = [
./set-locale.nix
../ipa/client.nix
];
# System-wide shell config so all users (including IPA accounts) get the
# same management aliases as the local nixos user's Home Manager provides.
programs.bash = {
shellAliases = {
"Switch-nix" = switchCmd;
"Test-nix" = testCmd;
};
interactiveShellInit = buildImageFn;
};
networking.networkmanager.enable = true;
# 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. Harmless no-op on hosts without ZFS.
boot.zfs.forceImportRoot = false;
time.timeZone = vars.timeZone;
services.qemuGuest.enable = true;
environment.systemPackages = with pkgs; [
vim
btop
git
gcr
jq
];
# Secrets shared by every host, decrypted at activation via each host's
# SSH host key (sops-nix derives the age key from
# /etc/ssh/ssh_host_ed25519_key automatically). hashedPassword secrets need
# neededForUsers so they're available before the normal secret-activation
# step — 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 has no *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 = {
# mutableUsers = false makes update-users-groups.pl enforce hashedPasswordFile
# on every activation, not just on newly-created accounts. Without this, a
# freshly-built proxmox disk image (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" ];
packages = with pkgs; [ tree ];
hashedPasswordFile = config.sops.secrets."nixos-hashedPassword".path;
openssh.authorizedKeys.keys = [ vars.adminSshKey ] ++ vars.extraAdminSshKeys;
};
};
services.openssh.enable = true;
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;
# };
}
-35
View File
@@ -1,35 +0,0 @@
# Shared activation-script logic to preserve the SSH host key across
# nixos-rebuild on platforms that embed the key via environment.etc (lxc and
# proxmox). When NIXOS_HOST_KEYS_DIR is not set the key is absent from
# environment.etc, and NixOS's etc activation removes any /etc file not in
# the new generation — which would destroy the live key and break sops-nix
# decryption permanently. These scripts save the key to /run before etc
# removes it, then restore it afterward.
#
# Explicit deps enforce the correct ordering: without them 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" ]; };
};
}
-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;
};
};
}
-45
View File
@@ -1,45 +0,0 @@
{ lib, pkgs, vars, ... }:
let
gid = toString vars.dockerAccessGid;
in
{
virtualisation.docker = {
enable = true;
package = pkgs.docker;
};
# Pin the docker group GID to match the IPA "docker-access" group so that
# IPA group membership alone grants access to the Docker socket. Any user
# whose supplementary groups (resolved by SSSD from IPA) include GID
# vars.dockerAccessGid will pass the socket group-permission check without
# any per-host users.groups.docker.members entry.
users.groups.docker.gid = lib.mkForce vars.dockerAccessGid;
users.users.${vars.primaryUser}.extraGroups = [ "docker" ];
environment.systemPackages = with pkgs; [
docker-compose
docker-buildx
];
# NixOS's group activation uses plain `groupmod` without --non-unique.
# When SSSD is active it exposes the IPA "docker-access" group at
# vars.dockerAccessGid via NSS, so groupmod sees that GID as already in
# use and silently skips the change (warning: "not applying GID change").
# This script runs after the normal "groups" step and applies the change
# with --non-unique (which lets the local docker group share the GID with
# the SSSD-provided IPA group). If the GID actually changed it also
# restarts docker.socket so the socket is recreated with the new GID.
system.activationScripts.docker-group-gid = {
deps = [ "groups" ];
text = ''
current=$(grep "^docker:" /etc/group | cut -d: -f3)
if [ "$current" != "${gid}" ]; then
${pkgs.shadow}/bin/groupmod --non-unique -g ${gid} docker
if ${pkgs.systemd}/bin/systemctl is-active --quiet docker.socket; then
${pkgs.systemd}/bin/systemctl stop docker.service docker.socket
rm -f /var/run/docker.sock
${pkgs.systemd}/bin/systemctl start docker.socket docker.service
fi
fi
'';
};
}
-79
View File
@@ -1,79 +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" ];
# FQDN in the storage.home zone — resolves to the Pacemaker vip-storage
# (192.168.20.229) on docker's eth1/vmbr2 interface. Using the DNS name
# rather than the raw IP means a future VIP renumber only requires a DNS
# update, not a NixOS rebuild. The storage.home zone is served by the same
# FreeIPA nameserver (domainControllerIp) that docker already uses, so
# resolution reaches it over eth0 without any extra routing.
nfsServer = vars.haStorageNfsFqdn;
storageRoot = vars.haStorageRoot;
in
{
fileSystems = {
${vars.nfsShares.dockerConfig.mountpoint} = {
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.dockerDatabases.mountpoint} = {
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.dockerVolumes.mountpoint} = {
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.nextcloudData.mountpoint} = {
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.raspiVolumes.mountpoint} = {
device = "${nfsServer}:${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;
};
};
}
-164
View File
@@ -1,164 +0,0 @@
# Cluster-wide HA config shared by both ha-server nodes.
#
# Covers everything that is identical on both nodes and references cluster
# topology (node IPs, hostnames, DRBD resource). Per-node identity
# (hostname, static IP, stateVersion) lives in hosts/ha-server-{1,2}/host.nix.
#
# Corosync authkey:
# /etc/corosync/authkey (mode 0400) is managed by sops-nix below.
# Bootstrap: run scripts/ha/cluster-init.sh on node1 to generate the key,
# then encrypt it with: sops -e --input-type binary /etc/corosync/authkey > secrets/ha-corosync-authkey
# Both host keys must be registered via sync-host-keys.sh first so both nodes can decrypt it.
#
# DRBD fencing:
# resource-only with crm-fence-peer.sh: DRBD calls the Pacemaker-aware
# crm-fence-peer.sh handler before promoting. The handler checks the CIB
# to confirm the peer's DRBD resource is stopped and returns 7 (successfully
# fenced), allowing safe promotion without requiring power-fencing (STONITH).
# The unfence handler crm-unfence-peer.sh clears the outdate flag when the
# peer reconnects. This is the correct setting for Pacemaker+DRBD clusters
# with STONITH disabled; crm-fence-peer.sh replaces the need for a separate
# STONITH device during the testing phase. Switch to resource-and-stonith
# once the fence_pve_ssh STONITH resource is active (see
# scripts/ha/cluster-enable-stonith.sh).
#
# PATH wrapper: when the DRBD kernel module invokes the fence-peer handler
# via the UMH (User Mode Helper) mechanism it provides a minimal PATH that
# omits /run/current-system/sw/bin. crm-fence-peer.sh calls cibadmin,
# crm_mon etc.; if those aren't found a pipeline in the script breaks with
# SIGPIPE. A process killed by signal has WEXITSTATUS() == 0, so the kernel
# sees exit code 0 and logs "fence-peer helper broken, returned 0", looping
# forever. The writeShellScript wrappers below prepend the NixOS sw path
# before exec-ing the real handler, giving it a working Pacemaker toolchain.
{ lib, pkgs, vars, ... }:
let
fencePeerWrapper = pkgs.writeShellScript "drbd-fence-peer" ''
export PATH="/run/current-system/sw/bin:/run/current-system/sw/sbin:$PATH"
exec /run/current-system/sw/lib/drbd/crm-fence-peer.sh "$@"
'';
unfencePeerWrapper = pkgs.writeShellScript "drbd-unfence-peer" ''
export PATH="/run/current-system/sw/bin:/run/current-system/sw/sbin:$PATH"
exec /run/current-system/sw/lib/drbd/crm-unfence-peer.sh "$@"
'';
in
{
# Root SSH access — same key set as the nixos user so all admin keys can reach root.
users.users.root.openssh.authorizedKeys.keys = [ vars.adminSshKey ] ++ vars.extraAdminSshKeys;
# Passwordless sudo for wheel — operator SSHes as nixos and uses sudo for
# cluster management commands (drbdadm, crm*, pcs, etc.)
security.sudo.wheelNeedsPassword = lib.mkForce false;
# DRBD lock-file directory (drbd-utils checks for it; missing → harmless but noisy warnings).
systemd.tmpfiles.rules = [ "d /var/lib/drbd 0750 root root -" ];
# Prevent drbd.service from auto-starting at boot / nixos-rebuild switch.
# Pacemaker's OCF drbd agent calls drbdadm up/down directly when managing
# the resource. If drbd.service also runs drbdadm up all while DRBD is
# already Primary under Pacemaker, apply-al fails with "device busy" (exit 20).
systemd.services.drbd.wantedBy = lib.mkForce [ ];
services.drbd = {
enable = true;
config = ''
global {
usage-count yes;
}
common {
net {
protocol C;
ping-int 1;
verify-alg sha256;
after-sb-0pri discard-zero-changes;
after-sb-1pri discard-secondary;
}
disk {
fencing resource-only;
}
handlers {
fence-peer "${fencePeerWrapper}";
unfence-peer "${unfencePeerWrapper}";
}
}
resource ha-data {
volume 0 {
device /dev/drbd0;
disk ${vars.haServerDrbdDisk};
meta-disk internal;
}
on ${vars.haServer1Host} {
address ${vars.haServer1StorageIp}:${toString vars.ports.haServerDrbd};
}
on ${vars.haServer2Host} {
address ${vars.haServer2StorageIp}:${toString vars.ports.haServerDrbd};
}
}
'';
};
# /etc/corosync/authkey — sops binary secret, identical on both nodes.
# Decryptable by both ha-server host keys (added by sync-host-keys.sh).
sops.secrets.corosync_authkey = {
sopsFile = ../../secrets/ha-corosync-authkey;
format = "binary";
path = "/etc/corosync/authkey";
mode = "0400";
restartUnits = [ "corosync.service" ];
};
# NixOS common config enables NetworkManager by default; HA cluster nodes
# need stable static IPs with predictable interface names — NM is not suitable.
networking.networkmanager.enable = lib.mkForce false;
# services.corosync.enable is set by modules/ha/pacemaker-stack.nix.
services.corosync = {
clusterName = "ha-cluster";
nodelist = [
# ring0: cluster-internal vmbr1 (primary heartbeat + DRBD path)
# ring1: LAN vmbr0 (backup heartbeat only — never carries DRBD)
{ nodeid = 1; name = vars.haServer1Host; ring_addrs = [ vars.haServer1StorageIp vars.haServer1Ip ]; }
{ nodeid = 2; name = vars.haServer2Host; ring_addrs = [ vars.haServer2StorageIp vars.haServer2Ip ]; }
];
};
networking.firewall = {
allowedTCPPorts = [
vars.ports.haServerPacemakerRemoted
vars.ports.haServerPcsd
vars.ports.haServerDrbd
];
allowedUDPPorts = [
vars.ports.haServerCorosync1
vars.ports.haServerCorosync2
vars.ports.haServerCorosyncCrypto
];
# Protocol separation: iSCSI (VLAN 20 / storage clients only),
# NFS (VLAN 2 / LAN only). Cluster-internal subnets accepted wholesale
# since they are isolated bridges with no external uplink.
extraCommands = ''
iptables -A nixos-fw -s ${vars.haServer1Ip}/32 -j nixos-fw-accept
iptables -A nixos-fw -s ${vars.haServer2Ip}/32 -j nixos-fw-accept
iptables -A nixos-fw -s ${vars.haStorageCidr} -j nixos-fw-accept
iptables -A nixos-fw -p tcp -s ${vars.haClientCidr} --dport ${toString vars.ports.haServerIscsi} -j nixos-fw-accept
iptables -A nixos-fw -p tcp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsRpcbind} -j nixos-fw-accept
iptables -A nixos-fw -p udp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsRpcbind} -j nixos-fw-accept
iptables -A nixos-fw -p tcp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsd} -j nixos-fw-accept
iptables -A nixos-fw -p udp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsd} -j nixos-fw-accept
iptables -A nixos-fw -p tcp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsMountd} -j nixos-fw-accept
iptables -A nixos-fw -p udp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsMountd} -j nixos-fw-accept
iptables -A nixos-fw -p tcp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsRpcbind} -j nixos-fw-accept
iptables -A nixos-fw -p udp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsRpcbind} -j nixos-fw-accept
iptables -A nixos-fw -p tcp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsd} -j nixos-fw-accept
iptables -A nixos-fw -p udp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsd} -j nixos-fw-accept
iptables -A nixos-fw -p tcp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsMountd} -j nixos-fw-accept
iptables -A nixos-fw -p udp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsMountd} -j nixos-fw-accept
'';
};
}
-99
View File
@@ -1,99 +0,0 @@
# LIO iSCSI target service (targetctl) for NixOS HA clusters.
#
# Provides the targetctl.service that saves/restores LIO configuration from
# /etc/target/saveconfig.json. Pacemaker manages this service via its
# systemd resource agent (class="systemd" type="targetctl").
#
# Why ExecStop is not simply "targetctl save":
# targetctl save writes the LIO config to JSON but does NOT remove the LIO
# target from the kernel's configfs. As a result, any fileio backing store
# that LIO has open (e.g. iscsi-lun.img on an XFS-over-DRBD filesystem)
# stays referenced in the kernel. The subsequent XFS umount from the
# Filesystem OCF resource then returns EBUSY and either hangs for the full
# op-stop timeout or fails outright, blocking the entire failover.
#
# The ExecStop script here additionally tears down the kernel LIO state
# via rtslib_fb after saving, so the backing-store file descriptor is
# released and umount succeeds immediately.
#
# Empty-config guard:
# The save step is skipped when no iSCSI targets are currently active.
# This prevents the secondary node (where LIO was never started) from
# overwriting a valid saveconfig.json with an empty one when Pacemaker
# stops the iscsi-target resource as part of a failover or cleanup.
{ pkgs, ... }:
let
python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]);
targetctl = "${python3}/bin/targetctl";
targetctlStop = pkgs.writeScript "targetctl-stop" ''
#!${python3}/bin/python3
import subprocess, sys
import rtslib_fb
root = rtslib_fb.RTSRoot()
targets = list(root.targets)
if targets:
subprocess.run(
["${targetctl}", "save", "/etc/target/saveconfig.json"],
capture_output=True,
)
print(f"saved {len(targets)} iSCSI target(s)")
else:
print("no active LIO targets — saveconfig.json unchanged")
for target in targets:
try:
for tpg in list(target.tpgs):
tpg.enable = False
target.delete()
except Exception as e:
print(f"warn (target): {e}", file=sys.stderr)
for so in list(root.storage_objects):
try:
so.delete()
except Exception as e:
print(f"warn (backstore): {e}", file=sys.stderr)
print("LIO kernel target cleared")
'';
in
{
boot.kernelModules = [
"target_core_mod"
"iscsi_target_mod"
"target_core_file"
"target_core_pscsi"
"target_core_user"
"configfs"
];
systemd = {
mounts = [{
where = "/sys/kernel/config";
what = "configfs";
type = "configfs";
wantedBy = [ "multi-user.target" ];
before = [ "targetctl.service" ];
}];
services.targetctl = {
description = "LIO iSCSI target config save/restore";
wantedBy = [ "multi-user.target" ];
after = [ "sys-kernel-config.mount" "network.target" ];
requires = [ "sys-kernel-config.mount" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${targetctl} restore /etc/target/saveconfig.json";
ExecStop = "${targetctlStop}";
};
unitConfig.ConditionFileNotEmpty = "/etc/target/saveconfig.json";
};
tmpfiles.rules = [
"d /etc/target 0750 root root -"
"f /etc/target/saveconfig.json 0640 root root -"
];
};
environment.systemPackages = [ pkgs.targetcli-fb ];
}
-94
View File
@@ -1,94 +0,0 @@
# Pacemaker + Corosync HA stack for NixOS with known-good workarounds.
#
# Issues fixed here (confirmed through live testing on NixOS 25.11):
#
# 1. StateDirectory ownership reset: systemd's StateDirectory=pacemaker
# creates /var/lib/pacemaker owned root:root. pacemaker-based (the CIB
# daemon) runs as the hacluster user and calls pcmk__daemon_can_write,
# which requires the CIB directory to be owned by hacluster or be
# group-writable by haclient. Workaround: remove StateDirectory and let
# ExecStartPre create every required subdirectory with correct ownership.
#
# 2. HA_SBIN_DIR wrong path: ocf-shellfuncs sets HA_SBIN_DIR to the Nix
# store path of the resource-agents derivation's /sbin, which doesn't
# exist. The DRBD OCF agent uses ${HA_SBIN_DIR}/crm_master, so it exits
# 127 without this override. Fix: export HA_SBIN_DIR=/run/current-system/sw/bin.
#
# 3. Broad PATH for OCF agents: the resource executor (pacemaker-execd) runs
# OCF agent scripts as children. NixOS provides no implicit PATH for
# system services; without an explicit PATH the agents can't find ip, ss,
# mount, umount, drbdadm, etc.
#
# 4. FUSER=true: the Filesystem OCF agent calls check_binary $FUSER (default:
# fuser from psmisc), which is not installed. Setting FUSER=true makes
# check_binary succeed (true is always in PATH) and the subsequent
# "$FUSER -km $mountpoint" becomes a no-op. Pair with force_unmount=false
# on each Filesystem resource unless you want lazy unmount behaviour.
{ lib, pkgs, ... }:
let
ocfBinPath = lib.concatStringsSep ":" [
"${pkgs.iproute2}/bin"
"${pkgs.iproute2}/sbin"
"${pkgs.iputils}/bin"
"${pkgs.util-linux}/bin"
"${pkgs.util-linux}/sbin"
"${pkgs.gawk}/bin"
"${pkgs.gnugrep}/bin"
"${pkgs.gnused}/bin"
"${pkgs.coreutils}/bin"
"${pkgs.bash}/bin"
"${pkgs.procps}/bin"
"${pkgs.xfsprogs}/bin"
"${pkgs.drbd}/bin"
"${pkgs.python3}/bin"
"/run/current-system/sw/bin"
"/run/current-system/sw/sbin"
"/usr/local/sbin"
"/usr/local/bin"
"/usr/sbin"
"/usr/bin"
"/sbin"
"/bin"
];
# Single pre-start script: schemas symlink + directory ownership.
# Runs before pacemakerd so pacemaker-based finds hacluster-owned dirs.
preStartCmd = "${pkgs.bash}/bin/bash -c '"
+ "ln -sfn ${pkgs.pacemaker}/share/pacemaker /var/lib/pacemaker/schemas; "
+ "for d in /var/lib/pacemaker /var/lib/pacemaker/cib /var/lib/pacemaker/cores "
+ "/var/lib/pacemaker/pengine /var/lib/pacemaker/blackbox "
+ "/var/lib/pacemaker/hostcache; do "
+ "mkdir -p \"\\$d\" && chown hacluster:pacemaker \"\\$d\" && chmod 2770 \"\\$d\"; "
+ "done'";
ocfEnv = {
PATH = lib.mkForce ocfBinPath;
OCF_ROOT = "${pkgs.ocf-resource-agents}/usr/lib/ocf";
HA_SBIN_DIR = "/run/current-system/sw/bin";
FUSER = "true";
};
in
{
users.groups.haclient = { };
services.corosync.enable = true;
services.pacemaker.enable = true;
systemd.services = {
pacemaker = {
serviceConfig = {
StateDirectory = lib.mkForce "";
ExecStartPre = lib.mkBefore [ preStartCmd ];
};
environment = ocfEnv;
};
pacemaker-execd.environment = ocfEnv;
};
environment.systemPackages = with pkgs; [
corosync
pacemaker
ocf-resource-agents
];
}
@@ -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
];
}
-210
View File
@@ -1,210 +0,0 @@
# Fully declarative FreeIPA domain membership.
#
# Imported by modules/common/configuration.nix — no per-host wiring needed.
# Enables itself automatically on any host that has a sops-encrypted keytab
# at secrets/<hostname>.keytab; is a no-op for all other hosts.
#
# To enroll a new host:
# 0. scripts/secrets/sync-host-keys.sh <flake-target>
# 1. scripts/ipa/create-nixos-ipa-host-account.sh [--ip <addr>] <hostname>
# (adds .sops.yaml rule, runs ipa host-add, encrypts keytab in one step)
# 2. git add secrets/<hostname>.keytab .sops.yaml && git commit
# 3. Deploy — no further steps required.
#
# Manual fallback (if the script isn't usable):
# a. On the FreeIPA server: ipa host-add <fqdn> [--ip-address=<ip>] --force
# b. On the FreeIPA server: ipa-getkeytab -s <ipa-server> -p host/<fqdn> -k /tmp/<host>.keytab
# c. From the repo root (path must match for sops creation rule to apply):
# cp /tmp/<host>.keytab secrets/<host>.keytab
# sops -e --input-type binary -i secrets/<host>.keytab
# d. Commit secrets/<host>.keytab and the updated .sops.yaml, then deploy.
#
# vars dependencies: homeDomain, ipaServer, domainControllerIp, ipaUser
{ config, lib, pkgs, vars, ... }:
let
keytabPath = ../../secrets + "/${config.networking.hostName}.keytab";
enabled = builtins.pathExists keytabPath;
realm = lib.strings.toUpper vars.homeDomain;
fqdn = "${config.networking.hostName}.${vars.homeDomain}";
# "sweet.home" -> "dc=sweet,dc=home"
basedn = lib.strings.concatMapStringsSep "," (c: "dc=${c}") (lib.strings.splitString "." vars.homeDomain);
# security.ipa.certificate expects a derivation (package), not a raw path.
caCertPkg = pkgs.writeText "ipa-ca.crt" (builtins.readFile ../../certs/ipa-ca.crt);
in
lib.mkIf enabled {
networking.domain = lib.mkDefault vars.homeDomain;
networking.nameservers = lib.mkDefault [ vars.domainControllerIp ];
security = {
ipa = {
enable = true;
domain = vars.homeDomain;
inherit realm;
server = vars.ipaServer;
certificate = caCertPkg;
inherit basedn;
ipaHostname = fqdn;
offlinePasswords = true;
cacheCredentials = true;
};
# Create the home directory on first login if it doesn't exist yet.
# IPA users have no pre-created home on the host; without this sshd
# opens a session to a non-existent directory and resets the connection.
# lightdm also needs this so the GUI login path can create the home dir
# if it was not pre-seeded by the tmpfiles rule above (e.g. on first boot
# before SSSD has resolved the user).
pam.services = {
sshd.makeHomeDir = true;
lightdm.makeHomeDir = true;
# pam_unix returns PAM_AUTHINFO_UNAVAIL without prompting when the local
# stub has "!" in shadow (account locked), so PAM_AUTHTOK is never set
# and pam_sss's use_first_pass fails with "No authentication token".
# Changing to try_first_pass makes pam_sss prompt independently when no
# prior module has set the token, restoring IPA password login via
# LightDM and su.
login.rules.auth.sss.settings = lib.mkForce { try_first_pass = true; };
su.rules.auth.sss.settings = lib.mkForce { try_first_pass = true; };
};
# HM with useUserPackages = true (flake.nix) sets users.users.${ipaUser}.packages,
# which forces the stub into /etc/passwd. pam_sss.so with the "localusers" flag
# (added by NixOS when SSSD is enabled) then skips SSSD for any user it finds in
# local /etc/passwd — including this stub — falling through to pam_unix, which has
# no password for the stub → sudo auth always fails.
#
# Fix: NOPASSWD for the IPA user. The IPA user already authenticated to reach a
# shell (SSH public key from IPA or Kerberos), so re-prompting via a broken PAM
# path is security theater on a single-admin homelab.
sudo.extraRules = [{
users = [ vars.ipaUser ];
commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
}];
};
systemd = {
# Fetch SSH public keys from IPA so users can log in with the key stored
# in their IPA profile rather than needing ~/.ssh/authorized_keys on every
# host. sss_ssh_authorizedkeys queries SSSD (which queries IPA LDAP).
#
# /nix/store is 1775 (group-writable by nixbld). OpenSSH 10.0+ rejects
# AuthorizedKeysCommand binaries whose path contains any group-writable
# component, silently skipping the command. Copy to /usr/local/bin (all
# components root-owned, 755) so the path passes sshd's safety check.
tmpfiles.rules = [
"d /usr/local 0755 root root - -"
"d /usr/local/bin 0755 root root - -"
"C+ /usr/local/bin/sss_ssh_authorizedkeys 0555 root root - ${pkgs.sssd}/bin/sss_ssh_authorizedkeys"
# Pre-create the IPA user's home dir so Home Manager activation succeeds
# even before their first login. On a fresh system SSSD may not have
# resolved the user yet — tmpfiles warns and skips in that case (non-fatal),
# and pam_mkhomedir covers the first-login path as a fallback.
"d /home/${vars.ipaUser} 0700 ${vars.ipaUser} ${vars.ipaUser} - -"
];
# security.ipa enables Kerberos (security.krb5) which causes systemd to
# start auth-rpcgss-module.service and rpc-gssd.service for Kerberos NFS
# authentication. LXC containers can't load the auth_rpcgss kernel module
# and don't have /var/lib/nfs/rpc_pipefs, so both services fail.
#
# The NixOS IPA module already adds a drop-in for auth-rpcgss-module.service
# with ConditionPathExists=/etc/krb5.keytab. We use lib.mkForce to win the
# text conflict and add ConditionVirtualization=!container alongside it so
# the service is skipped (not failed) in containers that do have a keytab.
# Same fix for rpc-gssd.service which also fails in containers.
units = lib.mkIf config.boot.isContainer {
"auth-rpcgss-module.service" = {
overrideStrategy = "asDropinIfExists";
text = lib.mkForce ''
[Unit]
ConditionPathExists=
ConditionPathExists=/etc/krb5.keytab
ConditionVirtualization=!container
'';
};
# rpc-gssd also has ConditionPathExists from the NixOS IPA module (and an
# X-Restart-Triggers store path from systemd.nix). Use mkForce to win;
# omit X-Restart-Triggers since this service is skipped in containers anyway.
"rpc-gssd.service" = {
overrideStrategy = "asDropinIfExists";
text = lib.mkForce ''
[Unit]
ConditionPathExists=
ConditionPathExists=/etc/krb5.keytab
ConditionVirtualization=!container
'';
};
};
# home-manager-<user>.service fails on first enrollment because /home/wayne
# doesn't exist until the user's first login (pam_mkhomedir creates it then).
# ConditionPathExists makes systemd skip the service (exit 0, condition not
# met) instead of failing. After first login the dir exists and subsequent
# rebuilds activate HM normally.
services."home-manager-${vars.ipaUser}".unitConfig.ConditionPathExists =
"/home/${vars.ipaUser}";
};
services.openssh.extraConfig = ''
AuthorizedKeysCommand /usr/local/bin/sss_ssh_authorizedkeys %u
AuthorizedKeysCommandUser nobody
'';
# Host keytab: pre-provisioned on the IPA server, sops-encrypted binary.
# Placed at /etc/krb5.keytab before SSSD starts so the host authenticates
# to IPA without running ipa-client-install.
sops.secrets."ipa-host-keytab" = {
sopsFile = keytabPath;
format = "binary";
path = "/etc/krb5.keytab";
owner = "root";
group = "root";
mode = "0600";
restartUnits = [ "sssd.service" ];
};
# NixOS requires isNormalUser/isSystemUser + group on any entry in
# users.users. HM with useUserPackages = true (set in flake.nix) adds a stub
# entry for each HM user so it can install packages to
# /etc/profiles/per-user/<name>/. This definition satisfies those assertions.
# With security.ipa setting "passwd: sss files" in nsswitch, SSSD's IPA entry
# takes priority for NSS lookups — this local stub is only a fallback when
# SSSD is unreachable (at which point auth fails anyway).
users.users.${vars.ipaUser} = {
isNormalUser = true;
group = "users";
extraGroups = [ "wheel" ];
createHome = false;
# "!" is not a password hash — it is the standard "account locked" marker.
# It cannot authenticate anyone locally. It exists solely so NixOS generates
# a shadow entry for this stub user; without one pam_unix returns
# PAM_AUTHINFO_UNAVAIL before prompting, which means PAM_AUTHTOK is never
# set and the subsequent pam_sss use_first_pass call has nothing to work
# with — blocking LightDM and su logins even when IPA/SSSD auth succeeds.
hashedPassword = "!";
};
# Home Manager config for the IPA primary user, applied on every enrolled
# host. Manages what IPA doesn't: dotfiles, user-scoped packages, session
# variables. Switch-nix/Test-nix/buildImage are system-wide (configuration.nix)
# so they don't need to be repeated here.
#
# homeDirectory uses mkForce because HM's NixOS integration module sets it to
# "/var/empty" for users not found in config.users.users at eval time (SSSD
# users aren't visible there).
home-manager.users.${vars.ipaUser} = { pkgs, ... }: {
home = {
username = vars.ipaUser;
homeDirectory = lib.mkForce "/home/${vars.ipaUser}";
stateVersion = "26.05";
packages = with pkgs; [ tmux sshfs ];
sessionVariables.EDITOR = lib.mkDefault "nano";
};
programs.home-manager.enable = true;
programs.bash.enable = true;
};
}
@@ -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;
};
}
-208
View File
@@ -1,208 +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")
../common/preserve-ssh-host-key.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).
#
# Any lxc-* host with an NFS fileSystem must be privileged: 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). Deriving this from
# fileSystems rather than a per-host override keeps it self-consistent:
# any new lxc-* host that declares an NFS mount automatically gets the
# privilege level it needs without a separate manual flag.
privileged = builtins.any
(fs: fs.fsType == "nfs" || fs.fsType == "nfs4")
(builtins.attrValues config.fileSystems);
};
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";
};
};
# 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
'';
};
}
-50
View File
@@ -1,50 +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
../common/preserve-ssh-host-key.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";
};
};
}
-42
View File
@@ -1,42 +0,0 @@
{ config, lib, vars, ... }:
let
# FQDN of the LAN NFS VIP (Pacemaker vip-lan, 192.168.2.229). Defined in
# variables.nix as haLanNfsFqdn; using the FQDN avoids systemd-resolved
# LLMNR quirks and survives a future VIP renumber via a DNS-only update.
nfsServer = vars.haLanNfsFqdn;
in
{
fileSystems.${vars.nfsShares.pxebootImages.mountpoint} = {
device = "${nfsServer}:${vars.haStorageRoot}/${vars.nfsShares.pxebootImages.subpath}";
fsType = "nfs";
options = [
"_netdev"
"noatime"
] ++ (if config.boot.isContainer
# NFSv4 requires rpc_pipefs (sunrpc filesystem), which Proxmox LXC
# containers block unless `features: mount=nfs` is set. Use NFSv3+nolock
# instead: no rpc_pipefs dependency at the protocol level, and rpcbind
# on the server handles port resolution without needing client-side
# sunrpc infrastructure. nofail keeps boot clean if server is unreachable.
then [ "nfsvers=3" "proto=tcp" "nolock" "nofail" ]
else [ "nfsvers=4.2" "x-systemd.automount" ]);
};
# NixOS pulls var-lib-nfs-rpc_pipefs.mount (the sunrpc filesystem) into
# nfs-client.target for any nfs fileSystems entry. In LXC containers the
# sunrpc mount is blocked by Proxmox's AppArmor profile, causing it to fail
# and the activation to report an error even though our mount uses nofail.
# Add ConditionVirtualization=!container via drop-in so systemd skips the
# unit entirely in containers (skip = inactive, not failed), which keeps
# nfs-client.target green and activation clean.
systemd.units = lib.mkIf config.boot.isContainer {
"var-lib-nfs-rpc_pipefs.mount" = {
overrideStrategy = "asDropin";
text = ''
[Unit]
ConditionVirtualization=!container
'';
};
};
}
@@ -1,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
'';
};
};
}
-53
View File
@@ -1,53 +0,0 @@
{ vars, ... }:
{
# Run dnsmasq on the LAN interface as a forwarding-only resolver for
# *.ts.net (Tailscale MagicDNS names). FreeIPA's bind-dyndb-ldap
# cannot reach vars.tailscaleResolverIp directly because the DC is not a
# Tailscale node. This host IS a Tailscale node and can reach it via
# tailscale0, so it acts as an intermediary: FreeIPA has a conditional
# forward zone for ts.net pointing here (vars.tailscaleRouterIp), and this
# dnsmasq instance forwards those queries onward to Tailscale's resolver.
#
# Configure FreeIPA once after deploying this host:
# kinit admin
# ipa dnsforwardzone-add ${vars.tailnetDomain} \
# --forwarder=${vars.tailscaleRouterIp} \
# --forward-policy=only
# Note: IPA refuses to shadow ts.net (a real public TLD); use the
# tailnet-specific subdomain (vars.tailnetDomain) instead.
services.dnsmasq = {
enable = true;
# NixOS's dnsmasq module defaults resolveLocalQueries to true, which adds
# 127.0.0.1 to networking.nameservers and makes dnsmasq bind to
# listen-address=127.0.0.1. This instance is not the host's local
# resolver — it only serves IPA's conditional forwarder for tailnet names.
# The host uses domainControllerIp directly (networking.nameservers in
# host.nix). Without this, all host DNS goes through dnsmasq, which has
# no upstream for general queries (no-resolv=true), breaking resolution.
resolveLocalQueries = false;
settings = {
# Listen only on the LAN interface — not tailscale0 or loopback.
# bind-interfaces prevents dnsmasq from binding to 0.0.0.0 and then
# filtering by interface later; combined with `interface` this ensures
# it genuinely listens only on eth0.
bind-interfaces = true;
interface = [ vars.lxcLanInterface ];
# Forward-only: no local /etc/hosts or /etc/resolv.conf reading, no
# negative caching of NXDOMAIN for names this instance doesn't serve.
# All ts.net queries come from FreeIPA's conditional forwarder and must
# be answered by Tailscale's resolver.
no-hosts = true;
no-resolv = true;
# Forward *.tailnetDomain to Tailscale's internal resolver, scoped to
# the tailnet-specific subdomain rather than all of ts.net (FreeIPA
# refuses to shadow ts.net, a real public TLD).
server = [ "/${vars.tailnetDomain}/${vars.tailscaleResolverIp}" ];
};
};
networking.firewall.allowedUDPPorts = [ vars.ports.dns ];
networking.firewall.allowedTCPPorts = [ vars.ports.dns ];
}
-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;
};
};
};
}
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."
-191
View File
@@ -1,191 +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
# Matches variables.nix's homeDomain -- the base LAN domain for service
# subdomains, FreeIPA Kerberos realm, and host FQDNs.
: "${HOME_DOMAIN:=sweet.home}"
export HOME_DOMAIN
# Matches variables.nix's ipaServer -- the FreeIPA server hostname.
# scripts/ipa/create-nixos-ipa-host-account.sh SSHes here to run
# ipa host-add and ipa-getkeytab.
: "${IPA_SERVER:=domain-controller.sweet.home}"
export IPA_SERVER
# nix_extra_opts: call as a plain statement (NOT inside $(...)/<(...) --
# 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
}
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
# gc-hosts.sh — Run nix-collect-garbage -d on all live NixOS hosts.
#
# The host list is rebuilt on every run:
# 1. This workstation (nixos) — always first
# 2. pve1 — always second (non-NixOS Proxmox node with Nix installed)
# 3. Every NixOS guest currently running on pve1 (discovered via pct/qm list)
#
# nix-cache is excluded: gc-ing the shared binary cache evicts store paths
# that other hosts depend on for substitution.
#
# NixOS hosts: tries "sudo -n nix-collect-garbage -d" first (works when
# wheelNeedsPassword = false, e.g. the HA cluster). Falls back to user-level
# "nix-collect-garbage -d" if sudo needs a password — still collects
# unreferenced store paths and old nixos-user profile generations, but leaves
# old system generations in place.
# pve1: runs "bash -l -c nix-collect-garbage -d" as the login user so
# /etc/profile is sourced and the Nix daemon's PATH is set up automatically.
#
# Usage (from repo root):
# bash scripts/gc-hosts.sh [--dry-run]
set -euo pipefail
cd "$(dirname "$0")/.."
source scripts/env.sh 2>/dev/null || true
source scripts/lib/nix-eval.sh 2>/dev/null || true
# ── config ────────────────────────────────────────────────────────────────────
: "${MAX_JOBS:=8}"
: "${NIXOS_USER:=nixos}"
: "${PVE1_SSH_USER:=${PROXMOX_SSH_USER:-wayne}}"
# GC connections use BatchMode — no interactive prompts, just succeed or fail.
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=10)
# Discovery connections do NOT use BatchMode so that sudo can prompt if needed
# (pct/qm list require root access on Proxmox).
SSH_QUERY_OPTS=(-o StrictHostKeyChecking=no -o ConnectTimeout=10)
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
*) echo "Unknown option: $arg" >&2; exit 1 ;;
esac
done
# ── build the host list ───────────────────────────────────────────────────────
# ORDERED_HOSTS: names in display/execution order.
# HOST_TARGET[name]: SSH target string (user@host).
# HOST_TYPE[name]: "nixos" (try sudo gc, fallback user) | "nix" (login-shell gc).
declare -a ORDERED_HOSTS=()
declare -A HOST_TARGET=()
declare -A HOST_TYPE=()
declare -A _SEEN_HOSTNAMES=() # dedup tracker
_add_host() {
local name="$1" target="$2" type="$3"
if [[ -n "${_SEEN_HOSTNAMES[$name]+_}" ]]; then return; fi
_SEEN_HOSTNAMES[$name]=1
ORDERED_HOSTS+=("$name")
HOST_TARGET[$name]="$target"
HOST_TYPE[$name]="$type"
}
# 1. Workstation (hard-wired first)
_add_host "nixos" "${NIXOS_USER}@nixos" "nixos"
# 2. pve1 (hard-wired second; non-NixOS, no system generations)
_add_host "pve1" "${PVE1_SSH_USER}@${PVE1_HOST}" "nix"
# 3. Dynamically discover running NixOS guests on pve1
#
# create-proxmox-resource.sh names every guest after its NixOS hostname:
# pct create ... --hostname <nixos-hostname> (LXC)
# qm create ... --name <nixos-hostname> (VM)
# So pct/qm list output already contains the NixOS hostname directly.
# We validate against the flake to filter out non-NixOS guests on pve1
# (e.g. FreeIPA, Proxmox Backup Server) that share the same Proxmox node.
echo "Discovering running guests on ${PVE1_HOST}..."
# Eval the flake once to get the set of hostnames that are actually NixOS.
# Values are NixOS hostnames (e.g. "docker"); keys are flake targets ("lxc-docker").
nixos_hostnames=""
nixos_hostnames="$(
nix eval --json "${NIX_EVAL_FLAGS[@]}" .#nixosConfigurations \
--apply 'cfgs: builtins.attrValues (builtins.mapAttrs (_: cfg: cfg.config.networking.hostName) cfgs)' \
2>/dev/null | jq -r '.[]' | sort -u
)" || { echo " warning: flake eval failed — non-NixOS guests will not be filtered" >&2; }
# SSH_QUERY_OPTS (no BatchMode) so sudo can prompt if needed for pct/qm.
if ssh "${SSH_QUERY_OPTS[@]}" "${PVE1_SSH_USER}@${PVE1_HOST}" "true" 2>/dev/null; then
running_guests="$(
ssh "${SSH_QUERY_OPTS[@]}" "${PVE1_SSH_USER}@${PVE1_HOST}" bash -s <<'DISCOVER'
sudo pct list 2>/dev/null | awk 'NR>1 && $2=="running" { print $NF }'
sudo qm list 2>/dev/null | awk 'NR>1 && $3=="running" { print $2 }'
DISCOVER
)" || running_guests=""
while IFS= read -r hostname; do
[[ -z "$hostname" ]] && continue
# Exclude nix-cache.
case "$hostname" in *nix-cache*) continue ;; esac
# Skip if not a flake-managed NixOS host (filters non-NixOS pve1 guests).
if [[ -n "$nixos_hostnames" ]] && ! grep -qxF "$hostname" <<< "$nixos_hostnames"; then
continue
fi
# Skip if already in the list (e.g. a proxmox-gui guest whose hostname is nixos).
if [[ -n "${_SEEN_HOSTNAMES[$hostname]+_}" ]]; then continue; fi
echo " + $hostname"
_add_host "$hostname" "${NIXOS_USER}@${hostname}" "nixos"
done <<< "$(echo "$running_guests" | sort -u)"
else
echo " warning: ${PVE1_HOST} unreachable — skipping dynamic host discovery" >&2
fi
echo ""
echo "Hosts: ${ORDERED_HOSTS[*]}"
echo ""
# ── dry-run ───────────────────────────────────────────────────────────────────
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "[dry-run] commands that would run:"
for host in "${ORDERED_HOSTS[@]}"; do
target="${HOST_TARGET[$host]}"
type="${HOST_TYPE[$host]}"
if [[ "$type" == "nixos" ]]; then
echo " ssh ${SSH_OPTS[*]} $target 'sudo -n nix-collect-garbage -d'"
echo " # fallback: ssh ... $target 'nix-collect-garbage -d'"
else
echo " ssh ${SSH_OPTS[*]} $target 'bash -l -c nix-collect-garbage -d'"
fi
done
exit 0
fi
# ── gc worker ─────────────────────────────────────────────────────────────────
gc_one() {
local host="$1" target="${HOST_TARGET[$1]}" type="${HOST_TYPE[$1]}" logfile="$2"
if ! ssh "${SSH_OPTS[@]}" "$target" "true" 2>>"$logfile"; then
echo "unreachable"; return
fi
if [[ "$type" == "nixos" ]]; then
if ssh "${SSH_OPTS[@]}" "$target" "sudo -n nix-collect-garbage -d" \
>>"$logfile" 2>>"$logfile"; then
echo "ok(sudo)"; return
fi
echo "[sudo needs password — falling back to user-level gc]" >>"$logfile"
if ssh "${SSH_OPTS[@]}" "$target" "nix-collect-garbage -d" \
>>"$logfile" 2>>"$logfile"; then
echo "ok(user)"; return
fi
else
# Non-NixOS node: use a login shell so /etc/profile is sourced and the
# Nix daemon's bin dir is on PATH (set up by /etc/profile.d/nix-daemon.sh
# which the Nix installer adds to /etc/profile).
if ssh "${SSH_OPTS[@]}" "$target" "bash -l -c 'nix-collect-garbage -d'" \
>>"$logfile" 2>>"$logfile"; then
echo "ok"; return
fi
fi
echo "failed:$?"
}
# ── parallel execution ────────────────────────────────────────────────────────
echo "Running gc on ${#ORDERED_HOSTS[@]} hosts (up to ${MAX_JOBS} parallel)..."
echo ""
TMPDIR_GC="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_GC"' EXIT
declare -A LOGS=()
job_count=0
for host in "${ORDERED_HOSTS[@]}"; do
logfile="${TMPDIR_GC}/${host}.log"
resultfile="${TMPDIR_GC}/${host}.result"
LOGS[$host]="$logfile"
: > "$logfile"
( result="$(gc_one "$host" "$logfile")"; echo "$result" > "$resultfile" ) &
(( job_count++ )) || true
if [[ "$job_count" -ge "$MAX_JOBS" ]]; then
wait -n 2>/dev/null || wait
(( job_count-- )) || true
fi
done
wait
# ── summary ───────────────────────────────────────────────────────────────────
echo "Results:"
echo "──────────────────────────────"
ok_hosts=()
warn_hosts=()
fail_hosts=()
for host in "${ORDERED_HOSTS[@]}"; do
result="$(cat "${TMPDIR_GC}/${host}.result" 2>/dev/null || echo "failed:missing")"
case "$result" in
ok|"ok(sudo)"|"ok(user)")
printf " %-22s %s\n" "$host" "$result"
ok_hosts+=("$host") ;;
unreachable)
printf " %-22s UNREACHABLE\n" "$host"
warn_hosts+=("$host") ;;
*)
printf " %-22s FAILED (%s)\n" "$host" "$result"
fail_hosts+=("$host") ;;
esac
done
echo ""
echo " ${#ok_hosts[@]} succeeded, ${#warn_hosts[@]} unreachable, ${#fail_hosts[@]} failed"
for host in "${warn_hosts[@]+"${warn_hosts[@]}"}" "${fail_hosts[@]+"${fail_hosts[@]}"}"; do
logfile="${LOGS[$host]}"
if [[ -s "$logfile" ]]; then
echo ""
echo "── $host ──"
cat "$logfile"
fi
done
echo ""
[[ "${#fail_hosts[@]}" -eq 0 ]]
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff. Show more