Compare 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
100 changed files with 1078 additions and 6917 deletions

No files matched your search

Submodule .claude/worktrees/scripts-dedup deleted from e578443914.
-29
View File
@@ -1,29 +0,0 @@
name: Check NixOS configurations
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
eval-hosts:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19
# Scoped to files changed since the PR base / previous push -- see
# scripts/codex-maintenance.sh. CI never passes --full-check: that
# full sweep is for local/manual use, since it's slow enough to time
# out this runner.
- name: Run maintenance checks (secrets, fmt, lint, eval -- changed files only)
env:
MAINT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: bash scripts/codex-maintenance.sh
-32
View File
@@ -1,32 +0,0 @@
name: Update flake.lock
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
jobs:
update-flake-lock:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19
- name: Update and commit flake.lock
run: |
set -euo pipefail
nix --extra-experimental-features 'nix-command flakes' flake update
if git diff --quiet -- flake.lock; then
echo "No flake.lock changes detected"
exit 0
fi
git config user.name "gitea-actions"
git config user.email "gitea-actions@nix-cache.local"
git add flake.lock
git commit -m "chore: update flake.lock"
git push
-11
View File
@@ -1,11 +0,0 @@
#!/usr/bin/env bash
# Blocks commits containing secrets. Installed via:
# git config core.hooksPath .githooks
# (scripts/codex-setup.sh does this automatically in Codex sessions.)
set -euo pipefail
if command -v gitleaks >/dev/null 2>&1; then
gitleaks protect --staged -v
else
nix-shell -p gitleaks --run "gitleaks protect --staged -v"
fi
-29
View File
@@ -1,29 +0,0 @@
name: Check NixOS configurations
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
eval-hosts:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19
# Scoped to files changed since the PR base / previous push -- see
# scripts/codex-maintenance.sh. CI never passes --full-check: that
# full sweep is for local/manual use, since it's slow enough to time
# out this runner.
- name: Run maintenance checks (secrets, fmt, lint, eval -- changed files only)
env:
MAINT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: bash scripts/codex-maintenance.sh
-37
View File
@@ -1,37 +0,0 @@
name: Update flake.lock
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-flake-lock:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v19
- name: Update flake.lock
run: |
nix --extra-experimental-features 'nix-command flakes' flake update
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
add-paths: flake.lock
branch: chore/update-flake-lock
title: "chore: update flake.lock"
commit-message: "chore: update flake.lock"
body: |
This is an automated update of `flake.lock` generated by the scheduled workflow.
It updates pinned flake inputs so dependency updates can be reviewed and merged via PR.
+3 -17
View File
@@ -3,23 +3,9 @@
result result
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
-51
View File
@@ -1,51 +0,0 @@
keys:
- &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
- &lxc-nix-cache age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
- &lxc-docker age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
- &lxc-minimal age1jy444f9d9stygj4p3w9kh54cqcfr654tvr75tdvee5cxsgtdtc9q3v60ep
- &lxc-pxe-boot age1fxxzpnfse8nd9wz78ht3m0plrmraacf4cpga0pe8fm2tdnqcgy8q7qsyvp
- &lxc-gui age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
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
- *docker
- *server
- *nix-cache
- *lxc-minimal
- *nix-minimal
- *lxc-nix-cache
- *proxmox-minimal
- *lxc-docker
- *lxc-pxe-boot
- *lxc-gui
- path_regex: secrets/nix-cache\.yaml$
key_groups:
- age:
- *admin
- *nix-cache
- *lxc-nix-cache
- path_regex: secrets/server\.yaml$
key_groups:
- age:
- *admin
- *server
- path_regex: secrets/docker\.yaml$
key_groups:
- age:
- *admin
- *docker
-50
View File
@@ -1,50 +0,0 @@
# AGENTS.md
## Repo purpose
This repository contains flake-based NixOS configurations for Wayne's LAN
servers and workstation.
The flake exposes NixOS configurations named `<platform>-<buildtype>`
(platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`,
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`), generated from `modules/platforms/*`
and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
every combination is built — `pxe-boot` has no `linode` variant. See
`README.md` for the full current target list; treat `flake.nix` as the
source of truth since this list can drift.
Do not deploy, switch, reboot, repartition, format disks, or run destructive
install commands from this repository unless explicitly asked.
## Safety rules
- Never run `nixos-rebuild switch`, `boot`, `test`, `nixos-install`, `parted`,
`mkfs`, `mkswap`, `swapon`, `mount`, or destructive disk commands in Codex.
- Validation work should be limited to evaluation, linting, formatting checks,
and `nix build --dry-run --no-link`.
- Do not add secrets, tokens, private keys, password hashes, or live credentials
to the repo.
- Treat `flake.nix`, Home Manager config, and Nix config files as public.
- If you find committed tokens or hashes, flag them immediately and recommend
rotation/removal.
## Expected commands
Use these commands when validating changes:
```bash
bash scripts/codex-setup.sh
bash scripts/codex-maintenance.sh
```
With no flags, `codex-maintenance.sh` scopes fmt-check/statix/eval to files
changed against a base ref — this is what CI runs on every push/PR. For the
full sweep (every host, every package — slow; CI never runs this), use
`bash scripts/codex-maintenance.sh --full-check` (add `--dry-run` for build
planning on top of whichever scope is active).
Host evaluation is safe when limited to drvPath checks:
```bash
nix eval .#nixosConfigurations.<host>.config.system.build.toplevel.drvPath --raw
```
-348
View File
@@ -1,348 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repo purpose
Flake-based NixOS configuration for Wayne's LAN servers and workstation. There is
no application code here — changes are Nix module edits that affect real
machines when deployed.
## Safety rules (read before touching anything)
- **Never** run `nixos-rebuild switch|boot|test`, `nixos-install`, `parted`,
`mkfs`, `mkswap`, `swapon`, `mount`, or any other destructive disk/deploy
command from an agent session, even if asked indirectly. Deployment is done
manually by the operator on the target host.
- Validation is limited to evaluation, linting, formatting checks, and
`nix build --dry-run --no-link`.
- Do not add secrets, tokens, private keys, or new password hashes to the repo.
- This repo currently contains **committed password hashes** in
`modules/installer/common.nix` (the auto-installer's own root/nixos login —
a deliberate, documented choice, see `docs/auto-installer.md`, not
accidental tech debt) and **SSH public keys** in `variables.nix`
(`vars.adminSshKey`, `vars.remoteBuilderAuthorizedKeys`) plus a couple of
per-host `KEY` values for beszel-agent auth (`hosts/server/host.nix`,
`hosts/nix-cache/host.nix`). Don't use the installer's hardcoded hash as a
template for a *real* host — every other host uses sops-nix
(`hashedPasswordFile`, see "Security Notes" in `README.md`). Flag any *new*
secret-like string you encounter instead of committing it.
- `host-keys/` is gitignored — locally-generated *private* SSH host keys for
the auto-installer (see `docs/auto-installer.md`). Never commit its
contents; if `git status` ever shows it as trackable, something is wrong.
## 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/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/secrets/`
- `scripts/secrets/sync-host-keys.sh` — generates/registers SSH host keys
and their `.sops.yaml`/`secrets/*.yaml` recipients for flake targets,
idempotently (`--all`, `<target>`, `--remove`, `--regenerate-all-keys`,
all with `--dry-run`). The primary tool for provisioning a new host's
secrets access — see "Creating a new machine" in `docs/auto-installer.md`.
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a
key by an arbitrary name without touching `.sops.yaml`. Still useful to
pre-generate a key before its flake target exists yet, since
`sync-host-keys.sh` can only act on targets `nixosConfigurations` already
has.
- `scripts/secrets/rotate-admin-key.sh <backup-admin-key> [--new-key-file
<path>] [--dry-run]` — rotates `.sops.yaml`'s `&admin` age key: decrypts
with a backed-up copy of the key currently trusted as `&admin` (verified
by deriving its public key and comparing, not taken on faith), replaces
the `&admin` line with a new key already present in the environment
(defaults to wherever sops/age itself would look), and runs
`sops updatekeys` on every `secrets/*.yaml`. One-way: the old key can no
longer decrypt anything re-encrypted this way. This is the automation
for the manual steps `sync-host-keys.sh`/`create-proxmox-resource.sh`
print when they bootstrap a brand-new, not-yet-trusted key on a machine
with no prior admin access.
- `scripts/secrets/backup-admin-key.sh <dest-path> [--key-file <path>]
[--force] [--dry-run]` — copies the local sops age key (source
resolution matches sops/age itself: `$SOPS_AGE_KEY` inline, then
`--key-file`, then `$SOPS_AGE_KEY_FILE`, then the XDG default) to an
arbitrary destination path with `0600` permissions, validating it's a
real age identity and round-tripping the public key before and after the
write. Refuses to overwrite an existing `<dest-path>` without `--force`.
Purely a local filesystem copy — never touches `.sops.yaml`/
`secrets/*.yaml` or the repo at all. The resulting file is exactly what
`rotate-admin-key.sh` expects as its backup-key argument.
- `scripts/secrets/sync-nix-cache-host-key.sh [--check] [--dry-run]
[--host <name>]` — detects drift between the ed25519 SSH host key
nix-cache is actually serving right now (via `ssh-keyscan`) and
`vars.nixCacheHostKey` (`variables.nix`), the value
`modules/nix-cache/remote-builder-client.nix` bakes into every real
client's declarative `programs.ssh.knownHosts` and
`configure-nix-cache-client.sh` hardcodes as its own default for
non-NixOS clients. That value has no automatic source of truth — it's
set once from whatever nix-cache's host key happened to be at the time,
and silently goes stale if the host is ever rebuilt/recreated with a new
key, breaking every client's distributed-build SSH trust with no error
that points back here. `--check` (used by `codex-maintenance.sh`, which
treats an unreachable nix-cache — e.g. from a non-LAN CI runner — as a
silent skip rather than a failure) only reports drift; the no-flags form
updates both files in place. Declarative clients still need a rebuild to
pick up the fix.
### `scripts/proxmox/`
- `scripts/proxmox/create-proxmox-resource.sh` — builds a `lxc-*`/
`proxmox-*` target's tarball/disk image and creates it on a real Proxmox
node (`pct create` against the tarball as a CT template / `qm create`+
`importdisk`), or reconfigures an existing resource's cores/memory/disk
size (`--modify`, always requires typing the VMID back to confirm).
Checks for an already-uploaded image on the node before building
(`--force-rebuild` to skip that and always rebuild), and probes
nix-cache's substituter/remote-builder reachability once up front rather
than letting every `nix build` call retry against it individually.
Refuses to create a target whose host identity already exists live on
the node (checked directly via `qm`/`pct`, not any file in this repo)
unless `--allow-duplicate-host` is passed. `--dry-run` throughout both
modes. The first time it has to bootstrap build tooling on a node (i.e.
`nix` wasn't already on its `PATH`), it also runs
`scripts/proxmox/configure-nix-cache-client.sh` there (non-fatally — a
failure just falls back to building from source / `cache.nixos.org`) so
the node substitutes from and can offload builds to nix-cache on every
subsequent run, not just this one.
- `scripts/proxmox/configure-nix-cache-client.sh [--dry-run]
[--no-remote-builder] [--no-restart]` — the non-NixOS equivalent of
`modules/nix-cache/client.nix`/`remote-builder-client.nix`, for a plain
Debian machine with the Nix package manager (not NixOS) already
installed: run as root *on that machine* to add nix-cache as a
substituter in `/etc/nix/nix.conf` (`https://cache.nixos.org/` kept as
fallback) via `extra-substituters`/`extra-trusted-public-keys` so it
layers on top of whatever's already there instead of clobbering it, and,
if `/root/.ssh/nixremote` is already present (see docs/nix-cache.md
"Remote builder SSH keys"), configures it as a distributed-build
machine too and trusts nix-cache's SSH host key in
`/etc/ssh/ssh_known_hosts`. Idempotent (re-running replaces its own
marked block rather than duplicating it); restarts `nix-daemon` by
default so the change takes effect immediately.
### `scripts/lib/`
Sourced by the scripts above, never run directly:
- `nix-bootstrap.sh` — `NIX_CONFIG`/`ensure_nix_profile`, shared by
`codex-setup.sh`/`codex-maintenance.sh` and the remote build commands
`create-proxmox-resource.sh` runs over SSH.
- `nix-eval.sh` — `NIX_EVAL_FLAGS` plus `list_flake_targets`/
`flake_target_hostname` flake-introspection helpers.
- `ssh-host-keys.sh` — `generate_host_ed25519_key`/`ssh_pubkey_to_age`,
shared by `sync-host-keys.sh` and `prepare-host-key.sh`.
- `sops-age.sh` — `age_pubkey_from_identity_file`/`sops_yaml_admin_pubkey`/
`sops_updatekeys` plus the shared sops/age default key-file resolution,
shared by `backup-admin-key.sh`, `rotate-admin-key.sh`, and
`sync-host-keys.sh`.
- `confirm.sh` — `confirm_typed`, the "type X back to confirm" destructive-
action prompt shared by `create-proxmox-resource.sh` and
`sync-host-keys.sh`.
- `sync-host-keys-edit-sops.py` — the `.sops.yaml` anchor/key_groups editor
`sync-host-keys.sh` shells out to (see that script for why: precise,
idempotent YAML edits are impractical in bash).
### Top level
- `scripts/env.sh` — shared config (`PROXMOX_HOST`, storage pool, bridge,
default cores/memory) sourced by `create-proxmox-resource.sh`. Add new
cross-script config here instead of duplicating it per-script.
- `scripts/bump-nixpkgs-release.sh` — bumps `flake.nix`'s `nixpkgs.url`/
`home-manager.url` in place. Exists because flake input URLs can't
reference `variables.nix` (confirmed empirically — `nix flake metadata`
errors on it), so this is the closest equivalent to a single source of
truth for the tracked release.
`sync-host-keys.sh`, `create-proxmox-resource.sh`, and
`rotate-admin-key.sh` genuinely mutate real state when run for real (not
`--dry-run`): real `secrets/*.yaml` recipients, real Proxmox VMs/
containers, real revocation of decrypt access. They require the
operator's own SSH/sops access, which an agent session doesn't have — but
don't suggest running any of them non-dry-run without the operator's
explicit go-ahead even if it becomes technically reachable.
`backup-admin-key.sh` only writes a key copy to a path the operator gives
it — lower-stakes than the others, but it still handles a real private
key, so treat its destination path choice as the operator's call too.
## Architecture
`flake.nix` is the single entry point. It generates one
`nixosConfigurations.<platform>-<buildtype>` attribute per target via the
`mkTarget` function, composed from:
```
nixosSystem {
modules = [
disko.nixosModules.disko
sops-nix.nixosModules.sops
./modules/common/configuration.nix
./modules/platforms/${platform}.nix # what it runs on
./modules/build-types/${buildType}.nix # what it's for
hostPath # hosts/<name>/host.nix — per-machine identity
home-manager.nixosModules.home-manager { ... }
] ++ (client-only modules, for every buildType except "nix-cache" itself)
}
```
Platforms: `linode`, `proxmox`, `lxc`. Build types: `minimal`, `nix-cache`,
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`. Not
every combination is built — e.g. `pxe-boot` has no `linode` variant
(PXE/DHCP/TFTP need LAN L2 adjacency a Linode VPS doesn't have), and
`tor-relay` currently only exists as `lxc-tor-relay`. Treat `flake.nix`'s
`generatedTargets` as the source
of truth for which hosts exist — `README.md`, `AGENTS.md`,
`docs/flake-lock-automation.md`, and the CI eval workflows
(`.github/workflows/check-nixos.yml`, `.gitea/workflows/check-nixos.yml`) list
hosts by hand (or, for the CI workflows, evaluate the flake dynamically) and
can drift from it, so re-check them against `flake.nix` when adding or
removing a host.
### Composition pattern
- `hosts/<name>/host.nix` — per-machine identity **only**: hostname, hostId,
per-machine secrets, `system.stateVersion`. These files carry no `imports`
of their own beyond narrow parameterized helpers (see
`modules/beszel/host-token.nix` below) — all shared behavior comes from the
platform/build-type modules composed in `flake.nix`, not from the host file.
- `modules/platforms/{linode,proxmox,lxc}.nix` — platform-specific config:
boot method, guest tooling, and (for linode/proxmox) the hypervisor-specific
hardware config, imported directly by the platform module itself
(`../hardware-configuration/vm/{proxmox,linode}.nix`) — **not** wired in
from `flake.nix`. `lxc.nix` has no hardware-configuration counterpart since
containers share the host kernel; instead it imports nixpkgs' own
`virtualisation/proxmox-lxc.nix`, which gives every `lxc-*` host a
`config.system.build.tarball` output — a plain rootfs tarball, used as a
`pct create ... vztmpl` CT template (**not** `pct restore`, which expects
`vzdump` backup-archive metadata this doesn't have), no install step —
see `docs/auto-installer.md`.
- `modules/build-types/*.nix` — what a system is for:
minimal/server/docker/gui/pxe-boot/nix-cache/tailscale-exit-node/tor-relay.
- `modules/common/configuration.nix` — base NixOS config imported by every
host: locale, users, nix settings, git.
- `modules/common/home.nix` / `hosts/nixos/home.nix` — Home Manager config for
the `nixos` user; the `nixos` workstation (`gui` build type) has its own,
other hosts share `modules/common/home.nix`.
- `modules/disko/proxmox.nix` — declarative disk layout (GPT: ESP + swap +
ext4 root) via disko, used by all Proxmox-VM hosts (`proxmox-*`, not
`lxc-*`). Also carries `imageSize`/`imageName`, letting every `proxmox-*`
host be built as a standalone, `qm importdisk`-ready `.raw` image with no
install step — see `docs/proxmox-images.md`.
- `modules/disko/linode.nix` — `linode-*`'s disko config, deliberately
different in kind from the Proxmox one: Linode provisions and sizes
`/dev/sda`/`/dev/sdb` itself as whole, unpartitioned devices before the OS
boots, so this declares them with `destroy = false` (disko never wipes
them) and a bare `filesystem`/`swap` content type instead of a partition
table — idempotent against an already-provisioned disk, never destructive.
- `modules/boot/efi.nix` — systemd-boot + EFI vars, paired with the disko module.
- `modules/installer/` — the auto-installer environment (ISO, also served as
PXE netboot): `common.nix` (shared config + the generated
`auto-install.sh`), `iso.nix`, `host-keys.nix` (optionally bakes
`host-keys/` into the image under `--impure`). See
`docs/auto-installer.md`.
- `modules/pxe-boot/stage-installer-artifacts.nix` — builds the installer's
netboot image and stages it on the `pxe-boot` host so its iPXE menu can
chain straight to it. See `docs/pxe-boot.md`.
- `modules/nix-cache/{client,server,remote-builder-client}.nix` — binary cache
substituter + SSH remote-builder wiring; see `docs/nix-cache.md` for the
full design (per-host local stores, no shared `/nix/store`, and how the
`nixremote` signing/SSH keys fit together).
- `modules/beszel/host-token.nix` — parameterized helper module
(`{ name, sopsFile }`) that wires a host's beszel-agent sops secret/template
and `environmentFile`; used by `hosts/server/host.nix` and
`hosts/nix-cache/host.nix` to avoid duplicating that boilerplate.
- `modules/tailscale/`, `modules/docker/`, `modules/networking/`,
`modules/traefik/`, `modules/tor/`, `modules/services/*` — single-purpose,
single-host
feature modules (e.g. `docker/enable-service.nix`,
`services/zfs/enable-service.nix`). Grep `modules/build-types/*.nix` for
each build type's `imports` list to see which modules apply where.
New host = new `hosts/<name>/host.nix` + a matching
`mkTarget { platform; buildType; hostPath; }` entry added to `flake.nix`'s
`generatedTargets`, composed from existing `modules/*` pieces rather than
duplicating config.
### Other docs worth reading before touching these areas
- `docs/nix-cache.md` — nix-cache binary cache/remote-builder design and key
handling.
- `docs/pxe-boot.md` — the `pxe-boot` host's iPXE/TFTP/HTTP boot chain and
directory layout under `/srv/pxe`.
- `docs/auto-installer.md` — the installer environment (ISO/netboot/Proxmox
LXC), `host-keys/` and the sops-nix pre-seeding problem it solves, and why
`lxc-*` hosts are deliberately excluded from its menu.
- `docs/proxmox-images.md` — building `proxmox-*` hosts as standalone `.raw`
disk images (disko's image builder) instead of installing, and deploying
the result to Proxmox.
- `docs/flake-lock-automation.md` — how `flake.lock` updates flow through CI
(scheduled `nix flake update` PR + host-eval-on-PR workflow) and why hosts
should track the committed lock file rather than `nixos-rebuild --upgrade-all`.
+1 -161
View File
@@ -1,162 +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`
- **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`,
`gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
Not every combination exists — `pxe-boot` has no `linode` variant, since
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have, and
`tor-relay` currently only exists as `lxc-tor-relay`. The full list:
| Target | Purpose |
| --- | --- |
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS |
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox — previously the flat `nix-minimal` target |
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder — previously the flat `nix-cache` target |
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host — previously the flat `server` target |
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack — previously the flat `docker` target |
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation — previously the flat `nixos` target |
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host — previously the flat `pxe-boot` target |
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node |
| `lxc-tor-relay` | Tor middle relay |
Which variant of a given buildtype is actually deployed isn't tracked
anywhere in this repo — that's live infrastructure state, not something a
committed file can keep accurate, and it changes independently of the code.
Check the Proxmox node itself, or `/etc/flake-target` on a running host (see
below), if you need to know what's really out there right now.
`scripts/proxmox/create-proxmox-resource.sh`'s duplicate-host guard works the same
way: it checks the Proxmox node directly rather than any file here.
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
fixed regardless of which platform it's built for — see
`flake-target-refactor-spec.md` for the full rationale. Every deployed host
stamps its own active target name into `/etc/flake-target` at build time, so
`nixos-rebuild switch --flake .#$(cat /etc/flake-target)` always picks up the
right one even after a platform migration changes the flake attribute name.
List hosts with:
```bash
nix eval --json .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]'
```
## Layout
| Path | Purpose |
| --- | --- |
| `flake.nix` | Flake inputs, the `mkTarget` platform × build-type generator, and `nixosConfigurations` outputs |
| `variables.nix` | Single source of truth for shared values (LAN domain/CIDR, hostnames, timezone, primary username, storage root, NFS share subpaths/mountpoints, service ports, ...) — passed to every module and Home Manager config as the `vars` argument via `specialArgs`/`extraSpecialArgs` |
| `hosts/<name>/host.nix` | Per-machine identity: hostname, hostId, per-machine secrets, `system.stateVersion` |
| `hosts/nixos/home.nix` | Workstation-specific Home Manager config (used by the `gui` build type) |
| `modules/platforms/` | Platform-specific config: virtualisation guest tools, boot method, hardware config (`linode.nix`, `proxmox.nix`, `lxc.nix`) |
| `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, locally-generated SSH host keys for the auto-installer — see `docs/auto-installer.md` |
| `docs/` | Operational notes for cache, builders, lock updates, boot services, the auto-installer, and Proxmox image builds |
| `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.
This repository's git *history* still contains secrets committed before this
migration (see `remove-sensetive-info-refactor.md`) — those are being
scrubbed and rotated separately; don't treat the repo as safe to make public
until that's finished.
+24
View File
@@ -0,0 +1,24 @@
{ config, pkgs, lib, ... }:
let
mySwitchCmd = ''
sudo nixos-rebuild switch \
--no-write-lock-file \
--refresh \
--flake git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(hostname)
'';
myTestCmd = ''
sudo nixos-rebuild test \
--no-write-lock-file \
--refresh \
--flake git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(hostname)
'';
in {
programs.bash = {
enable = true;
shellAliases = {
"Switch-nix" = mySwitchCmd;
"Test-nix" = myTestCmd;
};
};
}
+76
View File
@@ -0,0 +1,76 @@
{ config, lib, pkgs, ... }:
{
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
];
# Use the GRUB 2 boot loader.
boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
# Set your time zone.
time.timeZone = "Australia/Brisbane";
# Enable QEMU agent
services.qemuGuest.enable = true;
# Enable docker-compose
environment.systemPackages = with pkgs; [
vim
btop
git
gcr
];
#Set root password
users.users.root = {
hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
};
# Define a user account. Don't forget to set a password with ‘passwd’.
users.users.nixos = {
isNormalUser = true;
extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
packages = with pkgs; [
tree
];
hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream"
];
};
# Enable the OpenSSH daemon.
services.openssh.enable = true;
#Enable flakes
nix.settings.experimental-features = "nix-command flakes";
nix.settings = {
substituters = [
"http://nix-cache"
# "https://cache.nixos.org/"
];
trusted-public-keys = [
"cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
];
};
programs.git = {
enable = true;
package = pkgs.git;
config = {
credential.helper = "store";
};
};
}
@@ -5,26 +5,22 @@
{ {
imports = 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 = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-label/nixos";
fsType = "ext4";
}; };
# boot.loader.grub.device = "/dev/sda2"; # or "nodev" for efi only
# fileSystems."/" = swapDevices =
# { device = "/dev/disk/by-label/nixos"; [ { device = "/dev/disk/by-label/swap"; }
# fsType = "ext4"; ];
# };
# swapDevices =
# [ { 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;
# };
}
-286
View File
@@ -1,286 +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/auto-install.sh`,
discovers available hosts from this same flake, lets the operator choose a
target, applies that host's Disko storage configuration, installs NixOS, and
reboots.
**This applies to every `nixosConfigurations` target except `lxc-*` hosts —
see "LXC hosts" immediately below for why those are different.**
## LXC hosts
`lxc-*` targets (`lxc-minimal`, `lxc-nix-cache`, `lxc-server`, `lxc-docker`,
`lxc-gui`, `lxc-pxe-boot`, `lxc-tailscale-exit-node`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the
interactive menu deliberately excludes them. Don't try to select one there;
`nixos-install` would bind-mount `/` onto `/mnt` (LXC containers have no raw
disk to partition) and then refuse to touch the filesystem it's currently
running on — it's designed to protect exactly this case, so it just fails.
`modules/platforms/lxc.nix` imports nixpkgs' own
`virtualisation/proxmox-lxc.nix` module, which gives every `lxc-*` host a
`config.system.build.tarball` output — a complete, directly Proxmox-importable
container image, no install step at all:
```sh
nix build .#nixosConfigurations.lxc-minimal.config.system.build.tarball
```
This is a plain rootfs tarball, not a `vzdump` backup archive — restoring it
with `pct restore` fails ("archive contains no configuration file"), since
that command expects backup-archive metadata this tarball doesn't have. Use
it as a CT *template* instead: drop it under Proxmox's template storage
(conventionally `/var/lib/vz/template/cache/` for the `local` storage, or
the GUI's "Create CT" → upload-as-template flow) and create a container
from it, supplying all config on the command line since a template has none
of its own:
```sh
pct create <vmid> local:vztmpl/<file>.tar.xz \
--unprivileged 1 --features nesting=1,keyctl=1 \
--rootfs local-lvm:8 --hostname <name> --cores 2 --memory 2048 --swap 2048 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp
pct start <vmid>
```
Every one of those extra flags is load-bearing, confirmed by actually
booting one:
- `--unprivileged 1` — `modules/platforms/lxc.nix` sets
`proxmoxLXC.privileged = false`, so the image assumes it's running
unprivileged. `pct create`'s own CLI default for this flag is
privileged (unlike the web UI, whose checkbox defaults the other way)
— omit it and you get a privileged container running a NixOS config
that assumes unprivileged, a real mismatch.
- `--features nesting=1,keyctl=1` — required for a modern (v247+)
systemd guest to boot unprivileged at all. Without it, AppArmor denies
the nested user namespaces and credential mounts systemd routinely
uses (even plain getty units) — every getty crash-loops on a denied
`/run/credentials/*` mount every ~3s (this is what garbage on the
console turns out to be) while core services like `nsncd` fail the
same way, and the system never finishes activating.
- `--swap 2048` — `--memory` doesn't touch swap; it silently stays at
Proxmox's own 512M default otherwise. Match it to `--memory` unless
you deliberately want otherwise.
First boot runs `boot.postBootCommands` (registers the Nix store DB and
system profile) — there's no separate activation step to run yourself.
`scripts/proxmox/create-proxmox-resource.sh --type lxc --host <name>` automates all
of this (host-key handling, building the tarball directly on the Proxmox
node itself, `pct create` with the flags above) — see its `--help`.
Host keys still need pre-seeding the same way as any other host — the
sops-nix activation-vs-first-boot race is identical regardless of how the
image reaches the machine. Unlike the ISO/PXE installer (where
`modules/installer/host-keys.nix` bakes *every* `host-keys/` entry into
`/etc/host-keys/` for `auto-install.sh` to pick from and copy at install
time — see "Host keys" below), an `lxc-*` tarball has no install step to
copy anything during, so `modules/platforms/lxc.nix` bakes this *one*
target's key straight into `/etc/ssh/ssh_host_ed25519_key(.pub)` directly,
keyed by its own exact flake target name (`config.environment.etc` can't
be read back from within a module still contributing to it, so this comes
in via `specialArgs.flakeTarget`, set by `flake.nix`'s `mkTarget`):
```sh
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" \
nix build .#nixosConfigurations.lxc-nix-cache.config.system.build.tarball --impure
```
Confirmed the hard way: without this, the tarball's own built-in system
just generates a fresh host key at first boot like any host would, which
can never match whatever `.sops.yaml` actually trusts for that target —
`sops-install-secrets` fails with `Error getting data key: 0 successful
groups required, got 0`, and *every* secret (including this host's own
login) permanently fails to decrypt, silently — no error in the boot log
at all, since the activation step that would install secrets only runs on
a from-scratch first activation and skips silently once `/run/current-system`
already exists. `scripts/proxmox/create-proxmox-resource.sh` always builds with
`NIXOS_HOST_KEYS_DIR` set for this reason.
## Layout
- `modules/installer/common.nix` — shared by every installer target: SSH
access, users, the generated `/etc/auto-install.sh` script, and the
`programs.bash.loginShellInit` hook that runs it on login.
- `modules/installer/iso.nix` — ISO/netboot-specific: imports the stock
`installation-cd-minimal.nix` module plus `common.nix`. Also used, paired
with `netboot-minimal.nix`, to build the PXE netboot variant (see
`docs/pxe-boot.md`).
- `modules/installer/host-keys.nix` — optionally bakes pre-generated SSH
host keys into the image; see "Host keys" below.
- `scripts/secrets/sync-host-keys.sh` — admin-workstation tool that generates,
registers, and (via `--remove`/`--regenerate-all-keys`) retires host
keys; see "Creating a New Machine" below.
- `scripts/secrets/prepare-host-key.sh` — narrower predecessor: generates a single
key by an arbitrary name without touching `.sops.yaml`. Still useful for
pre-generating a key *before* its flake target exists (`sync-host-keys.sh`
can only act on targets `nixosConfigurations` already has); otherwise
`sync-host-keys.sh` does the same thing and more.
Flake outputs:
```nix
nixosConfigurations.installer # ISO/netboot installer image
packages.x86_64-linux.iso # installer ISO/netboot image
packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
```
```sh
nix build .#iso
nix build .#pxe
```
There's no `nixosConfigurations.proxmox-lxc` (installer-boots-as-an-LXC-
container) or `packages.x86_64-linux.lxc`/`.all` anymore. Both existed only
to let the installer itself run as an LXC container so you could
`nixos-install` some *other* host from within it — but LXC targets are
excluded from the install menu (same bind-mount problem as any LXC
`nixos-install`), and now have their own direct tarball path anyway (see
"LXC hosts" above), which left the installer's own LXC form with no real
use case.
The `pxe` variant is also built automatically as part of the `pxe-boot` host
itself (`modules/pxe-boot/stage-installer-artifacts.nix`) and served over
iPXE — see `docs/pxe-boot.md`.
## 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.
## 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
`/etc/auto-install.sh`:
1. Queries `nixosConfigurations` from this flake over the network (`git+https://<lanDomain>/beatzaplenty/nixos.git`) — this happens at *install* time, not build time, so a generic installer image always sees whatever hosts are currently committed, without needing a rebuild.
2. Presents them as a menu; confirms the choice.
3. Skips the `nix-cache` substituter when installing a `nix-cache` host itself (consistent with that host's own runtime config).
4. Runs `disko --mode destroy,format,mount` (see "Storage" above — every host reachable through this menu has a Disko config, so this is unconditional).
5. Installs the target's SSH host key from `/etc/host-keys` or `/root/host-keys` (see "Host keys" above).
6. Runs `nixos-install --flake <url>#<choice> --no-root-password`.
7. Cleans up and reboots.
## Creating a new machine
Do this instead of jumping straight to a plain install whenever the target
host consumes any sops-nix secret — as of this writing, that's every host
(`modules/common/configuration.nix` puts the root/nixos password hash and the
GitHub token behind sops-nix for all of them).
1. **Add the flake target** — `hosts/<name>/host.nix` plus the matching
`mkTarget { ... }` entry in `flake.nix`'s `generatedTargets` (see
"Composition pattern" in `CLAUDE.md`). No secrets involved yet, so this
is safe to commit on its own if you want a clean history.
2. **On your admin workstation, generate and register its host key:**
```sh
./scripts/secrets/sync-host-keys.sh <flake-target>
```
This generates `host-keys/<flake-target>_ssh_host_ed25519_key(.pub)`,
adds it as a new `.sops.yaml` anchor, works out which `secrets/*.yaml`
files this specific host actually references (from its own
`config.sops.secrets`, not guessed), adds it to each one's
`key_groups`, and re-encrypts them with `sops updatekeys` — no manual
YAML editing. Safe to re-run; it only fills in what's missing.
Doing this for every host that needs one at once — after adding several
new targets, or just to catch up any that were missed — is
`./scripts/secrets/sync-host-keys.sh --all`. See `scripts/secrets/sync-host-keys.sh --help`
for its other modes (`--remove`, `--regenerate-all-keys`).
3. **Commit and push.** The flake build the installer uses has to see the
new recipient before you install, or decryption fails on first boot
regardless of the next step.
4. **Build the installer image with keys baked in** (or reuse an already-serving `pxe-boot` host, which does this automatically once redeployed):
```sh
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build .#iso --impure
```
5. **Boot it on the target machine**, log in, select the new host's flake
target from the menu, confirm. `auto-install.sh` finds the baked-in key,
runs Disko + `nixos-install`, and reboots.
6. **Verify after reboot:**
```sh
ssh <new-host> ls /run/secrets/
```
If that's empty or login fails, the host's age key most likely wasn't in
`.sops.yaml` (or wasn't re-encrypted into the secrets file it needs) when
`nixos-install` ran — fix `.sops.yaml`/`secrets/*.yaml`, push, then re-run
`nixos-install --flake .#<hostname> --no-root-password` from a rescue
environment against the existing `/mnt`, or just redo the install.
## Safety
This installer is destructive: `disko --mode destroy,format,mount` erases
any disk defined by the selected host's Disko configuration. Always verify
the selected host profile and target machine before confirming.
-83
View File
@@ -1,83 +0,0 @@
# flake.lock automation
This repository uses CI workflows to keep `flake.lock` up to date on a schedule
and to verify that declared NixOS hosts still evaluate after dependency updates.
## What this automation does
- A scheduled workflow runs `nix flake update` once per week.
- On GitHub, any resulting `flake.lock` change is proposed through a pull request.
- On Gitea, the workflow can commit and push `flake.lock` directly when PR automation is not configured.
- A separate CI workflow runs `scripts/codex-maintenance.sh` before merge.
Its default mode scopes eval to the hosts/packages a change can affect,
determined from a git diff against the PR base — but a `flake.lock` change
is treated as repo-wide and always falls back to evaluating every host, so
a lock-file update PR still gets full coverage. Hosts are still listed
dynamically via
`nix eval --json .#nixosConfigurations --apply builtins.attrNames` rather
than hand-enumerated, so that fallback can't drift as `<platform>-<buildtype>`
targets are added or removed. See `README.md` for the current target list.
## Why hosts should stop using `--upgrade-all`
`flake.lock` is the source of truth for pinned dependency versions in a flake-based workflow. Normal host rebuilds should consume the committed lock file instead of upgrading dependencies ad-hoc on each machine.
Recommended rebuild command:
```bash
sudo nixos-rebuild switch --flake git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(cat /etc/flake-target)
```
Flake attribute names are `<platform>-<buildtype>` (e.g. `proxmox-docker`)
and no longer match `hostname`, since a host's hostname stays fixed while
the platform backing it can change. Each `nixosConfiguration` stamps its own
active target name into `/etc/flake-target` at build time, which is what the
command above reads.
Using the committed lock file keeps all hosts aligned and makes updates auditable through CI and code review.
Codex and automated review sessions must not run rebuilds. Limit checks to
evaluation, linting, formatting, and dry-run builds.
## Command differences
- `nix flake update`
- Updates flake input pins in `flake.lock`.
- Should be run in CI or in a dedicated update PR workflow.
- `nixos-rebuild --upgrade`
- Primarily for channel-based workflows; not the normal path for flake-pinned deployments.
- `nixos-rebuild --upgrade-all`
- Aggressively updates package sources and bypasses coordinated lock-file updates.
- Avoid for routine flake-based host rebuilds.
## nix-cache and remote builder fit
With `nix-cache` acting as a binary cache and remote builder, lock-file updates become safer and more reproducible:
- CI verifies host evaluations against the updated lock file.
- Builds can be performed once on the remote builder.
- Built artifacts can be served via `nix-cache` to other hosts, reducing rebuild time and drift.
## Token and secret handling
Do **not** commit access tokens into `flake.nix`, `flake.lock`, or any other tracked file.
If private source access is needed:
- configure tokens locally in `~/.config/nix/nix.conf` or equivalent machine-local config, or
- provide tokens through CI secrets/environment variables.
## GitHub Actions setup notes
- Ensure `GITHUB_TOKEN` has permission to create branches and pull requests (workflow sets `contents: write` and `pull-requests: write`).
- The update workflow uses `peter-evans/create-pull-request` with branch `chore/update-flake-lock`.
- The evaluation workflow runs on pull requests, pushes to `main`, and manual dispatch.
## Gitea Actions runner setup notes
- Ensure the runner image includes Git and can execute the Nix installer action.
- For direct push mode, grant workflow push permission to the repository.
- The workflow sets commit identity to:
- `user.name = gitea-actions`
- `user.email = gitea-actions@nix-cache.local`
- Commits are only created when `flake.lock` actually changes.
-91
View File
@@ -1,91 +0,0 @@
# nix-cache architecture
This repository configures `nix-cache` as a **binary cache server** and a **remote builder** for other hosts.
## Important design notes
- This is **not** a shared `/nix/store` setup.
- Every machine still keeps and uses its own local `/nix/store`.
- Clients prefer `http://nix-cache` for substitutes and keep `https://cache.nixos.org/` as fallback.
- Clients can offload builds to `nix-cache` through SSH (`nix.distributedBuilds`).
- Client hosts import `modules/nix-cache/client.nix` and, when remote building is enabled, `modules/nix-cache/remote-builder-client.nix`.
- The `nix-cache` host imports `modules/nix-cache/server.nix`.
## Binary cache signing key
`modules/nix-cache/client.nix` hardcodes every client's trust in one
specific public key (`cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4=`).
That means whichever host is currently playing the `nix-cache` role has to
use that *exact* keypair — not a freshly generated one — or no client will
accept substitutes from it (they'd just silently fall back to building
from source). So unlike most per-host secrets, this one can't be
self-generated on first boot; it's managed via sops-nix like every other
secret in this repo, sourced from `secrets/nix-cache.yaml`'s
`cache-priv-key` entry (`modules/nix-cache/server.nix`).
**Adding or rotating the value:**
```bash
nix-shell -p sops --run 'sops secrets/nix-cache.yaml'
```
Add (or replace) a `cache-priv-key` entry with the private key file's exact
contents. If you don't have it yet, generate a keypair once:
```bash
nix-store --generate-binary-cache-key nix-cache-1 cache-priv.pem cache-pub.pem
```
— paste `cache-priv.pem`'s contents into the `cache-priv-key` entry above,
delete both local files afterward, and update
`trusted-public-keys` in `modules/nix-cache/client.nix` (and every already-built
client) to match `cache-pub.pem` if this is a genuine rotation rather than
a first-time bootstrap. Any `nixos-configurations.*-nix-cache` host picks
the new key up automatically on next activation — no more manual
`/etc/nix/cache-priv.pem` install step.
## Remote builder SSH keys
Each client authenticates as `nixremote` using its **own default root SSH
identity** (`/root/.ssh/id_ed25519`) — not a separately-named or shared
keypair. If a client doesn't have one yet:
```bash
sudo ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519
```
Then add its `.pub` contents as a new entry in `vars.remoteBuilderAuthorizedKeys`
(`variables.nix`) and rebuild `nix-cache` to pick it up (that list is
declarative — an imperative `ssh-copy-id nixremote@nix-cache` won't stick;
it gets overwritten on every rebuild). Verify with:
```bash
sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache nix-store --version
```
The committed `remoteBuilderAuthorizedKeys` entries are public SSH keys
only. Keep the matching private keys on client hosts and out of the
repository.
nix-cache's own SSH *host* key is trusted declaratively via
`programs.ssh.knownHosts` in `modules/nix-cache/remote-builder-client.nix`,
sourced from `vars.nixCacheHostKey` (`variables.nix`) — every client rebuild
picks it up automatically, so distributed builds don't fail with "Host key
verification failed" on a client that has never manually SSH'd to nix-cache
before. If nix-cache's host key is ever rotated or the host rebuilt from
scratch, update `vars.nixCacheHostKey` to match its new
`/etc/ssh/ssh_host_ed25519_key.pub`.
## Manual verification
After deployment:
```bash
curl http://nix-cache/nix-cache-info
nix store ping --store http://nix-cache
nix show-config | grep -E 'substituters|trusted-public-keys|builders-use-substitutes'
sudo ssh -i /root/.ssh/id_ed25519 nixremote@nix-cache nix-store --version
nix build nixpkgs#hello --builders 'ssh://nixremote@nix-cache x86_64-linux /root/.ssh/id_ed25519 4 2 big-parallel,kvm,nixos-test,benchmark' -L
nix path-info -r nixpkgs#hello
curl -I "http://nix-cache/$(basename "$(nix path-info nixpkgs#hello)").narinfo"
```
-116
View File
@@ -1,116 +0,0 @@
# Proxmox VM disk images
`proxmox-*` hosts (VM platform, not `lxc-*`) can be built as standalone,
ready-to-attach `.raw` disk images via disko's own image-builder — no
`nixos-install`, no live installer boot. This uses the same `disko.devices`
config (`modules/disko/proxmox.nix`) already used to format a real disk on
install, so there's nothing host-specific to write; it's available for every
`proxmox-*` target automatically.
`scripts/proxmox/create-proxmox-resource.sh --type vm --host <name>` automates the
whole walkthrough below (and the equivalent LXC one) end to end, including
host-key handling and building the image directly on the Proxmox node
itself (no local build, no image transfer) — see its `--help`. The steps
here are what it runs under the hood, useful for doing any of it by hand
or understanding what it does before you trust it against real
infrastructure.
## Building
```sh
nix build .#nixosConfigurations.proxmox-server.config.system.build.diskoImagesScript
sudo ./result --build-memory 2048
```
This produces `<hostname>.raw` in the current directory (e.g. `server.raw`
for `proxmox-server`, matching `networking.hostName`, not the flake attribute
name — every `proxmox-*` host gets a distinctly named image instead of all
of them producing an identical `main.raw`). The script builds inside a
temporary QEMU VM and moves the finished image out to the working directory
when done; `--build-memory` controls how much RAM that build VM gets.
`disko.devices.disk.main.imageSize` (currently `20G`, in
`modules/disko/proxmox.nix`) sets the image's total size — disko doesn't
support auto-resizing, so this needs to comfortably fit ESP + swap + root at
build time. Grow the virtual disk (and resize the filesystem) in Proxmox
after attaching if a host needs more than that; this is the normal way to
size these images, not a one-time decision to get exactly right up front.
## Host keys
The disko image script runs a real activation pass inside its temporary
build VM while constructing the image — the same sops-nix
activation-before-first-boot problem the installer and LXC tarball workflows
have (see `docs/auto-installer.md`) applies here too, unmodified. Disko has
a native mechanism for it:
```sh
sudo ./result \
--pre-format-files host-keys/server_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \
--pre-format-files host-keys/server_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \
--build-memory 2048
```
Generate the key first with `scripts/secrets/sync-host-keys.sh <hostname>`, same
as any other host — see `docs/auto-installer.md` for the full walkthrough
(it registers the new key in `.sops.yaml` and re-encrypts the affected
`secrets/*.yaml` files too, no manual editing needed).
## Deploying to Proxmox
The image needs **UEFI (OVMF)**, not Proxmox's default SeaBIOS —
`modules/boot/efi.nix` uses `systemd-boot`, which only works with UEFI
firmware. `virtio-scsi` is safe to use as the disk bus:
`hardware-configuration/vm/proxmox.nix` already includes `virtio_scsi` in
its initrd kernel modules.
1. Copy the image to the Proxmox host:
```sh
scp server.raw root@<proxmox-host>:/var/lib/vz/import/
```
2. Create an empty VM shell (no disk yet) — replace `<vmid>` with a free ID
and `<storage>` with your storage pool's name (`pvesm status` or
Datacenter → Storage in the web UI):
```sh
qm create <vmid> --name proxmox-server --memory 2048 --cores 2 \
--net0 virtio,bridge=vmbr0 \
--bios ovmf --machine q35 \
--scsihw virtio-scsi-pci \
--efidisk0 <storage>:1,efitype=4m,pre-enrolled-keys=0
```
(`--efidisk0` is required for UEFI — it's where OVMF persists boot-entry
NVRAM; without it, systemd-boot's boot entry may not survive a reboot.)
3. Import the raw disk into storage:
```sh
qm importdisk <vmid> /var/lib/vz/import/server.raw <storage>
```
This prints the resulting disk identifier (e.g. `vm-<vmid>-disk-1`).
4. Attach it and set it as the boot disk:
```sh
qm set <vmid> --scsi0 <storage>:vm-<vmid>-disk-1
qm set <vmid> --boot order=scsi0
```
5. Boot it:
```sh
qm start <vmid>
```
No install step — it boots straight into the already-activated system.
## Why not `nix build .#nixosConfigurations.<host>.config.system.build.vm`?
That's a different, unrelated feature — `system.build.vm` (`nixos-rebuild
build-vm`) produces an ephemeral QEMU script for locally testing a
configuration, not a distributable disk image. It's not part of this
workflow.
-105
View File
@@ -1,105 +0,0 @@
# pxe-boot
The `pxe-boot` host serves HTTP boot assets for iPXE clients — including a
self-staged copy of this flake's own auto-installer netboot image, see
`docs/auto-installer.md` for what that image actually is and does once
booted.
## Host Role
- Hostname: `pxe-boot`
- Web service: nginx on TCP port 80
- PXE root: `/srv/pxe`
- HTTP root for scripts and images: `/srv/pxe/http`
- TFTP root for first-stage bootloaders: `/srv/pxe/tftp`
- iPXE entry script: `/srv/pxe/http/boot.ipxe`
- Generated iPXE menu: `/srv/pxe/http/menu.ipxe`
- SystemRescue iPXE script: `/srv/pxe/http/systemrescue.ipxe`
- TFTP fallback script: `/srv/pxe/tftp/autoexec.ipxe`
- Boot binaries copied from the Nix `ipxe` package:
- `/srv/pxe/tftp/ipxe.efi`
- `/srv/pxe/tftp/undionly.kpxe`
## Directory Layout
The host creates these directories with systemd tmpfiles:
```text
/srv/pxe
/srv/pxe/http
/srv/pxe/http/images
/srv/pxe/http/nixos
/srv/pxe/http/systemrescue
/srv/pxe/http/ubuntu
/srv/pxe/http/rescue
/srv/pxe/tftp
```
Mount shared image storage under `/srv/pxe/http`, preferably
`/srv/pxe/http/images` unless a menu entry expects files in a specific
directory such as `/srv/pxe/http/nixos`.
The HTTP iPXE chain is:
```text
undionly.kpxe or ipxe.efi
-> autoexec.ipxe from the TFTP root, when iPXE requests it
-> http://192.168.2.247/boot.ipxe
-> http://192.168.2.247/menu.ipxe
```
The generated menu currently exposes entries for:
- NixOS installer
- SystemRescue environment
- iPXE shell
- Reboot
The NixOS installer entry chain-loads `/srv/pxe/http/nixos/netboot.ipxe`,
which is 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. All three
files (`bzImage`, `initrd`, `netboot.ipxe`) are built from this flake's own
`modules/installer/iso.nix` netboot image (the same one `nix build .#pxe`
produces) and staged automatically by
`modules/pxe-boot/stage-installer-artifacts.nix` via `systemd.tmpfiles.rules`
— no manual operator step required.
The SystemRescue entry expects the source ISO at:
```text
/srv/pxe/http/images/systemrescue.iso
```
The `stage-systemrescue.service` oneshot extracts that ISO into:
```text
/srv/pxe/http/systemrescue
```
The rescue menu entry then chains `http://192.168.2.247/systemrescue.ipxe`,
which loads the SystemRescue kernel and initramfs from the extracted tree and
uses `archiso_http_srv` to fetch the squashfs payload over HTTP.
## Validation
Safe evaluation check:
```bash
nix eval .#nixosConfigurations.proxmox-pxe-boot.config.system.build.toplevel.drvPath --raw
```
After deployment by an operator, basic service checks are:
```bash
curl http://pxe-boot/boot.ipxe
curl http://pxe-boot/menu.ipxe
curl http://pxe-boot/systemrescue.ipxe
curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/vmlinuz
curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/sysresccd.img
```
During a successful BIOS chainload, TFTP should deliver `undionly.kpxe` once,
then nginx should log requests for `/boot.ipxe` and `/menu.ipxe`. Repeated TFTP
downloads of `undionly.kpxe` indicate the iPXE stage is still not reaching the
HTTP chain.
Generated
+28 -73
View File
@@ -1,38 +1,17 @@
{ {
"nodes": { "nodes": {
"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": {
@@ -74,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"
} }
}, },
@@ -95,16 +73,15 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1783740085, "lastModified": 1765860045,
"narHash": "sha256-qajyHfZY29G2oEQk+uHxmsJcRoBUBXP9maTpFlwP/dI=", "narHash": "sha256-7Lxp/PfOy4h3QIDtmWG/EgycaswqRSkDX4DGtet14NE=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "3cd22efe6471dc7365c822bd9ad73a21e55f38fb", "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"
} }
@@ -116,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": {
@@ -131,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": {
@@ -147,27 +124,25 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1784011430, "lastModified": 1765779637,
"narHash": "sha256-lDebytrYdd47IBLwvNOD+6AGeoqZ78CIKlp70hzW280=", "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "8eeec934ae0dbeca3d7868c059568a65c08b2fc3", "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": {
"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": {
@@ -180,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": {
@@ -193,26 +168,6 @@
"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": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
+81 -136
View File
@@ -1,158 +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";
};
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
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};
vars = import ./variables.nix;
# Generates a nixosConfiguration from a platform (what it runs on) and
# a build type (what it's for), plus the per-identity host.nix that
# carries the bits that must stay fixed regardless of platform
# (hostName, hostId, per-machine secrets). Every build type except
# nix-cache itself consumes the nix-cache substituter and remote
# builder.
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix }:
let
flakeTarget = "${platform}-${buildType}";
in
nixpkgs.lib.nixosSystem {
inherit system; inherit system;
modules = [ modules = [
inputs.disko.nixosModules.disko ./hosts/nixos/configuration.nix
sops-nix.nixosModules.sops ./common/hardware-configuration.nix
./modules/common/configuration.nix home-manager.nixosModules.home-manager {
./modules/platforms/${platform}.nix home-manager.useGlobalPkgs = true;
./modules/build-types/${buildType}.nix home-manager.useUserPackages = true;
hostPath home-manager.users.nixos = import ./hosts/nixos/home.nix;
{ 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 specialArgs = { inherit inputs; };
# contributing to environment.etc can't read the merged
# environment.etc it's itself contributing to).
specialArgs = { inherit inputs vars netbootSystem flakeTarget; };
}; };
docker = nixpkgs.lib.nixosSystem {
# Generated platform x build-type matrix. pxe-boot has no linode
# variant (PXE/DHCP/TFTP need LAN L2 adjacency, which a Linode VPS
# doesn't have).
generatedTargets = {
linode-minimal = mkTarget { platform = "linode"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
proxmox-minimal = mkTarget { platform = "proxmox"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
lxc-minimal = mkTarget { platform = "lxc"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
linode-nix-cache = mkTarget { platform = "linode"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
proxmox-nix-cache = mkTarget { platform = "proxmox"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
lxc-nix-cache = mkTarget { platform = "lxc"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
linode-server = mkTarget { platform = "linode"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
proxmox-server = mkTarget { platform = "proxmox"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
lxc-server = mkTarget { platform = "lxc"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
linode-docker = mkTarget { platform = "linode"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
proxmox-docker = mkTarget { platform = "proxmox"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
lxc-docker = mkTarget { platform = "lxc"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
linode-gui = mkTarget { platform = "linode"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
proxmox-gui = mkTarget { platform = "proxmox"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
lxc-gui = mkTarget { platform = "lxc"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
proxmox-pxe-boot = mkTarget { platform = "proxmox"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
lxc-pxe-boot = mkTarget { platform = "lxc"; buildType = "pxe-boot"; hostPath = ./hosts/pxe-boot/host.nix; };
linode-tailscale-exit-node = mkTarget { platform = "linode"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
proxmox-tailscale-exit-node = mkTarget { platform = "proxmox"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
lxc-tailscale-exit-node = mkTarget { platform = "lxc"; buildType = "tailscale-exit-node"; hostPath = ./hosts/tailscale-exit-node/host.nix; };
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
};
# Auto-install environments (migrated from the former nix-auto-installer
# flake): a self-contained NixOS installer that boots, discovers this
# flake's own nixosConfigurations over the network, and runs
# nixos-install against whichever one the operator picks. These are
# deliberately not part of the platform x build-type matrix above —
# they're throwaway boot media, not persistent hosts, so they skip
# disko/sops-nix/home-manager and just need `vars`.
installerTargets = {
installer = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./modules/installer/iso.nix ];
specialArgs = { inherit vars; };
};
};
# Same installer environment, built as netboot (kernel + initrd +
# iPXE script) instead of an ISO — this is what packages.pxe bundles.
netbootSystem = nixpkgs.lib.nixosSystem {
inherit system; inherit system;
modules = [ modules = [
./modules/installer/iso.nix ./hosts/docker/configuration.nix
({ modulesPath, ... }: { ./common/hardware-configuration.nix
imports = [ home-manager.nixosModules.home-manager {
(modulesPath + "/installer/netboot/netboot-minimal.nix") home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
]; ];
})
];
specialArgs = { inherit vars; };
}; };
kuma = nixpkgs.lib.nixosSystem {
in inherit system;
{ modules = [
./hosts/kuma/configuration.nix
nixosConfigurations = generatedTargets // installerTargets; ./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
# Buildable auto-installer artifacts (`nix build .#<name>`). No `lxc` home-manager.useGlobalPkgs = true;
# variant (installer-boots-as-an-LXC-container) or `all` bundle home-manager.useUserPackages = true;
# anymore — lxc-* and proxmox-* hosts deploy via their own tarball/ home-manager.users.nixos = import ./common/home.nix;
# 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; }
]; ];
}; };
server = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/server/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
];
};
nix-cache = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/nix-cache/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
];
};
nix-minimal = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./hosts/nix-minimal/configuration.nix
./common/hardware-configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.nixos = import ./common/home.nix;
}
];
};
};
}; };
} }
+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?
}
-11
View File
@@ -1,11 +0,0 @@
_:
{
networking.hostName = "docker";
networking.hostId = "007f0200";
boot.zfs.forceImportRoot = false;
# Preserved from the pre-refactor `docker` target — stateVersion must never
# be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+77
View File
@@ -0,0 +1,77 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ config, lib, pkgs, inputs, ... }:
let
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
docker
pytz
]);
in
{
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
networking.hostName = "kuma"; # Define your hostname.
virtualisation.docker.enable = true;
# Enable docker-compose
environment.systemPackages = with pkgs; [
docker-compose
docker_25
];
# Mount docker persistent data
# fileSystems."/mnt/docker-persistent-data" = {
# device = "/dev/disk/by-label/docker-data";
# fsType = "ext4";
# options = [ "defaults" "nofail" "noatime" ];
# };
#Add sym links to data on users home folder
# system.userActivationScripts.createDockerSymlink.text = ''
# ln -sf /mnt/docker-persistent-data/docker /home/nixos/docker
# '';
# system.userActivationScripts.createSetupSymlink.text = ''
# ln -sf /mnt/docker-persistent-data/setup /home/nixos/setup
# '';
users.users.nixos.extraGroups = [ "docker" ];
services.openssh.settings.PermitRootLogin = "yes";
# Open ports in the firewall.
networking.firewall.allowedTCPPorts = [ 80 8080 443 ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.sta>
system.stateVersion = "25.05"; # Did you read the comment?
}
+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?
}
-21
View File
@@ -1,21 +0,0 @@
{ vars, ... }:
{
imports = [
(import ../../modules/beszel/host-token.nix {
name = "nix-cache";
sopsFile = ../../secrets/nix-cache.yaml;
})
];
networking.hostName = vars.nixCacheHost;
services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
};
# Preserved from the pre-refactor `nix-cache` target — stateVersion must
# never be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+76
View File
@@ -0,0 +1,76 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
{ config, lib, pkgs, inputs, ... }:
{
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
networking.hostName = "nix-minimal"; # Define your hostname.
environment.systemPackages = with pkgs; [
sshfs
fuse3 # needed for modern sshfs
];
# Enable FUSE (if not already)
boot.extraModprobeConfig = ''
options fuse user_allow_other
'';
# Allow mounting with user permissions (optional, see note)
# security.wrappers.sshfs = {
# source = "${pkgs.sshfs}/bin/sshfs";
# owner = "nixos";
# group = "users";
# permissions = "4755";
# fileSystems."/mnt/proxmox-iso" = {
# device = "root@pve:/var/lib/vz/template/iso";
# fsType = "fuse.sshfs";
# options = [
# "IdentityFile=/home/nixos/.ssh/id_ed25519.pub" # or your SSH key path
# "allow_other"
# "reconnect"
# "ServerAliveInterval=15"
# "ServerAliveCountMax=3"
# "StrictHostKeyChecking=no" # only if you're OK with this
# ];
# };
# };
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ 80 8080 443 ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.
# system.copySystemConfiguration = true;
# This option defines the first version of NixOS you have installed on this particular machine,
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
#
# Most users should NEVER change this value after the initial install, for any reason,
# even if you've upgraded your system to a new NixOS release.
#
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
# to actually do that.
#
# This value being lower than the current NixOS release does NOT mean your system is
# out of date, out of support, or vulnerable.
#
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
# and migrated your data accordingly.
#
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.sta>
system.stateVersion = "25.05"; # Did you read the comment?
}
-12
View File
@@ -1,12 +0,0 @@
_:
{
# Preserves the hostname of the existing, already-deployed machine
# (previously the flat `nix-minimal` target) — the flake attribute name
# changed, the real machine's hostname did not.
networking.hostName = "nix-minimal";
# Preserved from the pre-refactor `nix-minimal` target — stateVersion must
# never be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+133
View File
@@ -0,0 +1,133 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).
{ config, pkgs, lib, inputs, ... }:
let
nixosConfEditor = builtins.getFlake "github:snowfallorg/nixos-conf-editor";
in {
#{
environment.systemPackages = with pkgs; [
inputs.nixos-conf-editor.packages.${pkgs.system}.nixos-conf-editor
nodejs
appimage-run
seahorse
vscode
p7zip
popsicle # balena-etcher
shotcut
gimp
pdfarranger
terminator
libreoffice-qt
transmission_4-qt
];
imports =
[ # Include the results of the hardware scan.
../../common/configuration.nix
];
# Bootloader.
boot.loader.grub.useOSProber = true;
networking.hostName = "nixos"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Select internationalisation properties.
i18n.defaultLocale = "en_AU.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "en_AU.UTF-8";
LC_IDENTIFICATION = "en_AU.UTF-8";
LC_MEASUREMENT = "en_AU.UTF-8";
LC_MONETARY = "en_AU.UTF-8";
LC_NAME = "en_AU.UTF-8";
LC_NUMERIC = "en_AU.UTF-8";
LC_PAPER = "en_AU.UTF-8";
LC_TELEPHONE = "en_AU.UTF-8";
LC_TIME = "en_AU.UTF-8";
};
# Enable the X11 windowing system.
services.xserver.enable = true;
# Enable the Cinnamon Desktop Environment.
services.xserver.displayManager.lightdm.enable = true;
services.xserver.desktopManager.cinnamon.enable = true;
# services.xserver.desktopManager.xfce.enable = true;
# Configure keymap in X11
services.xserver.xkb = {
layout = "au";
variant = "";
};
# Enable CUPS to print documents.
services.printing.enable = true;
# Enable sound with pipewire.
# services.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
#jack.enable = true;
# use the example session manager (no others are packaged yet so this is enabled by default,
# no need to redefine it in your config for now)
#media-session.enable = true;
};
# Enable touchpad support (enabled default in most desktopManager).
# services.xserver.libinput.enable = true;
users.users.nixos.extraGroups = [ "networkmanager" ]; # Enable ‘sudo’ for the user.
# Install firefox.
programs.firefox.enable = true;
system.stateVersion = "25.05"; # Did you read the comment?
nix.settings.experimental-features = "nix-command flakes";
services.xrdp.enable = true;
services.xrdp.defaultWindowManager = "cinnamon-session";
services.xrdp.openFirewall = true;
nixpkgs.config.allowUnfree = true;
services.gnome.gnome-keyring.enable = true;
# security.pam.services.lightdm.enableGnomeKeyring = true;
# services.gnome.gnome-keyring.enable = true;
security.pam.services.login.enableGnomeKeyring = true;
services.xserver.displayManager.sessionCommands = ''
eval $(gnome-keyring-daemon --start --components=secrets,ssh)
export SSH_AUTH_SOCK
'';
# systemd.services.nextcloud-appimage = {
# enable = true;
# Unit = {
# Description = "Nextcloud AppImage client";
# After = [ "graphical-session.target" ];
# Wants = [ "graphical-session.target" ]; # optional but helpful
# };
# Service = {
# ExecStart = "/run/current-system/sw/bin/appimage-run /home/nixos/Applications/Nextcloud.AppImage --background";
# Restart = "on-failure";
# # You can add RestartSec = "5s"; if you like
# };
# Install = {
# WantedBy = [ "default.target" ];
# };
# };
}
+52 -55
View File
@@ -1,77 +1,32 @@
{ 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
programs.home-manager.enable = true; # mandatory to activate HM
# Optional: packages # Optional: packages
packages = with pkgs; [ home.packages = with pkgs; [
git git
vim vim
tmux tmux
nextcloud-client nextcloud-client
# vscode # vscode
chromium chromium
]; ];
# Optional: set environment vars # Optional: set environment vars
sessionVariables = { home.sessionVariables = {
EDITOR = "vim"; EDITOR = "vim";
}; };
file = {
".local/share/applications/proxmox-chromium-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox (Chromium)
Exec=chromium --app=https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --window-size=1920,1080 --window-position=0,0
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false
Categories=Hypervisor;
StartupWMClass=PVE
'';
".local/share/applications/pbs-chromium-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox Backup Server (Chromium)
Exec=chromium --app=https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --window-size=1920,1080 --window-position=0,0
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
'';
".local/share/applications/proxmox-firefox-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox (Firefox)
Exec=firefox --new-instance https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --profile ProxmoxWebApp --window-size=1920,1080 --class ProxmoxWebApp
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false
Categories=Hypervisor;
StartupWMClass=PVE
'';
".local/share/applications/pbs-firefox-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox Backup Server (Firefox)
Exec=firefox --new-window https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --profile PbsWebApp --window-size=1920,1080 --class PbsWebApp
Icon=${config.home.homeDirectory}/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
StartupWMClass=PBS
'';
};
};
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 = {
@@ -79,4 +34,46 @@
# 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
'';
};
} }
-9
View File
@@ -1,9 +0,0 @@
_:
{
networking.hostName = "nixos";
# Preserved from the pre-refactor `nixos` target — stateVersion must never
# be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
-9
View File
@@ -1,9 +0,0 @@
_:
{
networking.hostName = "pxe-boot";
# Preserved from the pre-refactor `pxe-boot` target — stateVersion must
# never be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
+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?
}
-24
View File
@@ -1,24 +0,0 @@
{ vars, ... }:
{
imports = [
(import ../../modules/beszel/host-token.nix {
name = "server";
sopsFile = ../../secrets/server.yaml;
})
];
networking.hostName = vars.nfsServerHost;
networking.hostId = "6689f93e";
services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
LOG_LEVEL = "debug";
};
# Preserved from the pre-refactor `server` target — stateVersion must never
# be bumped on an already-installed machine.
system.stateVersion = "25.05";
}
-12
View File
@@ -1,12 +0,0 @@
_:
{
networking.hostName = "exit-node";
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
# for pool-import safety, and this host does neither.
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
# flake's current nixpkgs release rather than being pinned to an older one.
system.stateVersion = "26.05";
}
-12
View File
@@ -1,12 +0,0 @@
_:
{
networking.hostName = "tor-relay";
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
# for pool-import safety, and this host does neither.
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
# flake's current nixpkgs release rather than being pinned to an older one.
system.stateVersion = "26.05";
}
-18
View File
@@ -1,18 +0,0 @@
{ vars, ... }:
{
services.beszel.agent.enable = true;
services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}";
};
# The upstream module runs beszel-agent under DynamicUser with
# ProtectSystem = "strict" and no StateDirectory, so /var/lib/beszel-agent
# (where the agent persists its hub-pairing fingerprint, per
# https://github.com/henrygd/beszel/discussions/1542) isn't writable --
# every restart silently fails to save it and regenerates a fresh one in
# memory, permanently desyncing from whatever the hub has on record after
# the very first successful pairing. Give it real persistent storage.
systemd.services.beszel-agent.serviceConfig.StateDirectory = "beszel-agent";
}
-11
View File
@@ -1,11 +0,0 @@
{ name, sopsFile }:
{ config, ... }:
{
sops.secrets."beszel-token".sopsFile = sopsFile;
sops.templates."${name}-beszel.env".content = ''
TOKEN=${config.sops.placeholder."beszel-token"}
'';
services.beszel.agent.environmentFile = config.sops.templates."${name}-beszel.env".path;
}
-6
View File
@@ -1,6 +0,0 @@
_:
{
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
}
-45
View File
@@ -1,45 +0,0 @@
{ pkgs, vars, ... }:
{
# Pins the Docker Engine version, carried forward from the pre-refactor
# `docker` target's inline pkgs overlay.
nixpkgs.overlays = [
(final: prev: {
docker = prev.docker_29;
docker_cli = prev.docker_29;
})
];
imports = [
../docker/mount-data.nix
../docker/enable-service.nix
../docker/nextcloud-cron-job.nix
../docker/docker-health-to-gotify.nix
../tailscale/enable-service.nix
../traefik/rotate-logs.nix
../raspi/mount-data.nix
../services/enable-rpcbind.nix
];
environment.systemPackages = with pkgs; [
nfs-utils
];
boot.supportedFilesystems = [ "nfs" ];
systemd.tmpfiles.rules = [
"L+ /home/${vars.primaryUser}/docker - - - - ${vars.nfsShares.dockerConfig.mountpoint}"
"d /mnt/docker 0755 ${vars.primaryUser} users -"
"d ${vars.nfsShares.raspiVolumes.mountpoint} 0755 ${vars.primaryUser} users -"
];
users.users.${vars.primaryUser}.extraGroups = [ "docker" ];
services.openssh.settings.PermitRootLogin = "yes";
networking.firewall.allowedTCPPorts = [
vars.ports.dockerHttp
vars.ports.dockerExtra
vars.ports.dockerHttps
vars.ports.beszelHub
];
}
-73
View File
@@ -1,73 +0,0 @@
{ config, pkgs, lib, inputs, vars, ... }:
{
environment.systemPackages = with pkgs; [
inputs.nixos-conf-editor.packages.${pkgs.stdenv.hostPlatform.system}.nixos-conf-editor
nodejs
appimage-run
seahorse
vscode
p7zip
popsicle # balena-etcher
shotcut
gimp
pdfarranger
terminator
libreoffice-qt
transmission_4-qt
];
boot.loader.grub.useOSProber = true;
services = {
xserver = {
enable = true;
displayManager = {
lightdm.enable = true;
sessionCommands = ''
eval $(gnome-keyring-daemon --start --components=secrets,ssh)
export SSH_AUTH_SOCK
'';
};
desktopManager.cinnamon.enable = true;
xkb = {
layout = "au";
variant = "";
};
};
printing.enable = true;
pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
xrdp = {
enable = true;
defaultWindowManager = "cinnamon-session";
openFirewall = true;
};
gnome.gnome-keyring.enable = true;
};
security = {
rtkit.enable = true;
pam.services.login.enableGnomeKeyring = true;
};
# The networkmanager group only exists when NM is actually enabled — the
# lxc platform module force-disables it, so don't add the user to a group
# that won't exist there.
users.users.${vars.primaryUser}.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
programs.firefox.enable = true;
nixpkgs.config.allowUnfree = true;
}
-16
View File
@@ -1,16 +0,0 @@
{ lib, pkgs, config, vars, ... }:
{
networking.networkmanager.enable = true;
# The networkmanager group only exists when NM is actually enabled — the
# lxc platform module force-disables it, so don't add the user to a group
# that won't exist there.
users.users.${vars.primaryUser}.extraGroups = lib.mkIf config.networking.networkmanager.enable [ "networkmanager" ];
environment.systemPackages = with pkgs; [
inetutils
mtr
sysstat
];
}
-8
View File
@@ -1,8 +0,0 @@
{ ... }:
{
imports = [
../nix-cache/server.nix
../beszel/enable-agent.nix
];
}
-160
View File
@@ -1,160 +0,0 @@
{ config, lib, pkgs, inputs, vars, ... }:
let
pxeRoot = "/srv/pxe";
httpRoot = "${pxeRoot}/http";
tftpRoot = "${pxeRoot}/tftp";
pxeBaseUrl = "http://${vars.pxeServerIp}";
bootIpxe = pkgs.writeText "boot.ipxe" ''
#!ipxe
dhcp
echo Booting from PXE server...
chain ${pxeBaseUrl}/menu.ipxe
'';
autoexecIpxe = pkgs.writeText "autoexec.ipxe" ''
#!ipxe
dhcp
chain ${pxeBaseUrl}/boot.ipxe
'';
systemRescueIpxe = pkgs.writeText "systemrescue.ipxe" ''
#!ipxe
set base ${pxeBaseUrl}
kernel ''${base}/systemrescue/sysresccd/boot/x86_64/vmlinuz initrd=sysresccd.img archisobasedir=sysresccd archiso_http_srv=''${base}/systemrescue/ ip=dhcp checksum
initrd ''${base}/systemrescue/sysresccd/boot/x86_64/sysresccd.img sysresccd.img
boot
'';
stageSystemRescue = pkgs.writeShellScript "stage-systemrescue" ''
set -eu
iso="${httpRoot}/images/systemrescue.iso"
staged="${httpRoot}/systemrescue"
tmp="${httpRoot}/.systemrescue.tmp"
previous="${httpRoot}/.systemrescue.previous"
if [ ! -e "$iso" ]; then
echo "SystemRescue ISO not found at $iso; skipping staging."
exit 0
fi
rm -rf "$tmp"
mkdir -p "$tmp"
${pkgs.libarchive}/bin/bsdtar -C "$tmp" -xf "$iso"
test -f "$tmp/sysresccd/boot/x86_64/vmlinuz"
test -f "$tmp/sysresccd/boot/x86_64/sysresccd.img"
chmod -R a+rX "$tmp"
rm -rf "$previous"
if [ -e "$staged" ]; then
mv "$staged" "$previous"
fi
mv "$tmp" "$staged"
rm -rf "$previous"
'';
menuIpxe = pkgs.writeText "menu.ipxe" ''
#!ipxe
set base ${pxeBaseUrl}
menu PXE Boot Menu
item nixos NixOS Installer
item rescue Rescue Environment
item shell iPXE Shell
item reboot Reboot
choose target && goto ''${target}
:nixos
chain ''${base}/nixos/netboot.ipxe
:rescue
chain ''${base}/systemrescue.ipxe
:shell
shell
:reboot
reboot
'';
in
{
imports = [
../pxe-boot/stage-installer-artifacts.nix
];
environment.systemPackages = with pkgs; [
ipxe
];
services = {
nginx = {
enable = true;
virtualHosts."pxe-boot" = {
default = true;
root = httpRoot;
locations."/" = {
extraConfig = ''
autoindex on;
'';
};
};
};
# TFTP is only used to deliver the initial iPXE bootloader. After iPXE
# starts, all further assets are fetched via nginx over HTTP.
atftpd = {
enable = true;
root = tftpRoot;
extraOptions = [
"--verbose=5"
];
};
openssh.settings.PermitRootLogin = "yes";
};
systemd.tmpfiles.rules = [
"d ${pxeRoot} 0755 root root -"
"d ${httpRoot} 0755 root root -"
"d ${httpRoot}/images 0755 root root -"
"d ${httpRoot}/nixos 0755 root root -"
"d ${httpRoot}/systemrescue 0755 root root -"
"d ${httpRoot}/ubuntu 0755 root root -"
"d ${httpRoot}/rescue 0755 root root -"
"d ${tftpRoot} 0755 root root -"
"C+ ${httpRoot}/boot.ipxe 0644 root root - ${bootIpxe}"
"C+ ${httpRoot}/menu.ipxe 0644 root root - ${menuIpxe}"
"C+ ${httpRoot}/systemrescue.ipxe 0644 root root - ${systemRescueIpxe}"
"C+ ${tftpRoot}/autoexec.ipxe 0644 root root - ${autoexecIpxe}"
"C+ ${tftpRoot}/ipxe.efi 0644 root root - ${pkgs.ipxe}/ipxe.efi"
"C+ ${tftpRoot}/undionly.kpxe 0644 root root - ${pkgs.ipxe}/undionly.kpxe"
];
systemd.services.stage-systemrescue = {
description = "Stage SystemRescue ISO contents for HTTP PXE boot";
after = [
"local-fs.target"
"systemd-tmpfiles-setup.service"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = stageSystemRescue;
};
};
networking.firewall.allowedTCPPorts = [ vars.ports.pxeBootHttp ];
networking.firewall.allowedUDPPorts = [ vars.ports.pxeBootTftp ];
}
-28
View File
@@ -1,28 +0,0 @@
{ vars, lib, ... }:
{
imports = [
../beszel/enable-agent.nix
../services/zfs/enable-service.nix
];
boot.zfs.extraPools = [ (lib.removePrefix "/" vars.storageRoot) ];
systemd.services.nfs-server = {
after = [ "zfs-mount.service" ];
requires = [ "zfs-mount.service" ];
};
services.nfs.server = {
enable = true;
exports = ''
${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}(rw,sync,no_subtree_check,no_root_squash)
'';
};
networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd ];
}
@@ -1,21 +0,0 @@
{ ... }:
{
imports = [
../tailscale/exit-node.nix
];
# "server", not "both": this build type only ever advertises itself as an
# exit node (see ../tailscale/exit-node.nix) -- it doesn't advertise LAN
# subnet routes, so it doesn't need the "client"-side loose reverse-path
# filtering that "both" would also turn on. Deliberately left unbundled
# from LAN-subnet-route advertisement so this build type stays valid on
# every platform, including linode (a remote VPS with no network path to
# the home LAN at all).
services.tailscale.useRoutingFeatures = "server";
# Forwarded exit-node traffic arrives on tailscale0 already
# tailscale-authenticated -- the firewall's normal per-port allow-list
# would otherwise drop it. Standard NixOS/Tailscale exit-node guidance.
networking.firewall.trustedInterfaces = [ "tailscale0" ];
}
-7
View File
@@ -1,7 +0,0 @@
{ ... }:
{
imports = [
../tor/enable-relay.nix
];
}
-50
View File
@@ -1,50 +0,0 @@
{ config, pkgs, lib, vars, ... }:
let
# Flake attribute names are now <platform>-<buildtype> (e.g. proxmox-docker)
# and no longer match networking.hostName, since a host's hostname stays
# fixed while the platform backing it can change. Each nixosConfiguration
# stamps its own active target name into /etc/flake-target at build time.
mySwitchCmd = ''
sudo nixos-rebuild switch \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
'';
myTestCmd = ''
sudo nixos-rebuild test \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
'';
# lxc-* hosts pre-seed their SSH host key at build time (see
# modules/platforms/lxc.nix) so sops-nix's .sops.yaml recipient matches on
# first boot -- without it, secrets permanently fail to decrypt (see that
# file's comment for the confirmed failure). That requires --impure plus
# NIXOS_HOST_KEYS_DIR pointing at the repo's host-keys/ dir, same pattern
# docs/auto-installer.md uses for the installer ISO. A function, not a
# shellAlias, since the target name has to interpolate into the middle of
# the flake attribute path, not just append after it. Must be run from the
# repo root, same as every other host-keys/ command in this repo.
buildImageFn = ''
buildImage() {
if [ -z "$1" ]; then
echo "usage: buildImage <flake-target> (e.g. lxc-docker)" >&2
return 1
fi
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
".#nixosConfigurations.$1.config.system.build.tarball"
}
'';
in
{
programs.bash = {
enable = true;
shellAliases = {
"Switch-nix" = mySwitchCmd;
"Test-nix" = myTestCmd;
};
initExtra = buildImageFn;
};
}
-122
View File
@@ -1,122 +0,0 @@
{ config, lib, pkgs, vars, ... }:
{
imports =
[
# Include the results of the hardware scan.
# ./hardware-configuration.nix
./set-locale.nix
];
# Use the GRUB 2 boot loader.
# boot.loader.grub.enable = true;
#boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
# No host declares a DNS search domain anywhere else, and cross-host
# references throughout this repo (vars.nfsServerHost, vars.nixCacheHost,
# vars.dockerHost, ...) are bare short names, not FQDNs -- resolving them
# depends entirely on whatever network stack happens to be in play
# picking up the DHCP-advertised domain as a search suffix. NetworkManager
# does that by default, which is why this went unnoticed on
# NetworkManager-managed hosts, but LXC containers (modules/platforms/lxc.nix
# force-disables NetworkManager and Proxmox writes their systemd-networkd
# config itself) never get one. Confirmed live on lxc-docker: systemd-resolved
# had no search domain for eth0, "server" failed to resolve
# ("Name or service not known") while "server.sweet.home" resolved fine via
# the same DNS server, so every NFS mount in modules/docker/mount-data.nix
# failed even after fixing the automount/mount=nfs bugs. This applies the
# search domain globally via systemd-resolved's own config rather than the
# per-link DHCP path, so it isn't at the mercy of whichever component owns
# a given host's interface file.
networking.search = [ vars.homeDomain ];
# Recommended over the true default (bypasses ZFS's own import safeguards)
# per the option's own docs; matches hosts/docker/host.nix and
# modules/services/zfs/enable-service.nix, which already set this
# explicitly. Harmless no-op on hosts that don't use ZFS at all.
boot.zfs.forceImportRoot = false;
# Set your time zone.
time.timeZone = vars.timeZone;
# Enable QEMU agent
services.qemuGuest.enable = true;
# Enable docker-compose
environment.systemPackages = with pkgs; [
vim
btop
git
gcr
];
# Secrets shared by every host, decrypted at activation via each host's
# existing SSH host key (sops-nix derives the age key from
# /etc/ssh/ssh_host_ed25519_key automatically — see modules/common/README
# or docs/ for the sops workflow). hashedPassword/hashedPasswordFile need
# neededForUsers so they're available before the normal secret-activation
# step, since user creation happens very early in boot.
sops = {
defaultSopsFile = ../../secrets/common.yaml;
secrets = {
"root-hashedPassword".neededForUsers = true;
"nixos-hashedPassword".neededForUsers = true;
"nix-github-token" = { };
};
# nix.conf doesn't support a *File-style option for access-tokens, so the
# token is rendered into a runtime-only file (never touches the Nix store)
# and pulled in via nix.conf's native !include directive.
templates."nix-github-token.conf".content = ''
access-tokens = github.com=${config.sops.placeholder."nix-github-token"}
'';
};
nix.extraOptions = ''
!include ${config.sops.templates."nix-github-token.conf".path}
'';
#Set root password
users.users.root = {
hashedPasswordFile = config.sops.secrets."root-hashedPassword".path;
};
# Define a user account. Don't forget to set a password with ‘passwd’.
users.users.${vars.primaryUser} = {
isNormalUser = true;
extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
packages = with pkgs; [
tree
];
hashedPasswordFile = config.sops.secrets."nixos-hashedPassword".path;
openssh.authorizedKeys.keys = [
vars.adminSshKey
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
];
};
# Enable the OpenSSH daemon.
services.openssh.enable = true;
#Enable flakes
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = true;
};
programs.git = {
enable = true;
package = pkgs.git;
config = {
credential.helper = "store";
};
};
}
-69
View File
@@ -1,69 +0,0 @@
{ config, pkgs, lib, vars, ... }:
let
remote = "root@proxmox-ip:/var/lib/vz/template/iso";
localMount = "${config.home.homeDirectory}/proxmox-iso";
in
{
imports = [
./aliases.nix
];
home = {
username = vars.primaryUser;
homeDirectory = "/home/${vars.primaryUser}";
stateVersion = "25.11"; # match your NixOS stateVersion
# Optional: packages
packages = with pkgs; [
git
vim
tmux
nano
sshfs
];
# Optional: set environment vars
sessionVariables = {
EDITOR = "nano";
};
};
programs.home-manager.enable = true; # mandatory to activate HM
programs.bash.enable = true;
# GitHub access-tokens setting used to live here in plaintext; it's now
# rendered system-wide from a sops-nix secret via nix.extraOptions in
# modules/common/configuration.nix instead (covers the daemon for every
# user, not just this one).
# systemd.user.services.mount-proxmox-iso = {
# Unit = {
# Description = "Mount Proxmox ISO dir via SSHFS";
# After = [ "network-online.target" ];
# Wants = [ "network-online.target" ];
# };
# Service = {
# Type = "simple";
# ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${localMount}";
# ExecStart = "${pkgs.sshfs}/bin/sshfs -o IdentityFile=${config.home.homeDirectory}/.ssh/id_ed25519,allow_other,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3 root@proxmox-ip:/var/lib/vz/template/iso ${localMount}";
# ExecStop = "${pkgs.fuse3}/bin/fusermount3 -u ${localMount}";
# Restart = "on-failure";
# };
# Install = {
# WantedBy = [ "default.target" ];
# };
# };
# Optional: enable bash (or zsh, fish...)
# programs.bash.enable = true;
# Optional: manage dotfiles via symlinks
# home.file = {
# ".tmux.conf".source = ./dotfiles/tmux.conf;
# ".config/nvim/init.vim".source = ./dotfiles/init.vim;
# };
}
-18
View File
@@ -1,18 +0,0 @@
_:
{
i18n.defaultLocale = "en_AU.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "en_AU.UTF-8";
LC_IDENTIFICATION = "en_AU.UTF-8";
LC_MEASUREMENT = "en_AU.UTF-8";
LC_MONETARY = "en_AU.UTF-8";
LC_NAME = "en_AU.UTF-8";
LC_NUMERIC = "en_AU.UTF-8";
LC_PAPER = "en_AU.UTF-8";
LC_TELEPHONE = "en_AU.UTF-8";
LC_TIME = "en_AU.UTF-8";
};
}
-35
View File
@@ -1,35 +0,0 @@
_:
{
# Linode provisions and sizes these disks itself (via the Linode
# dashboard/API) before the OS ever boots, and presents them as whole,
# unpartitioned block devices — /dev/sda is the root filesystem directly,
# /dev/sdb is swap directly, no partition table on either. Nothing here
# should ever repartition or resize them:
# - `destroy = false` skips each disk entirely during disko's destroy
# stage (see disko's disk.destroy option) — no wipefs, ever.
# - the filesystem content type's own create step only runs mkfs if the
# device isn't already formatted (checked via `blkid`), so re-running
# this against an already-provisioned Linode disk is a no-op.
disko.devices.disk = {
main = {
device = "/dev/sda";
destroy = false;
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
swap = {
device = "/dev/sdb";
destroy = false;
content = {
type = "swap";
};
};
};
}
-70
View File
@@ -1,70 +0,0 @@
{ config, vars, ... }:
{
disko.devices = {
disk.main = {
type = "disk";
device = "/dev/sda";
# Only used when building a standalone disk image directly (`nix build
# .#nixosConfigurations.<host>.config.system.build.diskoImagesScript`)
# rather than formatting a real device — see docs/proxmox-images.md.
# imageSize sets the .raw file's total size (root's "100%" below fills
# whatever's left after ESP + swap within it); imageName keeps each
# host's image distinctly named instead of every proxmox-* host
# producing an identical "main.raw".
imageSize = vars.proxmoxImageSize;
imageName = config.networking.hostName;
content = {
type = "gpt";
partitions = {
esp = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
extraArgs = [
"-F"
"32"
"-n"
"boot"
];
};
};
swap = {
size = "8G";
content = {
type = "swap";
randomEncryption = false;
# label = "swap";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
extraArgs = [
"-L"
"nixos"
];
};
};
};
};
};
};
}
@@ -1,28 +0,0 @@
{ pkgs, vars, ... }:
{
systemd.services.docker-health-to-gotify = {
description = "Alert Gotify when Docker containers become unhealthy";
after = [ "docker.service" ];
serviceConfig = {
Type = "oneshot";
# Run as root so it can read /etc/secrets and access docker socket
# User = "root";
#EnvironmentFile = "-/etc/secrets/docker-health-alert.env";
ExecStart = "${pkgs.bash}/bin/bash /home/${vars.primaryUser}/docker/monitoring/gotify/docker-health-to-gotify.sh";
StandardOutput = "journal";
StandardError = "journal";
};
path = with pkgs; [ docker curl coreutils gnused ];
};
systemd.timers.docker-health-to-gotify = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "1min";
AccuracySec = "15s";
Persistent = true;
};
};
}
-23
View File
@@ -1,23 +0,0 @@
{ pkgs, ... }:
{
# virtualisation.docker.enable = true;
virtualisation.docker = {
enable = true;
package = pkgs.docker;
# listenOptions = [
# "unix:///var/run/docker.sock"
# "tcp://0.0.0.0:2375"
#];
# daemon.settings = {
# metrics-addr = "0.0.0.0:9323";
# experimental = true;
# };
};
environment.systemPackages = with pkgs; [
docker-compose
docker-buildx
];
}
-70
View File
@@ -1,70 +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" ];
in
{
fileSystems = {
${vars.nfsShares.dockerConfig.mountpoint} = {
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.dockerDatabases.mountpoint} = {
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.dockerVolumes.mountpoint} = {
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.nextcloudData.mountpoint} = {
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
${vars.nfsShares.raspiVolumes.mountpoint} = {
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
fsType = "nfs";
options = [
"nfsvers=4.2"
"_netdev"
"noatime"
] ++ automountOpts;
};
};
}
-22
View File
@@ -1,22 +0,0 @@
{ pkgs, vars, ... }:
{
# Create nextcloud cron scheduled task
systemd.services.nextcloud = {
description = "Nextcloud scheduled task";
script = ''${pkgs.bash}/bin/bash ~/docker/services-up.sh --profile nextcloud exec -u 33 nextcloud-webapp php ./cron.php'';
serviceConfig = {
Type = "oneshot";
User = vars.primaryUser;
};
path = with pkgs; [ docker docker-compose ];
};
systemd.timers.nextcloud = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*:0/5";
Persistent = true;
};
};
}
@@ -1,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";
}
-240
View File
@@ -1,240 +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
];
# Write auto-install script to /root
etc."auto-install.sh" = {
text = ''
#!/run/current-system/sw/bin/bash
set -eux
set -euo pipefail
export FLAKE_BASE_URL="git+https://${vars.lanDomain}/beatzaplenty/nixos.git"
echo "Fetching available NixOS hosts from flake..."
# Two categories deliberately excluded from the menu:
# lxc-* — these build a config.system.build.tarball meant for
# `pct restore` on Proxmox directly, not an install.
# Running nixos-install against one here would
# bind-mount / onto /mnt and then refuse to touch the
# filesystem it's currently running on — see
# docs/auto-installer.md.
# installer — this *is* the installer image's own flake target,
# not a deployable host; "installing" it means
# nixos-install-ing a copy of the installer into
# itself.
mapfile -t options < <(
nix eval --json --no-use-registries --no-accept-flake-config --extra-experimental-features "flakes nix-command" \
"''${FLAKE_BASE_URL}#nixosConfigurations" \
--apply builtins.attrNames \
| jq -r '.[]
| select(startswith("lxc-") | not)
| select(. != "installer")'
)
if [[ ''${#options[@]} -eq 0 ]]; then
echo "ERROR: No NixOS hosts found in ''${FLAKE_BASE_URL}#nixosConfigurations" >&2
exit 1
fi
echo "Note: lxc-* targets aren't installed this way — build them with"
echo " nix build .#nixosConfigurations.<name>.config.system.build.tarball"
echo "and 'pct restore' the result on Proxmox directly. See docs/auto-installer.md."
echo "Choose the flake profile to install:"
select choice in "''${options[@]}"; do
if [[ -n "$choice" ]]; then
echo "You selected: $choice"
break
else
echo "Invalid selection. Try again."
fi
done
echo "Starting install with flake: ''${FLAKE_BASE_URL}#''${choice}"
# Optional: confirm before proceeding
read -rp "Proceed with installation? (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
# A nix-cache host is *the* substituter/remote-builder for every other
# host once installed (its own config explicitly excludes itself from
# using either — see buildType != "nix-cache" in the nixos flake.nix).
# Installing one shouldn't depend on a nix-cache substituter either,
# for the same reason — plus in practice "nix-cache" only resolves over
# Tailscale, which a fresh installer environment was never connected to
# anyway, so it's dead weight even for non-nix-cache installs until
# that's sorted out. Override it away here specifically for nix-cache
# targets to keep install-time behaviour consistent with run-time.
nix_extra_opts=()
if [[ "''${choice}" == *-nix-cache ]]; then
echo "Installing a nix-cache host — skipping the nix-cache substituter."
nix_extra_opts+=(--option substituters "https://cache.nixos.org/")
fi
# Every host reachable through this menu has a Disko config (lxc-*
# is filtered out above, and is the only category that doesn't —
# see docs/auto-installer.md), so this can run unconditionally: no
# need to probe the flake first and branch on whether Disko applies.
disko --mode destroy,format,mount \
--flake "''${FLAKE_BASE_URL}#''${choice}" "''${nix_extra_opts[@]}" --yes-wipe-all-disks
# sops-nix derives this host's decryption key from its own SSH host key
# at *activation* time, which runs before systemd would otherwise
# generate one on first boot. Without pre-seeding it here, secrets
# (including the login password) fail to decrypt on first boot.
# Generate the key with scripts/secrets/prepare-host-key.sh first.
#
# Two places a key can come from, checked in order:
# /etc/host-keys — baked into this image at build time (see
# modules/installer/host-keys.nix; only present
# if built with NIXOS_HOST_KEYS_DIR set)
# /root/host-keys — scp'd in manually after boot (older fallback,
# still supported for images built without keys)
mkdir -p /root/host-keys
if [[ -f "/etc/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
echo "Found baked-in SSH host key for ''${choice}, installing to target..."
install -D -m 0600 "/etc/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
install -D -m 0644 "/etc/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
elif [[ -f "/root/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
echo "Found pre-seeded SSH host key for ''${choice}, installing to target..."
install -D -m 0600 "/root/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
install -D -m 0644 "/root/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
else
echo "WARNING: no SSH host key found for ''${choice} (checked /etc/host-keys and /root/host-keys)"
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
echo "Run scripts/secrets/prepare-host-key.sh for host ''${choice} on your admin workstation first,"
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, or scp the result to"
echo "/root/host-keys/ on this machine."
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
mkdir -p /mnt/install-tmp
export TMPDIR=/mnt/install-tmp
nixos-install \
--flake "''${FLAKE_BASE_URL}#''${choice}" \
"''${nix_extra_opts[@]}" \
--no-root-password
rm -rf /mnt/install-tmp
# Redundant copy of the host's private key — the real one is now at
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
# up on its own since it was written imperatively, not declaratively.
rm -rf /root/host-keys
sleep 10
reboot
'';
mode = "0755";
};
};
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/auto-install.sh
touch "$HOME/.auto_install_ran"
fi
'';
services.openssh.enable = true;
services.openssh.settings = {
PermitRootLogin = "yes";
PasswordAuthentication = true;
};
# nixpkgs' own installer profile (profiles/installation-device.nix, pulled
# in via installation-cd-minimal.nix) sets initialHashedPassword = "" for
# both users — its own passwordless-login convention for install media.
# That's a second, non-null password option alongside our hashedPassword
# below, which NixOS warns about as ambiguous precedence. Force it null
# rather than adopting passwordless login: this image now also boots over
# LAN PXE with PasswordAuthentication enabled, so passwordless root SSH
# would be reachable by anyone on the LAN, not just local console.
users.users.root = {
hashedPassword =
"$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
initialHashedPassword = lib.mkForce null;
};
users.users.${vars.primaryUser} = {
isNormalUser = true;
extraGroups = [
"wheel"
];
shell = pkgs.bashInteractive;
hashedPassword =
"$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
initialHashedPassword = lib.mkForce null;
openssh.authorizedKeys.keys = [
vars.adminSshKey
];
};
system.stateVersion = "26.05";
}
-37
View File
@@ -1,37 +0,0 @@
{ lib, ... }:
let
# host-keys/ is gitignored (private key material must never be committed),
# which means flakes' git-filtered source tree can never see it via a
# normal relative path — referencing it at all requires stepping outside
# pure evaluation. builtins.getEnv is neutered to "" under normal
# `nix build`/`nix eval` (no error, just empty), so this whole module is a
# silent no-op unless the operator explicitly opts in with --impure and
# the env var set — safe by default, including in CI.
#
# NIXOS_HOST_KEYS_DIR=$(pwd)/host-keys nix build .#iso --impure
#
# See docs/auto-installer.md.
hostKeysDirStr = builtins.getEnv "NIXOS_HOST_KEYS_DIR";
hasHostKeysDir = hostKeysDirStr != "" && builtins.pathExists hostKeysDirStr;
hostKeysDir = /. + hostKeysDirStr;
keyFileNames =
if hasHostKeysDir
then
lib.filter
(name: lib.hasSuffix "_ssh_host_ed25519_key" name || lib.hasSuffix "_ssh_host_ed25519_key.pub" name)
(lib.attrNames (builtins.readDir hostKeysDir))
else [ ];
in
{
environment.etc = lib.listToAttrs (map
(name: {
name = "host-keys/${name}";
value = {
source = hostKeysDir + "/${name}";
mode = "0400";
};
})
keyFileNames);
}
-8
View File
@@ -1,8 +0,0 @@
{ modulesPath, ... }:
{
imports = [
"${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix"
./common.nix
];
}
@@ -1,12 +0,0 @@
{ config, lib, ... }:
{
options.myNetworking.ipForwarding.enable = lib.mkEnableOption "Enable IPv4 and IPv6 forwarding";
config = lib.mkIf config.myNetworking.ipForwarding.enable {
boot.kernel.sysctl = {
"net.ipv4.ip_forward" = 1;
"net.ipv6.conf.all.forwarding" = 1;
};
};
}
-15
View File
@@ -1,15 +0,0 @@
{ vars, ... }:
{
nix.settings = {
substituters = [
"http://${vars.nixCacheHost}"
"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 nix-store --version
# Trust nix-cache's SSH host key declaratively so the nix-daemon (root)
# can connect the first time without a manual ssh-keyscan/known_hosts
# step on every new client.
programs.ssh.knownHosts.${vars.nixCacheHost} = {
hostNames = [ vars.nixCacheHost ];
publicKey = vars.nixCacheHostKey;
};
nix = {
distributedBuilds = true;
buildMachines = [
{
hostName = vars.nixCacheHost;
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} = {
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}";
};
}
-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;
};
}
-148
View File
@@ -1,148 +0,0 @@
{ lib, modulesPath, flakeTarget, ... }:
let
# Bakes this exact flake target's pre-generated SSH host key straight
# into /etc/ssh/ -- mirrors modules/installer/host-keys.nix's
# builtins.getEnv pattern (impure and empty under normal `nix
# build`/`nix eval`, so this is a no-op unless explicitly opted into
# with NIXOS_HOST_KEYS_DIR=... --impure), but places the key directly
# rather than staging it under /etc/host-keys/ for a later manual copy
# -- this is the whole system for a `lxc-*` host, built straight to a
# pct-restorable tarball with no install step, so there's no later copy
# step to stage for.
#
# Without this, config.system.build.tarball's built-in system just
# generates a fresh host key at first boot like any other host would --
# but sops-nix derives its decryption key from *this* file, and
# .sops.yaml only trusts whatever key scripts/secrets/sync-host-keys.sh already
# registered for this exact target name. A freshly-generated key can
# never match that, so every secret (including this host's own login)
# permanently fails to decrypt. Confirmed live: sops-install-secrets
# errored with "Error getting data key: 0 successful groups required,
# got 0" -- the container's actual host key's age fingerprint didn't
# match the one registered in .sops.yaml at all.
hostKeysDirStr = builtins.getEnv "NIXOS_HOST_KEYS_DIR";
hasHostKeysDir = hostKeysDirStr != "" && builtins.pathExists hostKeysDirStr;
hostKeysDir = /. + hostKeysDirStr;
# flakeTarget ("${platform}-${buildType}") comes in via specialArgs from
# flake.nix's mkTarget -- exactly the name scripts/secrets/sync-host-keys.sh
# registers keys under. Deliberately not read back from
# config.environment.etc."flake-target" (which is set to the same value)
# -- this module also *contributes* to environment.etc below, and a
# module reading the merged value of an option it's still defining is a
# circular dependency (confirmed: "infinite recursion encountered").
privKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key";
pubKeyFile = hostKeysDir + "/${flakeTarget}_ssh_host_ed25519_key.pub";
hasKeyForThisTarget =
hasHostKeysDir
&& builtins.pathExists privKeyFile
&& builtins.pathExists pubKeyFile;
in
{
# LXC containers share the host kernel — Proxmox starts them by exec'ing
# /sbin/init directly, no bootloader/initrd involved — and Proxmox has its
# own container hostname/network provisioning outside Nix. nixpkgs' own
# virtualisation/proxmox-lxc.nix module already handles all of this
# correctly (boot.isContainer, loader.initScript, systemd-networkd) and,
# critically, provides config.system.build.tarball — a directly
# `pct restore`-able container image, no nixos-install/bind-mount needed
# (nixos-install refuses to touch the filesystem it's currently running
# on, which is exactly what bind-mounting / onto /mnt for an installer
# LXC container does).
imports = [
(modulesPath + "/virtualisation/proxmox-lxc.nix")
];
proxmoxLXC = {
# host.nix declares each host's real hostname (networking.hostName);
# keep that instead of letting Proxmox's ambient container config win.
manageHostName = true;
# Unprivileged matches how these containers are actually created.
privileged = false;
};
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. Regular secrets
# (nix-serve's key, beszel's token, etc.) work anyway because sops-nix
# provides its own systemd service for those.
#
# A systemd service, not boot.postBootCommands: tried that first (it's
# a genuine, generally-invoked hook -- nixos/modules/system/boot/stage-2-init.sh,
# which becomes this container's actual /sbin/init, unconditionally
# runs it) but switch-to-configuration behaves differently that early in
# boot (raw stage-2-init.sh, before systemd itself has even started) --
# confirmed live it silently failed to rewrite /etc/shadow from there
# even in "test" mode, despite the exact same command working reliably
# every time when run post-boot (i.e. as a normal systemd service, which
# is what this is). Not fully root-caused why the early context
# specifically breaks it; a real systemd service sidesteps needing to.
#
# /etc/shadow already has PLACEHOLDER entries for every declared user
# baked in at build time (part of constructing the system closure).
# update-users-groups.pl deliberately never overwrites an *existing*
# shadow entry -- a correct safety property in general (don't clobber a
# real user's real password on a config rebuild) -- but on a genuine
# first boot that only means the real hashedPasswordFile-derived hash
# never gets the chance to be applied either, since the placeholder is
# already "seen". Safe to clear here specifically: there is no real
# password yet to protect on a first boot.
#
# "test" mode, not "boot": confirmed live "boot" mode aborts partway
# through (before rewriting /etc/shadow) on a warning that "/boot" is on
# a different filesystem -- a real check for a host with a bootloader to
# update, meaningless for a container that has none
# (boot.loader.{grub,systemd-boot}.enable are both false above), but it
# still aborts the script. "test" runs every activation step without
# touching boot-loader state at all.
#
# ConditionPathExists (systemd-native, not a bash-level check) means
# this only ever runs once, on the genuine first boot -- systemd itself
# skips even starting it on every later boot once the marker exists.
# switch-to-configuration is otherwise the operator's call per this
# repo's own safety rules, not something to run on every boot.
systemd.services.nixos-lxc-first-boot-activate = {
description = "Complete first-boot NixOS activation (users, secrets) for this LXC container";
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionPathExists = "!/var/lib/nixos-lxc-first-boot-activated";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
rm -f /etc/shadow
/run/current-system/bin/switch-to-configuration test
mkdir -p /var/lib
touch /var/lib/nixos-lxc-first-boot-activated
'';
};
}
-9
View File
@@ -1,9 +0,0 @@
{ ... }:
{
imports = [
../hardware-configuration/vm/proxmox.nix
../boot/efi.nix
../disko/proxmox.nix
];
}
@@ -1,23 +0,0 @@
{ netbootSystem, ... }:
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.
inherit (netbootSystem.config.system.boot.loader) kernelFile;
in
{
# Builds this flake's own installer netboot image (the same one
# `nix build .#pxe` produces) and stages it where menu.ipxe's :nixos
# entry expects it, so the pxe-boot host is self-contained — no manual
# operator step to populate /srv/pxe/http/nixos after deploy.
systemd.tmpfiles.rules = [
# 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 /srv/pxe/http/nixos 0755 root root -"
"C+ /srv/pxe/http/nixos/${kernelFile} 0644 root root - ${netbootSystem.config.system.build.kernel}/${kernelFile}"
"C+ /srv/pxe/http/nixos/initrd 0644 root root - ${netbootSystem.config.system.build.netbootRamdisk}/initrd"
"C+ /srv/pxe/http/nixos/netboot.ipxe 0644 root root - ${netbootSystem.config.system.build.netbootIpxeScript}/netboot.ipxe"
];
}
-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;
}
-27
View File
@@ -1,27 +0,0 @@
_:
{
imports = [ ./enable-service.nix ];
services.tailscale = {
# Enables the sysctl forwarding settings exit nodes/subnet routers need;
# without this, --advertise-exit-node has no effect.
useRoutingFeatures = "server";
# Lets peers reach this node directly over the tailscale UDP port
# instead of relaying through DERP.
openFirewall = true;
# extraSetFlags (tailscale set, via the always-on tailscaled-set
# service), not extraUpFlags -- extraUpFlags is only ever applied by
# tailscaled-autoconnect, which itself only runs when
# services.tailscale.authKeyFile is set (nothing in this repo sets one,
# so tailscale up is a manual, one-time operator step on every host that
# uses this service). extraSetFlags has no such gate, so
# --advertise-exit-node self-reapplies on every boot once the operator
# has authenticated the node once.
extraSetFlags = [
"--advertise-exit-node"
];
};
}
-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."
-313
View File
@@ -1,313 +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).
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"
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})..."
for host in "${hosts[@]}"; do
echo "==> $host"
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of
# 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.
case "$host" in
lxc-*)
echo "==> $host (tarball)"
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
;;
proxmox-*)
echo "==> $host (diskoImagesScript)"
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
;;
esac
done
fi
echo
if ! $eval_packages; then
echo "No packages affected by changed files; skipping package eval."
else
echo "Evaluating buildable packages..."
for pkg in "${all_packages[@]}"; do
echo "==> packages.x86_64-linux.${pkg}"
nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
done
fi
if $dry_run; then
echo
echo "Running dry-run builds for the active scope. This will not create result symlinks."
for host in "${hosts[@]:-}"; do
echo "==> Dry-run build: $host"
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel"
case "$host" in
lxc-*)
echo "==> Dry-run build: $host (tarball)"
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball"
;;
proxmox-*)
echo "==> Dry-run build: $host (diskoImagesScript)"
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
;;
esac
done
if $eval_packages; then
echo
echo "Running dry-run builds for packages."
for pkg in "${all_packages[@]}"; do
echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
done
fi
fi
echo
echo "Maintenance checks complete."
-87
View File
@@ -1,87 +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
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."
-164
View File
@@ -1,164 +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.
# SSH-reachable Proxmox node that scripts/proxmox/create-proxmox-resource.sh runs
# pct/qm on. Matches the Proxmox web UI hostname already used in
# hosts/nixos/home.nix's desktop shortcuts (pve.<homeDomain> from
# variables.nix) -- change this if that's not actually reachable over SSH,
# or if you're targeting a different node in a multi-node cluster.
: "${PROXMOX_HOST:=pve.sweet.home}"
: "${PROXMOX_SSH_USER:=root}"
# 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:=/root/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-lvm}" # 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 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
# 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
}
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
# Shared "type X to confirm" prompt for scripts/proxmox/create-proxmox-resource.sh
# (--modify, and replacing an existing --allow-duplicate-host resource) and
# scripts/secrets/sync-host-keys.sh (--regenerate-all-keys) -- three destructive
# confirmations that all work the same way (echo the expected value back
# exactly), kept in one place so the prompt/comparison logic can't drift.
# Source alongside env.sh:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/confirm.sh"
#
# Deliberately does NOT print anything on mismatch or decide exit-vs-return
# -- callers vary on both (a top-level script exits, a subcommand function
# returns; wording differs too), so that stays at the call site.
# confirm_typed <expected> <prompt>
# Prints <prompt> via `read -rp`, then reports (via exit status) whether the
# typed input matched <expected> exactly.
confirm_typed() {
local expected="$1" prompt="$2" input
read -rp "$prompt" input
[[ "$input" == "$expected" ]]
}
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/env bash
# Shared Nix bootstrap for scripts/codex-setup.sh and
# scripts/codex-maintenance.sh: the nix.conf settings both need in effect
# before a single `nix` command runs (flakes enabled, never honor a flake
# input's own nixConfig, no "dirty tree" warning spam), plus a helper to
# pull an already-installed Nix's daemon/profile script onto PATH if it
# isn't there yet. Source this instead of copying it -- see CLAUDE.md.
export NIX_CONFIG="${NIX_CONFIG:-}
experimental-features = nix-command flakes
accept-flake-config = false
warn-dirty = false
"
ensure_nix_profile() {
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
fi
}
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/env bash
# Shared flake-introspection helpers for scripts/*.sh. Source alongside
# env.sh:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-eval.sh"
#
# NIX_EVAL_FLAGS: --no-use-registries so a call here never resolves through
# the user's global flake registry (every call targets this repo's own
# flake, or an explicit github: ref, not a registry alias); --no-accept-flake-config
# so a flake input's own nixConfig (e.g. a dependency's substituters) is
# never honored -- matches accept-flake-config = false already set repo-wide
# (see lib/nix-bootstrap.sh / CLAUDE.md). Reuse this array rather than
# retyping the two flags at each call site.
declare -a NIX_EVAL_FLAGS=(--no-use-registries --no-accept-flake-config)
# list_flake_targets <flake_ref>
# Prints the attribute names under <flake_ref>#nixosConfigurations, one per
# line, e.g.:
# list_flake_targets . # from inside the repo
# list_flake_targets "$repo_root" # from anywhere
list_flake_targets() {
local flake_ref="$1"
nix eval --json "${NIX_EVAL_FLAGS[@]}" \
"${flake_ref}#nixosConfigurations" --apply builtins.attrNames \
| jq -r '.[]'
}
# flake_target_hostname <flake_ref> <target>
# Prints one nixosConfigurations target's config.networking.hostName.
# Empty (not an error under set -e) if the target doesn't exist or the
# eval otherwise fails -- callers that need to distinguish "empty" from
# "eval failed" should check $? themselves instead of relying on this.
flake_target_hostname() {
local flake_ref="$1" target="$2"
nix eval --raw "${NIX_EVAL_FLAGS[@]}" \
"${flake_ref}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null
}
-52
View File
@@ -1,52 +0,0 @@
#!/usr/bin/env bash
# Shared sops/age helpers for scripts/secrets/backup-admin-key.sh,
# scripts/secrets/rotate-admin-key.sh, and scripts/secrets/sync-host-keys.sh -- all three
# derive an age public key from a private identity file the same way, two
# of them resolve the same sops/age default key-file path, and two of them
# run `sops updatekeys` the same way. Kept in one place so they can't drift
# apart. Source alongside env.sh:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/sops-age.sh"
#
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
# nix_extra_opts) if the caller has already set it, same convention as
# lib/ssh-host-keys.sh. Falls back to no extra options if the caller never
# sourced env.sh.
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
declare -a NIX_OPTS=()
fi
# sops/age's own default identity-file resolution order, minus $SOPS_AGE_KEY
# itself (an inline identity, not a path -- callers that accept it check it
# separately, before falling back to this).
: "${DEFAULT_SOPS_AGE_KEY_FILE:=${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}}"
# age_pubkey_from_identity_file <identity-file>
# Prints the age public key for a private identity file (age-keygen -y).
age_pubkey_from_identity_file() {
local identity_file="$1"
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '${identity_file}'"
}
# sops_yaml_admin_pubkey <sops-yaml-path>
# Prints .sops.yaml's current &admin age public key, or empty (not an error
# under set -e) if no such anchor line exists -- callers that need to treat
# "missing" as fatal check for an empty result themselves.
sops_yaml_admin_pubkey() {
local sops_yaml="$1"
grep -E '^ - &admin age1' "$sops_yaml" 2>/dev/null | awk '{print $NF}' || true
}
# sops_updatekeys <secrets-file> [key-file]
# Re-encrypts <secrets-file> for .sops.yaml's current recipient set. If
# <key-file> is given, decrypts with that identity (SOPS_AGE_KEY_FILE)
# instead of whatever's ambient -- needed when the ambient default key
# doesn't match yet (e.g. mid-rotation, decrypting with the outgoing key).
sops_updatekeys() {
local secrets_file="$1" key_file="${2:-}"
if [[ -n "$key_file" ]]; then
SOPS_AGE_KEY_FILE="$key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
"sops updatekeys --yes '${secrets_file}'"
else
nix-shell "${NIX_OPTS[@]}" -p sops --run "sops updatekeys --yes '${secrets_file}'"
fi
}
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env bash
# Shared SSH-host-key / age-conversion helpers for scripts/secrets/sync-host-keys.sh
# and scripts/secrets/prepare-host-key.sh -- both generate the same kind of key
# (ed25519, no passphrase, the sops-nix age-derivation input) and convert it
# to an age recipient the same way; kept in one place so the two can't
# drift apart.
#
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
# nix_extra_opts) if the caller has already set it, so a decision to avoid
# an unreachable nix-cache is reused here instead of probed again. Falls
# back to no extra options if the caller never sourced env.sh.
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
declare -a NIX_OPTS=()
fi
# generate_host_ed25519_key <hostname> <keyfile>
# Writes <keyfile> and <keyfile>.pub. Caller is responsible for refusing to
# overwrite an existing keyfile -- this always runs ssh-keygen fresh.
generate_host_ed25519_key() {
local hostname="$1" keyfile="$2"
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
"ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
}
# ssh_pubkey_to_age <pubkeyfile>
# Prints the age public key derived from an ed25519 SSH public key file.
ssh_pubkey_to_age() {
local pubkeyfile="$1"
nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${pubkeyfile}'"
}
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env python3
# Companion to scripts/sync-host-keys.sh. Applies a set of additive edits
# to .sops.yaml via targeted line insertion -- not a full YAML parse and
# re-serialize -- so every untouched byte of the file is guaranteed to stay
# exactly as it was (a full round-trip through a generic YAML library was
# tested and silently reformatted the whole file's indentation style).
#
# Reads a JSON edit plan from stdin:
# {
# "add_keys": [{"host": "lxc-server", "age_key": "age1..."}],
# "add_aliases": [{"host": "lxc-server", "basename": "common.yaml"}],
# "remove_keys": ["lxc-server"],
# "remove_aliases_for_hosts": ["lxc-server"]
# }
# Idempotent: an anchor or alias that's already present/absent is left
# alone -- adding what's already there, or removing what's already gone,
# is a no-op rather than an error.
# Prints a JSON summary of what actually changed to stdout, so the caller
# knows which secrets/*.yaml files need `sops updatekeys` and which don't.
import json
import re
import sys
KEY_LINE_RE = re.compile(r"^ - &\S+ age1")
def add_keys(lines, add_keys):
existing_anchors = set()
for line in lines:
m = re.match(r"^ - &(\S+) age1", line)
if m:
existing_anchors.add(m.group(1))
new = [e for e in add_keys if e["host"] not in existing_anchors]
if not new:
return lines, []
last_idx = None
for i, line in enumerate(lines):
if KEY_LINE_RE.match(line):
last_idx = i
if last_idx is None:
print("ERROR: no ' - &<name> age1...' line found under keys: in .sops.yaml", file=sys.stderr)
sys.exit(1)
insert = [f" - &{e['host']} {e['age_key']}\n" for e in new]
lines = lines[: last_idx + 1] + insert + lines[last_idx + 1 :]
return lines, [e["host"] for e in new]
def add_aliases(lines, add_aliases):
changed_files = []
for e in add_aliases:
host = e["host"]
basename = e["basename"]
target = "path_regex: secrets/" + basename.replace(".", r"\.") + "$"
rule_start = None
for i, line in enumerate(lines):
if target in line:
rule_start = i
break
if rule_start is None:
print(
f"WARNING: no creation_rule for secrets/{basename} in .sops.yaml "
f"(needed by {host}) -- add one manually, then re-run.",
file=sys.stderr,
)
continue
rule_end = len(lines)
for i in range(rule_start + 1, len(lines)):
if re.match(r"^ - path_regex:", lines[i]):
rule_end = i
break
block = lines[rule_start:rule_end]
if any(re.search(r"\*" + re.escape(host) + r"\s*$", line) for line in block):
continue # already present
last_alias_idx = None
last_alias_indent = None
for i in range(rule_start, rule_end):
m = re.match(r"^(\s*)- \*\S+\s*$", lines[i])
if m:
last_alias_idx = i
last_alias_indent = m.group(1)
if last_alias_idx is None:
print(
f"WARNING: creation_rule for secrets/{basename} has no existing "
f"'- *alias' line to anchor the insertion point -- add {host} manually.",
file=sys.stderr,
)
continue
new_line = f"{last_alias_indent}- *{host}\n"
lines = lines[: last_alias_idx + 1] + [new_line] + lines[last_alias_idx + 1 :]
changed_files.append(basename)
return lines, changed_files
def remove_keys(lines, hosts):
hosts = set(hosts)
removed = []
kept = []
for line in lines:
m = re.match(r"^ - &(\S+) age1", line)
if m and m.group(1) in hosts:
removed.append(m.group(1))
continue
kept.append(line)
return kept, removed
def remove_aliases_for_hosts(lines, hosts):
hosts = set(hosts)
if not hosts:
return lines, []
changed_files = []
result = []
current_basename = None
current_block_changed = False
def flush():
if current_block_changed and current_basename:
changed_files.append(current_basename)
for line in lines:
if re.match(r"^ - path_regex:", line):
flush()
current_block_changed = False
m = re.search(r"path_regex: secrets/(.+)\$", line)
current_basename = m.group(1).replace(r"\.", ".") if m else None
result.append(line)
continue
if current_basename is not None and any(
re.search(r"\*" + re.escape(h) + r"\s*$", line) for h in hosts
):
current_block_changed = True
continue # drop this alias line
result.append(line)
flush()
return result, changed_files
def main():
sops_path = sys.argv[1]
edits = json.load(sys.stdin)
with open(sops_path) as f:
lines = f.readlines()
lines, added_keys = add_keys(lines, edits.get("add_keys", []))
lines, added_alias_files = add_aliases(lines, edits.get("add_aliases", []))
lines, removed_keys = remove_keys(lines, edits.get("remove_keys", []))
lines, removed_alias_files = remove_aliases_for_hosts(
lines, edits.get("remove_aliases_for_hosts", [])
)
with open(sops_path, "w") as f:
f.writelines(lines)
changed_files = sorted(set(added_alias_files) | set(removed_alias_files))
json.dump(
{
"added_keys": added_keys,
"removed_keys": removed_keys,
"changed_secrets_files": changed_files,
},
sys.stdout,
)
print()
if __name__ == "__main__":
main()
@@ -1,182 +0,0 @@
#!/usr/bin/env bash
# Points a non-NixOS Debian machine's Nix install at nix-cache: adds it as
# a substituter (with cache.nixos.org kept as fallback) and, once the
# remote-builder private key is installed, as a distributed-build machine
# too.
#
# This is the non-NixOS equivalent of modules/nix-cache/client.nix +
# modules/nix-cache/remote-builder-client.nix -- those two only apply to
# hosts built from this flake. A plain Debian box with Nix installed
# (single- or multi-user install, nix-daemon running) has no NixOS module
# system to pick that config up, so this edits /etc/nix/nix.conf by hand
# instead. Run this ON the target Debian machine, as root.
#
# The values below mirror variables.nix / modules/nix-cache/client.nix in
# this repo -- update both if nix-cache is ever rebuilt with a new host
# key or the cache signing key is rotated (see docs/nix-cache.md).
#
# REMOTE_BUILDER_KEY defaults to this machine's own default root SSH
# identity (matches modules/nix-cache/remote-builder-client.nix's
# convention for real NixOS clients: authenticate as nixremote with the
# host's own default key, added individually to
# vars.remoteBuilderAuthorizedKeys, rather than a separately-named or
# shared keypair) -- generate one with
# `ssh-keygen -t ed25519 -N '' -f /root/.ssh/id_ed25519` if this machine
# doesn't have one yet, then add its .pub to vars.remoteBuilderAuthorizedKeys
# and rebuild nix-cache.
#
# Usage:
# sudo ./configure-nix-cache-client.sh [--dry-run] [--no-remote-builder] [--no-restart]
#
# Env overrides (defaults match variables.nix):
# NIX_CACHE_HOST, NIX_CACHE_HOST_KEY, REMOTE_BUILDER_USER, REMOTE_BUILDER_KEY
set -euo pipefail
: "${NIX_CACHE_HOST:=nix-cache}"
: "${NIX_CACHE_HOST_KEY:=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPeWgMsdaiz4axT/deFc1+0B5bN+GX/NOeW9bbQ0c/IT lxc-nix-cache}"
: "${REMOTE_BUILDER_USER:=nixremote}"
: "${REMOTE_BUILDER_KEY:=/root/.ssh/id_ed25519}"
CACHE_PUB_KEY="cache.local-1:usoWYanY3Kpq2+kDIS2nhWoLZiRxanmdysdzqCFBHW4="
FALLBACK_URL="https://cache.nixos.org/"
FALLBACK_PUB_KEY="cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
NIX_CONF="/etc/nix/nix.conf"
KNOWN_HOSTS="/etc/ssh/ssh_known_hosts"
MARKER_BEGIN="# BEGIN nix-cache client config (configure-nix-cache-client.sh)"
MARKER_END="# END nix-cache client config"
dry_run=0
with_remote_builder=1
restart_daemon=1
for arg in "$@"; do
case "$arg" in
--dry-run) dry_run=1 ;;
--no-remote-builder) with_remote_builder=0 ;;
--no-restart) restart_daemon=0 ;;
-h|--help)
sed -n '2,20p' "$0"
exit 0
;;
*)
echo "ERROR: unknown argument: $arg" >&2
exit 1
;;
esac
done
if [[ "$dry_run" -eq 0 && "$EUID" -ne 0 ]]; then
echo "ERROR: must run as root (writes $NIX_CONF and, unless --no-remote-builder, $KNOWN_HOSTS)." >&2
exit 1
fi
if ! command -v nix >/dev/null 2>&1; then
echo "ERROR: no 'nix' binary on PATH -- install the Nix package manager first." >&2
exit 1
fi
if [[ ! -f "$NIX_CONF" ]]; then
echo "ERROR: $NIX_CONF not found -- expected an existing multi-user Nix install." >&2
exit 1
fi
builder_line=""
if [[ "$with_remote_builder" -eq 1 ]]; then
if [[ -f "$REMOTE_BUILDER_KEY" ]]; then
case "$(uname -m)" in
x86_64) nix_system="x86_64-linux" ;;
aarch64) nix_system="aarch64-linux" ;;
*)
echo "WARNING: unrecognized architecture '$(uname -m)' -- skipping remote builder, keeping substituter config." >&2
with_remote_builder=0
;;
esac
if [[ "$with_remote_builder" -eq 1 ]]; then
builder_line="builders = ssh://${REMOTE_BUILDER_USER}@${NIX_CACHE_HOST} ${nix_system} ${REMOTE_BUILDER_KEY} 4 2 big-parallel,kvm,nixos-test,benchmark"
fi
else
echo "WARNING: $REMOTE_BUILDER_KEY not found -- skipping remote builder config (substituter still configured)." >&2
echo " See docs/nix-cache.md 'Remote builder SSH keys' for how to install it, then re-run this script." >&2
with_remote_builder=0
fi
fi
block="$(cat <<EOF
$MARKER_BEGIN
extra-substituters = http://${NIX_CACHE_HOST} ${FALLBACK_URL}
extra-trusted-public-keys = ${CACHE_PUB_KEY} ${FALLBACK_PUB_KEY}
EOF
)"
if [[ "$with_remote_builder" -eq 1 ]]; then
block="${block}
builders-use-substitutes = true
${builder_line}"
fi
block="${block}
$MARKER_END"
echo "== nix.conf block to install =="
echo "$block"
echo "================================"
if [[ "$dry_run" -eq 1 ]]; then
echo "(--dry-run: not writing $NIX_CONF)"
else
tmp_conf="$(mktemp)"
trap 'rm -f "$tmp_conf"' EXIT
if grep -qF "$MARKER_BEGIN" "$NIX_CONF"; then
awk -v begin="$MARKER_BEGIN" -v end="$MARKER_END" -v block="$block" '
$0 == begin { print block; skip = 1; next }
$0 == end { skip = 0; next }
skip { next }
{ print }
' "$NIX_CONF" > "$tmp_conf"
else
cp "$NIX_CONF" "$tmp_conf"
printf '\n%s\n' "$block" >> "$tmp_conf"
fi
cp "$NIX_CONF" "${NIX_CONF}.bak.$(date +%Y%m%d%H%M%S)"
install -m 0644 "$tmp_conf" "$NIX_CONF"
echo "Updated $NIX_CONF (backup saved alongside it)."
fi
if [[ "$with_remote_builder" -eq 1 ]]; then
known_hosts_line="${NIX_CACHE_HOST} ${NIX_CACHE_HOST_KEY}"
if [[ "$dry_run" -eq 1 ]]; then
echo "(--dry-run: would ensure this line is present in $KNOWN_HOSTS)"
echo " $known_hosts_line"
else
mkdir -p "$(dirname "$KNOWN_HOSTS")"
touch "$KNOWN_HOSTS"
if ! grep -qF "$known_hosts_line" "$KNOWN_HOSTS" 2>/dev/null; then
echo "$known_hosts_line" >> "$KNOWN_HOSTS"
echo "Added nix-cache's SSH host key to $KNOWN_HOSTS."
fi
fi
fi
if [[ "$dry_run" -eq 0 && "$restart_daemon" -eq 1 ]]; then
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet nix-daemon 2>/dev/null; then
systemctl restart nix-daemon
echo "Restarted nix-daemon to pick up the new config."
else
echo "nix-daemon not managed by systemd (or not running) -- restart it manually to pick up the new config."
fi
fi
cat <<EOF
Done. Verify with:
curl http://${NIX_CACHE_HOST}/nix-cache-info
nix show-config | grep -E 'substituters|trusted-public-keys|builders'
EOF
if [[ "$with_remote_builder" -eq 1 ]]; then
cat <<EOF
ssh -i ${REMOTE_BUILDER_KEY} ${REMOTE_BUILDER_USER}@${NIX_CACHE_HOST} nix-store --version
nix build nixpkgs#hello -L
EOF
fi
-836
View File
@@ -1,836 +0,0 @@
#!/usr/bin/env bash
# Creates new Proxmox VMs/LXC containers from this flake, and reconfigures
# existing ones -- the manual workflows in docs/proxmox-images.md (VM) and
# docs/auto-installer.md's "LXC hosts" section (container), automated.
#
# Images are built directly on the Proxmox node (PROXMOX_REMOTE_REPO_DIR /
# --remote-repo-dir in scripts/env.sh), not on whatever machine runs this
# script -- there's no multi-gigabyte image to transfer afterward. The first
# time a node doesn't have that repo path yet, it's bootstrapped: cloned from
# this checkout's own `origin` remote, then scripts/codex-setup.sh installs
# the build tooling (Nix, etc.). Every run after that just `git pull`s it and
# copies over the locally-managed host-keys/ (gitignored, so a git pull
# alone wouldn't carry it) before building.
#
# Usage:
# scripts/proxmox/create-proxmox-resource.sh --type lxc|vm --host <name> [options]
# scripts/proxmox/create-proxmox-resource.sh --type lxc|vm --list
# scripts/proxmox/create-proxmox-resource.sh --modify --vmid <n> [--cores N] [--memory MB] [--grow-disk GB]
#
# SAFETY:
# - The default (create) mode only ever creates a NEW resource -- it
# refuses to run if the target VMID already exists on the node, or if
# a VM/CT identified as --host already exists under any other VMID
# (checked live against the node; --allow-duplicate-host overrides).
# - --allow-duplicate-host distinguishes an exact match (same --type
# *and* --host, e.g. re-running --type lxc --host docker while an
# lxc-docker container already exists -- almost always a redeploy of
# the same target to pick up a rebuilt image) from a cross-type match
# (a different platform sharing the same host identity, e.g. a
# proxmox-docker VM coexisting with lxc-docker). Only the exact match
# is destroyed and replaced, after typing the hostname back to
# confirm (outside --dry-run) -- a cross-type match is always left
# untouched, matching-or-not.
# - --modify only ever touches a resource you name explicitly via
# --vmid, shows exactly what will change first, and (outside
# --dry-run) always requires typing that VMID back to confirm before
# anything is sent to the node. There is no bulk/implicit modify.
# - Outside of --allow-duplicate-host's exact-match replace above,
# neither mode can start/stop/delete a resource.
#
# See --help for the full option list.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/nix-eval.sh
source "${repo_root}/scripts/lib/nix-eval.sh"
# shellcheck source=../lib/confirm.sh
source "${repo_root}/scripts/lib/confirm.sh"
sync_keys="${repo_root}/scripts/secrets/sync-host-keys.sh"
usage() {
cat <<EOF
Usage: $0 --type lxc|vm --host <name> [options] (create)
$0 --type lxc|vm --list (list --host values)
$0 --modify --vmid <n> [options] (reconfigure)
Create mode (default):
--type lxc|vm lxc = container, built as a CT template tarball.
vm = VM, built as a Disko .raw disk image (UEFI/OVMF).
--host <name> Which host identity to deploy -- matches
config.networking.hostName (server, docker,
nix-cache, nixos, pxe-boot, nix-minimal). Use
--list to see what's available for --type.
--name <name> Proxmox display name/hostname (default: --host's
value, e.g. nix-cache -- for lxc this becomes the
guest's real networking.hostName too, since
proxmoxLXC.manageHostName pulls it from Proxmox's
own container config, so it must match host.nix
regardless of build type)
--vmid <n> Numeric VMID (default: next free, via
\`pvesh get /cluster/nextid\` on the node).
Refuses to run if this ID already exists.
--disk-size <GB> lxc only: rootfs size for \`pct create\`
(default: \$PROXMOX_DEFAULT_LXC_DISK_GB, ${PROXMOX_DEFAULT_LXC_DISK_GB}).
--image <path> Use this local image/tarball (uploaded to the
node via scp) instead of checking the node /
building one there from the flake.
--force-rebuild Skip the "does the node already have this
image" check -- always build fresh and
overwrite what's there.
--remote-repo-dir <path> Where this flake repo lives (or gets
cloned) on the node, and is built from
(default: \$PROXMOX_REMOTE_REPO_DIR, ${PROXMOX_REMOTE_REPO_DIR}).
--allow-duplicate-host Required if a VM/CT identified as --host
already exists on the node (checked live via
qm/pct, not any file in this repo) --
otherwise refused, since it'd share that
host's hostName/hostId. An existing resource
of this *same* --type (e.g. re-running --type
lxc --host docker over an existing lxc-docker)
is destroyed and replaced, after confirming --
a different --type sharing the same --host
(e.g. a proxmox-docker VM) is always left
untouched.
Modify mode (reconfigure an EXISTING resource -- requires --modify):
--modify Switch to modify mode.
--vmid <n> Required: which existing resource to change.
Type/VM-vs-CT is auto-detected on the node.
--grow-disk <GB> Grow the primary disk by this many GB
(qm/pct resize; Proxmox only supports
growing, never shrinking, an existing disk).
At least one of --cores / --memory / --grow-disk is required. Always
prints the current -> new values and requires typing the VMID back to
confirm, even outside --dry-run.
Shared:
--cores <n> create: default \$PROXMOX_DEFAULT_CORES (${PROXMOX_DEFAULT_CORES}).
modify: omit to leave unchanged.
--memory <MB> create: default \$PROXMOX_DEFAULT_MEMORY_MB (${PROXMOX_DEFAULT_MEMORY_MB}).
modify: omit to leave unchanged.
--swap <MB> lxc only, create time: \`--memory\` doesn't
touch swap -- it silently stays at Proxmox's
own 512M default otherwise. (default: matches
whatever --memory resolves to)
--storage <pool> (default: \$PROXMOX_STORAGE, ${PROXMOX_STORAGE})
--iso-storage <pool> (default: \$PROXMOX_ISO_STORAGE, ${PROXMOX_ISO_STORAGE})
--bridge <bridge> (default: \$PROXMOX_BRIDGE, ${PROXMOX_BRIDGE})
--node <host> Proxmox node to SSH into (default:
\$PROXMOX_HOST, ${PROXMOX_HOST})
--dry-run Print the full plan; touch nothing
local or remote, no prompts.
-h, --help
Config for --storage/--bridge/--node/etc. lives in scripts/env.sh -- edit
that instead of passing the same flag every time.
EOF
}
dry_run=0
modify=0
type=""
host=""
name=""
vmid=""
cores=""
memory=""
swap=""
disk_size=""
grow_disk=""
image=""
storage="$PROXMOX_STORAGE"
iso_storage="$PROXMOX_ISO_STORAGE"
bridge="$PROXMOX_BRIDGE"
node="$PROXMOX_HOST"
remote_repo_dir="$PROXMOX_REMOTE_REPO_DIR"
do_list=0
allow_duplicate_host=0
force_rebuild=0
while [[ $# -gt 0 ]]; do
case "$1" in
--type) type="$2"; shift 2 ;;
--host) host="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--vmid) vmid="$2"; shift 2 ;;
--cores) cores="$2"; shift 2 ;;
--memory) memory="$2"; shift 2 ;;
--swap) swap="$2"; shift 2 ;;
--disk-size) disk_size="$2"; shift 2 ;;
--grow-disk) grow_disk="$2"; shift 2 ;;
--image) image="$2"; shift 2 ;;
--storage) storage="$2"; shift 2 ;;
--iso-storage) iso_storage="$2"; shift 2 ;;
--bridge) bridge="$2"; shift 2 ;;
--node) node="$2"; shift 2 ;;
--remote-repo-dir) remote_repo_dir="$2"; shift 2 ;;
--list) do_list=1; shift ;;
--allow-duplicate-host) allow_duplicate_host=1; shift ;;
--force-rebuild) force_rebuild=1; shift ;;
--modify) modify=1; shift ;;
--dry-run) dry_run=1; shift ;;
-h | --help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
done
ssh_target="${PROXMOX_SSH_USER}@${node}"
remote() {
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] ssh ${ssh_target} -- $*"
else
ssh "$ssh_target" "$@"
fi
}
# ============================================================ modify mode
cmd_modify() {
if [[ -z "$vmid" ]]; then
echo "ERROR: --modify requires --vmid." >&2
exit 1
fi
if [[ -z "$cores" && -z "$memory" && -z "$grow_disk" ]]; then
echo "ERROR: --modify needs at least one of --cores / --memory / --grow-disk." >&2
exit 1
fi
echo "Looking up VMID ${vmid} on ${node}..."
local kind current_cores current_memory disk_key
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1; then
kind="vm"
disk_key="scsi0"
elif ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
kind="lxc"
disk_key="rootfs"
else
echo "ERROR: VMID ${vmid} doesn't exist on ${node} -- nothing to modify." >&2
exit 1
fi
local config_cmd="qm config ${vmid}"
[[ "$kind" == "lxc" ]] && config_cmd="pct config ${vmid}"
local current_config
current_config="$(ssh "$ssh_target" "$config_cmd")"
current_cores="$(echo "$current_config" | grep -oP '^cores:\s*\K\S+' || echo '?')"
current_memory="$(echo "$current_config" | grep -oP '^memory:\s*\K\S+' || echo '?')"
echo
echo "VMID ${vmid} is a ${kind} on ${node}. Planned changes:"
[[ -n "$cores" ]] && echo " cores: ${current_cores} -> ${cores}"
[[ -n "$memory" ]] && echo " memory: ${current_memory} MB -> ${memory} MB"
[[ -n "$grow_disk" ]] && echo " ${disk_key}: grow by +${grow_disk}G (Proxmox can only grow, not shrink, an existing disk)"
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] Nothing was changed."
return
fi
echo
if ! confirm_typed "$vmid" "Type the VMID (${vmid}) to confirm these changes: "; then
echo "Cancelled -- input didn't match ${vmid}."
exit 1
fi
local set_cmd="qm set"
local resize_cmd="qm resize"
[[ "$kind" == "lxc" ]] && set_cmd="pct set" && resize_cmd="pct resize"
if [[ -n "$cores" || -n "$memory" ]]; then
local args=""
[[ -n "$cores" ]] && args="${args} --cores ${cores}"
[[ -n "$memory" ]] && args="${args} --memory ${memory}"
remote "${set_cmd} ${vmid}${args}"
fi
if [[ -n "$grow_disk" ]]; then
remote "${resize_cmd} ${vmid} ${disk_key} +${grow_disk}G"
fi
echo
echo "Done. VMID ${vmid} updated."
}
if [[ "$modify" -eq 1 ]]; then
cmd_modify
exit 0
fi
# ============================================================= create mode
if [[ "$type" != "lxc" && "$type" != "vm" ]]; then
echo "ERROR: --type must be 'lxc' or 'vm'." >&2
usage >&2
exit 1
fi
platform_prefix="lxc"
[[ "$type" == "vm" ]] && platform_prefix="proxmox"
[[ -z "$cores" ]] && cores="$PROXMOX_DEFAULT_CORES"
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
# --- discover / resolve the flake target from --host --------------------
# Emits "<target>\t<hostName>" pairs for every ${platform_prefix}-* flake
# target -- the one source both --list and the --host lookup below read
# from, so they can never see a different set of targets from each other.
targets_for_platform() {
local target
for target in $(list_flake_targets "$repo_root" 2>/dev/null | grep -- "^${platform_prefix}-"); do
printf '%s\t%s\n' "$target" "$(flake_target_hostname "$repo_root" "$target")"
done
}
list_hosts() {
local target hostname
while IFS=$'\t' read -r target hostname; do
printf ' %-12s -> %s\n' "$hostname" "$target"
done < <(targets_for_platform)
}
if [[ "$do_list" -eq 1 ]]; then
echo "Available --host values for --type ${type}:"
list_hosts
exit 0
fi
if [[ -z "$host" ]]; then
echo "ERROR: --host is required (or use --list to see options)." >&2
exit 1
fi
flake_target=""
while IFS=$'\t' read -r target hostname; do
if [[ "$hostname" == "$host" ]]; then
flake_target="$target"
break
fi
done < <(targets_for_platform)
if [[ -z "$flake_target" ]]; then
echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2
echo "Available:" >&2
list_hosts >&2
exit 1
fi
# The container/VM's real identity is --host (e.g. "nix-cache"), validated
# above against config.networking.hostName -- not the flake target name
# (e.g. "lxc-nix-cache"), which is build-type-specific and only exists to
# pick which platform variant to build. Defaulting --name to the flake
# target would make lxc's --hostname (which proxmoxLXC.manageHostName
# feeds straight into the guest's real hostname) disagree with host.nix.
[[ -z "$name" ]] && name="$host"
# --- refuse to duplicate a host that's already live on the node ---------
# Queries the node itself (qm/pct's own name/hostname config), not any
# static list in this repo -- a file can't track whether a resource still
# actually exists, and this used to be checked against variables.nix's
# deployedTargets, which drifted stale (it kept naming a VM as "the real
# deployment" well after that VM had been destroyed, blocking its own
# redeploy) until that list was dropped in favour of this live check. This
# only catches guests identified with the default --name (== --host, what
# this script itself always uses unless --name is overridden) -- a guest
# manually renamed on the node afterwards wouldn't match, but nothing here
# creates guests that way.
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] would check ${node} for an existing VM/CT identified as '${host}'"
if [[ "$allow_duplicate_host" -eq 1 ]]; then
echo "[dry-run] --allow-duplicate-host: an existing ${type} named '${host}' would be" \
"destroyed and replaced; a different-type match would be left untouched"
fi
else
echo
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
ssh_check_status=0
existing="$(ssh "$ssh_target" bash -s -- "$host" <<'REMOTE_SCRIPT'
target="$1"
for id in $(qm list 2>/dev/null | awk 'NR>1{print $1}'); do
n="$(qm config "$id" 2>/dev/null | grep -oP '^name:\s*\K\S+' || true)"
[[ "$n" == "$target" ]] && echo "vm ${id} ${n}"
done
for id in $(pct list 2>/dev/null | awk 'NR>1{print $1}'); do
n="$(pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
done
exit 0
REMOTE_SCRIPT
)" || ssh_check_status=$?
if [[ "$ssh_check_status" -ne 0 ]]; then
echo "ERROR: couldn't reach ${node} (ssh exited ${ssh_check_status}) to check for an" >&2
echo "existing '${host}' resource -- refusing to guess. Fix connectivity and retry," >&2
echo "or pass --allow-duplicate-host if you're sure none exists (this skips the" >&2
echo "check entirely)." >&2
exit 1
fi
# Split into "exact" (same resource kind as --type -- i.e. literally this
# same host+platform combo already exists, almost always a redeploy of
# the same target to test a rebuilt image) vs "cross-type" (a different
# platform sharing this host identity, e.g. a stopped proxmox-docker VM
# coexisting with an lxc-docker container -- a deliberate, valid setup
# this script has never managed and still won't). Read via a herestring
# (not a pipe) so the appends below survive outside the loop.
this_kind="$type"
exact_matches=""
cross_matches=""
if [[ -n "$existing" ]]; then
while read -r kind id n; do
[[ -z "$kind" ]] && continue
if [[ "$kind" == "$this_kind" ]]; then
exact_matches+="${kind} ${id} ${n}"$'\n'
else
cross_matches+="${kind} ${id} ${n}"$'\n'
fi
done <<<"$existing"
fi
if [[ -n "$exact_matches" && "$allow_duplicate_host" -ne 1 ]]; then
echo "ERROR: '${host}' already exists on ${node} as this same resource type:" >&2
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})" >&2
done
echo "Refusing to create a second ${this_kind} sharing this identity. Pass" >&2
echo "--allow-duplicate-host to destroy it and create a fresh one in its place" >&2
echo "(after confirming), or use --modify to reconfigure the existing one instead." >&2
exit 1
fi
if [[ -n "$cross_matches" && "$allow_duplicate_host" -ne 1 ]]; then
echo "ERROR: '${host}' already exists on ${node} as a different resource type:" >&2
echo "$cross_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})" >&2
done
echo "Refusing to create a second resource sharing this identity. Pass" >&2
echo "--allow-duplicate-host to create one anyway (it gets its own distinct" >&2
echo "sops key and VMID -- the existing resource above is left untouched)," >&2
echo "or use --modify to reconfigure the existing one instead." >&2
exit 1
fi
if [[ -n "$cross_matches" ]]; then
echo "--allow-duplicate-host: '${host}' also exists on ${node} as a different resource" \
"type -- leaving it untouched:"
echo "$cross_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})"
done
fi
if [[ -n "$exact_matches" ]]; then
echo "--allow-duplicate-host: '${host}' already exists on ${node} as this same resource" \
"type -- it will be destroyed and replaced:"
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})"
done
echo
if ! confirm_typed "$host" "Type the hostname (${host}) to confirm destroying the above and replacing it: "; then
echo "Cancelled -- input didn't match ${host}." >&2
exit 1
fi
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo "==> Destroying ${kind} VMID ${id} (${n})..."
if [[ "$kind" == "vm" ]]; then
# qm destroy has no --force to stop-then-destroy in one call (pct's
# does) -- stop explicitly first if it's running.
if ssh "$ssh_target" "qm status ${id}" 2>/dev/null | grep -q running; then
ssh "$ssh_target" "qm stop ${id}"
fi
ssh "$ssh_target" "qm destroy ${id} --purge 1"
else
ssh "$ssh_target" "pct destroy ${id} --force 1 --purge 1"
fi
done
fi
fi
echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'"
# Decide on nix-cache once, here -- this is the earliest point that needs
# it (sync-host-keys.sh below needs nix-shell packages regardless of
# whether an image ends up getting built later), and the decision is
# exported so that subprocess -- and this script's own later build step,
# if it gets there -- both reuse it instead of probing again.
nix_extra_opts
# --- make sure this target has a registered host key --------------------
echo
echo "==> Ensuring host key exists and is registered..."
sync_args=("$flake_target")
[[ "$dry_run" -eq 1 ]] && sync_args+=(--dry-run)
bash "$sync_keys" "${sync_args[@]}"
# --- VMID: pick one, and refuse to touch anything that already exists ---
echo
if [[ -z "$vmid" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
vmid="<next-free-vmid>"
echo "[dry-run] would ask ${node} for the next free VMID (pvesh get /cluster/nextid)"
else
vmid="$(ssh "$ssh_target" "pvesh get /cluster/nextid" | tr -d '[:space:]')"
echo "Auto-assigned VMID: ${vmid}"
fi
else
echo "Requested VMID: ${vmid}"
fi
if [[ "$dry_run" -eq 0 ]]; then
# qm/pct status exits non-zero (and prints "does not exist") for a free
# ID on that resource type -- but a VMID could exist as the OTHER
# resource type (e.g. requested a CT id that's actually a VM), so check
# both. Any success here means something is already using this ID --
# refuse to go anywhere near it. (Reconfiguring an existing resource is
# --modify's job, not this one's.)
if ssh "$ssh_target" "qm status ${vmid}" >/dev/null 2>&1 \
|| ssh "$ssh_target" "pct status ${vmid}" >/dev/null 2>&1; then
echo "ERROR: VMID ${vmid} already exists on ${node}. Refusing to touch an" >&2
echo "existing resource here -- use --modify to reconfigure it, pick a" >&2
echo "different --vmid, or omit it to auto-assign." >&2
exit 1
fi
fi
# --- resolve the remote path -- fixed naming (not the nix store's own
# derivation-hash-based filename), so a later run can check for it by name.
# lxc uploads as a CT *template* (Proxmox's "vztmpl" content type, under
# iso_storage) -- config.system.build.tarball is a plain rootfs tarball,
# not a vzdump backup archive, so it's created with `pct create ... vztmpl`,
# not restored with `pct restore` (that expects backup-archive metadata
# this tarball doesn't have, and fails with "archive contains no
# configuration file").
remote_dir="/var/lib/vz/import"
remote_filename="${flake_target}.raw"
if [[ "$type" == "lxc" ]]; then
remote_dir="/var/lib/vz/template/cache"
remote_filename="${flake_target}.tar.xz"
fi
remote_path="${remote_dir}/${remote_filename}"
# --- ensure the flake repo (+ tooling) exists on the node, and is current --
# Bootstraps once (git clone from this checkout's own `origin`, then
# scripts/codex-setup.sh installs Nix + friends) if ${remote_repo_dir}
# doesn't exist yet on the node; otherwise just `git pull`s it, so the image
# built there reflects what's actually committed and pushed. Only called
# right before an actual remote build below -- reusing an image already on
# the node, or an explicit --image, never touch the node's checkout at all.
ensure_remote_repo() {
echo
echo "==> Ensuring ${remote_repo_dir} exists and is current on ${node}..."
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would ensure ${remote_repo_dir} exists on ${node} (clone if missing, git pull if present), and would verify/bootstrap build tooling there (scripts/codex-setup.sh) if \`nix\` isn't already on PATH -- and if that bootstrap actually ran, would also configure ${node} as a nix-cache client (scripts/proxmox/configure-nix-cache-client.sh)"
return
fi
if ssh "$ssh_target" "test -d '${remote_repo_dir}/.git'"; then
echo "Repo present -- pulling latest..."
ssh "$ssh_target" "cd '${remote_repo_dir}' && git pull --ff-only"
else
local origin_url
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
if [[ -z "$origin_url" ]]; then
echo "ERROR: ${remote_repo_dir} doesn't exist on ${node}, and this checkout has no" >&2
echo "'origin' remote to clone from. Set one (git remote add origin <url>) or create" >&2
echo "${remote_repo_dir} on ${node} yourself (e.g. git clone), then re-run." >&2
exit 1
fi
echo "Not present -- cloning from ${origin_url}..."
ssh "$ssh_target" "git clone '${origin_url}' '${remote_repo_dir}'"
fi
# Trivial check, run every time (not just right after a fresh clone) --
# confirmed live: a first bootstrap can clone the repo successfully and
# still leave the node without a working `nix` (e.g. the node had no
# `sudo`, which the Nix installer's root path depends on -- see the fix
# in scripts/codex-setup.sh), and a later run with the repo already
# present would otherwise never retry it. Sources
# scripts/lib/nix-bootstrap.sh's ensure_nix_profile first -- a
# single-user Nix install typically only gets sourced into login shells,
# and ssh's non-interactive command execution is neither, so a
# freshly-installed `nix` still wouldn't be on PATH here without it.
#
# Just `nix` today -- the only thing the remote build commands below
# actually invoke -- but a list (not a single hardcoded check) so a
# future remote step needing another tool can add itself here instead of
# growing a parallel check.
local remote_required_cmds=(nix)
local tooling_check_cmd="cd '${remote_repo_dir}' && . scripts/lib/nix-bootstrap.sh && ensure_nix_profile"
local cmd
for cmd in "${remote_required_cmds[@]}"; do
tooling_check_cmd="${tooling_check_cmd} && command -v ${cmd}"
done
if ssh "$ssh_target" "$tooling_check_cmd" >/dev/null 2>&1; then
echo "Build tooling already present on ${node}."
else
echo "==> Bootstrapping build tooling on ${node} (scripts/codex-setup.sh)..."
ssh "$ssh_target" "cd '${remote_repo_dir}' && bash scripts/codex-setup.sh"
# Only on this first-time bootstrap, not every run -- a node that
# already has tooling either already went through this once, or had
# it configured some other way, and re-running is harmless but
# pointless. Non-fatal: this only makes the node's own builds faster
# (substitute from nix-cache instead of building from source) and
# offloadable to it as a remote builder -- worth trying, not worth
# aborting the image build over if nix-cache happens to be down right
# now. Needs ensure_nix_profile first, same as the tooling_check_cmd
# above -- ssh's non-interactive command execution won't have picked
# up a freshly single-user-installed `nix` otherwise.
echo "==> Configuring ${node} as a nix-cache substituter/remote-builder client..."
if ! ssh "$ssh_target" "cd '${remote_repo_dir}' && . scripts/lib/nix-bootstrap.sh && ensure_nix_profile && bash scripts/proxmox/configure-nix-cache-client.sh"; then
echo "WARNING: configure-nix-cache-client.sh failed on ${node} -- continuing without it (${node} will build from source / against cache.nixos.org only)." >&2
fi
fi
}
# --- sync locally-managed host-keys/ to the node ---------------------------
# Gitignored (see .gitignore), so `git pull` above never carries it -- both
# build paths need it present as NIXOS_HOST_KEYS_DIR / --pre-format-files
# input on the node itself now that the build runs there. scp (not rsync,
# not already a dependency anywhere else in this repo) mirrors how this
# script already transfers the --image case below.
sync_remote_host_keys() {
echo
echo "==> Syncing host-keys/ to ${node}..."
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would copy ${repo_root}/host-keys/ to ${ssh_target}:${remote_repo_dir}/host-keys/"
return
fi
ssh "$ssh_target" "mkdir -p '${remote_repo_dir}/host-keys'"
scp -pr "${repo_root}/host-keys/." "${ssh_target}:${remote_repo_dir}/host-keys/"
}
# --- build (or reuse an image already on the node) ------------------------
echo
local_image=""
image_already_remote=0
if [[ -n "$image" ]]; then
[[ -f "$image" ]] || { echo "ERROR: --image '${image}' not found." >&2; exit 1; }
local_image="$image"
echo "Using provided image: ${local_image}"
elif [[ "$force_rebuild" -eq 1 ]]; then
echo "--force-rebuild: skipping the existing-image check on ${node}."
else
echo "==> Checking whether ${node} already has ${remote_path}..."
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would check: ssh ${ssh_target} -- test -f ${remote_path}"
elif ssh "$ssh_target" "test -f '${remote_path}'" 2>/dev/null; then
echo "Found it -- reusing, skipping build (use --force-rebuild to override)."
image_already_remote=1
else
echo "Not found -- will build."
fi
fi
if [[ "$image_already_remote" -eq 0 && -z "$local_image" ]]; then
ensure_remote_repo
sync_remote_host_keys
# Relayed into the remote build below exactly as decided by the local
# nix_extra_opts call earlier in this script -- that decision (whether
# nix-cache is reachable) is made once, locally, same as it always has
# been; only *where* the resulting "${NIX_OPTS[@]}" gets used as a `nix
# build` flag moves to the node. NIX_EXTRA_OPTS is already a %q-quoted
# string built for exactly this eval-based reconstruction (see env.sh).
nix_opts_display=""
if [[ ${#NIX_OPTS[@]} -gt 0 ]]; then
printf -v nix_opts_display '%q ' "${NIX_OPTS[@]}"
nix_opts_display=" ${nix_opts_display% }"
fi
if [[ "$type" == "lxc" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would build on ${node}: NIXOS_HOST_KEYS_DIR=\$(pwd)/host-keys nix build --impure \\"
echo "[dry-run] --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.tarball"
echo "[dry-run] would stage the result at ${remote_path}"
local_image="<built-tarball>"
else
echo "==> Building LXC tarball for ${flake_target} on ${node}..."
# Built as a single already-%q-quoted command string, not separate ssh
# argv elements -- ssh joins remote command args with plain spaces and
# hands the result to the remote shell to re-split, which would
# otherwise scatter NIX_EXTRA_OPTS (itself several space-separated,
# %q-quoted tokens) across the wrong positional parameters below.
printf -v remote_cmd 'bash -s -- %q %q %q %q %q' \
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS"
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
set -euo pipefail
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"
declare -a NIX_OPTS=()
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
cd "$repo_dir"
# A single-user Nix install only gets sourced into login shells; this ssh
# session is neither, so `nix` wouldn't otherwise be on PATH here even
# right after a successful install.
. scripts/lib/nix-bootstrap.sh
ensure_nix_profile
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
".#nixosConfigurations.${target}.config.system.build.tarball" \
--out-link "result-${target}"
built="$(find "result-${target}/tarball" -maxdepth 1 -type f | head -1)"
if [[ -z "$built" ]]; then
echo "ERROR: no tarball found under result-${target}/tarball after build." >&2
exit 1
fi
mkdir -p "$dest_dir"
cp "$built" "${dest_dir}/${dest_name}"
echo "Built and staged: ${dest_dir}/${dest_name}"
REMOTE_SCRIPT
local_image="$remote_path"
echo "Built on ${node}: ${remote_path}"
fi
else
# PROXMOX_SSH_USER defaults to root (env.sh), which needs no sudo and
# can't assume it's even installed on a minimal node -- only shell out
# through sudo when actually running as a non-root SSH user.
sudo_prefix="sudo"
sudo_display="sudo "
if [[ "$PROXMOX_SSH_USER" == "root" ]]; then
sudo_prefix=""
sudo_display=""
fi
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would build on ${node}: nix build --no-use-registries --no-accept-flake-config${nix_opts_display} \\"
echo "[dry-run] .#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript"
echo "[dry-run] would run: ${sudo_display}./result-${flake_target} \\"
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key \\"
echo "[dry-run] --pre-format-files host-keys/${flake_target}_ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key.pub \\"
echo "[dry-run] --build-memory 2048"
echo "[dry-run] would stage the result at ${remote_path}"
local_image="<built-image>.raw"
else
echo "==> Building Disko image for ${flake_target} on ${node}..."
# See the LXC branch above for why this is one %q-quoted command
# string rather than separate ssh argv elements.
printf -v remote_cmd 'bash -s -- %q %q %q %q %q %q' \
"$remote_repo_dir" "$flake_target" "$remote_dir" "$remote_filename" "$NIX_EXTRA_OPTS" "$sudo_prefix"
ssh "$ssh_target" "$remote_cmd" <<'REMOTE_SCRIPT'
set -euo pipefail
repo_dir="$1"; target="$2"; dest_dir="$3"; dest_name="$4"; nix_extra_opts_str="$5"; sudo_prefix="$6"
declare -a NIX_OPTS=()
[[ -n "$nix_extra_opts_str" ]] && eval "NIX_OPTS=(${nix_extra_opts_str})"
cd "$repo_dir"
. scripts/lib/nix-bootstrap.sh
ensure_nix_profile
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
".#nixosConfigurations.${target}.config.system.build.diskoImagesScript" \
--out-link "result-${target}"
$sudo_prefix "./result-${target}" \
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
--pre-format-files "host-keys/${target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
--build-memory 2048
built="$(find . -maxdepth 1 -name '*.raw' -newer "result-${target}" | head -1)"
if [[ -z "$built" ]]; then
echo "ERROR: no .raw image found in ${repo_dir} after build." >&2
exit 1
fi
mkdir -p "$dest_dir"
mv "$built" "${dest_dir}/${dest_name}"
echo "Built and staged: ${dest_dir}/${dest_name}"
REMOTE_SCRIPT
local_image="$remote_path"
echo "Built on ${node}: ${remote_path}"
fi
fi
fi
# --- upload -- only for an explicit --image; a build above stages its
# result directly at ${remote_path} on the node already, and reusing an
# image already on the node needs nothing transferred either. ------------
echo
if [[ -n "$image" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would upload: scp ${local_image} ${ssh_target}:${remote_path}"
else
echo "==> Uploading to ${node}:${remote_path}..."
ssh "$ssh_target" "mkdir -p ${remote_dir}"
scp "$local_image" "${ssh_target}:${remote_path}"
fi
fi
# --- create -----------------------------------------------------------------
echo
if [[ "$type" == "lxc" ]]; then
echo "==> Creating LXC container ${vmid} (${name})..."
local_disk_size="${disk_size:-$PROXMOX_DEFAULT_LXC_DISK_GB}"
# --memory doesn't touch swap -- it silently stays at Proxmox's own
# 512M default otherwise (confirmed live: --memory 2048 left swap at
# 512). Default to matching whatever --memory resolved to above.
local_swap="${swap:-$memory}"
# --unprivileged 1: modules/platforms/lxc.nix sets proxmoxLXC.privileged
# = false, so the NixOS config inside the image assumes it's running as
# an unprivileged container (cgroup/capability/mount expectations baked
# in at boot). `pct create`'s own CLI default for this flag is
# privileged (unlike the web UI, which defaults its checkbox the other
# way) -- leaving it unset creates 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 actually boot unprivileged -- confirmed live: without this,
# AppArmor denies the nested user namespaces and credential mounts
# systemd routinely uses (even plain getty units), and every getty
# crash-loops on a denied mount every ~3s (visible as garbage on the
# console) while core services like nsncd fail the same way.
#
# ...,mount=nfs;nfs4: without it AppArmor blanket-denies the `nfs`/
# `rpc_pipefs` mount syscalls any NFS client share needs -- confirmed
# live on lxc-docker: `mount: /var/lib/nfs/rpc_pipefs: permission
# denied`. The value's `;` (Proxmox's own multi-fstype separator for
# this one feature, per PVE::LXC's use of PVE::ParseUtils::split_list)
# must stay single-quoted here: create_cmd is sent to `remote()`, which
# hands the whole string to `ssh` as a single command for the *remote*
# shell to parse -- unquoted, that `;` would be read as a remote
# command separator and silently truncate this into two commands.
create_cmd="pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged 1 --features '${PROXMOX_DEFAULT_LXC_FEATURES}' --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
remote "$create_cmd"
remote "pct start ${vmid}"
else
echo "==> Creating VM ${vmid} (${name})..."
# pre-enrolled-keys=0 disables OVMF's Secure Boot key pre-enrollment --
# required, or systemd-boot (unsigned) can't be trusted by the firmware.
# --agent 1: wires up the virtio-serial channel QEMU exposes to the guest.
# modules/common/configuration.nix sets services.qemuGuest.enable = true
# on every host, so the guest-side qemu-ga daemon is already running --
# without this flag Proxmox never creates the channel it listens on, so
# `qm guest exec`/`qm agent` and the UI's IP-address display silently
# never work for any VM this script creates.
remote "qm create ${vmid} --name ${name} --memory ${memory} --cores ${cores} \
--net0 virtio,bridge=${bridge} --bios ovmf --machine q35 --scsihw virtio-scsi-pci \
--efidisk0 ${storage}:1,efitype=4m,pre-enrolled-keys=0 --agent enabled=1"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] ssh ${ssh_target} -- qm importdisk ${vmid} ${remote_path} ${storage}"
echo "[dry-run] (would parse the resulting disk identifier from that output)"
echo "[dry-run] ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<parsed-disk-id>"
else
importdisk_output="$(ssh "$ssh_target" "qm importdisk ${vmid} ${remote_path} ${storage}")"
echo "$importdisk_output"
disk_id="$(echo "$importdisk_output" | grep -oP "(?<=Successfully imported disk as ')[^']+" | sed 's/^unused[0-9]*://')"
if [[ -z "$disk_id" ]]; then
echo "ERROR: couldn't parse the imported disk identifier from qm importdisk's output above." >&2
echo "The VM shell (${vmid}) and imported disk both exist -- finish attaching it by hand:" >&2
echo " ssh ${ssh_target} -- qm set ${vmid} --scsi0 ${storage}:<disk-id-from-output-above>" >&2
echo " ssh ${ssh_target} -- qm set ${vmid} --boot order=scsi0" >&2
exit 1
fi
remote "qm set ${vmid} --scsi0 ${disk_id}"
fi
remote "qm set ${vmid} --boot order=scsi0"
remote "qm start ${vmid}"
fi
echo
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] Nothing was built, uploaded, or created."
else
echo "Done. ${name} (VMID ${vmid}) should be booting on ${node}."
fi
-150
View File
@@ -1,150 +0,0 @@
#!/usr/bin/env bash
# Backs up the local sops age key (the private key that decrypts
# secrets/*.yaml -- normally the one trusted as &admin) to an arbitrary
# destination path, e.g. a USB drive or other offline storage, so it can
# later be restored and handed to rotate-admin-key.sh if this machine's
# copy is ever lost, or to run either script from a different machine.
#
# Usage:
# scripts/secrets/backup-admin-key.sh <dest-path> [--key-file <path>] [--force] [--dry-run]
#
# Source key resolution matches sops/age's own default order:
# $SOPS_AGE_KEY (inline identity text) if set, else
# --key-file if given, else
# $SOPS_AGE_KEY_FILE if set, else
# ${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
sops_yaml="${repo_root}/.sops.yaml"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/sops-age.sh
source "${repo_root}/scripts/lib/sops-age.sh"
# Pin cwd for the same reason rotate-admin-key.sh does: age/sops calls
# below should never depend on wherever the caller's shell happened to be.
cd "$repo_root"
usage() {
cat <<EOF
Usage: $0 <dest-path> [--key-file <path>] [--force] [--dry-run]
<dest-path> Where to write the backup. Parent directories are
created as needed. Written with 0600 permissions.
--key-file <path> Read the key from here instead of the default
sops/age resolution (\$SOPS_AGE_KEY_FILE, then
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt).
Ignored if \$SOPS_AGE_KEY is set (that always wins,
same precedence sops/age itself uses).
--force Overwrite <dest-path> if it already exists.
--dry-run Print what would happen; write nothing.
EOF
}
dry_run=0
force=0
key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
dry_run=1
shift
;;
--force)
force=1
shift
;;
--key-file)
key_file="${2:?--key-file requires a path}"
shift 2
;;
-h | --help)
usage
exit 0
;;
--*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
args+=("$1")
shift
;;
esac
done
if [[ "${#args[@]}" -ne 1 ]]; then
usage >&2
exit 1
fi
dest="${args[0]}"
nix_extra_opts
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
echo "==> Source: \$SOPS_AGE_KEY (inline identity from the environment)."
src_content="$SOPS_AGE_KEY"
else
[[ -s "$key_file" ]] || {
echo "ERROR: no key found. \$SOPS_AGE_KEY is unset and ${key_file} doesn't exist or is empty." >&2
exit 1
}
echo "==> Source: ${key_file}"
src_content="$(cat "$key_file")"
fi
# Round-trip through a private scratch file (rather than trusting the
# source string as-is) so age-keygen -y validates it's a real identity
# before anything is written to <dest-path>.
scratch="$(mktemp)"
trap 'rm -f "$scratch"' EXIT
( umask 077; printf '%s\n' "$src_content" > "$scratch" )
src_pub="$(age_pubkey_from_identity_file "$scratch")" || {
echo "ERROR: source doesn't look like a valid age identity (age-keygen -y failed)." >&2
exit 1
}
echo " public key: ${src_pub}"
current_admin_pub="$(sops_yaml_admin_pubkey "$sops_yaml")"
if [[ -n "$current_admin_pub" && "$current_admin_pub" != "$src_pub" ]]; then
echo "NOTE: this key does not match .sops.yaml's current &admin entry (${current_admin_pub})."
echo " Backing it up anyway -- this script doesn't require it to be the admin key."
fi
if [[ -e "$dest" && "$force" -ne 1 ]]; then
echo "ERROR: ${dest} already exists. Pass --force to overwrite." >&2
exit 1
fi
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] would write $(wc -c <"$scratch" | tr -d ' ') bytes to ${dest} (mode 0600)"
[[ -e "$dest" ]] && echo "[dry-run] would overwrite existing file (--force given)"
echo "[dry-run] Nothing was written. Re-run without --dry-run to apply this."
exit 0
fi
mkdir -p "$(dirname "$dest")"
install -m 600 "$scratch" "$dest"
dest_pub="$(age_pubkey_from_identity_file "$dest")"
if [[ "$dest_pub" != "$src_pub" ]]; then
echo "ERROR: ${dest} was written but its public key doesn't match the source -- investigate before relying on this backup." >&2
exit 1
fi
cat <<EOF
Done. Backed up to: ${dest}
public key: ${dest_pub}
This is a private key -- store it somewhere offline/secure, not in this
repo or anywhere it'd get committed. Restore it with:
scripts/secrets/rotate-admin-key.sh ${dest}
EOF
-83
View File
@@ -1,83 +0,0 @@
#!/usr/bin/env bash
# Generates a new machine's SSH host key by an arbitrary name, before it
# necessarily has a flake target yet -- prints the .sops.yaml snippet to
# add by hand. For any host that already has a flake target,
# scripts/secrets/sync-host-keys.sh <target> does this same job plus the
# .sops.yaml/key_groups registration and re-encryption automatically; use
# this script only to pre-generate a key ahead of adding the flake target
# itself.
#
# Why a host key is needed at all: sops-nix derives each host's decryption key from
# its own /etc/ssh/ssh_host_ed25519_key at *activation* time, but that
# activation runs before systemd would otherwise generate this key on
# first boot (sshd-keygen is a normal systemd service gated behind
# multi-user.target; activation scripts run earlier than that). Without
# pre-seeding, secrets — including the root/nixos login password — fail
# to decrypt on the machine's very first boot.
#
# This script only touches your admin workstation and this repo's
# .sops.yaml (it never contacts the target machine). Run it, follow the
# printed next steps, then use the resulting key with the auto-install.sh
# prompt (see modules/installer/common.nix) when you actually install the
# new machine.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/ssh-host-keys.sh
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
hostname="${1:?usage: scripts/secrets/prepare-host-key.sh <hostname>}"
sops_yaml="${repo_root}/.sops.yaml"
if [[ ! -f "$sops_yaml" ]]; then
echo "ERROR: $sops_yaml not found — is this script still under nixos/scripts/?" >&2
exit 1
fi
keydir="${repo_root}/host-keys"
mkdir -p "$keydir"
keyfile="${keydir}/${hostname}_ssh_host_ed25519_key"
if [[ -f "$keyfile" ]]; then
echo "ERROR: $keyfile already exists. Remove it first if you want to regenerate." >&2
exit 1
fi
nix_extra_opts
generate_host_ed25519_key "$hostname" "$keyfile"
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
cat <<EOF
Generated: ${keyfile}(.pub)
=== 1. Add this line under keys: in ${sops_yaml} ===
- &${hostname} ${age_pub}
=== 2. Add *${hostname} to whichever creation_rules key_groups this host needs ===
(e.g. secrets/common.yaml always; add a per-host secrets/${hostname}.yaml
block too if this host will get its own secrets, same pattern as
nix-cache/server.)
=== 3. Re-encrypt every secrets file you just added it to ===
nix-shell -p sops --run 'sops updatekeys ${repo_root}/secrets/common.yaml'
=== 4. Commit + push this repo so the flake build picks up the new recipient ===
=== 5. Get the key onto the installer, one of two ways ===
a) Rebuild the installer image with all host-keys/ baked in (see
docs/auto-installer.md):
NIXOS_HOST_KEYS_DIR="${keydir}" nix build .#iso --impure
(or .#pxe — --impure is required since host-keys/ is gitignored and
flakes can't see it otherwise)
b) Or, for an image already built without keys, scp it in after boot:
scp ${keyfile}{,.pub} root@<target-ip>:/root/host-keys/
Then continue with /etc/auto-install.sh as normal — it checks
/etc/host-keys (baked in) before /root/host-keys (scp'd) and installs
whichever it finds before running nixos-install.
EOF
-193
View File
@@ -1,193 +0,0 @@
#!/usr/bin/env bash
# Rotates the &admin sops age key: decrypts with a backed-up copy of the
# key CURRENTLY trusted as &admin, replaces .sops.yaml's &admin entry with
# a new key already present in this environment, and re-encrypts every
# secrets/*.yaml for the new recipient set. After this runs, the old key
# can no longer decrypt anything -- this is a real, one-way handoff of
# trust, not a preview.
#
# This is the automation for the manual steps create-proxmox-resource.sh /
# sync-host-keys.sh print when they bootstrap a brand-new, not-yet-trusted
# age key on a machine that's never had admin access before:
#
# scripts/secrets/rotate-admin-key.sh /path/to/backed-up/admin/keys.txt
#
# The backup key's *public* key must match .sops.yaml's current &admin
# entry -- this script verifies that by deriving it, it doesn't just trust
# the filename or take it on faith. The new key defaults to wherever sops
# itself would already look ($SOPS_AGE_KEY_FILE, then the XDG default), so
# the common case is just pointing this at the restored backup.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
sops_yaml="${repo_root}/.sops.yaml"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/sops-age.sh
source "${repo_root}/scripts/lib/sops-age.sh"
# sops resolves .sops.yaml by walking up from the process's cwd, not from
# the target file's own path -- if this script were invoked from somewhere
# other than the repo root (or from inside another checkout/worktree that
# happens to have its own .sops.yaml), `sops updatekeys` would silently
# re-encrypt against the WRONG config's recipient list instead of this
# repo's. Pin cwd here so every sops/age call below is unambiguous
# regardless of where the caller's shell started out.
cd "$repo_root"
usage() {
cat <<EOF
Usage: $0 <path-to-backed-up-admin-key> [--new-key-file <path>] [--dry-run]
<path-to-backed-up-admin-key> age identity file for the key CURRENTLY
trusted as &admin. Only ever read -- never
copied or modified.
--new-key-file <path> age identity file for the key to promote
to &admin. Defaults to \$SOPS_AGE_KEY_FILE,
then
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt
(sops/age's own default resolution order).
--dry-run Print what would change; touches nothing
(.sops.yaml untouched, no sops updatekeys
calls).
EOF
}
dry_run=0
new_key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
dry_run=1
shift
;;
--new-key-file)
new_key_file="${2:?--new-key-file requires a path}"
shift 2
;;
-h | --help)
usage
exit 0
;;
--*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
args+=("$1")
shift
;;
esac
done
if [[ "${#args[@]}" -ne 1 ]]; then
usage >&2
exit 1
fi
backup_key="${args[0]}"
[[ -s "$backup_key" ]] || { echo "ERROR: backup key file not found or empty: ${backup_key}" >&2; exit 1; }
[[ -s "$new_key_file" ]] || { echo "ERROR: new key file not found or empty: ${new_key_file}" >&2; exit 1; }
nix_extra_opts
echo "==> Deriving public keys..."
old_pub="$(age_pubkey_from_identity_file "$backup_key")"
new_pub="$(age_pubkey_from_identity_file "$new_key_file")"
echo " backup (old admin) key: ${old_pub}"
echo " new admin key: ${new_pub}"
if [[ "$old_pub" == "$new_pub" ]]; then
echo "ERROR: backup key and new key are identical -- nothing to rotate." >&2
exit 1
fi
current_admin_pub="$(sops_yaml_admin_pubkey "$sops_yaml")"
if [[ -z "$current_admin_pub" ]]; then
echo "ERROR: couldn't find a '&admin age1...' line in ${sops_yaml}." >&2
exit 1
fi
if [[ "$current_admin_pub" != "$old_pub" ]]; then
echo "ERROR: ${backup_key} doesn't match the current &admin key in .sops.yaml." >&2
echo " .sops.yaml &admin: ${current_admin_pub}" >&2
echo " backup key pubkey: ${old_pub}" >&2
echo "Wrong backup file, or .sops.yaml has already moved on -- not touching anything." >&2
exit 1
fi
mapfile -t secrets_files < <(find "${repo_root}/secrets" -maxdepth 1 -name '*.yaml' | sort)
if [[ "${#secrets_files[@]}" -eq 0 ]]; then
echo "ERROR: no secrets/*.yaml files found under ${repo_root}/secrets." >&2
exit 1
fi
# sops_can_decrypt <key-file> <secrets-file>: used both to confirm the
# backup key still works before touching anything, and again after
# rotation to confirm the new key does too.
sops_can_decrypt() {
local key_file="$1" secrets_file="$2"
SOPS_AGE_KEY_FILE="$key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
"sops -d '${secrets_file}'" >/dev/null
}
echo "==> Confirming the backup key can actually decrypt..."
if ! sops_can_decrypt "$backup_key" "${secrets_files[0]}"; then
echo "ERROR: backup key failed to decrypt $(basename "${secrets_files[0]}") -- aborting." >&2
exit 1
fi
echo " OK: decrypted $(basename "${secrets_files[0]}")"
if [[ "$dry_run" -eq 1 ]]; then
echo
echo "[dry-run] would replace .sops.yaml's &admin line:"
echo "[dry-run] - ${current_admin_pub}"
echo "[dry-run] + ${new_pub}"
echo "[dry-run] would then re-encrypt (sops updatekeys --yes) for the new recipient set:"
for f in "${secrets_files[@]}"; do
echo "[dry-run] secrets/$(basename "$f")"
done
echo
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
exit 0
fi
echo "==> Rotating .sops.yaml's &admin key..."
sed -i "s|^ - &admin age1[a-z0-9]*| - \&admin ${new_pub}|" "$sops_yaml"
grep -qF "$new_pub" "$sops_yaml" || {
echo "ERROR: sed edit didn't take -- .sops.yaml left unchanged, check it by hand." >&2
exit 1
}
echo " Updated."
echo "==> Re-encrypting secrets/*.yaml for the new recipient set..."
for f in "${secrets_files[@]}"; do
echo "==> $(basename "$f")"
sops_updatekeys "$f" "$backup_key"
done
echo "==> Verifying the new key can decrypt everything..."
for f in "${secrets_files[@]}"; do
if ! sops_can_decrypt "$new_key_file" "$f"; then
echo "ERROR: new key failed to decrypt $(basename "$f") after rotation -- investigate before committing." >&2
exit 1
fi
echo " OK: $(basename "$f")"
done
cat <<EOF
Done. .sops.yaml's &admin key is now:
${new_pub}
The old key (${old_pub}) can no longer decrypt any secrets/*.yaml
re-encrypted above.
Review the diff, then commit:
git add .sops.yaml secrets/*.yaml
git commit -m "Rotate sops admin age key"
EOF
-446
View File
@@ -1,446 +0,0 @@
#!/usr/bin/env bash
# Manages host-keys/ + .sops.yaml + secrets/*.yaml recipients together, so
# a flake target's SSH host key and its sops registration never drift out
# of sync with each other or with the flake itself.
#
# sync-host-keys.sh --all Generate/register every flake
# target missing a key.
# sync-host-keys.sh <target> Same, for just one target.
# sync-host-keys.sh --remove Interactively remove one
# locally-managed key.
# sync-host-keys.sh --regenerate-all-keys Remove and freshly regenerate
# every locally-managed key.
#
# "Generate/register" is idempotent and additive only: an existing
# host-keys/ file is never touched, and .sops.yaml only ever gains an
# anchor/alias it doesn't already have -- safe to re-run any time, e.g.
# right after adding a new host to flake.nix.
#
# --remove and --regenerate-all-keys only ever operate on anchors that have
# a corresponding host-keys/<name>_ssh_host_ed25519_key file. Anchors
# without one (&admin, and any anchor for an already-deployed host whose
# real /etc/ssh key was registered by hand, e.g. &docker/&server/&nix-cache
# today) are never listed, removed, or regenerated -- this tooling only
# ever touches keys it itself manages.
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
sops_yaml="${repo_root}/.sops.yaml"
keydir="${repo_root}/host-keys"
editor="${repo_root}/scripts/lib/sync-host-keys-edit-sops.py"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=../lib/nix-eval.sh
source "${repo_root}/scripts/lib/nix-eval.sh"
# shellcheck source=../lib/ssh-host-keys.sh
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
# shellcheck source=../lib/sops-age.sh
source "${repo_root}/scripts/lib/sops-age.sh"
# shellcheck source=../lib/confirm.sh
source "${repo_root}/scripts/lib/confirm.sh"
mkdir -p "$keydir"
usage() {
cat <<EOF
Usage: $0 --all [--dry-run]
$0 <flake-target> [--dry-run]
$0 --remove [--dry-run]
$0 --regenerate-all-keys [--dry-run]
--all Generate + register a host key for every flake
target that's missing one.
<flake-target> Same, for just one target (e.g. lxc-server).
Reports if it already has one.
--remove Interactively pick one locally-managed key to
remove from .sops.yaml and host-keys/.
--regenerate-all-keys Remove every locally-managed key and generate
fresh replacements for every current flake
target. Destructive -- requires typed
confirmation.
--dry-run Combine with any of the above: print what would
change (host-keys/ files, .sops.yaml anchors and
key_groups, which secrets/*.yaml would be
re-encrypted) without touching anything. No keys
generated, no files written, no sops calls,
no prompts for confirmation.
EOF
}
# --- step 0: make sure we can actually decrypt anything at all -------------
#
# Registering a host means editing .sops.yaml and then running
# `sops updatekeys`, which has to decrypt each secrets file with an
# existing recipient's key before it can re-encrypt it for the new one.
# Check this before doing anything else, the same order sops/age itself
# resolves a usable key in: SOPS_AGE_KEY (inline), then SOPS_AGE_KEY_FILE,
# then the XDG default path.
ensure_admin_decrypt_key() {
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
echo "Using SOPS_AGE_KEY from the environment."
return
fi
local key_file="$DEFAULT_SOPS_AGE_KEY_FILE"
if [[ -s "$key_file" ]]; then
echo "Found existing sops age key at ${key_file}."
return
fi
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] No sops age decryption key found (checked \$SOPS_AGE_KEY, \$SOPS_AGE_KEY_FILE, ${key_file})."
echo "[dry-run] Would generate a new one here -- continuing the dry run without one; any"
echo "[dry-run] 'would re-encrypt' output below couldn't actually run for real yet."
return
fi
echo "No sops age decryption key found (checked \$SOPS_AGE_KEY, \$SOPS_AGE_KEY_FILE, ${key_file})."
echo "Generating a new one at ${key_file}..."
mkdir -p "$(dirname "$key_file")"
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -o '${key_file}'" 2>&1 | grep -v "^Public key:" || true
local new_pub
new_pub="$(age_pubkey_from_identity_file "$key_file")"
cat <<EOF
A brand-new age key was just generated -- it cannot decrypt anything that
already exists in secrets/*.yaml, since nothing was ever encrypted for it.
That trust can't be bootstrapped automatically (nobody can decrypt a file
for a recipient that didn't exist when it was last encrypted).
To actually use this key:
1. Have someone who currently CAN decrypt replace the &admin entry in
.sops.yaml with this public key:
${new_pub}
2. They re-encrypt every secrets/*.yaml:
sops updatekeys --yes secrets/common.yaml
sops updatekeys --yes secrets/nix-cache.yaml
sops updatekeys --yes secrets/server.yaml
3. Re-run this script.
Exiting without making any other changes.
EOF
exit 1
}
discover_targets() {
# installer is the one nixosConfigurations target that doesn't import
# sops-nix at all (see CLAUDE.md's "Security Notes" -- hardcoded login
# password instead) -- config.sops.secrets doesn't exist for it.
list_flake_targets "$repo_root" | grep -v '^installer$'
}
locally_managed_hosts() {
for f in "$keydir"/*_ssh_host_ed25519_key.pub; do
[[ -e "$f" ]] || continue
basename "$f" _ssh_host_ed25519_key.pub
done
}
add_keys_json="[]"
add_aliases_json="[]"
dry_run=0
queue_host_sync() {
local host="$1"
local keyfile="${keydir}/${host}_ssh_host_ed25519_key"
local has_local_key=0 has_anchor=0
[[ -f "$keyfile" ]] && has_local_key=1
grep -qE "^ - &${host} age1" "$sops_yaml" && has_anchor=1
if [[ "$has_local_key" -eq 0 && "$has_anchor" -eq 1 ]]; then
echo "SKIP ${host}: .sops.yaml already has an &${host} anchor, but"
echo " host-keys/${host}_ssh_host_ed25519_key is missing locally."
echo " Not generating a replacement -- it wouldn't match whatever's"
echo " already registered (and possibly deployed). Remove the"
echo " &${host} line from .sops.yaml first if you really want a"
echo " fresh key, then re-run."
return 1
fi
if [[ "$has_local_key" -eq 0 ]]; then
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] ${host}: would generate host key"
else
echo "==> ${host}: generating host key"
generate_host_ed25519_key "$host" "$keyfile"
fi
else
echo "==> ${host}: host key already present"
fi
if [[ "$has_anchor" -eq 0 ]]; then
local age_pub
if [[ "$dry_run" -eq 1 ]]; then
age_pub="dry-run-placeholder-not-a-real-key"
else
age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
fi
add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \
'. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")"
fi
echo "==> ${host}: checking which secrets files it references"
local basenames
mapfile -t basenames < <(
nix eval --json --no-use-registries --no-accept-flake-config \
"${repo_root}#nixosConfigurations.${host}.config.sops.secrets" \
--apply 'builtins.mapAttrs (n: v: baseNameOf v.sopsFile)' \
| jq -r '[.[]] | unique | .[]'
)
local basename
for basename in "${basenames[@]}"; do
add_aliases_json="$(jq --arg host "$host" --arg basename "$basename" \
'. + [{host: $host, basename: $basename}]' <<<"$add_aliases_json")"
done
}
# In dry-run, this runs the exact same edit logic (so idempotency/what's-
# actually-new is determined for real, not guessed) but against a scratch
# copy of .sops.yaml that's discarded afterward -- the real file is never
# opened for writing, and `sops updatekeys` never runs.
apply_edit_plan() {
local plan="$1"
local target="$sops_yaml"
local tmpfile=""
if [[ "$dry_run" -eq 1 ]]; then
tmpfile="$(mktemp)"
cp "$sops_yaml" "$tmpfile"
target="$tmpfile"
fi
local result
result="$(echo "$plan" | nix-shell "${NIX_OPTS[@]}" -p python3 --run "python3 '${editor}' '${target}'")"
[[ -n "$tmpfile" ]] && rm -f "$tmpfile"
local added removed changed
added="$(jq -r '.added_keys[]?' <<<"$result")"
removed="$(jq -r '.removed_keys[]?' <<<"$result")"
changed="$(jq -r '.changed_secrets_files[]?' <<<"$result")"
if [[ -z "$added" && -z "$removed" && -z "$changed" ]]; then
echo "Nothing changed in .sops.yaml."
return
fi
local prefix=""
[[ "$dry_run" -eq 1 ]] && prefix="[dry-run] would "
[[ -n "$added" ]] && echo "${prefix}Add .sops.yaml anchors: $(tr '\n' ' ' <<<"$added")"
[[ -n "$removed" ]] && echo "${prefix}Remove .sops.yaml anchors: $(tr '\n' ' ' <<<"$removed")"
if [[ -n "$changed" ]]; then
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would re-encrypt:"
while IFS= read -r basename; do
[[ -z "$basename" ]] && continue
echo " secrets/${basename}"
done <<<"$changed"
else
echo "Re-encrypting affected secrets files..."
while IFS= read -r basename; do
[[ -z "$basename" ]] && continue
echo "==> secrets/${basename}"
sops_updatekeys "${repo_root}/secrets/${basename}"
done <<<"$changed"
fi
fi
}
flush_additions() {
if [[ "$add_keys_json" == "[]" && "$add_aliases_json" == "[]" ]]; then
echo "Nothing to do -- every requested target already has a fully registered host key."
return
fi
echo
echo "Applying .sops.yaml edits..."
local plan
plan="$(jq -n --argjson add_keys "$add_keys_json" --argjson add_aliases "$add_aliases_json" \
'{add_keys: $add_keys, add_aliases: $add_aliases}')"
apply_edit_plan "$plan"
echo
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
else
echo "Done. Review the .sops.yaml / secrets/*.yaml diff, then commit and push --"
echo "the flake build the installer uses has to see the new recipient(s) before"
echo "any of these hosts can decrypt their secrets on first boot."
fi
}
cmd_all() {
echo "Discovering flake targets..."
local targets
mapfile -t targets < <(discover_targets)
local host
for host in "${targets[@]}"; do
queue_host_sync "$host" || true
done
flush_additions
}
cmd_target() {
local host="$1"
local targets
mapfile -t targets < <(discover_targets)
if ! printf '%s\n' "${targets[@]}" | grep -qxF "$host"; then
echo "ERROR: '${host}' is not a current nixosConfigurations target." >&2
echo "Current targets:" >&2
printf ' %s\n' "${targets[@]}" >&2
exit 1
fi
queue_host_sync "$host" || exit 1
flush_additions
}
cmd_remove() {
local hosts
mapfile -t hosts < <(locally_managed_hosts)
if [[ "${#hosts[@]}" -eq 0 ]]; then
echo "No locally-managed keys in host-keys/ -- nothing to remove."
return
fi
echo "Locally-managed keys:"
local i=1 host
for host in "${hosts[@]}"; do
local registered="not registered in .sops.yaml"
grep -qE "^ - &${host} age1" "$sops_yaml" && registered="registered in .sops.yaml"
printf ' %d) %s (%s)\n' "$i" "$host" "$registered"
i=$((i + 1))
done
local choice
read -rp "Remove which one? (number, or blank to cancel): " choice
if [[ -z "$choice" ]]; then
echo "Cancelled."
return
fi
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#hosts[@]} )); then
echo "ERROR: invalid selection." >&2
exit 1
fi
local target="${hosts[$((choice - 1))]}"
if [[ "$dry_run" -ne 1 ]]; then
read -rp "Really remove '${target}'? Its host-keys/ files will be deleted and it will lose access to every secrets file it can currently decrypt. (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Cancelled."
return
fi
fi
local plan
plan="$(jq -n --arg host "$target" \
'{remove_keys: [$host], remove_aliases_for_hosts: [$host]}')"
apply_edit_plan "$plan"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would delete host-keys/${target}_ssh_host_ed25519_key(.pub)."
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
else
rm -f "${keydir}/${target}_ssh_host_ed25519_key" "${keydir}/${target}_ssh_host_ed25519_key.pub"
echo "Removed host-keys/${target}_ssh_host_ed25519_key(.pub)."
echo
echo "Review the diff, then commit and push."
fi
}
cmd_regenerate_all() {
local hosts
mapfile -t hosts < <(locally_managed_hosts)
if [[ "${#hosts[@]}" -eq 0 ]]; then
echo "No locally-managed keys in host-keys/ -- nothing to regenerate."
return
fi
echo "This will remove and freshly regenerate ALL locally-managed keys:"
printf ' %s\n' "${hosts[@]}"
echo
echo "Every host above will need its new key baked into a rebuilt install"
echo "image/tarball before it can decrypt secrets again."
if [[ "$dry_run" -ne 1 ]]; then
if ! confirm_typed "REGENERATE" "Type REGENERATE to confirm: "; then
echo "Cancelled."
return
fi
fi
echo
local hosts_json
hosts_json="$(printf '%s\n' "${hosts[@]}" | jq -R . | jq -s .)"
local plan
plan="$(jq -n --argjson hosts "$hosts_json" \
'{remove_keys: $hosts, remove_aliases_for_hosts: $hosts}')"
apply_edit_plan "$plan"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] would delete ${#hosts[@]} host-keys/ file pair(s)."
echo "[dry-run] would then generate fresh replacements for the same hosts"
echo "[dry-run] (not simulated further here -- run without --dry-run, or"
echo "[dry-run] preview a specific target with: $0 <target> --dry-run)."
echo
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
return
fi
echo "Removing existing keys..."
local host
for host in "${hosts[@]}"; do
rm -f "${keydir}/${host}_ssh_host_ed25519_key" "${keydir}/${host}_ssh_host_ed25519_key.pub"
done
echo "Removed ${#hosts[@]} host-keys/ file pair(s)."
echo
echo "Regenerating fresh keys for every current flake target..."
cmd_all
}
main() {
local args=()
local arg
for arg in "$@"; do
if [[ "$arg" == "--dry-run" ]]; then
dry_run=1
else
args+=("$arg")
fi
done
set -- "${args[@]+"${args[@]}"}"
if [[ "$dry_run" -eq 1 ]]; then
echo "[dry-run] no changes will be made"
echo
fi
nix_extra_opts
ensure_admin_decrypt_key
case "${1:-}" in
--all)
cmd_all
;;
--remove)
cmd_remove
;;
--regenerate-all-keys)
cmd_regenerate_all
;;
-h | --help | "")
usage
;;
--*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
cmd_target "$1"
;;
esac
}
main "$@"
-110
View File
@@ -1,110 +0,0 @@
#!/usr/bin/env bash
# Detects and fixes drift between the ed25519 SSH host key nix-cache is
# actually serving right now and vars.nixCacheHostKey (variables.nix) --
# the value modules/nix-cache/remote-builder-client.nix bakes into every
# client's declarative programs.ssh.knownHosts, and
# scripts/proxmox/configure-nix-cache-client.sh hardcodes as its own
# default for non-NixOS clients.
#
# This value has no automatic source of truth: nix-cache's host key is
# generated once (first boot / container recreate) and never touches this
# repo again unless someone remembers to update it by hand afterwards. It
# drifted silently once already -- confirmed live: variables.nix recorded
# a key that no longer matched what nix-cache actually presented, which
# would fail every real client's SSH host-key verification for
# distributed builds without ever producing an obvious error pointing
# back here (a client just sees "Host key verification failed" against
# *some* key, with no hint that the trusted value itself was stale).
#
# codex-maintenance.sh runs this in --check mode on every invocation so
# that drift surfaces as a warning instead of a future debugging session.
#
# Usage:
# scripts/secrets/sync-nix-cache-host-key.sh [--check] [--dry-run] [--host <name>]
#
# --check Only report drift (exit 1 if found, 2 if nix-cache is
# unreachable); never writes. For CI/maintenance use.
# --dry-run Show what would change; never writes.
# --host Override the hostname to scan (default: variables.nix's
# nixCacheHost / env.sh's NIX_CACHE_HOST).
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
variables_nix="${repo_root}/variables.nix"
client_script="${repo_root}/scripts/proxmox/configure-nix-cache-client.sh"
# shellcheck source=../env.sh
source "${repo_root}/scripts/env.sh"
check_only=0
dry_run=0
host="${NIX_CACHE_HOST}"
while [[ $# -gt 0 ]]; do
case "$1" in
--check) check_only=1; shift ;;
--dry-run) dry_run=1; shift ;;
--host)
host="${2:?--host requires a hostname}"
shift 2
;;
-h|--help)
sed -n '2,23p' "$0"
exit 0
;;
*)
echo "ERROR: unknown argument: $1" >&2
exit 1
;;
esac
done
current_value="$(grep -oE 'nixCacheHostKey = "[^"]+"' "$variables_nix" | sed -E 's/nixCacheHostKey = "(.*)"/\1/')"
if [[ -z "$current_value" ]]; then
echo "ERROR: couldn't find nixCacheHostKey in $variables_nix" >&2
exit 1
fi
current_type_blob="$(awk '{print $1, $2}' <<<"$current_value")"
current_label="$(awk '{print $3}' <<<"$current_value")"
echo "Scanning ${host} for its current ed25519 SSH host key..."
nix_extra_opts
scanned="$(nix-shell "${NIX_OPTS[@]}" -p openssh --run "ssh-keyscan -t ed25519 -T 5 '${host}'" 2>/dev/null | grep -v '^#' | head -1 || true)"
if [[ -z "$scanned" ]]; then
echo "ERROR: couldn't reach ${host} (or got no ed25519 host key back) via ssh-keyscan." >&2
exit 2
fi
scanned_type_blob="$(awk '{print $2, $3}' <<<"$scanned")"
if [[ "$current_type_blob" == "$scanned_type_blob" ]]; then
echo "Up to date: ${host}'s host key matches variables.nix's nixCacheHostKey."
exit 0
fi
echo "DRIFT DETECTED:"
echo " variables.nix has: $current_type_blob"
echo " ${host} is now: $scanned_type_blob"
if [[ "$check_only" -eq 1 ]]; then
echo
echo "Run 'scripts/secrets/sync-nix-cache-host-key.sh' (no flags) to fix." >&2
exit 1
fi
new_value="${scanned_type_blob} ${current_label}"
if [[ "$dry_run" -eq 1 ]]; then
echo "(--dry-run: would update variables.nix and ${client_script##*/} to:)"
echo " $new_value"
exit 0
fi
sed -i "s|nixCacheHostKey = \"[^\"]*\"|nixCacheHostKey = \"${new_value}\"|" "$variables_nix"
sed -i "s|NIX_CACHE_HOST_KEY:=[^}]*}|NIX_CACHE_HOST_KEY:=${new_value}}|" "$client_script"
echo "Updated variables.nix and ${client_script##*/} to:"
echo " $new_value"
echo
echo "This only takes effect on already-deployed NixOS clients after their"
echo "next rebuild (programs.ssh.knownHosts is declarative). Review with"
echo "'git diff', then run 'bash scripts/codex-maintenance.sh' before committing."
-108
View File
@@ -1,108 +0,0 @@
root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str]
nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,type:str]
nix-github-token: ENC[AES256_GCM,data:OfNRGJg16Ede6EilWUetCs9za+xk5/Lsa3SpVajsqz8PMdA1xQNeCWdX7ZAMdijHClpBhU6ETFGsXvt41O9aORS951uijeGSW7/NH35/bnPISrKdYeBx/+xEiqwH,iv:QGU3v7xOy89uzRTCb1U9ICyJ8XYIpXrUsDt12aL3g2Y=,tag:Bde2wcWNv8H4WLxSEUAodg==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzdUlybVpDamVxYk94MDEv
VjNiVjJkWjZvYjN1SUhMSG5ZOS91a3FrNDJRCi9IQWh4RVdwdkhwQjd6TTBhcW9J
alhLMlArM1VvVkE5VUxOQ1ZWNmw2SmcKLS0tIFRvWklUN0xxSFdKSk1vakExQ3Rv
OWpmSGRTUHlvVysvR2N4UHRYWHgvMHMKqapmFB4ct1FTPa1hMWyylvLycUvOEFop
enZI5SV1F86HOTEhK0QbnEW3jMl4ZXOtAlwcys2oE9a783MSgVPxeg==
-----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEc0lHazMrVUR2UUlaQ21O
T2Jha0EvUmJzVC96Wksxay9GVlEwU2lZWVRRClBQdWVqTjNTYnBrQldMeWR0dWNz
QTN6T0VtQzhpM3UwKzVjUjl0Rkp4VzQKLS0tIEg5VzhUT1VTamQzZjNYT2orMnY0
Q1BpNzI2cVk3by9Ic3lrQ25sM05YMlkKXVofmCu7iI/my1o47p2eUzhXuP/V0NS8
mmIerz2v2uUwoS252qSU4a2vmxPOgWM0Os1vWsYamsapofQc6TaU2Q==
-----END AGE ENCRYPTED FILE-----
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYZHVnTEk2SjUwdTdKdWNV
dVFPNDFvdzY2VUlzMU5nK0t0UlUxU3Ewa0g0CjJCd0ZrMjhRZldaUDNIS1pGZTZn
Z2h2MEJ6TUpna1NNdXl4R3d1S1djVDQKLS0tIFJqMlZ3b0Zzalp2N0hIc0NHMzU4
RGRoVjdvMEkxam9DRzdYaW16enAyQ3cKUZTDqvWnmEMKHhI430coKHw3raIiD/o1
1BtDWOmKFFcuaF5mRx/qTUjEwU5OZWOujqLSPoVbmsbhyxsG3XQSHg==
-----END AGE ENCRYPTED FILE-----
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzbXdrajlVQU5RditMckh6
ZGhvcU01UEdMbURlcG9mZzg0RTJhcHJ4QlQwCjgyRGVXYytxUFB0WXhadHQ1YVpF
VUJpVzE2SzFPc3FHV25FQmVSQzNJTm8KLS0tIG9DazRkUkwyNHNQMnp5R3RySDBo
aGFLUFBjZjVxeWFzTXFldHVRNkNyYTAKd6eS5lks4+3SV1bFBQWyPi7OZcvRMDIc
kYD5C/vMkKlvBYyODCWJyim2xgM/nyQNcf/q4BUS3HF/RidRdJh9NQ==
-----END AGE ENCRYPTED FILE-----
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFZEhIT1REUTFtdkIzaDFY
dGU4VjU3QlpPU1Rod1dTUWhBbjc0WTR5bUF3CkNGY3F2Q251aXlGNCt5ZjFWMTE1
RjY4bHZ1T29aK3N0SktjdUpZbSs4QVEKLS0tIDZrMWd3aFVRMEZMU1N1dlZxYTZS
U3gxWU5EVVRXUGxJVlZQZmpZRmdhbW8KJ8yD9laK2T1qn0z2uYNeI80rtMlOVi2M
qIPn/FSoWnKQ7NotSTGUtEhC+f/nXfUQrBd5fsfmja++hrevTmkqYw==
-----END AGE ENCRYPTED FILE-----
recipient: age1jy444f9d9stygj4p3w9kh54cqcfr654tvr75tdvee5cxsgtdtc9q3v60ep
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0N1owUGpuWWxWb2t2Mk5Z
QlZ6Nm5lcnNBY0d1NldBQXY3OThLM2UxbkY4CkZyRUl4S1JRaDhnemRIYWR1OHZx
aWliVWh3V1RBL0R3Q0RqOE1SM0VBblUKLS0tIGpaUjJxdnV0UW55T01HcFBsdHR2
aEJ3UzJ6RklZVFRrNGMraUtGYXVZd0kKaQmzjnI6xgV7H1YcoI+gRn2IVfOLTJBY
rO43fvVtZ79c1as6CL+GoEIJY/uVA/3M4gQ0vYbDDn57vm5OOJW8jg==
-----END AGE ENCRYPTED FILE-----
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMWTcwd0JLS0lybWREaU5h
NHczcEQxeUx4U09oV2k5ZXpKTGxxR3ZYd2tnCkVXR3kyTHZSSWNaUCszL3Rvakhm
bVl5bnFRVkhJRFVaY1NQYTNrd2JKMTAKLS0tIDJTT0YvUE5HUWVKRnhXY2tvMjI1
VXNlcnJqcDM5d3poVU1SKzQyNWdpRzQKTht/ko7cy7OY0wGfza4eierYS7q/nCFH
YsG74ez3piUQ1rdJRi0e29QWz4xL3JeU9oE+tr0rMB6WvgTmuX5gwQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLWmtNOVVvRW9QbU50OUxv
TUYxWXJ4cFVaMjVnVHJVK1VTZzhDd2tCMWpnCllPWnl2bjcxemF6cFp1blZnMEMy
clhqdlVzdEZTYnY3b0ZhR2hsMXRlQTgKLS0tIHEzNU9yNmhackt6R2dLT2dZSEFJ
UnJXeWVyQld6NFhXUWowUlVXaEtKVUkK2Saa8w9/SOWw1VXWkUg62ay31bSIqHWB
8QmNafddV0WQ8oYcD8ZMf8Bm8jP9yLMUDOiVzVZiiTGWEx7EbUQV3Q==
-----END AGE ENCRYPTED FILE-----
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2OEs4dWJ5UTBoY2hHanZY
N3VNMWdGVkJHMlVublBmN2NiMUNtRnVnaXdrCnYzV3EvRzV1OXNyOFVKc2lXMFov
Mkgvc2dFdyt0MGJIMlFPckpBeDZ5c0UKLS0tIERrOGc4aWtFSTJWN1M2V0hyQ3hS
TC9HYVMyVWVBc1lHbUlqbnpsOWJobU0KkZZph2dnKwi4w+RXf7RXoHCYkXxmuLNj
rinQQlQeOwQptBn7+kRJubNjyHOlSHXhpsbV3/IkJRNMhw69knzasQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoWkljeFFBS3hhOG0relVz
V0JUK2hWaklxTGVhMWR4bytPczhjcldheno4CmZUTWszREVUdVVQazNOdXAvY0FN
bEtQSDJveHplNTE0a0FvWEFxVlEreDgKLS0tIGNKUkNVQ3BDQ3d0V1lyY0VsaHda
cDNHa3lYTkxBeVFlSGtIMVFScmxsazAK5ZyJ35/jjFRhQdG1PMOcEJ3MJf96i8DZ
AvCw9jc7Hsbs6+LoZ45K1QKoWoZmBkmAatbksXo3jnAzEHmcQqODKQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1fxxzpnfse8nd9wz78ht3m0plrmraacf4cpga0pe8fm2tdnqcgy8q7qsyvp
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtWGRLTCt5dXZMR1d6a3lD
Z0t1SDAwY3V3MXI1MGt5RGkrc1JHSVRUUzNZCi9UOTBTcVY3NkhFb1FFOS9vbTY2
S1BqNmd2eWVXbnliOFJWRDY0UmhYV3MKLS0tIExYNlJxcTBidnpObHgweHlqNHhz
OXpwRHhyOGhNRXNTWDBIUVJCR0VPTkkKQZh9e7lOINL0khHS6tBehCrm+SX5Q6XE
XL4FpBsJ5+MnxTx1O/bu6/f7OvZqfnxhQv0lkKOMJnkZfPZx5D+meg==
-----END AGE ENCRYPTED FILE-----
recipient: age190htw7prp4vln076dxjx3gxxaq06h0zl0te7cqgpx79vl3lhkaes8suy05
lastmodified: "2026-07-19T02:30:40Z"
mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.1
-35
View File
@@ -1,35 +0,0 @@
beszel-token: ENC[AES256_GCM,data:meuzUP/6wCssJDVTgbC0XwiLZPMGyDl55HEIiON9xOXCD9k6,iv:TDqWcp+8Mxd8wN09r5otQRQXq3XTeQphaTWxvvuLTAs=,tag:cRPZQGlwB/dTguBAheWPQg==,type:str]
cache-priv-key: ENC[AES256_GCM,data:6vQKIf7eS0WNL2Eptoi4VWr18SRMZfN/H/aFUUtXdMYQY5LLyBp2EHRKqZcGFuh1nZhUdAxUztq/CVXx+QFxKW+ElHxCxUSp0QqI1fdSkBkKZb8hlit5SoX9JtLzZGg0HBNM3nJu,iv:0J+xmrPJhInHhFR/c41ACjuTfaIoMkQFSfbL2KkgFa8=,tag:f4s9Szs5oprVVRSyXaX48A==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPWE1HTUhiSUp5ZEUwWEpI
bGpkZlBIMUo5ZlYrQ09SN3Q1a0ZkQ0ZnOEhVCnJPNEZQenVWWGZiODlzQzNEc1Zq
c3l4OWZJTElJc2Y2UE15OGtEUzhyY1EKLS0tIHNJUStyWnlQWjZBbEZjQ3UwdUpz
ZndoUDR6bisrNGJCUHk3TGI4bTZaMFUK87fFsm9ne9s+PK2pcwtrDjqyGBss2r2E
8lhqoeiKZ2j96z8kP/7ChzovwTCmqdcmAQuyNQD+ZAFijseipSvfbQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUYi9SRFFGV3Z6cFd2Znk5
b2FLbWtzTllJMDBUaGk0NTViOTNBa2hQclVZClZHKzNhbGVjQUJhWkFWdTFBMG5a
cUFJdUdyVG5HQXJRRnJId3hqRTN2cXMKLS0tIEFMRjh3WE1ON0U2TTNTZ3hxMTR4
ZGRlemlIbDZKeExmVHROc3Eyak5DdzQKaLwIVDi6BN4cxpVxJoqTYvJETPOp4thc
l9uVMvIGuEsEZgDsvShw1dYLljd+uGy/A+dXbcxIUCP/mmPkwmd1Pw==
-----END AGE ENCRYPTED FILE-----
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArOWovSW9DeFpxL0VDUDQ3
SHUwTzJVZUtPV01ZRkdCUXZGL2lTRCtCNFNnCjBSNExqRW5mTEN5SFVucHJHSzZt
cDlNc3BjY3M1c1k1Z2tkVEg4R1pacGsKLS0tIEFWbHNKZW0vbVh1Y2VhQW93OUwx
MWV0eW9sOXdQd0l2ZjlWOEVVc1dwcTgK2s4p9xoNkawH2OkGsl80bNIo3ad5vn4W
Z2w+jwppSoUmbQnD3WFbLmSSxmuobmU8HILwElv6SZu+KE3aspF6XA==
-----END AGE ENCRYPTED FILE-----
recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
lastmodified: "2026-07-19T23:30:21Z"
mac: ENC[AES256_GCM,data:kLGE2xawQT7mx+sfw68hmGk5nCEGiEjZrqTEl9B1dtQmTrMwmoVr/1RISi4LfJrwxy31mDgff4lcIL4wIJuM373uk3X8j4RNyYQNTfKEkORT6r8NHeepNs267O77pKGd7OmcM4MT/BqOnB8ELS7Wlf2ect7CAlvUUVyc8icxgZE=,iv:EYLDsHYHZ1XOQXafOTqHHWpk/OBNq/R6IJnOBYV33E4=,tag:thxrCPC5oGvDjhK7Dz87YA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.1
-25
View File
@@ -1,25 +0,0 @@
beszel-token: ENC[AES256_GCM,data:cbQOXhLzNk4g9d6hvm2DH7Q5ApTPCTzsW2txflDT2dD/UPIE,iv:V19MI1GEo5/0205Hrt7JImfkjduFiZ7f9aIkDVaI8mU=,tag:WCArgdrnIOudVe/Tw+oxRw==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURmMzN3hrSlNrUkkvVWNl
L3M1dEhWeW14N0RFNVRPci9QK1YyTFdqRVVJCk5WaWswT2NicldkYzZjbVhYU2xu
MGFsNmUzeTN2TS9wOEdvRURpVUVYZXMKLS0tIDZ6MEdPTVhCaTQ2UXFWTUFtc0pm
MFlJb0c2WXJtMGRLZEZYY0pZWWpFWm8K/mlYZIe8UC0QU+1mq3NtrtTF5b2m5hCK
+K0QiZLTKmmDcr4bRhZ32VE7R7GRwtMNnOP/mElZvPAyWyHHhRiOHg==
-----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURHlUL0RNMUtNallEcG5p
eG4xby8yVzdQUTBaNkl4ano0YjBMcDd0Wm5NCkZvaXNPZm9wemJkMmNSdGdOaTI4
Z1RwUnhiRUpCMWZaeWtlSVBmNW5KOXMKLS0tIDk0R0k3ZHczTFNCWUZxSWF0M0FJ
MGlZMmtuSFYrcG1meDNMWDNqSjFxcE0KDu2dAc0gqmmPkpbpBe4YohM7rYmUwEkI
V2FUQwjlvh50svtjCVdYbx2xuq4sQLnKelk/q1onLw60FwsVfzD8sQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
lastmodified: "2026-07-19T02:30:40Z"
mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.1
-168
View File
@@ -1,168 +0,0 @@
{
# Network / domains
lanDomain = "gitea.lan.ddnsgeek.com"; # Gitea/DDNS domain
homeDomain = "sweet.home"; # base LAN domain for service subdomains (pve., docker.)
tailnetDomain = "tail13f623.ts.net"; # Tailscale MagicDNS suffix
lanCidr = "192.168.2.0/24"; # LAN subnet
pxeServerIp = "192.168.2.247"; # pxe-boot host's LAN IP
pbsIp = "192.168.2.108"; # Proxmox Backup Server LAN IP
# Cross-host references (LAN hostnames/users other hosts reach over the network)
nixCacheHost = "nix-cache"; # substituter/remote-builder hostname
nfsServerHost = "server"; # NFS export source hostname
dockerHost = "docker"; # docker-compose stack host
# Raspberry Pi's own Tailscale hostname (not fronted by `server` — it
# exports its own NFS share directly). Resolved as
# "${raspberryPiHost}.${tailnetDomain}" in modules/raspi/mount-data.nix.
raspberryPiHost = "raspberrypi";
remoteBuilderUser = "nixremote"; # remote builder SSH user
# nix-cache's own SSH host public key (not a secret — the private half
# never leaves the host). Wired into every client's
# programs.ssh.knownHosts by modules/nix-cache/remote-builder-client.nix
# so distributed builds don't hit "Host key verification failed" on a
# fresh client that has never manually ssh'd to nix-cache before. Update
# this if nix-cache's host key is ever rotated or the host is rebuilt
# from scratch.
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPeWgMsdaiz4axT/deFc1+0B5bN+GX/NOeW9bbQ0c/IT lxc-nix-cache";
# Public keys authorized to SSH in as remoteBuilderUser on the nix-cache
# host (modules/nix-cache/server.nix) — one per client host that's allowed
# to use it as a distributed builder.
remoteBuilderAuthorizedKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDEA1S2ikpObREgbP5uVBWMxIOGbY8B+Wx7VTZK1m6t root@server"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPAYIT9ormlmxZ0SziyDQaUntnKI8HK9/s3Qac1ZKjP2 root@docker"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKKKzoEPl/ZW9KBRHBcp6/ThOngGpwMv5EhkTlgC4aDf root@nixos"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIGtOWOCS+ImHc7NehguoyD7PbonGosKMZqc9+QR3v/h root@nixos"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxXTQxFnArK5HXG7czeoybZebCGfxpUdusJkPn+BCSp root@server"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJEj26SL/emsVjW2YhRucJVp2kTz8WgcEQgjBEBLRikk root@claude"
];
# Admin SSH public key, authorized on the primary user of every host and
# the installer image's nixos/root users.
adminSshKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream";
# System
timeZone = "Australia/Brisbane";
# Main interactive user on every host. Every module that grants this user
# a group, a home directory, or tmpfiles ownership should reference
# vars.primaryUser rather than the literal "nixos", so renaming it is a
# one-line change.
primaryUser = "nixos";
# Storage
storageRoot = "/tank"; # ZFS pool root on `server`
# NFS datasets exported from `storageRoot` on `nfsServerHost` and mounted
# by client hosts. `subpath` is relative to `storageRoot` — combined with
# it to build both the export line in modules/build-types/server.nix and
# the "<nfsServerHost>:<storageRoot>/<subpath>" device string each client
# mount uses in modules/docker/mount-data.nix. `mountpoint` is the
# absolute local path clients mount it at, referenced by that same file's
# fileSystems attribute name plus every other place that needs to know
# where the share lives locally (modules/build-types/docker.nix's
# tmpfiles rules, modules/traefik/rotate-logs.nix's log path). Renaming a
# dataset or moving where it's mounted only needs changing it here — the
# export and every client reference follow automatically.
nfsShares = {
dockerConfig = {
subpath = "docker/config";
mountpoint = "/mnt/docker/config";
};
dockerDatabases = {
subpath = "docker/databases";
mountpoint = "/mnt/docker/databases";
};
dockerVolumes = {
subpath = "docker/volumes";
mountpoint = "/mnt/docker/volumes";
};
nextcloudData = {
subpath = "docker/nextcloud-data";
mountpoint = "/mnt/nextcloud-data";
};
raspiVolumes = {
subpath = "raspi/volumes";
mountpoint = "/mnt/raspi-backup";
};
};
# The Raspberry Pi's own NFS export — not under storageRoot/nfsServerHost,
# served directly by the Pi itself over Tailscale (see raspberryPiHost
# above) and mounted at raspiMountpoint by modules/raspi/mount-data.nix.
raspiNfsPath = "/home/raspi/raspi";
raspiMountpoint = "/mnt/raspi";
# Every literal port referenced from modules/ or hosts/, grouped by the
# service/host that opens or connects to it — kept as separate entries
# even where two happen to share a number today (e.g. nixCacheHttp and
# pxeBootHttp are both 80) so changing one service's port can never
# silently change an unrelated one.
ports = {
# nix-cache's nginx reverse proxy in front of nix-serve
# (modules/nix-cache/server.nix).
nixCacheHttp = 80;
# pxe-boot's nginx asset server, also used to build pxeBaseUrl
# (modules/build-types/pxe-boot.nix).
pxeBootHttp = 80;
# pxe-boot's atftpd TFTP server — UDP, not TCP
# (modules/build-types/pxe-boot.nix).
pxeBootTftp = 69;
# `server`'s NFS exports need both the portmapper (rpcbind) and the
# NFS data port itself opened (modules/build-types/server.nix).
nfsRpcbind = 111;
nfsd = 2049;
# Opened on the docker host's firewall for the Traefik-fronted
# container stack (docker-compose config lives in the separate
# /home/debian/docker repo, not here): 80/443 are Traefik's own
# HTTP/HTTPS listeners; 8080 is an additional exposed service whose
# exact backend isn't declared in this repo (modules/build-types/docker.nix).
dockerHttp = 80;
dockerHttps = 443;
dockerExtra = 8080;
# Beszel monitoring hub, reachable at
# http://<dockerHost>.<homeDomain>:<beszelHub> from every agent
# (modules/beszel/enable-agent.nix, hosts/nixos/home.nix).
beszelHub = 8090;
# Proxmox VE and Proxmox Backup Server web UIs, opened as desktop
# shortcuts on the gui build type (hosts/nixos/home.nix).
pveWeb = 8006;
pbsWeb = 8007;
# Tor relay's ORPort — the port other Tor relays connect to for onion
# routing traffic (modules/tor/enable-relay.nix). Tor's own conventional
# default; opened via services.tor.openFirewall rather than
# networking.firewall.allowedTCPPorts directly, but kept here anyway so
# it's not a bare literal duplicated between the relay's settings and
# anything else that ever needs to reference it.
torRelayOrPort = 9001;
};
# .raw disk image size for every proxmox-* host's standalone Disko image
# build (modules/disko/proxmox.nix, config.system.build.diskoImagesScript
# — see docs/proxmox-images.md). Root fills whatever's left after the ESP
# and swap partitions within this total.
proxmoxImageSize = "20G";
# nix-cache's Nix store garbage collection retention
# (modules/nix-cache/server.nix).
nixCacheGcMaxAge = "30d";
# Traefik access log rotation, watched on the docker host at
# nfsShares.dockerVolumes.mountpoint (modules/traefik/rotate-logs.nix).
traefikLogRotate = {
maxSize = "100M"; # rotate once a log file exceeds this size
keep = 20; # number of rotated logs to retain before deleting the oldest
};
}