Archived
Compare commits
367
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33730e6ccf | ||
|
|
65f89806cb | ||
|
|
c3007097a6 | ||
|
|
9724babcea | ||
|
|
7055bcdb97 | ||
|
|
d973da487c | ||
|
|
274d54a774 | ||
|
|
ad274d99fb | ||
|
|
bd8d93d890 | ||
|
|
53b9a64826 | ||
|
|
0ba837817e | ||
|
|
80f86b086b | ||
|
|
a351cbcf80 | ||
|
|
2aa625d566 | ||
|
|
288835db29 | ||
|
|
559c538a3d | ||
|
|
feee2f1679 | ||
|
|
120240f14a | ||
|
|
b0ccbb1162 | ||
|
|
95d4db5609 | ||
|
|
6f8c6c8ef1 | ||
|
|
627aad8c29 | ||
|
|
745f4d6fb4 | ||
|
|
c5f8bb4d1d | ||
|
|
e337063a95 | ||
|
|
d8d14db505 | ||
|
|
be05c63a67 | ||
|
|
ff695c1917 | ||
|
|
fa2a9a595e | ||
|
|
a90c4909d5 | ||
|
|
649be34dcf | ||
|
|
3c3c5ae821 | ||
|
|
eadb1e35ce | ||
|
|
71d052e737 | ||
|
|
e76486efbe | ||
|
|
302c3b671f | ||
|
|
0900ff25ae | ||
|
|
79d2ed0314 | ||
|
|
8932e52d72 | ||
|
|
4822f0b290 | ||
|
|
f8de4c8abc | ||
|
|
ca18a9d29d | ||
|
|
7d113e302f | ||
|
|
bc68d8cf17 | ||
|
|
4728982c05 | ||
|
|
8734b23746 | ||
|
|
5f52cb0cd7 | ||
|
|
f4139eec13 | ||
|
|
afc57e3755 | ||
|
|
d7cad12720 | ||
|
|
bab83d1aad | ||
|
|
da0cd225be | ||
|
|
de4b4302c9 | ||
|
|
45fe770569 | ||
|
|
3797c8885e | ||
|
|
2fb6b1de00 | ||
|
|
a2f817b098 | ||
|
|
d9015e87d3 | ||
|
|
5dc41e2866 | ||
|
|
0aae9c3bcf | ||
|
|
2a426615cb | ||
|
|
7be80e7ab6 | ||
|
|
d8d2718289 | ||
|
|
e577fda538 | ||
|
|
ffd2705ee3 | ||
|
|
54e11853b5 | ||
|
|
5fcaa6ae87 | ||
|
|
a5ef1aa519 | ||
|
|
a919252a47 | ||
|
|
3fda26e46c | ||
|
|
d48e4d4f10 | ||
|
|
d5c13bc2d2 | ||
|
|
fe83640776 | ||
|
|
03066d716d | ||
|
|
bcf57246e0 | ||
|
|
9fb6d9da13 | ||
|
|
24dd5adfed | ||
|
|
489ed11406 | ||
|
|
f77a26402f | ||
|
|
9eb219360e | ||
|
|
a3fe69a89c | ||
|
|
aa7a98f884 | ||
|
|
af48b1aef6 | ||
|
|
33522a4613 | ||
|
|
217564ffc4 | ||
|
|
8e571c7592 | ||
|
|
7ea203e22d | ||
|
|
567e2536b1 | ||
|
|
f3c6f13e89 | ||
|
|
b9b3d06810 | ||
|
|
efa0ee26fe | ||
|
|
0c68f54e1f | ||
|
|
c9b449d60e | ||
|
|
a495de3f83 | ||
|
|
6cc79e0c36 | ||
|
|
4078230052 | ||
|
|
06dc25c983 | ||
|
|
aa2af2d169 | ||
|
|
1788faf093 | ||
|
|
33584d9a02 | ||
|
|
b25d9db82b | ||
|
|
94df02515d | ||
|
|
8b49deb1d7 | ||
|
|
fac343fa76 | ||
|
|
9f59ed4426 | ||
|
|
1f2dc03486 | ||
|
|
da75a23700 | ||
|
|
3d42dee0bf | ||
|
|
9fef163850 | ||
|
|
c64e4ad7d5 | ||
|
|
aeaa1f6479 | ||
|
|
bca8c2f9df | ||
|
|
153b2f0cfa | ||
|
|
c1ba633bff | ||
|
|
62edbd203d | ||
|
|
fa3d91303c | ||
|
|
59a01b35bf | ||
|
|
cf78588f00 | ||
|
|
7156c343bb | ||
|
|
32b65b0894 | ||
|
|
5046cae7b1 | ||
|
|
0286bb4dfa | ||
|
|
cf9bb14d4b | ||
|
|
6d53a62f85 | ||
|
|
4bf1ec450a | ||
|
|
8bafae6eb2 | ||
|
|
32323aee06 | ||
|
|
bd117a8dd5 | ||
|
|
12f6703e0f | ||
|
|
56bf912c4d | ||
|
|
0649c48b96 | ||
|
|
d871f2a45d | ||
|
|
74bdbd8c9a | ||
|
|
258d471b21 | ||
|
|
a86300f094 | ||
|
|
f8886b2215 | ||
|
|
e538222c24 | ||
|
|
004529aa88 | ||
|
|
35352cc9b2 | ||
|
|
70d459de15 | ||
|
|
60886c6a64 | ||
|
|
564f8548ec | ||
|
|
36c3c48474 | ||
|
|
c29d8f9df7 | ||
|
|
b82c868b7a | ||
|
|
6f6e9a5a11 | ||
|
|
1b588d0ea0 | ||
|
|
3212d80b63 | ||
|
|
e28d953b8d | ||
|
|
566135bbbb | ||
|
|
8af805241e | ||
|
|
d3c26d7e9e | ||
|
|
63f5acb18f | ||
|
|
074ed2cbe1 | ||
|
|
1cb6bdae2f | ||
|
|
bf74783623 | ||
|
|
c31606922e | ||
|
|
a2f2af77b6 | ||
|
|
7aa4c56660 | ||
|
|
100923e1a9 | ||
|
|
0e105923da | ||
|
|
2d1cd0674c | ||
|
|
abd0b83cb2 | ||
|
|
6ad9b5ffa4 | ||
|
|
88a9d4dd78 | ||
|
|
472c18922c | ||
|
|
33cd951205 | ||
|
|
903e5243a7 | ||
|
|
3c5081506b | ||
|
|
edb898d27b | ||
|
|
c32341b2d5 | ||
|
|
745bbe0732 | ||
|
|
48c5e5c094 | ||
|
|
287f42cd81 | ||
|
|
8434e90aec | ||
|
|
5141123b34 | ||
|
|
84488de426 | ||
|
|
ed2d86bead | ||
|
|
1b6ffd9117 | ||
|
|
bd9aef4624 | ||
|
|
16d345262d | ||
|
|
ee30e40ce4 | ||
|
|
79d24c7497 | ||
|
|
dcf1c8e780 | ||
|
|
27470bd854 | ||
|
|
72b58093b1 | ||
|
|
688904321a | ||
|
|
2fb0928ff5 | ||
|
|
d424a70110 | ||
|
|
60faf681b3 | ||
|
|
4fb1a6a204 | ||
|
|
a61f88d4a3 | ||
|
|
923ac6c178 | ||
|
|
cc986d1ab7 | ||
|
|
1fd0adee54 | ||
|
|
b0d480adb8 | ||
|
|
e78dd516a6 | ||
|
|
4e30618954 | ||
|
|
3259391451 | ||
|
|
31b57930c6 | ||
|
|
0e29abd0ac | ||
|
|
038103391d | ||
|
|
7f719eb6a9 | ||
|
|
47fdce6193 | ||
|
|
e68b4f8d14 | ||
|
|
c47052fc21 | ||
|
|
59a2e65bf5 | ||
|
|
4a928707f6 | ||
|
|
5be2bdb170 | ||
|
|
4abe01c0cb | ||
|
|
d838784dd5 | ||
|
|
e95a4b5f4d | ||
|
|
247d8fc96b | ||
|
|
5f63a48731 | ||
|
|
8d26f62b57 | ||
|
|
089190ef23 | ||
|
|
d36380b79a | ||
|
|
8dd20f6ced | ||
|
|
926df3d334 | ||
|
|
26eec71b8c | ||
|
|
554f484593 | ||
|
|
7de7c76597 | ||
|
|
a36805d8dd | ||
|
|
0e244b2bc7 | ||
|
|
61f8ffb35e | ||
|
|
b5c8e43688 | ||
|
|
bd674bc5d9 | ||
|
|
97fceee985 | ||
|
|
0fe04bd956 | ||
|
|
6862fb6455 | ||
|
|
4632c14191 | ||
|
|
54b795aba8 | ||
|
|
a0cc52c3cc | ||
|
|
0cdeba3b7a | ||
|
|
4bf0e14663 | ||
|
|
4444f4e70d | ||
|
|
4b4b7834d6 | ||
|
|
4ddef559a4 | ||
|
|
33b596d254 | ||
|
|
105d20b94c | ||
|
|
7d88bb7332 | ||
|
|
0711d0aae6 | ||
|
|
d08f55af3a | ||
|
|
03cd8e5755 | ||
|
|
3b15296cf0 | ||
|
|
aba8f7605d | ||
|
|
7880f54068 | ||
|
|
d4e67d5785 | ||
|
|
a73061909e | ||
|
|
d07236986c | ||
|
|
667348bc98 | ||
|
|
c5509c420b | ||
|
|
f1dcabd579 | ||
|
|
ca5914062e | ||
|
|
025937ec6d | ||
|
|
4594242d1d | ||
|
|
832e2bb39d | ||
|
|
b7f5ae83cb | ||
|
|
2dc246b4ec | ||
|
|
9deddd14e6 | ||
|
|
10ca67ac23 | ||
|
|
e4f726839f | ||
|
|
fb9f6185ec | ||
|
|
a4e303fa34 | ||
|
|
c3259ad3a2 | ||
|
|
fd2d9d8aeb | ||
|
|
bbaf012329 | ||
|
|
e18b3bb76e | ||
|
|
df6515ad6a | ||
|
|
c731705075 | ||
|
|
aa376a2448 | ||
|
|
141491233c | ||
|
|
3d6b55df79 | ||
|
|
84cd3a1a83 | ||
|
|
60f17723ba | ||
|
|
da267255b0 | ||
|
|
17b45fd23e | ||
|
|
9cb5f9627c | ||
|
|
25b4c6cdd2 | ||
|
|
9575986b93 | ||
|
|
15f90bc2f2 | ||
|
|
119be5aca0 | ||
|
|
84a9a12f4d | ||
|
|
f979f61be1 | ||
|
|
b455028547 | ||
|
|
c424cb1cb0 | ||
|
|
9d0b736a51 | ||
|
|
7a590a2d99 | ||
|
|
513c286a4a | ||
|
|
5e778c9045 | ||
|
|
274544a212 | ||
|
|
859f30a56c | ||
|
|
0b91b68333 | ||
|
|
27122baec0 | ||
|
|
0a8af8d8e4 | ||
|
|
2ca2f74bbc | ||
|
|
fa9c3f5572 | ||
|
|
be8fefd9f8 | ||
|
|
79fb3799a9 | ||
|
|
bb1a4d8a3e | ||
|
|
f5dcb13b86 | ||
|
|
41a6800c58 | ||
|
|
f9a6ada47b | ||
|
|
941aff5048 | ||
|
|
ccf11fa673 | ||
|
|
d42336b288 | ||
|
|
66453b3396 | ||
|
|
a4fc3d9b3b | ||
|
|
66d8680b26 | ||
|
|
ac21c48b52 | ||
|
|
d4d1fc795b | ||
|
|
a4566a6f80 | ||
|
|
551cf90cdd | ||
|
|
8e896e9067 | ||
|
|
b3bc217586 | ||
|
|
59f75cd31c | ||
|
|
cdfe2943ed | ||
|
|
c3ed7ccf46 | ||
|
|
61bd88fd15 | ||
|
|
b7d572c40e | ||
|
|
431f769af8 | ||
|
|
1f8dfe54da | ||
|
|
66ab3a3977 | ||
|
|
264dae9a92 | ||
|
|
da55436abb | ||
|
|
fbd36b28ea | ||
|
|
a93fe3cc3c | ||
|
|
ad82ffcfc1 | ||
|
|
4f4699d334 | ||
|
|
8667552141 | ||
|
|
059dd4c333 | ||
|
|
3c3d6ccfa7 | ||
|
|
075d1f77b7 | ||
|
|
1241a8e9ae | ||
|
|
ddfcaa0c68 | ||
|
|
49f0a156c3 | ||
|
|
f6a1bdb0cf | ||
|
|
d5d83c78e1 | ||
|
|
f385a420db | ||
|
|
b3eed0d061 | ||
|
|
7cb3429139 | ||
|
|
b3c2033fb2 | ||
|
|
ad4f69c05e | ||
|
|
bc29cfc5ef | ||
|
|
d9f520c959 | ||
|
|
7d155d6413 | ||
|
|
1a742a35c6 | ||
|
|
5ef1bef1c0 | ||
|
|
7f5d03c06c | ||
|
|
0e650af351 | ||
|
|
b4cdf89e71 | ||
|
|
9a65f15b58 | ||
|
|
1da2471a4a | ||
|
|
e171715448 | ||
|
|
3c8c57d2f9 | ||
|
|
175ea8c7c9 | ||
|
|
54dbee8543 | ||
|
|
29a5c6c4e8 | ||
|
|
9b9ec47150 | ||
|
|
8e08a5adef | ||
|
|
3eff3b000b | ||
|
|
ac40a14d79 | ||
|
|
f81abfb31d | ||
|
|
28db091ea0 | ||
|
|
8e9ba91536 | ||
|
|
fee0f1ef14 | ||
|
|
b80550d91b |
@@ -0,0 +1,21 @@
|
||||
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
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v19
|
||||
|
||||
- name: Run maintenance checks (secrets, fmt, lint, eval)
|
||||
run: bash scripts/codex-maintenance.sh
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v19
|
||||
|
||||
- name: Run maintenance checks (secrets, fmt, lint, eval)
|
||||
run: bash scripts/codex-maintenance.sh
|
||||
@@ -0,0 +1,37 @@
|
||||
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.
|
||||
+17
@@ -3,6 +3,23 @@
|
||||
result
|
||||
result-*
|
||||
|
||||
# Disko's proxmox-* image-builder writes the finished .raw disk image
|
||||
# directly into the current directory, not into a result-* symlink (see
|
||||
# docs/proxmox-images.md, scripts/create-proxmox-resource.sh) — several GB
|
||||
# each, never meant to be committed.
|
||||
*.raw
|
||||
|
||||
# Ignore automatically generated direnv output
|
||||
.direnv
|
||||
|
||||
# Python bytecode cache (scripts/lib/*.py)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Locally-generated SSH host keys staged for transfer to a new machine
|
||||
# during install (see scripts/prepare-host-key.sh) — never commit these.
|
||||
host-keys/
|
||||
|
||||
# Temporary Milestone 1 audit checklist (remove-sensetive-info-refactor.md)
|
||||
# - working notes only, never committed, deleted once every row is rotated.
|
||||
secrets-inventory.md
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
keys:
|
||||
- &admin age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- &lxc-minimal age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||
- &lxc-nix-cache age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||
|
||||
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
|
||||
|
||||
- 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
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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`), 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
|
||||
bash scripts/codex-maintenance.sh dry-run
|
||||
```
|
||||
|
||||
Host evaluation is safe when limited to drvPath checks:
|
||||
|
||||
```bash
|
||||
nix eval .#nixosConfigurations.<host>.config.system.build.toplevel.drvPath --raw
|
||||
```
|
||||
@@ -0,0 +1,225 @@
|
||||
# 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
|
||||
|
||||
# Full validation: secret grep, nixpkgs-fmt --check, statix lint, eval all hosts
|
||||
bash scripts/codex-maintenance.sh
|
||||
|
||||
# Same, plus a dry-run build (no result symlink) of every host's toplevel
|
||||
bash scripts/codex-maintenance.sh 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.
|
||||
|
||||
**In an interactive agent session**, prefer targeted checks over full-repo
|
||||
sweeps: after editing one or two hosts/modules, evaluate just the
|
||||
`nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball`
|
||||
/`diskoImagesScript`/package output affected) rather than looping over every
|
||||
host — `codex-maintenance.sh` evaluates 18 hosts plus every package/tarball/
|
||||
image variant now and is slow to run after each small change. Reserve a full
|
||||
`codex-maintenance.sh` run for changes that plausibly affect every host
|
||||
(`modules/common/*`, `flake.nix`, `variables.nix`) or as a final check before
|
||||
committing. This is a session-workflow preference only — it does not apply to
|
||||
CI, which should keep running the full script on every push/PR regardless of
|
||||
diff size; that's the point of it.
|
||||
|
||||
## Scripts
|
||||
|
||||
Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` also has:
|
||||
|
||||
- `scripts/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/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/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 has a real
|
||||
deployment elsewhere (`variables.nix`'s `deployedTargets`) unless
|
||||
`--allow-duplicate-host` is passed. `--dry-run` throughout both modes.
|
||||
- `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` and `create-proxmox-resource.sh` genuinely mutate real
|
||||
state when run for real (not `--dry-run`): real `secrets/*.yaml`
|
||||
recipients, real Proxmox VMs/containers. They require the operator's own
|
||||
SSH/sops access, which an agent session doesn't have — but don't suggest
|
||||
running either non-dry-run without the operator's explicit go-ahead even
|
||||
if it becomes technically reachable.
|
||||
|
||||
## 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`. 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). 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.
|
||||
- `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/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,2 +1,148 @@
|
||||
# nixos
|
||||
# NixOS LAN Configurations
|
||||
|
||||
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`
|
||||
|
||||
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. The full
|
||||
list:
|
||||
|
||||
| Target | Purpose |
|
||||
| --- | --- |
|
||||
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS (real, deployed) |
|
||||
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox (real, deployed — previously the flat `nix-minimal` target) |
|
||||
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
|
||||
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder (`proxmox-nix-cache` is the real, deployed one — previously the flat `nix-cache` target) |
|
||||
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host (`proxmox-server` is the real, deployed one — previously the flat `server` target) |
|
||||
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack (`proxmox-docker` is the real, deployed one — previously the flat `docker` target) |
|
||||
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation (`proxmox-gui` is the real, deployed one — previously the flat `nixos` target) |
|
||||
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host (`proxmox-pxe-boot` is the real, deployed one — previously the flat `pxe-boot` target) |
|
||||
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node (no deployed target yet; `lxc-tailscale-exit-node` is the one planned for actual use) |
|
||||
|
||||
The "(real, deployed)" targets above are also tracked machine-readably in
|
||||
`variables.nix`'s `deployedTargets` — keep both in sync when a deployment
|
||||
changes. `scripts/create-proxmox-resource.sh` reads that list to refuse
|
||||
creating a same-identity duplicate of an already-deployed host by accident.
|
||||
|
||||
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 dry-run
|
||||
bash scripts/codex-maintenance.sh
|
||||
```
|
||||
|
||||
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/create-proxmox-resource.sh --type lxc|vm --host <name>` automates
|
||||
either of the last two end to end (build, host-key registration, upload,
|
||||
`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.
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
{ 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 = [
|
||||
"nix-ccache-1:<base32‑pubkey‑hash‑from‑cache-pub.pem>"
|
||||
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
programs.git = {
|
||||
enable = true;
|
||||
package = pkgs.git;
|
||||
config = {
|
||||
credential.helper = "store";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{ 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 switch \
|
||||
--no-write-lock-file \
|
||||
--refresh \
|
||||
--flake git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#$(hostname)
|
||||
'';
|
||||
in {
|
||||
home.username = "nixos"; # your actual username
|
||||
home.homeDirectory = "/home/nixos";
|
||||
home.stateVersion = "25.05"; # match your NixOS stateVersion
|
||||
|
||||
programs.home-manager.enable = true; # mandatory to activate HM
|
||||
|
||||
programs.bash = {
|
||||
enable = true;
|
||||
shellAliases = {
|
||||
"Switch-nix" = mySwitchCmd;
|
||||
"Test-nix" = mySwitchCmd;
|
||||
};
|
||||
initExtra = ''
|
||||
# ensure loading of aliases/session variables
|
||||
[[ -f ~/.profile ]] && . ~/.profile
|
||||
'';
|
||||
};
|
||||
|
||||
# Optional: packages
|
||||
home.packages = with pkgs; [
|
||||
git
|
||||
vim
|
||||
tmux
|
||||
];
|
||||
|
||||
# Optional: set environment vars
|
||||
home.sessionVariables = {
|
||||
EDITOR = "vim";
|
||||
};
|
||||
|
||||
# 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;
|
||||
# };
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
# 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`) 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/create-proxmox-resource.sh --type lxc --host <name>` automates all
|
||||
of this (build, host-key handling, upload, `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/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/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/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/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/sync-host-keys.sh --all`. See `scripts/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.
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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 evaluates every configured host before merge, listed
|
||||
dynamically via `nix eval --json .#nixosConfigurations --apply builtins.attrNames`
|
||||
rather than hand-enumerated, so it 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.
|
||||
@@ -0,0 +1,74 @@
|
||||
# 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
|
||||
|
||||
On each client, install the private key used to authenticate as `nixremote`:
|
||||
|
||||
```bash
|
||||
sudo install -d -m 0700 /root/.ssh
|
||||
sudo install -m 0600 ./nixremote /root/.ssh/nixremote
|
||||
sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
||||
```
|
||||
|
||||
On `nix-cache`, install the matching public key used by `nixremote` authorized keys.
|
||||
|
||||
The committed `nixremote` authorized keys are public SSH keys only. Keep the
|
||||
matching private keys on client hosts and out of the repository.
|
||||
|
||||
## 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/nixremote nixremote@nix-cache nix-store --version
|
||||
nix build nixpkgs#hello --builders 'ssh://nixremote@nix-cache x86_64-linux /root/.ssh/nixremote 4 2 big-parallel,kvm,nixos-test,benchmark' -L
|
||||
nix path-info -r nixpkgs#hello
|
||||
curl -I "http://nix-cache/$(basename "$(nix path-info nixpkgs#hello)").narinfo"
|
||||
```
|
||||
@@ -0,0 +1,114 @@
|
||||
# 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/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 upload — 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/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.
|
||||
@@ -0,0 +1,105 @@
|
||||
# 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.
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
# Spec: Refactor Flake Targets into Platform × Build-Type Matrix
|
||||
|
||||
## Context
|
||||
|
||||
The flake at `~/nixos` currently defines these output targets (flat, ad-hoc naming):
|
||||
|
||||
- `docker`
|
||||
- `linode-minimal`
|
||||
- `nix-cache`
|
||||
- `nix-minimal`
|
||||
- `nixos`
|
||||
- `server`
|
||||
- `pxe-boot`
|
||||
|
||||
Some already follow a `platform-buildtype` convention (`linode-minimal`), most don't.
|
||||
`~/nix-auto-installer` is a related repo and should be checked for any coupling to
|
||||
these target names (scripts, docs, CI, or install automation that reference them by
|
||||
name) before renaming anything.
|
||||
|
||||
## Goal
|
||||
|
||||
Restructure the flake so targets are generated from two orthogonal concepts:
|
||||
|
||||
**Build types** (what the system is for):
|
||||
- `minimal`
|
||||
- `nix-cache`
|
||||
- `server`
|
||||
- `docker`
|
||||
- `pxe-boot`
|
||||
- `gui`
|
||||
|
||||
**Platforms** (what it's deployed on):
|
||||
- `linode` (Linode VM)
|
||||
- `proxmox` (Proxmox VM)
|
||||
- `lxc` (Proxmox LXC container)
|
||||
|
||||
Final targets should be named consistently as `<platform>-<buildtype>`, e.g.:
|
||||
|
||||
```
|
||||
linode-minimal proxmox-minimal lxc-minimal
|
||||
linode-nix-cache proxmox-nix-cache lxc-nix-cache
|
||||
linode-server proxmox-server lxc-server
|
||||
linode-docker proxmox-docker lxc-docker
|
||||
linode-pxe-boot proxmox-pxe-boot lxc-pxe-boot
|
||||
linode-gui proxmox-gui lxc-gui
|
||||
```
|
||||
|
||||
That's the full matrix (18 targets) if every build type applies to every platform.
|
||||
See **Open Questions** below — some combinations may not make sense and should be
|
||||
confirmed with me before being built out, not silently included or dropped.
|
||||
|
||||
## Migration mapping (old → new)
|
||||
|
||||
| Old target | New target | Notes |
|
||||
|--------------------|------------------------------------------------------|-------|
|
||||
| `linode-minimal` | `linode-minimal` | Already correct, keep as-is |
|
||||
| `nix-minimal` | likely `proxmox-minimal` or a platform-less base module | Ambiguous — see Open Questions |
|
||||
| `nix-cache` | base module consumed by `linode-nix-cache`, `proxmox-nix-cache`, `lxc-nix-cache` | Currently platform-less; needs to become a build-type module, not a standalone target |
|
||||
| `server` | base module consumed by `linode-server`, `proxmox-server`, `lxc-server` | Same as above |
|
||||
| `docker` | base module consumed by `linode-docker`, `proxmox-docker`, `lxc-docker` | Confirm docker actually makes sense as an LXC/VM guest build vs. a standalone container image — see Open Questions |
|
||||
| `pxe-boot` | TBD — may stay a single target rather than a per-platform one | See Open Questions |
|
||||
| `nixos` | TBD — unclear what this maps to in the new scheme | See Open Questions |
|
||||
|
||||
## Open Questions (Claude Code: raise these with me before implementing, don't guess)
|
||||
|
||||
1. **`nixos` target** — what is this currently used for (bare metal install, dev
|
||||
shell, template)? It doesn't obviously map to any of the six build types.
|
||||
2. **`nix-minimal` vs `linode-minimal`** — are these two different things, or is
|
||||
`nix-minimal` a leftover/duplicate?
|
||||
3. **`pxe-boot` and `gui` across all three platforms** — does PXE boot make sense
|
||||
for an LXC container or a cloud VM (Linode), or is it inherently bare-metal/
|
||||
network-boot only and should remain a single non-platform target? Does `gui`
|
||||
make sense inside an LXC container?
|
||||
4. **`docker` as a build type** — is this "a NixOS host configured to run Docker"
|
||||
(which would sensibly have linode/proxmox/lxc variants), or "a Docker container
|
||||
image built by the flake" (which wouldn't take a platform prefix at all, since
|
||||
it doesn't run on Linode/Proxmox/LXC as a guest OS)? These are structurally
|
||||
different and change how it should be wired in.
|
||||
5. Confirm whether all 18 combinations should actually exist, or whether this is
|
||||
meant to produce only the combinations that are genuinely useful (e.g. maybe no
|
||||
one needs `lxc-pxe-boot`).
|
||||
|
||||
## Implementation approach
|
||||
|
||||
1. **Inventory first.** Read the current `flake.nix` and any `nixosConfigurations`/
|
||||
`modules` structure. Map every existing target to what module(s) it actually
|
||||
pulls in. Don't assume — confirm against the real file contents.
|
||||
2. **Separate build-type and platform into their own module directories**, e.g.:
|
||||
```
|
||||
modules/build-types/minimal.nix
|
||||
modules/build-types/nix-cache.nix
|
||||
modules/build-types/server.nix
|
||||
modules/build-types/docker.nix
|
||||
modules/build-types/pxe-boot.nix
|
||||
modules/build-types/gui.nix
|
||||
|
||||
modules/platforms/linode.nix
|
||||
modules/platforms/proxmox.nix
|
||||
modules/platforms/lxc.nix
|
||||
```
|
||||
Build-type modules should contain only what makes a system "minimal" vs
|
||||
"server" vs "gui", etc. Platform modules should contain only what's specific
|
||||
to running as a Linode VM vs Proxmox VM vs LXC container (virtualisation
|
||||
guest tools, boot method, filesystem/image format, LXC-specific constraints
|
||||
like no kernel modules, etc).
|
||||
3. **Generate the target matrix programmatically** in `flake.nix` rather than
|
||||
hand-writing 18 near-identical `nixosConfigurations` entries — e.g. a small
|
||||
function that takes a platform name and build-type name, composes the two
|
||||
modules plus any shared base module, and produces the named output. This
|
||||
keeps future build types/platforms a one-line addition rather than a copy-paste
|
||||
job.
|
||||
4. **Only build combinations we've confirmed make sense** (see Open Questions) —
|
||||
don't emit all 18 by default if some are structurally invalid.
|
||||
5. **Preserve existing working configs during the transition.** Don't delete the
|
||||
old target names until their replacements build successfully — rename/alias
|
||||
at the end, not the start, so there's no window where the flake is broken.
|
||||
|
||||
## Verification
|
||||
|
||||
For every new target produced:
|
||||
```bash
|
||||
nix flake check
|
||||
nix build .#nixosConfigurations.<target>.config.system.build.toplevel
|
||||
```
|
||||
Confirm each builds without evaluation errors before considering it done. If a
|
||||
target fails to build, report which one and why rather than silently skipping it.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- Refactored `flake.nix` using the composed module + generated-matrix approach.
|
||||
- New `modules/build-types/*.nix` and `modules/platforms/*.nix` files.
|
||||
- Old flat target names removed only after their replacements are verified.
|
||||
- A short `README.md` (or section in existing docs) listing the final target
|
||||
names and what each one is for.
|
||||
- A summary at the end of what changed, what was removed, and any of the Open
|
||||
Questions above that got resolved differently than expected.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Don't touch `~/nix-auto-installer` contents beyond checking it for references
|
||||
to the old target names — if changes there are needed, flag them, don't make
|
||||
them without confirming.
|
||||
- Don't add new build types or platforms beyond the ones listed here.
|
||||
Generated
+234
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"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": {
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1650374568,
|
||||
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils-plus": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1715533576,
|
||||
"narHash": "sha256-fT4ppWeCJ0uR300EH3i7kmgRZnAVxrH+XtK09jQWihk=",
|
||||
"owner": "gytis-ivaskevicius",
|
||||
"repo": "flake-utils-plus",
|
||||
"rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "gytis-ivaskevicius",
|
||||
"repo": "flake-utils-plus",
|
||||
"rev": "3542fe9126dc492e53ddd252bb0260fe035f2c0f",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1783740085,
|
||||
"narHash": "sha256-qajyHfZY29G2oEQk+uHxmsJcRoBUBXP9maTpFlwP/dI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "3cd22efe6471dc7365c822bd9ad73a21e55f38fb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "release-26.05",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixos-conf-editor": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"snowfall-lib": "snowfall-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771149335,
|
||||
"narHash": "sha256-YPUIwyumbQOE2DUY8NIsHIUTGUQnDVhnTVUZMZDRwi4=",
|
||||
"owner": "snowfallorg",
|
||||
"repo": "nixos-conf-editor",
|
||||
"rev": "9f8b4519a2e0e8919b69b7572bc26dab54274a6f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "snowfallorg",
|
||||
"repo": "nixos-conf-editor",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771008912,
|
||||
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1784011430,
|
||||
"narHash": "sha256-lDebytrYdd47IBLwvNOD+6AGeoqZ78CIKlp70hzW280=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8eeec934ae0dbeca3d7868c059568a65c08b2fc3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-26.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"disko": "disko",
|
||||
"home-manager": "home-manager",
|
||||
"nixos-conf-editor": "nixos-conf-editor",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"sops-nix": "sops-nix"
|
||||
}
|
||||
},
|
||||
"snowfall-lib": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_2",
|
||||
"flake-utils-plus": "flake-utils-plus",
|
||||
"nixpkgs": [
|
||||
"nixos-conf-editor",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765361626,
|
||||
"narHash": "sha256-kX0Dp/kYSRbQ+yd9e3lmmUWdNbipufvKfL2IzbrSpnY=",
|
||||
"owner": "snowfallorg",
|
||||
"repo": "lib",
|
||||
"rev": "c566ad8b7352c30ec3763435de7c8f1c46ebb357",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "snowfallorg",
|
||||
"repo": "lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"sops-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1783174389,
|
||||
"narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -2,69 +2,155 @@
|
||||
description = "LAN NixOS configs";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
|
||||
nixos-conf-editor.url = "github:snowfallorg/nixos-conf-editor";
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager";
|
||||
url = "github:nix-community/home-manager/release-26.05";
|
||||
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";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, nixos-conf-editor, home-manager, ... } @ inputs:
|
||||
let system = "x86_64-linux"; in {
|
||||
nixosConfigurations = {
|
||||
# automatically use each host folder by name
|
||||
nixos = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
./hosts/nixos/configuration.nix
|
||||
./common/hardware-configuration.nix
|
||||
home-manager.nixosModules.home-manager {
|
||||
home-manager.useGlobalPkgs = true;
|
||||
home-manager.useUserPackages = true;
|
||||
home-manager.users.nixos = import ./hosts/nixos/home.nix;
|
||||
|
||||
}
|
||||
];
|
||||
specialArgs = { inherit inputs; };
|
||||
};
|
||||
docker = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
./hosts/docker/configuration.nix
|
||||
./common/hardware-configuration.nix
|
||||
home-manager.nixosModules.home-manager {
|
||||
home-manager.useGlobalPkgs = true;
|
||||
home-manager.useUserPackages = true;
|
||||
home-manager.users.nixos = import ./common/home.nix;
|
||||
}
|
||||
];
|
||||
};
|
||||
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;
|
||||
}
|
||||
];
|
||||
};
|
||||
outputs = { self, nixpkgs, nixos-conf-editor, home-manager, sops-nix, ... } @ inputs:
|
||||
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
inherit (nixpkgs) lib;
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
vars = import ./variables.nix;
|
||||
|
||||
# Generates a nixosConfiguration from a platform (what it runs on) and
|
||||
# a build type (what it's for), plus the per-identity host.nix that
|
||||
# carries the bits that must stay fixed regardless of platform
|
||||
# (hostName, hostId, per-machine secrets). Every build type except
|
||||
# nix-cache itself consumes the nix-cache substituter and remote
|
||||
# builder.
|
||||
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix }:
|
||||
let
|
||||
flakeTarget = "${platform}-${buildType}";
|
||||
in
|
||||
nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
inputs.disko.nixosModules.disko
|
||||
sops-nix.nixosModules.sops
|
||||
./modules/common/configuration.nix
|
||||
./modules/platforms/${platform}.nix
|
||||
./modules/build-types/${buildType}.nix
|
||||
hostPath
|
||||
{ environment.etc."flake-target".text = flakeTarget; }
|
||||
home-manager.nixosModules.home-manager
|
||||
{
|
||||
home-manager = {
|
||||
useGlobalPkgs = true;
|
||||
useUserPackages = true;
|
||||
extraSpecialArgs = { inherit vars; };
|
||||
users.nixos = import homeFile;
|
||||
};
|
||||
}
|
||||
] ++ lib.optionals (buildType != "nix-cache") [
|
||||
./modules/nix-cache/client.nix
|
||||
./modules/nix-cache/remote-builder-client.nix
|
||||
];
|
||||
# flakeTarget is passed via specialArgs (not read back from
|
||||
# config.environment.etc."flake-target" above) specifically so
|
||||
# modules/platforms/lxc.nix can use it to select its own host key
|
||||
# file without a same-option circular dependency (a module
|
||||
# contributing to environment.etc can't read the merged
|
||||
# environment.etc it's itself contributing to).
|
||||
specialArgs = { inherit inputs vars netbootSystem flakeTarget; };
|
||||
};
|
||||
|
||||
# Generated platform x build-type matrix. pxe-boot has no linode
|
||||
# variant (PXE/DHCP/TFTP need LAN L2 adjacency, which a Linode VPS
|
||||
# doesn't have).
|
||||
generatedTargets = {
|
||||
linode-minimal = mkTarget { platform = "linode"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
|
||||
proxmox-minimal = mkTarget { platform = "proxmox"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
|
||||
lxc-minimal = mkTarget { platform = "lxc"; buildType = "minimal"; hostPath = ./hosts/nix-minimal/host.nix; };
|
||||
|
||||
linode-nix-cache = mkTarget { platform = "linode"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
|
||||
proxmox-nix-cache = mkTarget { platform = "proxmox"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
|
||||
lxc-nix-cache = mkTarget { platform = "lxc"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
|
||||
|
||||
linode-server = mkTarget { platform = "linode"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
|
||||
proxmox-server = mkTarget { platform = "proxmox"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
|
||||
lxc-server = mkTarget { platform = "lxc"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
|
||||
|
||||
linode-docker = mkTarget { platform = "linode"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
|
||||
proxmox-docker = mkTarget { platform = "proxmox"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
|
||||
lxc-docker = mkTarget { platform = "lxc"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
|
||||
|
||||
linode-gui = mkTarget { platform = "linode"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
|
||||
proxmox-gui = mkTarget { platform = "proxmox"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
|
||||
lxc-gui = mkTarget { platform = "lxc"; buildType = "gui"; hostPath = ./hosts/nixos/host.nix; homeFile = ./hosts/nixos/home.nix; };
|
||||
|
||||
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; };
|
||||
};
|
||||
|
||||
# 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;
|
||||
modules = [
|
||||
./modules/installer/iso.nix
|
||||
({ modulesPath, ... }: {
|
||||
imports = [
|
||||
(modulesPath + "/installer/netboot/netboot-minimal.nix")
|
||||
];
|
||||
})
|
||||
];
|
||||
specialArgs = { inherit vars; };
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
|
||||
nixosConfigurations = generatedTargets // installerTargets;
|
||||
|
||||
# Buildable auto-installer artifacts (`nix build .#<name>`). No `lxc`
|
||||
# variant (installer-boots-as-an-LXC-container) or `all` bundle
|
||||
# anymore — lxc-* and proxmox-* hosts deploy via their own tarball/
|
||||
# disk-image outputs instead (see docs/auto-installer.md and
|
||||
# docs/proxmox-images.md), which left the installer's own LXC form
|
||||
# with no real use case: it's excluded from the install menu (same
|
||||
# bind-mount problem as any LXC nixos-install target) and nothing
|
||||
# else needed booting the installer itself as a container.
|
||||
packages.${system} = {
|
||||
iso = installerTargets.installer.config.system.build.isoImage;
|
||||
|
||||
pxe = pkgs.linkFarm "pxe" [
|
||||
{ name = "netboot.ipxe"; path = netbootSystem.config.system.build.netbootIpxeScript; }
|
||||
{ name = "initrd"; path = netbootSystem.config.system.build.netbootRamdisk; }
|
||||
{ name = "kernel"; path = netbootSystem.config.system.build.kernel; }
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# Edit this configuration file to define what should be installed on
|
||||
# your system. Help is available in the configuration.nix(5) man page, on
|
||||
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
|
||||
|
||||
{ config, lib, pkgs, inputs, ... }:
|
||||
|
||||
let
|
||||
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||
docker
|
||||
pytz
|
||||
]);
|
||||
in
|
||||
|
||||
{
|
||||
imports =
|
||||
[ # Include the results of the hardware scan.
|
||||
../../common/configuration.nix
|
||||
];
|
||||
|
||||
networking.hostName = "docker"; # Define your hostname.
|
||||
virtualisation.docker.enable = true;
|
||||
|
||||
# Enable docker-compose
|
||||
environment.systemPackages = with pkgs; [
|
||||
docker-compose
|
||||
];
|
||||
|
||||
# Mount docker persistent data
|
||||
fileSystems."/mnt/docker-persistent-data" = {
|
||||
device = "/dev/disk/by-label/docker-data";
|
||||
fsType = "ext4";
|
||||
options = [ "defaults" "nofail" "noatime" ];
|
||||
};
|
||||
|
||||
# Create nextcloud cron scheduled task
|
||||
systemd.services.nextcloud = {
|
||||
description = "Nextcloud scheduled task";
|
||||
script = ''docker-compose -f ~/docker/nextcloud/docker-compose.yml exec -u 33 webapp php ./cron.php'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "nixos";
|
||||
};
|
||||
path = with pkgs; [ docker docker-compose ];
|
||||
};
|
||||
|
||||
systemd.timers.nextcloud = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "*:0/5";
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
|
||||
# create update task
|
||||
|
||||
systemd.services.update-containers = {
|
||||
description = "Update Docker Compose Containers";
|
||||
after = [ "docker.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "nixos";
|
||||
WorkingDirectory = "/home/nixos/docker";
|
||||
ExecStart = "${pythonEnv}/bin/python3 /home/nixos/docker/update-containers.py";
|
||||
StandardOutput = "journal";
|
||||
StandardError = "journal";
|
||||
};
|
||||
|
||||
path = [ pkgs.docker pkgs.docker-compose ]; # Ensures docker CLI is available in $PATH
|
||||
};
|
||||
|
||||
|
||||
systemd.timers.update-containers = {
|
||||
description = "Weekly + Reboot container update";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "5min"; # Run 5 minutes after boot
|
||||
OnUnitActiveSec = "1w"; # Run every week after last run
|
||||
Persistent = true; # Catch up if system was off
|
||||
};
|
||||
};
|
||||
|
||||
#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?
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
_:
|
||||
|
||||
{
|
||||
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";
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
# Edit this configuration file to define what should be installed on
|
||||
# your system. Help is available in the configuration.nix(5) man page, on
|
||||
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
|
||||
|
||||
{ config, lib, pkgs, inputs, ... }:
|
||||
|
||||
{
|
||||
imports =
|
||||
[ # Include the results of the hardware scan.
|
||||
../../common/configuration.nix
|
||||
];
|
||||
|
||||
networking.hostName = "nix-cache"; # Define your hostname.
|
||||
|
||||
services.nix-serve.enable = true;
|
||||
services.nix-serve.secretKeyFile = "/etc/nix/cache-priv.pem";
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
virtualHosts."cache.local" = {
|
||||
locations."/".proxyPass = "http://${config.services.nix-serve.bindAddress}:${toString config.services.nix-serve.port}";
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ config.services.nginx.defaultHTTPListenPort ];
|
||||
# Open ports in the firewall.
|
||||
# networking.firewall.allowedTCPPorts = [ 80 8080 443 ];
|
||||
# networking.firewall.allowedUDPPorts = [ ... ];
|
||||
# Or disable the firewall altogether.
|
||||
# networking.firewall.enable = false;
|
||||
|
||||
# Copy the NixOS configuration file and link it from the resulting system
|
||||
# (/run/current-system/configuration.nix). This is useful in case you
|
||||
# accidentally delete configuration.nix.
|
||||
# system.copySystemConfiguration = true;
|
||||
|
||||
# This option defines the first version of NixOS you have installed on this particular machine,
|
||||
# and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
|
||||
#
|
||||
# Most users should NEVER change this value after the initial install, for any reason,
|
||||
# even if you've upgraded your system to a new NixOS release.
|
||||
#
|
||||
# This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
|
||||
# so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how
|
||||
# to actually do that.
|
||||
#
|
||||
# This value being lower than the current NixOS release does NOT mean your system is
|
||||
# out of date, out of support, or vulnerable.
|
||||
#
|
||||
# Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
|
||||
# and migrated your data accordingly.
|
||||
#
|
||||
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.sta>
|
||||
system.stateVersion = "25.05"; # Did you read the comment?
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{ 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";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
_:
|
||||
|
||||
{
|
||||
# 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";
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
# 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
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
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;
|
||||
|
||||
# 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" ];
|
||||
# };
|
||||
# };
|
||||
|
||||
}
|
||||
+69
-18
@@ -1,31 +1,82 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
{ config, pkgs, lib, vars, ... }:
|
||||
|
||||
{
|
||||
home.username = "nixos"; # your actual username
|
||||
home.homeDirectory = "/home/nixos";
|
||||
home.stateVersion = "25.05"; # match your NixOS stateVersion
|
||||
|
||||
programs.home-manager.enable = true; # mandatory to activate HM
|
||||
|
||||
# Optional: packages
|
||||
home.packages = with pkgs; [
|
||||
git
|
||||
vim
|
||||
tmux
|
||||
nextcloud-client
|
||||
imports = [
|
||||
../../modules/common/aliases.nix
|
||||
];
|
||||
|
||||
# Optional: set environment vars
|
||||
home.sessionVariables = {
|
||||
EDITOR = "vim";
|
||||
home = {
|
||||
username = vars.primaryUser;
|
||||
homeDirectory = "/home/${vars.primaryUser}";
|
||||
stateVersion = "25.05"; # match your NixOS stateVersion
|
||||
|
||||
# Optional: packages
|
||||
packages = with pkgs; [
|
||||
git
|
||||
vim
|
||||
tmux
|
||||
nextcloud-client
|
||||
# vscode
|
||||
chromium
|
||||
];
|
||||
|
||||
# Optional: set environment vars
|
||||
sessionVariables = {
|
||||
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...)
|
||||
programs.bash.enable = true;
|
||||
services.nextcloud-client = {
|
||||
services.nextcloud-client = {
|
||||
enable = true;
|
||||
# Optionally start in background directly
|
||||
startInBackground = true;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
_:
|
||||
|
||||
{
|
||||
networking.hostName = "nixos";
|
||||
|
||||
# Preserved from the pre-refactor `nixos` target — stateVersion must never
|
||||
# be bumped on an already-installed machine.
|
||||
system.stateVersion = "25.05";
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
_:
|
||||
|
||||
{
|
||||
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";
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
# 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
|
||||
};
|
||||
};
|
||||
|
||||
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?
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{ 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";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
_:
|
||||
|
||||
{
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{ 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";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{ 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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
_:
|
||||
|
||||
{
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{ 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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{ 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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{ 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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../nix-cache/server.nix
|
||||
../beszel/enable-agent.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
{ 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 ];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{ 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 ];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
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" ];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{ 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)
|
||||
'';
|
||||
in
|
||||
{
|
||||
programs.bash = {
|
||||
enable = true;
|
||||
shellAliases = {
|
||||
"Switch-nix" = mySwitchCmd;
|
||||
"Test-nix" = myTestCmd;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{ config, lib, pkgs, vars, ... }:
|
||||
|
||||
{
|
||||
imports =
|
||||
[
|
||||
# Include the results of the hardware scan.
|
||||
# ./hardware-configuration.nix
|
||||
./set-locale.nix
|
||||
];
|
||||
# Use the GRUB 2 boot loader.
|
||||
# boot.loader.grub.enable = true;
|
||||
#boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
|
||||
|
||||
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
|
||||
|
||||
# Recommended over the true default (bypasses ZFS's own import safeguards)
|
||||
# per the option's own docs; matches hosts/docker/host.nix and
|
||||
# modules/services/zfs/enable-service.nix, which already set this
|
||||
# explicitly. Harmless no-op on hosts that don't use ZFS at all.
|
||||
boot.zfs.forceImportRoot = false;
|
||||
|
||||
# Set your time zone.
|
||||
time.timeZone = vars.timeZone;
|
||||
|
||||
# Enable QEMU agent
|
||||
services.qemuGuest.enable = true;
|
||||
|
||||
# Enable docker-compose
|
||||
environment.systemPackages = with pkgs; [
|
||||
vim
|
||||
btop
|
||||
git
|
||||
gcr
|
||||
];
|
||||
|
||||
# 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";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{ 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;
|
||||
# };
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
_:
|
||||
|
||||
{
|
||||
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";
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
_:
|
||||
|
||||
{
|
||||
# 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{ 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"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{ 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{ 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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{ config, lib, pkgs, vars, ... }:
|
||||
|
||||
{
|
||||
fileSystems = {
|
||||
${vars.nfsShares.dockerConfig.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
};
|
||||
|
||||
${vars.nfsShares.dockerDatabases.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
};
|
||||
|
||||
${vars.nfsShares.dockerVolumes.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
};
|
||||
|
||||
${vars.nfsShares.nextcloudData.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
};
|
||||
|
||||
${vars.nfsShares.raspiVolumes.mountpoint} = {
|
||||
device = "${vars.nfsServerHost}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
"nfsvers=4.2"
|
||||
"_netdev"
|
||||
"x-systemd.automount"
|
||||
"noatime"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{ 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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";
|
||||
}
|
||||
@@ -5,22 +5,26 @@
|
||||
|
||||
{
|
||||
imports =
|
||||
[ (modulesPath + "/profiles/qemu-guest.nix")
|
||||
[
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
];
|
||||
|
||||
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
|
||||
boot.initrd.kernelModules = [ ];
|
||||
boot.kernelModules = [ ];
|
||||
boot.extraModulePackages = [ ];
|
||||
boot = {
|
||||
initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
|
||||
initrd.kernelModules = [ ];
|
||||
kernelModules = [ "kvm-amd" ];
|
||||
extraModulePackages = [ ];
|
||||
};
|
||||
# boot.loader.grub.device = "/dev/sda2"; # or "nodev" for efi only
|
||||
|
||||
fileSystems."/" =
|
||||
{ device = "/dev/disk/by-label/nixos";
|
||||
fsType = "ext4";
|
||||
};
|
||||
# fileSystems."/" =
|
||||
# { device = "/dev/disk/by-label/nixos";
|
||||
# fsType = "ext4";
|
||||
# };
|
||||
|
||||
swapDevices =
|
||||
[ { device = "/dev/disk/by-label/swap"; }
|
||||
];
|
||||
# swapDevices =
|
||||
# [ { device = "/dev/disk/by-label/swap"; }
|
||||
# ];
|
||||
|
||||
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
|
||||
# (the default) this is the recommended approach. When using systemd-networkd it's
|
||||
@@ -0,0 +1,240 @@
|
||||
{ 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/prepare-host-key.sh first.
|
||||
#
|
||||
# Two places a key can come from, checked in order:
|
||||
# /etc/host-keys — baked into this image at build time (see
|
||||
# modules/installer/host-keys.nix; only present
|
||||
# if built with NIXOS_HOST_KEYS_DIR set)
|
||||
# /root/host-keys — scp'd in manually after boot (older fallback,
|
||||
# still supported for images built without keys)
|
||||
mkdir -p /root/host-keys
|
||||
if [[ -f "/etc/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
||||
echo "Found baked-in SSH host key for ''${choice}, installing to target..."
|
||||
install -D -m 0600 "/etc/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||
install -D -m 0644 "/etc/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||
elif [[ -f "/root/host-keys/''${choice}_ssh_host_ed25519_key" ]]; then
|
||||
echo "Found pre-seeded SSH host key for ''${choice}, installing to target..."
|
||||
install -D -m 0600 "/root/host-keys/''${choice}_ssh_host_ed25519_key" /mnt/etc/ssh/ssh_host_ed25519_key
|
||||
install -D -m 0644 "/root/host-keys/''${choice}_ssh_host_ed25519_key.pub" /mnt/etc/ssh/ssh_host_ed25519_key.pub
|
||||
else
|
||||
echo "WARNING: no SSH host key found for ''${choice} (checked /etc/host-keys and /root/host-keys)"
|
||||
echo "sops-nix secrets (including the login password) will NOT decrypt on first boot."
|
||||
echo "Run scripts/prepare-host-key.sh for host ''${choice} on your admin workstation first,"
|
||||
echo "then either rebuild this image with NIXOS_HOST_KEYS_DIR set, or scp the result to"
|
||||
echo "/root/host-keys/ on this machine."
|
||||
read -rp "Continue without a pre-seeded key anyway? (y/N): " skip_key
|
||||
if [[ ! "$skip_key" =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p /mnt/install-tmp
|
||||
export TMPDIR=/mnt/install-tmp
|
||||
|
||||
nixos-install \
|
||||
--flake "''${FLAKE_BASE_URL}#''${choice}" \
|
||||
"''${nix_extra_opts[@]}" \
|
||||
--no-root-password
|
||||
|
||||
|
||||
rm -rf /mnt/install-tmp
|
||||
# Redundant copy of the host's private key — the real one is now at
|
||||
# /etc/ssh/ssh_host_ed25519_key. Nothing NixOS-managed ever cleans this
|
||||
# up on its own since it was written imperatively, not declaratively.
|
||||
rm -rf /root/host-keys
|
||||
sleep 10
|
||||
reboot
|
||||
'';
|
||||
|
||||
mode = "0755";
|
||||
};
|
||||
};
|
||||
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{ 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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{ modulesPath, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
"${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix"
|
||||
./common.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{ 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{ 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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{ pkgs, vars, ... }:
|
||||
|
||||
{
|
||||
# Install the remote builder key on each client host (do not commit private keys):
|
||||
# sudo install -d -m 0700 /root/.ssh
|
||||
# sudo install -m 0600 ./nixremote /root/.ssh/nixremote
|
||||
# sudo ssh -i /root/.ssh/nixremote nixremote@nix-cache nix-store --version
|
||||
nix = {
|
||||
distributedBuilds = true;
|
||||
|
||||
buildMachines = [
|
||||
{
|
||||
hostName = vars.nixCacheHost;
|
||||
sshUser = vars.remoteBuilderUser;
|
||||
sshKey = "/root/.ssh/${vars.remoteBuilderUser}";
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
maxJobs = 4;
|
||||
speedFactor = 2;
|
||||
supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ];
|
||||
}
|
||||
];
|
||||
|
||||
settings = {
|
||||
builders-use-substitutes = true;
|
||||
max-jobs = "auto";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{ 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}";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../hardware-configuration/vm/linode.nix
|
||||
../disko/linode.nix
|
||||
];
|
||||
|
||||
networking = {
|
||||
usePredictableInterfaceNames = false;
|
||||
useDHCP = false;
|
||||
interfaces.eth0.useDHCP = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
{ 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/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/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
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../hardware-configuration/vm/proxmox.nix
|
||||
../boot/efi.nix
|
||||
../disko/proxmox.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{ 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"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{ vars, ... }:
|
||||
|
||||
{
|
||||
fileSystems.${vars.raspiMountpoint} = {
|
||||
device = "${vars.raspberryPiHost}.${vars.tailnetDomain}:${vars.raspiNfsPath}";
|
||||
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"
|
||||
];
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
_:
|
||||
|
||||
{
|
||||
services.rpcbind.enable = true;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{ 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 ==="
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{ 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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
_:
|
||||
|
||||
{
|
||||
services.tailscale.enable = true;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
_:
|
||||
|
||||
{
|
||||
services.tailscale = {
|
||||
enable = 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"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{ 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;
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
#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
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,134 @@
|
||||
# Spec: Remove Sensitive Information from NixOS Flake
|
||||
|
||||
## Goal
|
||||
|
||||
Every secret currently readable in plaintext anywhere in this repo (working tree *and* git history) gets removed, replaced with `sops-nix`-managed encrypted references, and rotated. When this is done, the repo should be safe to make public without exposing anything about the systems it configures.
|
||||
|
||||
Treat this as three sequential milestones. Do not start git history rewriting (Milestone 3) until Milestones 1 and 2 are fully verified and the flake still builds. This should be its own branch (`refactor/secrets`) until fully verified, then merged.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 1 — Audit
|
||||
|
||||
Before touching anything, produce a complete inventory. Do not guess at scope — grep the whole tree and the whole history.
|
||||
|
||||
1. Run a secret scanner across the working tree and full history. Use both, since they catch different things:
|
||||
- `gitleaks detect --source . -v --log-opts="--all"` (scans history too)
|
||||
- `trufflehog git file://. --since-commit=$(git rev-list --max-parents=0 HEAD) --only-verified=false`
|
||||
If neither is installed, add them via a temporary `nix-shell -p gitleaks trufflehog` — don't install anything globally on the host.
|
||||
|
||||
2. Manually grep for the categories below, since scanners miss config-specific patterns:
|
||||
- `hashedPassword`, `password`, `initialPassword`, `initialHashedPassword` in any `users.users.*` block
|
||||
- `age.secrets`, `sops.secrets` (if any partial secrets work already exists — check for it)
|
||||
- PSK / `preSharedKey`, `privateKeyFile` inline values (vs. file references) for WireGuard
|
||||
- `authKey`, `apiToken`, `api_key`, `token =`, `secret =` in service modules (Tailscale, Cloudflare, backup tools, etc.)
|
||||
- SSH private key material: search for `BEGIN OPENSSH PRIVATE KEY` / `BEGIN RSA PRIVATE KEY` literals
|
||||
- TLS cert/key pairs committed under e.g. `secrets/`, `certs/`, `pki/`
|
||||
- Real name, personal email, home address, or anything in comments/hostnames that maps a machine to your physical identity or network layout (e.g. hostnames like `wayne-desktop`, static LAN IPs, ISP-identifying info)
|
||||
- `.env` files, `secrets.nix`, `secrets.yaml`, or any file that looks like it was meant to be gitignored but wasn't
|
||||
|
||||
3. Produce `secrets-inventory.md` (temporary, delete before finishing) listing: file path, line, secret type, and which host/service it belongs to. This becomes the checklist for Milestone 2 — every row must be either migrated to sops or deleted, with nothing left unaccounted for.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 2 — Migrate to sops-nix
|
||||
|
||||
### 2.1 Set up sops-nix
|
||||
|
||||
1. Add the flake input:
|
||||
```nix
|
||||
sops-nix.url = "github:Mic92/sops-nix";
|
||||
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
```
|
||||
2. Import `sops-nix.nixosModules.sops` into each host's module list (or into a shared `common.nix` if all hosts use it).
|
||||
3. Generate an age keypair **per host** (not one shared key for everything — a compromised host shouldn't decrypt every other host's secrets):
|
||||
```
|
||||
nix-shell -p age --run "age-keygen -o /var/lib/sops-nix/key.txt"
|
||||
```
|
||||
Print the public key (`age-keygen -y`) for each host — you'll need it for `.sops.yaml`.
|
||||
4. Also generate one age key for yourself (your admin workstation) so you can edit secrets without needing to SSH into a host: store it at `~/.config/sops/age/keys.txt`, back it up somewhere outside this repo (password manager, offline). **If this key is lost, every secret encrypted with it is unrecoverable — losing the age key is equivalent to losing the secrets.**
|
||||
5. Create `.sops.yaml` at the repo root defining creation rules: which age public keys can decrypt which secrets files, keyed by path regex, so e.g. `secrets/hostA.yaml` is decryptable by your admin key + hostA's key, `secrets/hostB.yaml` by your admin key + hostB's key.
|
||||
|
||||
### 2.2 Migrate each secret category from the inventory
|
||||
|
||||
For each row in `secrets-inventory.md`:
|
||||
|
||||
- **Password hashes**: generate hash with `mkpasswd -m sha-512` (or `bcrypt` if your setup wants that), store under `sops.secrets."<name>/hashedPassword"`, reference via `users.users.<name>.hashedPasswordFile = config.sops.secrets."<name>/hashedPassword".path;`. Do not put the *plaintext* password anywhere, only the hash, and only the hash goes into the encrypted sops file.
|
||||
- **API tokens / auth keys**: move the raw value into the per-host sops YAML, reference in the module via `config.sops.secrets."<service>/token".path` — most NixOS service modules that take a token also accept a `*File` variant (e.g. `environmentFile`, `tokenFile`); use that instead of passing the value directly.
|
||||
- **Private keys / certs**: move the PEM/key content wholesale into a sops secret, output as a file with appropriate `sops.secrets.<name>.path`, `owner`, `mode`, `restartUnits` so the depending service (sshd, wireguard, nginx) reloads when the secret changes.
|
||||
- **Personal/identifying info**: this doesn't belong in sops (it's not "secret," it's just information you don't want public). Replace real names/emails with placeholders or move to a small untracked `local.nix` that's `.gitignore`'d and imported conditionally, with a documented template (`local.nix.example`) committed instead.
|
||||
|
||||
### 2.3 Verify before moving on
|
||||
|
||||
- `nixos-rebuild dry-build --flake .#<host>` succeeds for every host.
|
||||
- `sudo nixos-rebuild switch --flake .#<host>` on at least one real machine (or a VM) confirms secrets decrypt and services start.
|
||||
- Confirm decrypted secrets land under `/run/secrets/` (not the Nix store — anything placed in `/nix/store` is world-readable by design, so sops-nix's runtime-only placement is the whole point; double check no module accidentally pulls a secret path into a store-built config file).
|
||||
- Re-run the grep/scanner sweep from Milestone 1 against the *working tree only* (not history yet) — it should now come back clean.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 3 — Scrub git history
|
||||
|
||||
Do this only after Milestone 2 is merged to your main branch and confirmed working, since it rewrites every commit SHA from the point of the earliest offending commit onward.
|
||||
|
||||
**This is destructive and irreversible on your local clone. Back up first:**
|
||||
```
|
||||
cp -r /path/to/nixos-repo /path/to/nixos-repo-backup-$(date +%F)
|
||||
```
|
||||
|
||||
1. Install `git-filter-repo` (not the older `git filter-branch` / BFG — filter-repo is the currently maintained, faster, safer tool):
|
||||
```
|
||||
nix-shell -p git-filter-repo
|
||||
```
|
||||
2. Use the `secrets-inventory.md` list to build a list of literal strings/paths to strip. Two approaches, use both:
|
||||
- Path-based: if whole files were secret (e.g. `secrets.nix`, a `.env`, a private key file), remove them entirely from history:
|
||||
```
|
||||
git filter-repo --path secrets.nix --path .env --invert-paths
|
||||
```
|
||||
- Value-based: for secrets embedded inline in files you're keeping (not deleting the whole file), use `--replace-text` with a file listing each literal secret string to replace with `***REMOVED***`:
|
||||
```
|
||||
git filter-repo --replace-text expressions.txt
|
||||
```
|
||||
3. After filtering, verify: run the Milestone 1 scanners again against full history (`--log-opts="--all"`). They must come back clean.
|
||||
4. Force-push the rewritten history:
|
||||
```
|
||||
git push origin --force --all
|
||||
git push origin --force --tags
|
||||
```
|
||||
5. **Every other clone of this repo (other machines, WSL instances, CI) must be deleted and re-cloned fresh** — a `git pull` against rewritten history will not work cleanly and risks resurrecting the old commits. Don't try to reconcile old clones; throw them away and re-clone.
|
||||
6. If this repo has ever been pushed to a public host (GitHub, etc.) or a fork/mirror exists, treat every secret that was ever in history as **permanently compromised regardless of the rewrite** — caches, forks, and Wayback-style archives can retain old commits indefinitely. History scrubbing prevents *future* exposure via `git clone`; it does not undo past exposure.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 4 — Rotate everything
|
||||
|
||||
Because the secrets were exposed in history (even briefly, even in a private repo), the migration is not complete until every credential in the inventory has been **rotated**, not just re-encrypted. Re-encrypting an already-leaked value protects it going forward but doesn't undo the leak.
|
||||
|
||||
For each row in the original inventory:
|
||||
- Password hashes → change the actual account password, regenerate the hash, update the sops file.
|
||||
- API tokens/auth keys → revoke the old token in the issuing service's dashboard (Cloudflare, Tailscale, backup provider, etc.) and generate a new one.
|
||||
- SSH/WireGuard private keys → generate new keypairs, update the corresponding public key wherever it's trusted (authorized_keys, peer configs, etc.), retire the old ones.
|
||||
- TLS certs → reissue if the private key was exposed.
|
||||
|
||||
Keep `secrets-inventory.md` open during this step and check off each row as rotated. Delete the file only once every row is checked off — it should not be committed.
|
||||
|
||||
---
|
||||
|
||||
## Ongoing prevention
|
||||
|
||||
Add a pre-commit hook (or a `nix flake check` step) running `gitleaks protect --staged` so a secret can't be committed again by accident. Document in the repo README (briefly) that new secrets go through `sops <file>` to edit, never as plaintext in a tracked file.
|
||||
|
||||
---
|
||||
|
||||
## Definition of done
|
||||
|
||||
- [ ] Milestone 1 inventory complete and reviewed
|
||||
- [ ] All hosts have per-host age keys; admin key backed up outside the repo
|
||||
- [ ] Every inventoried secret migrated to sops-nix, referenced via `*File`/`sops.secrets.*.path`, nothing plaintext in the working tree
|
||||
- [ ] `nixos-rebuild dry-build` and at least one real `switch` verified per host
|
||||
- [ ] Working-tree scanner sweep clean
|
||||
- [ ] History rewritten with `git-filter-repo`, force-pushed, full-history scanner sweep clean
|
||||
- [ ] All other clones deleted and re-cloned from the rewritten history
|
||||
- [ ] Every credential in the original inventory rotated (not just re-encrypted)
|
||||
- [ ] Pre-commit secret scanning hook added
|
||||
- [ ] `secrets-inventory.md` deleted from the working directory (never committed)
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
#!/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 dry-run' before committing —"
|
||||
echo "a channel bump can shift option defaults across every host."
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export NIX_CONFIG="${NIX_CONFIG:-}
|
||||
experimental-features = nix-command flakes
|
||||
accept-flake-config = false
|
||||
warn-dirty = false
|
||||
"
|
||||
|
||||
MODE="${1:-validate}"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
hosts_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames)"
|
||||
hosts="$(echo "$hosts_json" | jq -r '.[]')"
|
||||
|
||||
echo "Hosts:"
|
||||
echo "$hosts"
|
||||
|
||||
echo
|
||||
echo "Checking for obvious committed secrets..."
|
||||
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
|
||||
|
||||
echo
|
||||
echo "Checking Nix formatting with nixpkgs-fmt..."
|
||||
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
|
||||
|
||||
echo
|
||||
echo "Running statix lint..."
|
||||
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#statix -- check .
|
||||
|
||||
echo
|
||||
echo "Evaluating host toplevel derivations..."
|
||||
for host in $hosts; do
|
||||
echo "==> $host"
|
||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#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 --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
|
||||
;;
|
||||
proxmox-*)
|
||||
echo "==> $host (diskoImagesScript)"
|
||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Evaluating buildable packages..."
|
||||
packages_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#packages.x86_64-linux --apply builtins.attrNames)"
|
||||
packages="$(echo "$packages_json" | jq -r '.[]')"
|
||||
for pkg in $packages; do
|
||||
echo "==> packages.x86_64-linux.${pkg}"
|
||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}"
|
||||
done
|
||||
|
||||
if [[ "$MODE" == "dry-run" ]]; then
|
||||
echo
|
||||
echo "Running dry-run builds for all hosts. This will not create result symlinks."
|
||||
for host in $hosts; do
|
||||
echo "==> Dry-run build: $host"
|
||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel"
|
||||
|
||||
case "$host" in
|
||||
lxc-*)
|
||||
echo "==> Dry-run build: $host (tarball)"
|
||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball"
|
||||
;;
|
||||
proxmox-*)
|
||||
echo "==> Dry-run build: $host (diskoImagesScript)"
|
||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Running dry-run builds for all packages."
|
||||
for pkg in $packages; do
|
||||
echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
|
||||
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}"
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Maintenance checks complete."
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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:"
|
||||
hosts="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')"
|
||||
echo "$hosts"
|
||||
|
||||
echo "Evaluating all host toplevel derivations..."
|
||||
for host in $hosts; do
|
||||
echo "==> Evaluating $host"
|
||||
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
|
||||
done
|
||||
|
||||
echo "Codex setup complete."
|
||||
Executable
+521
@@ -0,0 +1,521 @@
|
||||
#!/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.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/create-proxmox-resource.sh --type lxc|vm --host <name> [options]
|
||||
# scripts/create-proxmox-resource.sh --type lxc|vm --list
|
||||
# scripts/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.
|
||||
# - --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.
|
||||
# - Neither mode can start/stop/delete a resource. Not implemented on
|
||||
# purpose -- ask before adding it.
|
||||
#
|
||||
# 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"
|
||||
|
||||
sync_keys="${repo_root}/scripts/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 instead of
|
||||
checking the node / building one from the flake.
|
||||
--force-rebuild Skip the "does the node already have this
|
||||
image" check -- always build fresh and
|
||||
overwrite what's there.
|
||||
--allow-duplicate-host Required if --host already has a real
|
||||
deployment elsewhere (variables.nix's
|
||||
deployedTargets) -- otherwise refused, since
|
||||
it'd share that host's hostName/hostId.
|
||||
|
||||
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"
|
||||
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 ;;
|
||||
--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
|
||||
read -rp "Type the VMID (${vmid}) to confirm these changes: " confirm
|
||||
if [[ "$confirm" != "$vmid" ]]; 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 --------------------
|
||||
list_hosts() {
|
||||
local target hostname
|
||||
for target in $(nix eval --json --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames 2>/dev/null \
|
||||
| jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do
|
||||
hostname="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null)"
|
||||
printf ' %-12s -> %s\n' "$hostname" "$target"
|
||||
done
|
||||
}
|
||||
|
||||
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=""
|
||||
for target in $(nix eval --json --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames \
|
||||
| jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do
|
||||
hn="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations.${target}.config.networking.hostName")"
|
||||
if [[ "$hn" == "$host" ]]; then
|
||||
flake_target="$target"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
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 really deployed ----------
|
||||
# Checked by hostName, not exact flake target: proxmox-server being
|
||||
# deployed also blocks --type lxc --host server, since both would carry
|
||||
# the same hosts/server/host.nix identity (hostName, hostId).
|
||||
if [[ "$allow_duplicate_host" -eq 0 ]]; then
|
||||
deployed_targets_json="$(nix eval --json --no-use-registries --no-accept-flake-config \
|
||||
--file "${repo_root}/variables.nix" deployedTargets)"
|
||||
for dt in $(echo "$deployed_targets_json" | jq -r '.[]'); do
|
||||
dt_hostname="$(nix eval --raw --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations.${dt}.config.networking.hostName" 2>/dev/null || true)"
|
||||
if [[ "$dt_hostname" == "$host" ]]; then
|
||||
echo "ERROR: '${host}' already has a real deployment (${dt}, per variables.nix's" >&2
|
||||
echo "deployedTargets). Creating ${flake_target} would share its hostName/hostId --" >&2
|
||||
echo "refusing by default. Pass --allow-duplicate-host if you really mean to spin" >&2
|
||||
echo "up a separate test instance of this host (it'll still get its own distinct" >&2
|
||||
echo "sops key and VMID, never touching ${dt})." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
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}"
|
||||
|
||||
# --- 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 and upload (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
|
||||
# Mirrors the real build commands' "${NIX_OPTS[@]}" below -- nix_extra_opts
|
||||
# (called earlier, once) has already decided whether nix-cache is in play,
|
||||
# and the dry-run preview needs to reflect that decision instead of always
|
||||
# printing the same command regardless of outcome.
|
||||
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: NIXOS_HOST_KEYS_DIR=${repo_root}/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"
|
||||
local_image="<built-tarball>"
|
||||
else
|
||||
echo "==> Building LXC tarball for ${flake_target}..."
|
||||
NIXOS_HOST_KEYS_DIR="${repo_root}/host-keys" nix build --impure \
|
||||
--no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||
".#nixosConfigurations.${flake_target}.config.system.build.tarball" \
|
||||
--out-link "${repo_root}/result-${flake_target}"
|
||||
local_image="$(find "${repo_root}/result-${flake_target}/tarball" -maxdepth 1 -type f | head -1)"
|
||||
echo "Built: ${local_image}"
|
||||
fi
|
||||
else
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo "[dry-run] would build: 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 ./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"
|
||||
local_image="<built-image>.raw"
|
||||
else
|
||||
echo "==> Building Disko image script for ${flake_target}..."
|
||||
nix build --no-use-registries --no-accept-flake-config "${NIX_OPTS[@]}" \
|
||||
".#nixosConfigurations.${flake_target}.config.system.build.diskoImagesScript" \
|
||||
--out-link "${repo_root}/result-${flake_target}"
|
||||
echo "==> Running it (builds the .raw image in a temporary QEMU VM, needs sudo)..."
|
||||
( cd "$repo_root" && sudo "./result-${flake_target}" \
|
||||
--pre-format-files "host-keys/${flake_target}_ssh_host_ed25519_key" /etc/ssh/ssh_host_ed25519_key \
|
||||
--pre-format-files "host-keys/${flake_target}_ssh_host_ed25519_key.pub" /etc/ssh/ssh_host_ed25519_key.pub \
|
||||
--build-memory 2048 )
|
||||
local_image="$(find "$repo_root" -maxdepth 1 -name "*.raw" -newer "${repo_root}/result-${flake_target}" | head -1)"
|
||||
if [[ -z "$local_image" ]]; then
|
||||
echo "ERROR: expected a .raw image after the build but didn't find one in ${repo_root}." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Built: ${local_image}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- upload (skip entirely if reusing an image already on the node) ------
|
||||
echo
|
||||
if [[ "$image_already_remote" -eq 1 ]]; then
|
||||
: # nothing to upload
|
||||
elif [[ "$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
|
||||
|
||||
# --- 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.
|
||||
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.
|
||||
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"
|
||||
|
||||
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
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/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/create-proxmox-resource.sh ...)
|
||||
# since each one only sets a default if unset.
|
||||
|
||||
# SSH-reachable Proxmox node that scripts/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}"
|
||||
|
||||
# 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.
|
||||
: "${PROXMOX_DEFAULT_LXC_FEATURES:=nesting=1,keyctl=1}"
|
||||
|
||||
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
|
||||
|
||||
# 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=()
|
||||
if ! curl --silent --fail --max-time 3 "http://${NIX_CACHE_HOST}/nix-cache-info" >/dev/null 2>&1; 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 "")
|
||||
elif ! timeout 3 bash -c "cat < /dev/tcp/${NIX_CACHE_HOST}/22" >/dev/null 2>&1; 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
|
||||
printf -v NIX_EXTRA_OPTS '%q ' "${NIX_OPTS[@]}"
|
||||
export NIX_EXTRA_OPTS
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/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()
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/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/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)"
|
||||
|
||||
hostname="${1:?usage: scripts/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-shell -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
|
||||
|
||||
age_pub="$(nix-shell -p ssh-to-age --run "ssh-to-age -i '${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
|
||||
Executable
+441
@@ -0,0 +1,441 @@
|
||||
#!/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"
|
||||
|
||||
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="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
||||
|
||||
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="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '${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() {
|
||||
nix eval --json --no-use-registries --no-accept-flake-config \
|
||||
"${repo_root}#nixosConfigurations" --apply builtins.attrNames \
|
||||
| jq -r '.[] | select(. != "installer")'
|
||||
# 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.
|
||||
}
|
||||
|
||||
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"
|
||||
nix-shell "${NIX_OPTS[@]}" -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${host}' -f '${keyfile}'" >/dev/null
|
||||
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="$(nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${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}"
|
||||
nix-shell "${NIX_OPTS[@]}" -p sops --run "sops updatekeys --yes '${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
|
||||
read -rp "Type REGENERATE to confirm: " confirm
|
||||
if [[ "$confirm" != "REGENERATE" ]]; 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 "$@"
|
||||
@@ -0,0 +1,81 @@
|
||||
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+IFgyNTUxOSBpWjNSdEdZbUUzamswa08w
|
||||
aCtzSHB0bFVZMnYxTkpuM1psdVYzWW55SzMwCkliMWVOUlBqRG5wOGZjQVg4MkFz
|
||||
NEVkMXdkTjhWRlZmVGlzZElid2pUMXMKLS0tIGtHUmRCNXNhVmloUHYzQnE5YlBS
|
||||
YnVSQjJlT3JnQ1RNMm9xV2xKOGRZUDAKc4VTl9NEI9Rv8+4J3JTeHTt2h8Dr2IJv
|
||||
tfvoNJQM/w6RAJWNTkaDmzZa9OnUW+grDlBQKlDuAnr6fZmuNTH2hQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOR25UN1c0aE5SYWphbU0y
|
||||
SHRTZ1B0WC9NU3Z5VHpzTXpLSUxHTDI1ZFE0CnFpYUN1eGZQejJMblZPd1ROeUth
|
||||
dklZYVVNa1ZNZ1d4dW9vMCsvQWp1RkEKLS0tIERxakx5L0JrQitib1EyNDRMbDQ5
|
||||
Z3hDWUFEazdxczVhaHJYK3VZeEJSSDgKkw9T4ZuT+VHIF4WopqRHt8vW30kOysJ3
|
||||
vOq6EZ3Fqkgmoxm69Zp2gFnuE9GZIBy3VPQVLU2k6dZGJ3IvmLYeBA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0THJzMFBTTCtDMWRmZ25M
|
||||
dU55OVhBb0trWWRUNlArTnEzRjhiYngvRENNCjF4d0M5NlYyQW50TTdMRXpuUjRr
|
||||
M1NwV05JOHV6T2cxT2FheVpuZ0w2T0kKLS0tIFBxdlVpVEoxOUpSWjk1ejRsK1NM
|
||||
V1UwTU1scG91L2FIemtwSW5JbFlmeG8K/1WIlaIidy3x3ptoRpS/DG88064LQ6Mq
|
||||
GbfB0jfq5PILDQMMuZu5oIBY31SxwnhZ02Ns7gA67kgNIRSCmk9WyQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmRURQVGw1a1E1MkIzV2pa
|
||||
RG5HSkM0c0huUGVWcnFZOGlacGlGOHFMTVFrCm5OS09HRFc3TGVUYmtzTStxL0Q5
|
||||
N2hRMEcxUE9MTmZXL0wvME5EZXF1Z00KLS0tIHdHMVVHcTZzMmdXU0s4QlVqSS9Y
|
||||
ZUVmcWhPaURIUFJGR0V4bUZwKzM1bm8KlvGMNEClbLlfvJqNQHhd0dI4ihShLChF
|
||||
GI/fydgrBruw3Otv6KLZu3CBC7iNcKlvZxz+YGD2qbicmyQ5hAhDSQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5bWpFenBlQldna3RhSFpr
|
||||
bEJBaHJDMzM3OHlqTmozcWU0VDM1bTFUWFZzCnZtNHZjZ1U1RzNkUlBHZFozWXdt
|
||||
VVkxQjMvTDJtbFZnclpkUEd4TEVmNTAKLS0tIFlhV2ZSSzJLRVNoMmFyVktDOElR
|
||||
YUxqZUFoY1ZWeGlldGplMjVQa1A5aUUKWelY6yO7Mr6dRvj4MVMbq/Z9JgrAnahz
|
||||
BDhHqObzJrOCtfDCTWiYuP+0yvIFWItMWhGSMw9MwwivvwnrEa+ZuQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiK2hKb2VaVnoyaEVYUHZZ
|
||||
OWVUSGtONGs5dEljTjJQdlhEcjNjdjViT2pVCkpaOHVZMlpXOFRveVlMeXZqWmoz
|
||||
ZHJRQTR2dmJQSEozeTRGMEdUdFlmZ2MKLS0tIERWS1RVdW1jQytBZzlkb3puNjhH
|
||||
ZjdlZmtzNXVOQ25DeCthUzhRRm1MT2cKaxc7zGm57iJFSeYc2IPqF4Eaxa44nR37
|
||||
pWZw+erG4F9AAZ2F047q+oLKe0B8FLSF54IbcXdQhitgGNR7B2HVeA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1ekhUTG5VOFErL3pFeWZM
|
||||
RG9NVnN6NFl3bzlFeTQzdHFtZmhwem04alQ0ClpJRStObERMZ0w2V0NhR1FSeW96
|
||||
M1d2V2NjUkUrLzN2ZVNSbGY4bll5WmsKLS0tIC94dVFQcXJ6d3pLU0VHNEFGR0ls
|
||||
a1Q2UmNuSjVMNG5XZGZKV1VmNHFPQXcKQJrZGw/9fPnXeFZ4omrkEgrzwplhwvRW
|
||||
i0FXuepoU353sR7enyL34qPoOdm05ivowuPKNzkq8D4i5AF6vGv+YA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoZ2lHM2RBQ3lUK216dkcr
|
||||
Y3RrcXR3QTdFVjJRWlhpQWVFQ1cyZWNabkRFClAwRi9PSHF5ZWFSUzJuRXF3bU1R
|
||||
b0U5TFRZaFdmR1NMS3RRT3E3M2hUdE0KLS0tIEtrUy8ydkNyVHBiOVR6WEdjV2VN
|
||||
NVJHUVgwRkhxcmlwcFkrRlFwTEF6YVkKzXyJk0UnmUsvb+NzNVcf/gf7OEEt3P/K
|
||||
OIGxDrGfs/zNQgeKXNbQlQ4p4jOaybG8aCmX+A4qTk6/I8yY8LTJWg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||
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
|
||||
@@ -0,0 +1,35 @@
|
||||
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+IFgyNTUxOSBDbHd1Y1dpa2ZiQ2E1bXRP
|
||||
SnkrWHI0V2U1eGFKY0RZamZQQjluS0hGeENZCmduUFQyd1Q5RkZGMGFoenJhNmZh
|
||||
VmZ2OStNaFpJejFxQ2JUZXFpNThaMncKLS0tIEhJdGdQeXEzb25Nbk5YZktCWVUz
|
||||
NUJDUWRoTkd5R2pFblZjS0NSbVR5dkkKDYeW+zRpha04/CasFM91K6v1PpkNGHRu
|
||||
qAoKs9KSg9VxS7ya8RuLmylKRdpPkupm/8SXIJvQuCXp5LWmNJ4zkA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4UktiUUdmd092bnZMdFMz
|
||||
Q05xVlhnNzhPZUY4TjF5bjRCbklacTdJd1RBCnJ4bEVMaWRLZExIUFVEUDBXTnlV
|
||||
MDU4WUk5VlJjdlhVUVpzZXlnWHhqTjQKLS0tIE9iclhGVElPcU9OLzF0bEVjMjVp
|
||||
YzlQTGFYZEhLdTk3N3ZramVjVGRXREUKjOjqO/jX5iEKN43WiJ8qS/1mDfxdj8uO
|
||||
K7Yyc/Yj0qMvGwLNzphjmbT29P7dXk4Ht4TedqCd+8DfhpOgLVIaSA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjaXpvQU9yRUc3ektPMmU5
|
||||
Q0xLWU9mYXVOeTVBNGNsZzJPa1lkVlRUZHlvClhoSVV6YjB1Q3pTTzVvNFlNYkU4
|
||||
Y2RrbFk5SUFKSDVjeFU0Wlc2aGFUMFUKLS0tIDlVeXM4WWRUakg4Zk5mcjdJL3VC
|
||||
S1k5eFBpVnREQlZwYllpeTNhWW5GS1EKi2sMwyJJ0D8acjCZmxlcwdU1sglBuxR7
|
||||
2SEMsctdGC+5E3ilPXvPpZ5RONZHbXxn6kQRBlBv6AJERpGDzsfgfA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
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
|
||||
@@ -0,0 +1,25 @@
|
||||
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+IFgyNTUxOSBxaFZURjAxMGRJZEJ5MW1x
|
||||
ZjVwWEQrQlkwNmRibVNiL2RpTTFLeUVDQ3hRCkVIamVnZkM1MnlueloxMHVFQnBF
|
||||
RjV2bnUrZUo4WGZJTmR4Y0xITkxRUkUKLS0tIGlJdVQ5MFBubVhxRUVMWW0wSGpP
|
||||
UGdKNUNPYW9nek1UZ0tWbXd3QVNUNDgKIHOiKelITQdH5R4Nc3WF7mzz15D1f9on
|
||||
VaTdr5qkf8LNNvPI0fxsXA9is5cqeg+KbDRHtUumEhNp6Zrf8zWBkw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N0hvV01naytDSWVwK1B1
|
||||
MXM1ZkdpaVc2Q3FPV2VBcC83WmcvSjdGUGgwClJGSXZ6YW5JeFlValNJbjVhK29u
|
||||
bUFqN2dRQTI4ZkwyeXNWYk5JeWVJRXcKLS0tIDlMMkNBUnNUSTJwVVFmc2dlcEZS
|
||||
VGQ1VHR2dXB0M3RsalppVWxiUUROM0UKZM/4QDTam3LDTzjnDs41Ije50R7Q7GC4
|
||||
IZbUZjs72rBzY8IkJDbN9JidadEc4NAtMOJwXiJbpZGiCBNfc8+SXw==
|
||||
-----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
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
{
|
||||
# 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
|
||||
|
||||
# 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"
|
||||
];
|
||||
|
||||
# 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;
|
||||
};
|
||||
|
||||
# .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
|
||||
};
|
||||
|
||||
# Flake targets with a real, currently-running deployment somewhere —
|
||||
# matches README.md's Hosts table "(real, deployed)" annotations; update
|
||||
# both together. Not consumed by any NixOS module (nothing in the actual
|
||||
# system config should behave differently because of this) — it's read
|
||||
# by scripts/create-proxmox-resource.sh to refuse creating a same-identity
|
||||
# duplicate of an already-deployed host (shared hostName/hostId) unless
|
||||
# you explicitly pass --allow-duplicate-host.
|
||||
deployedTargets = [
|
||||
"linode-minimal"
|
||||
"proxmox-minimal"
|
||||
"proxmox-nix-cache"
|
||||
"proxmox-server"
|
||||
"proxmox-docker"
|
||||
"proxmox-gui"
|
||||
"proxmox-pxe-boot"
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user