docs: review and correct comments and documentation (#436)

This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-10-12 10:02:38 +08:00
1 parent f32d67a5cc
commit 9c38819517
50 files changed
+148 -148

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ projects live in [agents/](./agents/README.md).
## Validation
- For Nix changes, run `just fmt` and inspect the diff: it formats all Nix files. Nix style is
`nixfmt` with width 100.
`nixfmt`, whose default width is 100.
- For supported non-Nix files, use `prettier --write <file>` and `prettier --check <file>`;
configuration lives in `.prettierrc.yaml`. Spelling checks use `typos` and `.typos.toml`.
- Run `just test` for configuration changes; it evaluates `.#evalTests` across Linux and Darwin and
+1 -1
View File
@@ -125,7 +125,7 @@ verify-store:
repair-store *paths:
nix store repair {{paths}}
# Update all Nixpkgs inputs
# Update the secondary nixpkgs inputs (the primary `nixpkgs` needs `just up`)
[group('nix')]
up-nix:
nix flake update --commit-lock-file nixpkgs-stable nixpkgs-master nixpkgs-darwin nixpkgs-patched
+1 -1
View File
@@ -140,7 +140,7 @@ For macOS (nix-darwin):
# If you are deploying for the first time,
# 1. install nix & homebrew manually.
# 2. prepare the deployment environment with essential packages available
nix-shell -p just nushell
nix shell nixpkgs#just nixpkgs#nushell
# 3. comment home-manager's code in lib/macosSystem.nix to speed up the first deployment.
# Deploy the darwinConfiguration by hostname match (fern, frieren)
+20 -20
View File
@@ -33,18 +33,18 @@ small, reversible controls and narrow exceptions; benchmark before adopting expe
## Architecture and trust boundaries
| Boundary | Current design | Limitation |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Administration | Key-based SSH; root key login retained for Colmena/MicroVM deployment | Deployment credentials are root-equivalent; password login being disabled does not restrict stolen keys |
| Host network | Shared nftables firewall trusts the home LAN, tailnet interface and local Podman bridge; IPv6 link-local is accepted | These are broad trust zones, not service-level least privilege; router exposure and tailnet ACLs require separate verification |
| Monitoring | node/smartctl exporters bind the host LAN IPv4 (9100/9633); firewall rules restrict exporter ports 9100, 9835 and 9633 to the monitoring host over IPv4 | nvidia_gpu_exporter (9835) still binds wildcard; IPv6 denial is planned in the baseline implementation; current running hosts require separate verification |
| VM hosts | shoryu, shushou and youko run guests behind `br0`; ruby/kana are libvirt guests | A bridge is not network isolation; host compromise affects its guests |
| k3s | Three control-plane and three worker MicroVMs; Cilium, API VIP and NFS CSI | Cluster-admin, privileged/host-mounted pods and writable NFS data are powerful trust boundaries |
| MicroVM storage | Host store shared read-only through virtiofs; guest `/etc`, `/var` and `/home` persisted in separate images | Read-only store does not isolate the host-side virtiofs implementation or protect writable guest state |
| Desktop apps | Selected apps use nixpak/bubblewrap; Podman is rootless | Rules and shared directories vary per app; user namespaces are needed for these sandboxes |
| Secrets | agenix declarations here, encrypted data in the private nix-secrets repository | Recipients and plaintext file permissions must be reviewed; root on a recipient can read its decrypted secrets |
| Supply chain | Locked flake inputs; explicit binary-cache trust keys; normal user is not a Nix trusted-user | A trusted cache or imported Nix module can supply code executed as root; a lock file is not a security audit |
| Recovery | NixOS generations, btrbk/restic, hourly read-only snapshots of the REST store, and documented backup procedures | A rollback does not undo compromised data or secrets; backup configuration alone does not prove restore works; the offsite immutable copy is still planned |
| Boundary | Current design | Limitation |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Administration | Key-based SSH; root key login retained for Colmena/MicroVM deployment | Deployment credentials are root-equivalent; password login being disabled does not restrict stolen keys |
| Host network | Shared nftables firewall trusts the home LAN, tailnet interface and local Podman bridge; IPv6 link-local is accepted | These are broad trust zones, not service-level least privilege; router exposure and tailnet ACLs require separate verification |
| Monitoring | node/smartctl exporters bind the host LAN IPv4 (9100/9633); firewall rules restrict exporter ports 9100, 9835 and 9633 to the monitoring host over IPv4 | nvidia_gpu_exporter (9835) still binds wildcard; the IPv6 denial for those three ports is in the shared firewall; current running hosts require separate verification |
| VM hosts | shoryu, shushou and youko run guests behind `br0`; ruby/kana are libvirt guests | A bridge is not network isolation; host compromise affects its guests |
| k3s | Three control-plane and three worker MicroVMs; Cilium, API VIP and NFS CSI | Cluster-admin, privileged/host-mounted pods and writable NFS data are powerful trust boundaries |
| MicroVM storage | Host store shared read-only through virtiofs; guest `/etc`, `/var` and `/home` persisted in separate images | Read-only store does not isolate the host-side virtiofs implementation or protect writable guest state |
| Desktop apps | Selected apps use nixpak/bubblewrap; Podman is rootless | Rules and shared directories vary per app; user namespaces are needed for these sandboxes |
| Secrets | agenix declarations here, encrypted data in the private nix-secrets repository | Recipients and plaintext file permissions must be reviewed; root on a recipient can read its decrypted secrets |
| Supply chain | Locked flake inputs; explicit binary-cache trust keys; normal user is not a Nix trusted-user | A trusted cache or imported Nix module can supply code executed as root; a lock file is not a security audit |
| Recovery | NixOS generations, btrbk/restic, hourly read-only snapshots of the REST store, and documented backup procedures | A rollback does not undo compromised data or secrets; backup configuration alone does not prove restore works; the offsite immutable copy is still planned |
Sources: [host layout](hosts/README.md), [k3s deployment](hosts/k8s/README.md),
[firewall](modules/nixos/base/networking/firewall.nix), [SSH](modules/nixos/base/ssh.nix),
@@ -153,14 +153,14 @@ configuration and certificate lifecycle procedures. Never bypass TLS verificatio
hostname mismatch. Eval tests protect the domain join endpoint and server SAN declarations; live TLS
checks remain necessary after deployment.
## Implementation plan and completed audit findings
## Implementation status and completed audit findings
### Implementation status
| Component | Status | Runtime verification |
| ---------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------- |
| Architecture, agent-first policy, audits and canonical API tests | Included in this documentation change | Only the dated observations below were checked live |
| Explicit sysctl baseline and IPv6 exporter defence | Implemented in this baseline change | Not deployed or verified on running hosts |
| Component | Status | Runtime verification |
| ---------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------- |
| Architecture, agent-first policy, audits and canonical API tests | In this repository | Only the dated observations below were checked live |
| Explicit sysctl baseline and IPv6 exporter defence | In `hardening/` and the shared firewall | Not deployed or verified on running hosts |
### Baseline implementation checks
@@ -168,11 +168,11 @@ checks remain necessary after deployment.
- IPv6 exporter denial precedes broad LAN/tailnet/container rules. The monitoring configuration uses
IPv4 targets for these ports, so no scrape endpoint migration is needed.
- x86_64/aarch64 eval checks cover baseline values, normal host overrides and generated exporter
rules. Canonical API endpoint checks for all six cluster nodes already accompany this document.
rules. Canonical API endpoint checks cover all six cluster nodes.
- A reusable isolated VM check exercises allowed monitoring IPv4 access, denied other IPv4/IPv6
access to all three exporter ports, retained IPv6 loopback and a reachable ordinary IPv4/IPv6
service. Run `nix build .#checks.x86_64-linux.security-exporters`, importing only the shared
firewall, not real-host secrets or workloads, and does not replace deployment-time checks.
service. Run it with `nix build .#checks.x86_64-linux.security-exporters`. It imports only the
shared firewall, not real-host secrets or workloads, and does not replace deployment-time checks.
### Read-only audit snapshot — 2026-10-03
+6 -5
View File
@@ -48,7 +48,8 @@ A setting that works around a bug or a hardware quirk.
| WA-029 | mihomo `fake-ip-range6` is `2001:2::/64`, not the shipped ULA default — `modules/nixos/desktop/networking/mihomo/generate.nu` | the default `fdfe:dcba:9876::/64` sits in `fc00::/7`, which Local Network Access calls local, so a public page loaded over the v4 fake pool and pulling a dual-stack subresource prompts for local-network access (Chrome 154, Firefox). The same range also stalled ssh: a session to a fake AAAA authenticated and then never closed, on both `gvisor` and `mips`. `2001:2::/48` is RFC 5180 benchmarking -- the v6 twin of the `198.18.0.0/15` v4 default, absent from that table -- and both symptoms went with the move | Go back to the upstream default once mihomo ships a pool outside the LNA non-public table; the ULA default brings the ssh stall back | 2026-10-09 | on a browser or mihomo bump -- that table already grew to cover `2001:db8::/32` and `198.18.0.0/15` | active |
| WA-030 | mihomo unit sets `SKIP_SYSTEM_IPV6_CHECK=1` — `modules/nixos/desktop/networking/mihomo/default.nix` | mihomo checks at startup whether another NIC already holds an IPv6 address and, finding none, skips the TUN's v6 half -- but `strict-route` still installs its v6 blackhole (`ip -6 rule add priority 9000 unreachable`). On a cold boot that rule lands before networkd's Router Solicitation is answered, so the host can never send RS/NS, never gets a GUA, and mihomo never re-checks: self-locking until the core restarts. The env var forces the TUN v6 address up, which installs mihomo's normal v6 rules whose `lookup main suppress_prefixlength 0` keeps on-link neighbor discovery and the RA default route off the TUN | Drop once mihomo either re-runs the IPv6 check or skips the v6 strict-route blackhole when the v6 TUN is disabled | 2026-10-10 | on a mihomo bump | active |
If WA-008 freezes recur, the fallback is to restore
If WA-008 freezes recur, the fallback is to restore the parameters commented out in
`hosts/idols-ai/hardware-configuration.nix` —
`nvme_core.default_ps_max_latency_us=0 nvme_core.io_timeout=4294967295 pcie_aspm=off`; this is a
recovery procedure, not its removal condition.
@@ -71,10 +72,10 @@ A known limitation with no fix yet.
An unfinished feature or gap.
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
| ------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------- | ----------------------- | ------ |
| WA-013 | NixOS tests — `outputs/README.md` | full-host tests still rebuild custom packages (no private cache) and need the host ssh key to decrypt agenix; the shared-firewall security-exporters VM test now runs in CI (KVM) | Resolve when a private cache exists and tests can run per-service | 2024-03-13 | quarterly | active |
| WA-027 | NFS golden store `no_root_squash` — `hosts/12kingdoms-youko/nfs-golden.nix` | the existing PVC subdirectories are root-owned, so enabling `root_squash` would break running workloads until they are migrated; the CSI node plugin is hostNetwork and sources from the allowlisted node IPs | Drop when the PVC subdirectories are migrated and `root_squash` is enabled | 2026-10-07 | next maintenance window | active |
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
| ------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | ---------- | ----------------------- | ------ |
| WA-013 | NixOS tests — `outputs/README.md` | full-host tests still rebuild custom packages (no private cache) and need the host ssh key to decrypt agenix; the shared-firewall security-exporters VM test now runs in CI (KVM); `outputs/x86_64-linux/nixos-tests/idols-ruby.nix` is exposed as `packages.x86_64-linux.ruby-nixos-tests` but is not in `checks` or CI | Resolve when a private cache exists and tests can run per-service | 2024-03-13 | quarterly | active |
| WA-027 | NFS golden store `no_root_squash` — `hosts/12kingdoms-youko/nfs-golden.nix` | the existing PVC subdirectories are root-owned, so enabling `root_squash` would break running workloads until they are migrated; the CSI node plugin is hostNetwork and sources from the allowlisted node IPs | Drop when the PVC subdirectories are migrated and `root_squash` is enabled | 2026-10-07 | next maintenance window | active |
## Related
+5 -5
View File
@@ -102,11 +102,11 @@ recorded in [#428](https://github.com/ryan4yin/nix-config/pull/428).
door, and blast radius for PR bodies.
- **Write a `testing-strategy` skill**: E2E of real workflows first, unit tests for logic and edge
cases E2E cannot reach.
- **Merge `ponytail-review` with a comment and doc pruning pass** (reference:
`MidAutumnMoon/TaysiTsuki` `home/agents/skills/prune-comments-and-docs`). Worth borrowing from
`garrytan/gstack` `review/checklist.md`: a critical pass before an informational one, reading
consumers outside the diff for a new enum value or loosened validator, a do-not-flag list, and
"zero findings is valid". Keep review report-only.
- **Merge `ponytail-review` into the shipped `review-comments-and-docs` skill** (its doc-pruning
half came from `MidAutumnMoon/TaysiTsuki` `home/agents/skills/prune-comments-and-docs`). Worth
borrowing from `garrytan/gstack` `review/checklist.md`: a critical pass before an informational
one, reading consumers outside the diff for a new enum value or loosened validator, a do-not-flag
list, and "zero findings is valid". Keep review report-only.
- **Adapt `wayfinder` in the fork** so its map and tickets default to local Markdown and research
stays local, instead of GitHub issues and pushed `research/` branches.
- **Adapt `domain-modeling` in the fork** to record decisions in the repository's existing documents
+17 -11
View File
@@ -25,7 +25,7 @@ repository already runs** or **unlocks a concrete need**. "It exists and is newe
## A1. Tailscale Serve instead of caddy vhosts
**Current:** one caddy instance on `youko` fronts 12 vhosts on `*.writefor.fun` by Host/SNI, with
**Current:** one caddy instance on `youko` fronts 17 vhosts on `*.writefor.fun` by Host/SNI, with
two TLS chains — a private `ecc-ca` (only repo machines trust it) and a lego/Cloudflare DNS-01
wildcard for phone/TV clients — plus `file_server`, `header_up Host` (SigV4), a wildcard 404
fallback, and a root-readable Cloudflare token in agenix.
@@ -56,8 +56,9 @@ LAN-only client can run Tailscale.
## A2. systemd-resolved
**Current:** `services.resolved.enable` is set **nowhere**; hosts use dhcpcd or static config and
the default `/etc/resolv.conf`. Note `modules/nixos/base/networking/mdns.nix` already sets
**Current:** `services.resolved.enable` is set **nowhere**; hosts get DNS from systemd-networkd or
NetworkManager (or static config) and use the default `/etc/resolv.conf`. Note
`modules/nixos/base/networking/mdns.nix` already sets
`services.resolved.settings.Resolve.MulticastDNS = false` — currently inert, and it will activate
the moment resolved is enabled. mDNS itself is provided by avahi.
@@ -83,14 +84,16 @@ changes.
## A3. systemd-networkd instead of dhcpcd
**Current:** physical hosts use `networking.useDHCP` (dhcpcd script); VM hosts and MicroVM guests
use static config with `useDHCP = false`; the `br0` bridge on VM hosts is declared through
**Current:** dhcpcd is not used anywhere in this repo. `ai`, `kana` and `ruby` already set
`networking.useNetworkd = true`; `shoukei` and `akane` run NetworkManager; VM hosts and MicroVM
guests use static config with `useDHCP = false`. The `br0` bridge on VM hosts is declared through
`systemd.network.netdevs/networks` already.
**Proposed:** `networking.networkd.enable = true` with `networking.networks`/`networkdOptions`.
**Pros:** declarative per-interface config, one implementation for stage 1 and stage 2, no dhcpcd
shell script in the path, and VM hosts already speak `systemd.network` for the bridge.
anywhere in the path (nothing in this repo uses it), and VM hosts already speak `systemd.network`
for the bridge.
**Cons:** bigger blast radius than any other item — a mistake on a `br0` VM host cuts the guests and
possibly remote reachability; migration touches every DHCP host; behaviour differences around
@@ -152,8 +155,10 @@ the gain over a working `zramSwap` is mostly cosmetic.
## A6. nixos-facter instead of hand-written hardware-configuration.nix
**Current:** 8 hosts carry hand-written `hardware-configuration.nix` (plus deliberate per-host
quirks such as the NVMe APST workaround WA-008 and USB-SATA mitigations WA-009).
**Current:** 3 hosts carry hand-written `hardware-configuration.nix` (`shoryu`, `shoukei`, `ai`;
`shushou` and `youko` import `shoryu`'s, and the qemu guests use the shared
`modules/nixos/server/qemu-guest-hardware-configuration.nix`), plus deliberate per-host quirks such
as the NVMe APST workaround WA-008 and USB-SATA mitigations WA-009.
**Proposed:** `hardware.facter` from nixpkgs (nixos-facter-modules upstreamed), report generated on
the machine.
@@ -294,8 +299,9 @@ validates); no benefit for hosts without Secure Boot.
## B3. Declarative images with systemd-repart
**Current:** guest images come from the existing pipeline (nixos-generators / disko-based); k8s
guests are MicroVMs booting declared state images.
**Current:** libvirt guest images are one-time reflink copies of the VMs' former KubeVirt disks
(`hosts/12kingdoms-shoryu/libvirtd.nix`); k8s guests are MicroVMs booting declared state images
against a read-only host store, with no OS image built at all.
**Proposed:** `image.repart.enable` + `system.build.images.{qemu,raw-efi,raw,sd-card}`.
@@ -479,7 +485,7 @@ only.
| B5 | sysupdate | | | |
| B6 | measured boot / FIDO2 | | | |
| B7 | systemd-vmspawn | | | |
| C1–C4 | tooling + cleanups | | | |
| C1–C5 | tooling + cleanups | | | |
| D1–D7 | lab-only group | | | |
| E | rejected set | | | |
+2 -2
View File
@@ -72,8 +72,8 @@ hardening/
- https://gitlab.com/apparmor/apparmor/-/wikis/Documentation
- AppArmor.d is a set of over 1500 AppArmor profiles whose aim is to confine most Linux based
applications and processes.
- But all the profiles of AppArmor assume a FHS filesystem, which caused all apparmor policies
takes no effect on NixOS.
- But its profiles assume an FHS filesystem, so most of them never match the Nix store paths a
NixOS system actually executes.
- Apparmor on NixOS Roadmap:
- https://discourse.nixos.org/t/apparmor-on-nixos-roadmap/57217
- https://github.com/LordGrimmauld/aa-alias-manager
+1 -1
View File
@@ -3,7 +3,7 @@
...
}:
{
# AppArmor in complain mode: log violations instead of blocking.
# AppArmor policy for D-Bus.
services.dbus.apparmor = "enabled";
security.apparmor = {
+2 -1
View File
@@ -36,7 +36,8 @@ appimageTools.wrapAppImage {
substituteInPlace $out/share/applications/wechat.desktop --replace-fail AppRun wechat
'';
# Add these root paths to FHS sandbox to prevent WeChat from accessing them by default
# Prepare the WeChat data dirs before the sandbox starts. The isolation itself
# is the `--tmpfs /home` / `--tmpfs /root` in `extraBwrapArgs` below.
# Adapted from https://aur.archlinux.org/cgit/aur.git/tree/wechat-universal.sh?h=wechat-universal-bwrap
extraPreBwrapCmds = ''
XDG_DOCUMENTS_DIR="''${XDG_DOCUMENTS_DIR:-$(xdg-user-dir DOCUMENTS)}"
+1 -1
View File
@@ -78,7 +78,7 @@ let
# Shared NSS client-certificate store (imported outside Firefox).
(sloth.concat' sloth.homeDir "/.pki/nssdb")
# Unsure
# dconf settings. Not verified as needed; candidate for removal.
(sloth.concat' sloth.xdgConfigHome "/dconf")
];
};
+1 -1
View File
@@ -15,7 +15,7 @@ home/
│ │ └── ...
│ └── ...
├── hosts/ # Host-specific home manager entry modules
│ ├── linux/ # Linux host home modules (idols-ai, 12kingdoms-shoukei, idols-kana, idols-ruby, k3s-test-1-worker-*, etc.)
│ ├── linux/ # Linux host home modules (idols-ai, 12kingdoms-shoukei, idols-kana, idols-ruby)
│ └── darwin/ # macOS host home modules (fern, frieren)
└── darwin/ # macOS-specific home manager configurations
├── proxy/ # Proxy configurations
+1 -1
View File
@@ -38,7 +38,7 @@ Run `:tutor` inside Helix, or `hx --tutor` from the shell
### Terminal related
tuios shortcuts used often (leader is `Ctrl + B`; same idea as in the Neovim notes):
tuios shortcuts used often (leader is `Ctrl + B`):
| Action | tuios shortcut |
| ----------------- | -------------------------- |
+1 -1
View File
@@ -7,7 +7,7 @@
min-release-age=2
'';
# npm - set min release age (in minutes) for supply-chain security
# pnpm - same supply-chain policy as the npm setting above; its value is in minutes, not days
xdg.configFile."pnpm/config.yaml".text = ''
minimumReleaseAge: 2880
'';
-1
View File
@@ -22,7 +22,6 @@ in
# `work` for the ~/work tree, `personal` for everything else (a host with no
# ~/work gets `personal` everywhere).
programs.nushell.extraConfig = ''
# auto start tuios
if $nu.is-interactive and (not ("TUIOS_SESSION" in $env)) {
# Exit 3 means the daemon is stopped; stdout still lists saved sessions.
let listing = (^tuios ls --json | complete)
+1 -2
View File
@@ -83,8 +83,7 @@ Noctalia consolidates functionality that previously required multiple tools:
- **[`desktop-tools.nix`](./desktop-tools.nix)**: Wayland session tools (clipboard, brightness,
screen recording, auto-mount, virt-manager/virt-viewer, `wlogout` emergency fallback)
- **[`browsers.nix`](./browsers.nix)**: Web browsers
- **[`vscode.nix`](./vscode.nix)**: VS Code (GUI editor; the primary Zed config is shared from
[`home/base/gui/zed-editor.nix`](../../../base/gui/zed-editor.nix))
- **[`vscode.nix`](./vscode.nix)**: VS Code
- **[`media.nix`](./media.nix)**: Media players
- **[`gaming.nix`](./gaming.nix)**: Gaming applications
- **[`creative.nix`](./creative.nix)**: Creative software
+2 -2
View File
@@ -20,8 +20,8 @@
# discord # update too frequently, use the web version instead
];
# allow fontconfig to discover fonts and configurations installed through home.packages
# Install fonts at system-level, not user-level
# Fonts install at system level, not user level, so fontconfig does not
# scan home.packages for them.
fonts.fontconfig.enable = false;
}
+1 -1
View File
@@ -1,4 +1,4 @@
# XDG stands for "Cross-Desktop Group", with X used to mean "cross".
# XDG stands for "X Desktop Group", the former name of freedesktop.org.
# It's a bunch of specifications from freedesktop.org intended to standardize desktops and
# other GUI applications on various systems (primarily Unix-like) to be interoperable:
# https://www.freedesktop.org/wiki/Specifications/
+3 -5
View File
@@ -88,13 +88,11 @@ in
"/home/${username}/.terraform.d".d = permission;
};
# systemd-machine-id-commit.service would fail but it is not relevant
# in this specific setup for a persistent machine-id so we disable it
#
# see the firstboot example below for an alternative approach
# Mask the commit service: /etc/machine-id is persisted by preservation
# (see the `files` entry above), so there is no transient ID to commit.
systemd.suppressedSystemUnits = [ "systemd-machine-id-commit.service" ];
# let the service commit the transient ID to the persistent volume
# Inert while the unit is masked above; kept as the alternative approach.
systemd.services.systemd-machine-id-commit = {
unitConfig.ConditionPathIsMountPoint = [
""
@@ -47,7 +47,8 @@ in
})
];
# supported file systems, so we can mount any removable disks with these filesystems
# supported file systems, so we can mount any removable disks with these filesystems.
# mkForce replaces the whole list, so entries added by any other module are lost here.
boot.supportedFilesystems = lib.mkForce [
"ext4"
"btrfs"
@@ -57,10 +57,10 @@ in
keyFile = cryptKeyFile;
# The maximum size of the keyfile is 8192 KiB
# type `cryptsetup --help` to see the compiled-in key and passphrase maximum sizes
# to generate a key file:
# The key file must be at least keyFileOffset + keyFileSize bytes.
# dd bs=512 count=1024 iflag=fullblock if=/dev/random of=./hdd-luks-crypt-key
keyFileSize = 512 * 64; # match the `bs * count` of the `dd` command
keyFileOffset = 512 * 128; # match the `bs * skip` of the `dd` command
keyFileSize = 512 * 64;
keyFileOffset = 512 * 128;
fallbackToPassword = true;
allowDiscards = true;
};
@@ -95,6 +95,8 @@ document is the agreed design and the rationale.
## Rollout
Applied — immich runs on youko. The steps below are the procedure as deployed:
1. `../valkey.nix`: the shared Valkey instance.
2. `./default.nix`: the `services.immich` configuration (server, database, machine-learning,
tmpfiles).
@@ -30,15 +30,15 @@ in
# HDR/DoVi transcoding needs OpenCL for `tonemap_opencl`. With only Mesa
# installed, ffmpeg cannot create an OpenCL device (`Failed to get number of
# OpenCL platforms: -1001`) and the transcode aborts with exit code 237, so
# HDR titles fail to play. ROCm's clr provides the ICD for the Barceló iGPU
# (reported as `gfx90c`); `ocl-icd` finds it under
# HDR titles fail to play. ROCm's clr provides the ICD for the AMD Radeon iGPU
# (Vega, `gfx90c`); `ocl-icd` finds it under
# /run/opengl-driver/etc/OpenCL/vendors.
hardware.graphics.extraPackages = [ pkgs.rocmPackages.clr.icd ];
services.jellyfin = {
enable = true;
# AMD Barceló iGPU: VA-API hardware transcoding. renderD128 is world
# AMD Radeon iGPU (Vega, `gfx90c`): VA-API hardware transcoding. renderD128 is world
# readable/writable, and the user is in `render`/`video` as well.
hardwareAcceleration = {
enable = true;
@@ -55,7 +55,7 @@ in
# for decode/scale and falls back to the CPU `libx264`, which pins
# several cores per stream.
enableHardwareEncoding = true;
# Vega (Barceló) decodes these.
# Vega decodes these.
hardwareDecodingCodecs = {
h264 = true;
hevc = true;
@@ -119,7 +119,7 @@
chat_id = 586169186; # My Telegram ID
# Whether to notify about resolved alerts.
send_resolved = true;
# Disable notifications for resolved alerts
# Not disabled: resolved alerts are still notified (see send_resolved).
disable_notifications = false;
# Telegram's MarkdownV2 & Markdown are all very painful, we use html instead.
# https://core.telegram.org/bots/api#formatting-options
+7 -6
View File
@@ -20,9 +20,9 @@ Named after characters from "Oshi no Ko":
On 2026-04-27 `ai` was rebuilt on a new platform. The MSI board and i5-13600KF it was added with in
2023-05 (the host was originally named `msi-rtx4090`) gave way to the Colorful CVN Z890 ARK FROZEN +
Intel Core Ultra 7 270K Plus. The traces in the history are PR #257 (the NIC moved from `enp5s0` to
`enp130s0`) and PR #258 (Niri output names renamed, `hardware-intel.nix` added with the Arrow Lake
NPU, kernel moved to `linuxPackages_latest`). The 2×48G DDR5 kit was already in place at that point;
the repo has no record of when it went in.
`enp130s0`) and PR #258 (Niri output names renamed, `hardware-intel.nix` added). The kernel moved to
`linuxPackages_latest` in `77ca37ea`, the same day but outside #258. The 2×48G DDR5 kit was already
in place at that point; the repo has no record of when it went in.
On 2026-10-06 `ai` grew from 2×48G (96G) to 2×48G + 2×16G (128G) across all four DIMM slots, at
DDR5-4800 with XMP off — the only stable setting with four DIMMs on this board. The 16G pair must be
@@ -71,7 +71,8 @@ disks.
#### `k8s` - Kubernetes Infrastructure
- **VM Cluster**: 3 physical mini PCs (shoryu, shushou, youko) running all VMs
- **VM Cluster**: 3 physical mini PCs (shoryu, shushou, youko) run the k3s VMs; `akane` is a NixOS
VM under UTM on macOS
- **K3s Testing**: `k3s-test-1` control plane and workers, running as microVMs; placement and
rollout are documented in [`k8s/README.md`](./k8s/README.md)
@@ -122,8 +123,8 @@ just build-host <host>
```
`eval-host` only evaluates the configuration. `build-host` builds the system closure without
activating it. Host activation remains an explicit Colmena operation such as `just shoryu`,
`just shushou`, or `just youko`.
activating it. Host activation is always explicit: Colmena (`just shoryu`, `just shushou`,
`just youko`) from another machine, or `just local` on the machine itself.
## Distributed Building
+1 -1
View File
@@ -1,7 +1,7 @@
{ pkgs, ... }:
#############################################################
#
# Fern - MacBook Pro 2024 14-inch M4 Pro 48G, mainly for business.
# Frieren - MacBook Pro 2024 14-inch M4 Pro 48G, mainly for business.
#
#############################################################
let
+4 -4
View File
@@ -6,10 +6,10 @@ to run, but compatibility still depends on the game and translation layer.
## First activation and persistence
Run `just niri` on idols-ai from this branch's worktree, or after merging it. Log out and back in so
Steam sees `PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES=1`. That variable is what makes pressure-vessel
mount WiVRn's `$XDG_RUNTIME_DIR/wivrn/comp_ipc` socket into a game's container; without it the
game's OpenXR client fails with `ERROR_RUNTIME_UNAVAILABLE` and the headset stays black.
Run `just niri` on idols-ai. Log out and back in so Steam sees
`PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES=1`. That variable is what makes pressure-vessel mount
WiVRn's `$XDG_RUNTIME_DIR/wivrn/comp_ipc` socket into a game's container; without it the game's
OpenXR client fails with `ERROR_RUNTIME_UNAVAILABLE` and the headset stays black.
The root filesystem is a 4 GiB tmpfs. These directories are preserved with mode 0700:
+6 -4
View File
@@ -41,7 +41,8 @@ in
];
# Zram consumes physical memory for compression, which can cause a deadlock and system hang if the model size approaches the physical memory limit.
# Disable the whole module (zram device + its swappiness=180 sysctl tunings); this host uses a disk swapfile instead.
# Disable the whole module (zram device + its swappiness=180 sysctl tunings); this host uses a
# disk swapfile instead, created by disko at /swap (see ./disko-fs.nix). The two must move together.
modules.zram.enable = false;
# zswap: compressed writeback cache in front of the disk swapfile.
@@ -93,8 +94,9 @@ in
networking = {
inherit hostName;
# we use networkd instead
networkmanager.enable = false; # provides nmcli/nmtui for wifi adjustment
# off: this host uses networkd (below). NetworkManager would otherwise
# provide nmcli/nmtui for wifi adjustment.
networkmanager.enable = false;
useDHCP = false;
};
@@ -117,7 +119,7 @@ in
];
DNS = nameservers;
DHCP = "ipv6"; # enable DHCPv6 only, so we can get a GUA.
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguraton (SLAAC)
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguration (SLAAC)
LinkLocalAddressing = "ipv6";
# r8169 drops carrier on S3 resume; without this, networkd tears down the
# static address/route and mihomo's TUN auto-detect briefly loses its
+3 -5
View File
@@ -469,13 +469,11 @@ in
"D! /var/lib/systemd/coredump 0755 root root"
];
# systemd-machine-id-commit.service would fail but it is not relevant
# in this specific setup for a persistent machine-id so we disable it
#
# see the firstboot example below for an alternative approach
# Mask the commit service: /etc/machine-id is persisted by preservation
# (see the `files` entry above), so there is no transient ID to commit.
systemd.suppressedSystemUnits = [ "systemd-machine-id-commit.service" ];
# let the service commit the transient ID to the persistent volume
# Inert while the unit is masked above; kept as the alternative approach.
systemd.services.systemd-machine-id-commit = {
unitConfig.ConditionPathIsMountPoint = [
""
-3
View File
@@ -3,9 +3,6 @@
...
}:
{
# Part 1 of the backup plan: a local restic repository on this machine.
# The off-machine target (youko, or a cloud) is a later step.
#
# Be specific here and keep `exclude` minimal: an allowlist of the
# irreplaceable directories beats backing up a broad tree and excluding half
# of it. Large files anywhere are skipped by size (excludeLargerThan), which
+2 -2
View File
@@ -11,8 +11,8 @@
"auto" # Mount automatically with -a
# Ownership mapping - all files appear as specified user/group
"uid=1000" # Map to user ID 1000
"gid=1000" # Map to group ID 100
"uid=1000"
"gid=1000"
];
};
}
+3 -1
View File
@@ -52,10 +52,12 @@ in
matchConfig.Name = [ iface ];
networkConfig = {
Address = [ ipv4WithMask ];
# DNS goes to the proxy gateway, which resolves through the proxy;
# `nameservers` is the plain upstream-DNS alternative.
# DNS = nameservers;
DNS = [ proxyGateway ];
DHCP = "ipv6"; # enable DHCPv6 only, so we can get a GUA.
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguraton (SLAAC)
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguration (SLAAC)
LinkLocalAddressing = "ipv6";
};
routes = [
+3 -1
View File
@@ -61,10 +61,12 @@ in
matchConfig.Name = [ iface ];
networkConfig = {
Address = [ ipv4WithMask ];
# DNS goes to the proxy gateway, which resolves through the proxy;
# `nameservers` is the plain upstream-DNS alternative.
# DNS = nameservers;
DNS = [ proxyGateway ];
DHCP = "ipv6"; # enable DHCPv6 only, so we can get a GUA.
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguraton (SLAAC)
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguration (SLAAC)
LinkLocalAddressing = "ipv6";
};
routes = [
@@ -7,25 +7,8 @@
{
imports = mylib.scanPaths ./.;
# environment.systemPackages = with pkgs; [
# podman-compose
# ];
virtualisation = {
docker.enable = true;
# podman = {
# enable = true;
# # Create a `docker` alias for podman, to use it as a drop-in replacement
# dockerCompat = true;
# # Required for containers under podman-compose to be able to talk to each other.
# defaultNetwork.settings.dns_enabled = true;
# # Periodically prune Podman resources
# autoPrune = {
# enable = true;
# dates = "weekly";
# flags = [ "--all" ];
# };
# };
oci-containers = {
backend = "docker";
+2 -2
View File
@@ -31,8 +31,8 @@ let
# k3sExtraArgs = [
# # IPv4 Private CIDR(full) - 172.16.0.0/12
# # IPv4 Pod CIDR(full) - fdfd:cafe:00:0000::/64 ~ fdfd:cafe:00:7fff::/64
# # IPv4 Service CIDR(full) - fdfd:cafe:00:8000::/64 ~ fdfd:cafe:00:ffff::/64
# # IPv6 Pod CIDR(full) - fdfd:cafe:00:0000::/64 ~ fdfd:cafe:00:7fff::/64
# # IPv6 Service CIDR(full) - fdfd:cafe:00:8000::/64 ~ fdfd:cafe:00:ffff::/64
# "--cluster-cidr=172.18.0.0/16,fdfd:cafe:00:0002::/64"
# "--service-cidr=172.19.0.0/16,fdfd:cafe:00:8002::/112"
# ];
+1 -1
View File
@@ -9,7 +9,7 @@
etcSize ? 64,
varSize ? 4096,
homeSize ? 4096,
# extra stateful mounts (e.g. aquamarine's /data on the passed-through HDDs)
# extra stateful mounts, e.g. a guest's /data on passed-through HDDs
extraVolumes ? [ ],
...
}:
+6 -4
View File
@@ -23,9 +23,10 @@ in
"nfs"
];
# KVM for the microVMs and the libvirt domains.
# KVM for the microVMs and the libvirt domains. AMD-specific: an Intel VM host
# would need kvm-intel instead (see hosts/idols-ai/hardware-intel.nix).
boot.kernelModules = [ "kvm-amd" ];
boot.extraModprobeConfig = "options kvm_amd nested=1"; # for amd cpu
boot.extraModprobeConfig = "options kvm_amd nested=1";
boot.kernel.sysctl = {
# --- filesystem --- #
@@ -92,10 +93,11 @@ in
matchConfig.Name = [ "br0" ];
networkConfig = {
Address = [ ipv4WithMask ];
# DNS = nameservers;
# DNS goes to the proxy gateway, which resolves through the proxy; plain
# upstream DNS would be myvars.networking.nameservers.
DNS = [ proxyGateway ];
DHCP = "ipv6"; # enable DHCPv6 only, so we can get a GUA.
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguraton (SLAAC)
IPv6AcceptRA = true; # for Stateless IPv6 Autoconfiguration (SLAAC)
LinkLocalAddressing = "ipv6";
};
routes = [
+5 -4
View File
@@ -102,8 +102,9 @@ Platform-specific NixOS configuration:
## Usage
Modules are imported based on platform detection:
There is no automatic platform detection. Each host's entry under `outputs/<system>/src/` lists its
imports explicitly:
- **NixOS Systems**: Import `nixos/` modules
- **macOS Systems**: Import `darwin/` modules
- **All Systems**: Import `base/` modules for shared configuration
- **NixOS Systems**: import `nixos/` modules
- **macOS Systems**: import `darwin/` modules
- **Both**: import `base/` modules for shared configuration
+1 -2
View File
@@ -4,8 +4,7 @@
users.users."${myvars.username}" = {
home = "/Users/${myvars.username}";
# set user's default shell back to zsh
# `chsh -s /bin/zsh`
# The login shell is set imperatively (`chsh -s /bin/zsh`), not declaratively here.
# DO NOT change the system's default shell to nushell! it will break some apps!
# It's better to change only kitty/wezterm & other terminal emulator's shell to nushell!
};
+1 -1
View File
@@ -14,7 +14,7 @@ let
lanAddr = if hostAddr.homeOnly or false then null else hostAddr.ipv4 or null;
in
{
# enable the node exporter on all nixos hosts
# node_exporter on every host that has a static LAN IPv4 (see `lanAddr` above)
# https://github.com/NixOS/nixpkgs/blob/nixos-26.05/nixos/modules/services/monitoring/prometheus/exporters/node.nix
services.prometheus.exporters.node = lib.mkIf (lanAddr != null) {
enable = true;
+1 -1
View File
@@ -11,7 +11,7 @@
# Secure by default: X11 forwarding off everywhere; desktops re-enable it
# in modules/nixos/desktop/ssh.nix (needed for GUI forwarding).
X11Forwarding = lib.mkDefault false;
# root user is used for remote deployment, so we need to allow it
# root login stays available for Colmena/MicroVM deployment, keys only
PermitRootLogin = lib.mkDefault "prohibit-password";
PasswordAuthentication = false; # disable password login
};
+1 -1
View File
@@ -29,7 +29,7 @@
# misc
uinput = { };
# shared group for services that read/write the same data directory
# (e.g. sftpgo + transmission on aquamarine)
# (e.g. sftpgo + transmission on youko)
fileshare = { };
};
+2 -3
View File
@@ -54,9 +54,8 @@ in
"vm.watermark_boost_factor" = lib.mkDefault 0;
# vm.watermark_scale_factor - Controls kswapd wakeup frequency (range: 1-1000, default: 10)
# A higher value triggers background memory reclamation earlier (at 12.5% memory pressure).
# Value 125 means kswapd becomes active when free memory drops below 1/125 of total memory,
# balancing memory more proactively to prevent sudden swap storms at high swappiness values.
# Units are 1/10000 of total memory: the value below widens the low-to-min watermark gap and
# makes kswapd reclaim earlier, which avoids swap storms at the high swappiness above.
"vm.watermark_scale_factor" = lib.mkDefault 125;
# vm.page-cluster - Controls swap readahead (range: 0-6, default: 3)
+1 -1
View File
@@ -46,7 +46,7 @@ in
enable = true;
settings = {
default_session = {
# Wayland Desktop Manager is installed only for user ryan via home-manager!
# The Wayland session is installed for this user only, via home-manager.
user = myvars.username;
# .wayland-session is a script generated by home-manager, which links to the current wayland compositor(sway/hyprland or others).
# with such a vendor-no-locking script, we can switch to another wayland compositor without modifying greetd's config here.
+2 -1
View File
@@ -9,7 +9,8 @@
# security with gnome-kering
services.gnome = {
gnome-keyring.enable = true;
# Use gnome keyring's SSH Agent
# Not gcr's SSH agent: OpenSSH's own agent runs instead
# (programs.ssh.startAgent below).
# https://wiki.gnome.org/Projects/GnomeKeyring/Ssh
gcr-ssh-agent.enable = false;
};
-2
View File
@@ -10,8 +10,6 @@
#
# We write our hardware-configuration.nix, so that we can do some customization more easily.
#
# the url above is used by `nixos-generator` to generate the guest's qcow2 image file.
#
##############################################################################
{
imports = [
+6 -4
View File
@@ -249,8 +249,8 @@ cd ~/nix-config/nixos-installer/
rm -rf /mnt/etc
# install nixos
# NOTE: --no-root-password leaves root locked; log in as ryan with the password from
# vars/default.nix
# NOTE: --no-root-password leaves root locked; log in as ryan with the password whose hash is
# `initialHashedPassword` in vars/default.nix
nixos-install --root /mnt --flake .#shoukei --no-root-password --show-trace --verbose # install-1
# if you want to use a cache mirror, run this command instead
@@ -259,7 +259,8 @@ nixos-install --root /mnt --flake .#shoukei --no-root-password --show-trace --ve
# enter into the installed system, check password & users
# `su ryan` => `sudo -i` => enter ryan's password => successfully login
# if login failed, check ryan's initial password in vars/default.nix, and try again
# if login failed, check that the password you typed is the one hashed by
# `initialHashedPassword` in vars/default.nix, and try again
nixos-enter
@@ -327,7 +328,8 @@ cd ~/nix-config
just niri
```
Finally, to enable secure boot, follow the instructions in
Secure Boot is not configured for `shoukei` (Apple Silicon, Asahi firmware). The links below are the
setup for `ai`, kept here as a reference:
[lanzaboote - Quick Start](https://github.com/nix-community/lanzaboote/blob/master/docs/QUICK_START.md)
and
[hosts/idols-ai/secureboot.nix](https://github.com/ryan4yin/nix-config/blob/main/hosts/idols-ai/secureboot.nix)
+7 -3
View File
@@ -44,7 +44,8 @@ suite passes when every `expr` equals its `expected`.
#### Which tests cover a new host
Some tests iterate over every configuration, so a new host is covered automatically and must satisfy
them. Others name specific hosts and only cover a new host if you add it.
them. Others name specific hosts — or a name prefix, like `k3s-api-endpoint`, which covers every
`k3s-test-1-*` configuration — and only cover a new host if you add it.
| Kind | Tests |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -71,8 +72,11 @@ Problems:
agenix secrets.
- [ ] Maybe it's better to test every service separately, not the whole host?
No host defines a NixOS integration test yet: full-host tests are tracked as WA-013 in
[WORKAROUNDS.md](../WORKAROUNDS.md). What runs today is the eval set above, with `just test`.
Full-host tests are tracked as WA-013 in [WORKAROUNDS.md](../WORKAROUNDS.md).
`outputs/x86_64-linux/nixos-tests/idols-ruby.nix` is exported as
`packages.x86_64-linux.ruby-nixos-tests`, but it is not a `checks` entry, so CI never builds it and
nothing else runs it. What runs today is the eval set above with `just test`, plus one NixOS-driver
check of the shared firewall — `checks.x86_64-linux.security-exporters`, built in CI.
## Overview
+2 -1
View File
@@ -142,7 +142,8 @@ in
# Packages
packages = forAllSystems (system: allSystems.${system}.packages or { });
# Eval Tests for all NixOS & darwin systems.
# Eval Tests for all NixOS & darwin systems. Each system's `evalTests` lists its
# failures, so an empty attrset means that system passed.
evalTests = lib.lists.all (it: it.evalTests == { }) allSystemValues;
checks = forAllSystems (
+2 -1
View File
@@ -65,7 +65,8 @@ Placing a secret under `/etc/agenix/` with `environment.etc` has one trap: setti
`environment.etc` **copy** the file instead of symlinking it, and the copy is owned by root unless
`user` is set too. Widening the mode so the user can read a root-owned copy makes it readable by
every local account. Always set `user` together with `mode`. nix-darwin ignores both on
`environment.etc`, so `secrets/darwin.nix` chowns `/etc/agenix/*` after activation instead.
`environment.etc`, so `secrets/darwin.nix` re-applies each secret's owner and mode after activation;
a blanket chown over `/etc/agenix/*` would make the root-only secrets readable by the login user.
## Adding or Updating Secrets