Skip to content
shaoyanjiPublic

About

My Nix Configurations for darwin, nixos, home-manager, and WSL

Resources

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

1,985 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nixconfig

Multi-host Nix flake for NixOS, nix-darwin, Home Manager, and WSL-style container hosts.

Layout

  • flake/*: flake outputs and wiring.
  • hosts/*: host entrypoints plus local identity, storage, and networking.
  • modules/profiles/* and modules/services/*: canonical reusable host and service logic.
  • modules/user/*, modules/roles/*, and modules/shell/*: user and role commitments.
  • pkgs/*: package definitions.
  • docs/*: operator and architecture references.

Quick Start

  1. Prerequisites: NixOS/Darwin system with flakes enabled
  2. Clone repo: git clone <repo-url> && cd nixconfig
  3. List tasks: task --list-all to see all available tasks
  4. Build a host: task infra:plan:host:frieren
  5. Deploy: task infra:deploy:host:frieren (plan + apply + validate)

Module chains

Module chains compose as follows:

globalModulesNixos      → global → nixos → home-manager-shared → role:heim
globalModulesImpermanence → globalModulesNixos → +impermanence module
globalModulesContainers  → global → noDE (lean home-manager, no dms/niri)
globalModulesMacos       → global → macos (nix-darwin, no dms/niri)

base-node.nix (profile) provides the common NixOS baseline: kernel packages, SSH, keyd, networkmanager, console, sops, user devji, common dev packages, and boot loader defaults (systemd-boot + EFI). Container and desktop hosts import it via globalModulesContainers or desktop-client.nix. base-node.nix also imports modules/profiles/firewall-baseline.nix, which enables the firewall and only opens TCP/22 by default. Hosts should add service/interface-specific allowances explicitly.

Host Lifecycle

Commission a new host

Prerequisite: Bare-metal bootstrap. If this is a fresh machine (no OS yet), boot a NixOS minimal ISO and run Disko + nixos-install first. See Disko install below. The steps that follow assume NixOS is already booted and the flake is reachable.

1. Create configuration files

hosts/<name>/
├── configuration.nix    # Host-specific: services, networking, power, boot
├── hardware.nix         # Optional: GPU drivers, hardware acceleration, power tweaks
├── hardware-configuration.nix  # Generated by nixos-generate-config (disk layout)
├── <module>.nix         # Optional: host-local service modules

Start with a basic configuration.nix that imports the right module chain from flake/module-sets.nix and sets networking.hostName.

2. Register in the flake

Add an entry to flake/host-inventory.nix:

<name> = {
  kind = "nixos";                    # nixos | darwin | home
  system = "x86_64-linux";           # aarch64-linux, aarch64-darwin, etc.
  specialArgs = {inherit inputs self;};
  modules = globalModulesContainers ++ [../hosts/<name>/configuration.nix];
};

Pick the right module chain:

  • globalModulesNixos — desktop with display manager + niri
  • globalModulesImpermanence — desktop + impermanence + disko
  • globalModulesContainers — headless server (no desktop) — most common for servers
  • globalModulesMacos — nix-darwin on macOS
  • globalModulesHome — standalone home-manager only

3. Add SOPS key

Extract the host's age public key and add it to .sops.yaml so it can decrypt secrets:

# On the new host, after NixOS is installed:
nix-shell -p ssh-to-age --run "cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age"
# Add the output to .sops.yaml under creation_rules and age keys

Then rekey:

task infra:sops:update-keys

4. Discover hardware-specific settings

SSH into the new host and collect:

# NIC name(s)
ip link show
# → note the ethernet interface (e.g. enp1s0, eno1, enp2s0)

# CPU governor support
cpupower frequency-info | grep governor || cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors

# GPU (if applicable)
lspci | grep -i vga

# Storage layout
lsblk

Apply these findings in configuration.nix and hardware.nix:

  • NIC names in firewall rules and service interface bindings
  • GPU drivers for hardware acceleration (QuickSync, NVENC, etc.)
  • Power management (thermald, CPU governor, C-states)
  • Laptop-specific: lid handling, display off, battery conservation mode
Check nixos-hardware for pre-built profiles

This flake already has nixos-hardware as an input (github:NixOS/nixos-hardware/master). Before writing hardware config from scratch, check whether a profile already exists for your CPU/GPU/laptop:

Profile path What it provides Example import
common/cpu/intel/<arch>/ CPU microcode + kernel params common-cpu-intel-kaby-lake (arch examples: kaby-lake, tiger-lake, alder-lake)
common/gpu/intel/<arch>/ GPU driver packages + i915 params common-gpu-intel-kaby-lake (same arch values)
common/pc/laptop/ TLP/power-profiles-daemon default common-pc-laptop
common/pc/ssd/ fstrim service common-pc-ssd
lenovo/thinkpad/<model>/ ThinkPad-specific quirks lenovo-thinkpad-t420, lenovo-thinkpad-t440p (already in use)
lenovo/ideapad/<model>/ Ideapad-specific quirks Check repo listing for your model

How to discover: Browse available profiles at github.com/NixOS/nixos-hardware/tree/master. Module names use hyphens, not slashes (e.g. common-cpu-intel-kaby-lake).

Real example: Host aristotle is a ThinkPad T440 (Haswell i5). There's no dedicated t440 profile, but the similar T440p profile exists at lenovo/thinkpad/t440p/ and provides thinkpad_acpi fan control + Optimus power saving via bbswitch. You'd review the profile, decide which settings apply, and either import it or cherry-pick the relevant kernel params into your local config.

How to use: Add the module to your host's modules list in flake/host-inventory.nix:

modules = globalModulesContainers ++ [
  inputs.nixos-hardware.nixosModules.common-cpu-intel-kaby-lake
  ../hosts/<name>/configuration.nix
];

The profile's settings will merge with your local config — you only need to add override lines for anything you want to change.

Pitfall: nixos-hardware profiles are maintained by the community and may set kernel params or packages you don't need. Review what a profile does before importing it (the files are small — read the default.nix on GitHub). Not all profiles are actively maintained for every kernel version.

5. Build and validate

nix eval .#nixosConfigurations.<name>.config.networking.hostName  # → "<name>"
task infra:plan:host:<name>                                        # Dry-run build
task infra:apply:host:<name>                                       # First deploy (may take a while)

6. Post-deploy validation

ssh <name>  # CA-signed cert should work (inherited from base-node.nix)

# Verify key services are running
sudo systemctl status sshd

# Check hardware-specific settings took effect
# These are model-specific examples — adjust for your hardware:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor      # Should be: powersave
cat /sys/bus/platform/drivers/ideapad_acpi/*/conservation_mode  # Should be: 1 (Ideapad only)

7. CI warm-up (recommended)

Add the host to the CI build matrix so GitHub Actions pre-builds its closure and pushes to cache:

task lifecycle:ci:enable-host:<name>
# Then commit and push the CI workflow change

This avoids hours of compilation on the target machine. Only a small closure (like the main server) is enabled by default — new hosts are commented out until you're ready to CI-build them.

Only frieren (the NAS) is actively built in CI by default. Most hosts are commented out in the machine: list to save CI minutes.

To check what's currently enabled:

task lifecycle:ci:list

Decommission a host

When retiring a host (e.g. thinsandy → frieren), the goal is to leave zero references to the old host so the flake evaluates cleanly without it.

1. Audit dependencies

Cast a wide net first to catch every reference to the old host across all file types:

grep -rn "<old-name>" . --include='*.nix' --include='*.md' --include='*.yml' --include='*.yaml'

Then drill into nix import paths specifically to find dependency links:

grep -rn "hosts/<old-name>/" flake/ hosts/ --include='*.nix'

Key places to inspect:

  • CI workflows (.github/workflows/*.yml)
  • Deploy docs (.agents/deploy/hosts/<old-name>.md — delete if exists)
  • Agent skills (.agents/skills/*/SKILL.md)
  • Fleet pattern docs (docs/)
  • AGENTS.md, README.md, HANDOFF.md (if a migration handoff exists)

2. Migrate shared modules

If other hosts borrow modules from the old host (e.g. dns.nix, media-stack.nix), copy them to a home that matches their current owner:

cp hosts/<old-name>/<module>.nix hosts/<new-name>/
# Also copy any local imports the module references

3. Update import paths

Update configuration.nix on borrowing hosts to point to the new location:

# Before:
../../hosts/<old-name>/<module>.nix

# After:
./<module>.nix    # if moved to the same host dir

4. Fix hardware-specific traps

Copied modules often contain hardware assumptions from the old host:

What to check Why
NIC names (eno1, enp2s0, etc.) Old host's ethernet interface name hardcoded in firewall rules or service binds
CPU-specific kernel params e.g. intel_idle.max_cstate — safe to keep but verify
GPU driver packages Different GPUs need different hardware.graphics.extraPackages
Storage paths Bind mounts, data dirs, file system UUIDs

5. Remove from flake inventory

Delete the old host's entry from flake/host-inventory.nix. Keep the list alphabetically sorted.

6. Delete old host directory

rm -rf hosts/<old-name>

7. Clean up stale references

Artifact Action
.agents/deploy/hosts/<old-name>.md Delete — deploy notes for a host that no longer exists
.agents/skills/*/SKILL.md Remove any <old-name> from task tables (smoke checks, deploy aliases, etc.)
docs/*fleet-pattern.md (or similar) Remove from Current Hosts lists and validation examples
docs/codex-handoff.md Remove or update if the old host was mentioned
.github/workflows/nixcachix.yml Disable via task lifecycle:ci:disable-host:<old-name> (comments it out) — or remove manually
AGENTS.md Update CI pipeline section, module chain tables if stale
HANDOFF.md (if it exists for this migration) Mark migration complete, update module paths
README.md Update Quick Start examples, hosts table, and NAS client references
.sops.yaml Remove old host's age key entry (not strictly necessary — extra keys don't break anything — but keeps the file clean)

8. Validate

nix eval .#nixosConfigurations.<new-name>.config.networking.hostName  # → "<new-name>"
# Also verify the old host name no longer exists in nixosConfigurations:
nix eval '.#nixosConfigurations' --apply 'x: builtins.attrNames x' | grep <old-name>
# should return nothing if decommissioned

Quick-reference checklist

Commissioning

  • Create hosts/<name>/configuration.nix (and hardware-configuration.nix)
  • Register in flake/host-inventory.nix with correct module chain
  • Add age key to .sops.yaml and rekey secrets
  • SSH in, run ip link show to discover NIC name
  • Add NIC-aware firewall rules and service interface bindings
  • Add hardware-specific config: GPU drivers, power management, kernel params
  • Check nixos-hardware for pre-built profiles (CPU, GPU, laptop series)
  • nix eval → host name resolves
  • First deploy via task infra:apply:host:<name>
  • Validate: SSH, governor, services
  • Enable in CI: task lifecycle:ci:enable-host:<name>
  • Check what's enabled: task lifecycle:ci:list

Decommissioning

  • Audit: task lifecycle:decommission:audit:<old-name>
  • Disable in CI: task lifecycle:ci:disable-host:<old-name>
  • Remove deploy doc: task lifecycle:decommission:clean:<old-name> (CI + deploy doc)
  • Migrate borrowed modules to new owner, update import paths
  • Fix hardware-specific traps in copied modules (NIC names, GPU, etc.)
  • Remove old host entry from flake/host-inventory.nix
  • rm -rf hosts/<old-name>
  • Clean up: deploy docs, agent skills, fleet docs, CI, AGENTS.md, README.md
  • nix eval on remaining hosts still passes

Disko install from a NixOS minimal ISO

# Boot the NixOS minimal ISO, fetch your flake, and partition:
sudo nix run github:nix-community/disko -- --mode disko --flake /path/to/flake#hostname
# Then install:
sudo nixos-install --flake /path/to/flake#hostname
# Reboot into the freshly partitioned system.

Disko handles partitioning, formatting, and mounting — no manual fdisk/mkfs needed. Device paths are parameterised per-host (e.g. disko.nix {device = "/dev/sda";}).

Supported Hosts

Host ollama Role
frieren no ⭐ NAS server (Samba, NFS, Jellyfin, Paperless, DNS)
mtfuji yes Reference AI host (ollama; agent-era modules removed 2026-09)
kellerbench no Gaming backup rig
scratch no Lightweight niri desktop (eisen-style, tmpfs/zram IO diet)
stark no Dell 3477 AIO Steam Big Picture desktop (MX110 Optimus, legacy_580 offload)
fern no HP 15 laptop niri desktop (Ryzen 3 3250U, no Steam, autologin)
deckstation no Steam/gamescope kiosk

deckstation is a pure Steam install — no desktop environment, just greetd + tuigreet dropping into gamescope-session (Steam Big Picture). Uses globalModulesContainers so no dms/niri leaks in. Runs Sunshine GameStream/Moonlight host so any LAN client (phone, laptop, TV box) can launch the big screen remotely. Closure is minimalistic: ROCm/OpenCL compute packages are dropped from the AMD profile since Steam + gamescope only need Mesa + amdgpu.

Per-host quirks and exceptions: .agents/deploy/hosts/*.md

Pinning and updates

nixpkgs and all inputs are pinned via flake.lock. Update intentionally with lockfile bumps (for example nix flake update or targeted input updates), then review and commit flake.lock with the corresponding config changes. Note that a root nix flake update only advances the root inputs — branch-following transitive pins (e.g. impermanence/nixpkgs, dms/dank-qml-common) can stay stale for months. Run task dev:flake:update-transitive (or check first with task checks:flake:transitive) to catch those.

Flake outputs

  • nixosConfigurations
  • darwinConfigurations
  • homeConfigurations
  • packages
  • checks
  • devShells

Canonically assembled from flake/outputs.nix and lib/mk-nixos-host.nix.

Secrets & SSH CA

Secrets use sops-nix. Two separate encrypted files:

File Decryptors Contents
modules/secrets.yaml All hosts (via their ssh_host_ed25519_key age keys in .sops.yaml) App secrets, hashedPassword, API keys
modules/ssh-ca-key.yaml Workstations only (*devji, *sopsposeidon) SSH User CA private key

SSH CA workflow

Instead of managing per-host authorized_keys, servers trust a single CA public key embedded in modules/ssh-ca.nix. Workstations hold the CA private key (decrypted by sops-nix) and sign short-lived certificates.

As a workstation user, daily flow:

# Sign a 1-week cert for yourself
rotate-ssh-cert

# That's it. All hosts that trust the CA accept this cert automatically.
ssh frieren   # or any other host that trusts the CA

The rotate-ssh-cert script is available on any host with ssh.ca.enableClient = true. The cert is cached in ~/.ssh/id_ed25519-cert.pub and used automatically via CertificateFile in the SSH config.

Provisioning a new server

Follow the full Host Lifecycle → Commission guide above. The key SSH CA specifics:

  • ssh.ca.enable = true is inherited from base-node.nix — the CA is trusted automatically on every new host
  • No authorized_keys management needed — just sign a cert on your workstation (rotate-ssh-cert) and SSH in
  • The host's age key (from ssh_host_ed25519_key) must be in .sops.yaml for hashedPassword decryption — see Add SOPS key in the commissioning guide

Provisioning a new workstation

A workstation is any machine you SSH from and sign certificates on. This includes a fresh laptop or a new home-manager-only machine.

  1. Build the flake on the new machine

  2. Add the machine's age public key as a recipient in modules/ssh-ca-key.yaml:

    sops --rotate --add-age <NEW_AGE_KEY> modules/ssh-ca-key.yaml
  3. Commit and push the rekeyed file

  4. Rebuild on the new workstation — sops-nix places ~/.ssh/user_ca_key

  5. Run rotate-ssh-cert to sign your first certificate

Bootstrap caveat

The first SSH connection to a bare-metal machine (before NixOS is installed) is outside the CA model — you still use a USB installer, rescue ISO, or temporary password. The CA eliminates ongoing management once the host is in the fleet.

TOTP secrets with cloak

TOTP tokens are managed via cloak — a CLI OTP authenticator — with the accounts file encrypted in SOPS.

Viewing a TOTP:

task dev:cloak:view          # pick from a list (gum filter)
task dev:cloak:view:github   # or directly by name

Editing TOTP accounts:

task dev:cloak:edit          # opens the full SOPS file, navigate to the `cloak` key

Importing from Bitwarden:

Export from Bitwarden (Settings → Export → Unencrypted JSON .json), then:

task dev:cloak:sync-bw -- ./bitwarden_export.json

This prints the TOML to pipe into sops modules/secrets.yaml --extract '["cloak"]'. The scripts/bw-to-cloak.sh converter is also usable standalone:

./scripts/bw-to-cloak.sh bitwarden_export.json > ~/.cloak/accounts

totp-cli was replaced by cloak — it covers the same use case with simpler TOML storage.

Removing authorized-keys legacy

Once all hosts have been rebuilt with ssh.ca.enable = true, the authorized-keys.nix / authorized-keys.json gist fetch in base-node.nix is dead weight — it falls back silently and can be removed at your leisure.

  • The NAS client recovery profile now lives in modules/profiles/nas-client.nix, which automounts /Volumes/data from the NAS host (frieren, previously thinsandy) for non-NAS hosts so the compatibility path stays available without relying on hosts/common/localmounts.nix.
  • The agent-era tooling (nullclaw, zeroclaw, hermes, xs, pancakes-harness, qwen-code and their modules/scripts/docs) was removed in 2026-09; the git history retains it if anything is ever needed again.
  • The experimental devcontainer configuration was reverted; there is no current repo-provided devcontainer image, so use the Taskfiles, flake outputs, and hosted workflows directly.

Recent changes

Last updated: 2026-09-29

2026-09-29 — stark + fern hosts added; dank-greeter migration; transitive-input sweep. Two new hosts joined the fleet: stark (Dell Inspiron 24 3477 AIO, i5-7200U + MX110 Pascal dGPU on legacy_580 with PRIME render offload, eisen-style Steam Big Picture autologin, dual-disk disko with a 1 TB HDD Steam library) and fern (HP 15 laptop, Ryzen 3 3250U Vega 3, scratch-style niri desktop autologin, no Steam, single-disk disko). The greeter moved upstream from the dms flake to its own dank-greeter repo: all desktop hosts now use inputs.dank-greeter.nixosModules.default → programs.dms-greeter and dms is unpinned again. globalModulesHome gained nixpkgs.config.allowUnfree so standalone HM hosts (alarm/kali) eval like NixOS hosts do. A new task (checks:flake:transitive / dev:flake:update-transitive, backed by scripts/task/flake-transitive-sweep.sh) sweeps flake.lock transitive pins against upstream heads — a root nix flake update leaves those stale. authorized-keys.nix now appends two repo-side keys (bitlockerpremium Windows box + a Bitwarden key) to the fetched gist list.

2026-09-17 — scratch: Steam Remote Play kiosk converted to an eisen-style niri desktop. The Fujitsu ESPRIMO D556 now runs the globalModulesNixos chain (niri + DankMaterialShell greeter, autoLogin, role:heim userland) via base-desktop-environment.nix; Steam, the cage kiosk wrapper and the greetd autologin are gone, while the tmpfs/zram IO diet (zram 100%, journald volatile, ~/.cache on tmpfs, fstrim, gentle writeback sysctls) is kept verbatim for the shaky f2fs SSD. Legacy BIOS boot with GRUB on /dev/sda is unchanged. See .agents/deploy/hosts/scratch.md.

2026-09-17 — verntil/orb-cassini host files removed; thinsandy references purged. Orphaned hosts/verntil.nix and hosts/orb-cassini/ (never registered in the host inventory) were deleted, and remaining live references to the long-decommissioned thinsandy were replaced with frieren (the current NAS): deploy/logs menus, nas-client skip-list, heim's anki sync URL, nixoshmsymlinks skip-list, pi-hole DNS comments, the stale checks:nullclaw:smoke:thinsandy task, and the thinsandy age keys in .sops.yaml. Historical migration notes (HANDOFF.md, AUDIT.md) intentionally keep their thinsandy mentions.

2026-08-03 — Poseidon microVM disabled; frieren daily self-upgrade; scratch real disk UUIDs + GRUB; GC consolidation. The testvm microVM on poseidon no longer runs — the microvm imports and the microvm.vms block in hosts/poseidon/configuration.nix are commented out for easy re-enable (microbr bridge/NAT profile included). frieren (the NAS) now self-upgrades every morning at 04:00 via the canonical system.autoUpgrade module (no hand-rolled timer): it stages nixos-rebuild boot from github:shaoyanji/nixconfig#frieren first, then reboots into the new generation only if kernel/initrd/kernel-modules changed (allowReboot, reached only after a successful boot), otherwise applies a live switch. Persistent timer catches up if the NAS was off. scratch got its real disk UUIDs from the original machine gist (f2fs root + ext4 /boot) and now boots with GRUB (legacy BIOS) instead of systemd-boot; its redundant 14-day GC was dropped in favour of the global 10-day GC. See .agents/deploy/hosts/frieren.md and .agents/deploy/hosts/scratch.md.

2026-09-17 — Agent-era tooling removed. All agent-era modules (ai-host, hermes-defaults, ollama-cloud-defaults, nullclaw/zeroclaw + deployment wrappers, pancakes-harness, hermes-ai-mounts, ai-services-*), packages (nullclaw, pancakes-harness, qwen-code, xs, xs-helper, xs-materializer), their scripts/taskfiles/smoke-checks, and the fleet-pattern docs were deleted. hosts/mtfuji/ai.nix now only mounts the ollama data subvols and enables ollama; poseidon/garnixMachine/kellerbench agent blocks are gone; the nullclawFleetContract check and the xs-helper app output were dropped. The tailscale DNS target moved from the decommissioned thinsandy pi-hole IP to frieren's 100.97.61.65.

2026-04-30 — Task system consolidation. Deprecated legacy task aliases and menus, directing users to new infra: and dev: prefixed tasks. Simplified checks:nullclaw:smoke tasks and enhanced dev:git tasks with AI commit integration.

2026-04-23 — Manifest and dead-code cleanup. The AI-host manifest system (taskfiles/ai-host-manifest.json, scripts/task/ai-host-*.sh, and taskfiles/services-ai-hosts.yml) was removed. Host menus and validation tasks now use static host lists and direct smoke checks. The modules/user/ai/ directory and modules/goodies.nix were also removed; nothing in active host configs imported them. See AUDIT.md for the full decision log.

Documentation

Quick Reference

  • AGENTS.md - Agent routing helpers and task namespace summary
  • docs/task-control-plane.md - Task namespace policy and workflow examples
  • docs/frieren-access.md - frieren service-access runbook (LAN/tailnet matrix, DNS, direct ports)
  • taskfiles/README.md - Taskfile ownership map and shard reference

Deployment

  • .agents/deploy/README.md - Deploy routing and guardrails
  • .agents/deploy/hosts/*.md - Per-host deployment exceptions
  • USB.md - Sledgehammer live USB creation guide

Development

  • docs/codex-handoff.md - Codex session orientation
  • NIX-REFERENCE.md - Nix patterns and gotchas used in this repo
  • docs/userland-module-map.md - Userland module structure and ownership
  • docs/userland-package-ownership.md - Package ownership and role wiring

Historical

  • AUDIT.md - AI module cleanup audit (April 2026)
  • HANDOFF-REFACTOR.md - Refactoring progress (April 2026)
  • TODO.md - Current work tracking and completed tasks

Site Management

Manage site targets with task dev:site:list; build/preview/deploy the default target with task dev:site:build, task dev:site:preview, and task dev:site:deploy.

About

My Nix Configurations for darwin, nixos, home-manager, and WSL

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages