From db63c50a9dd4479942cbb6f546e6909deeb5a85c Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 27 Jul 2026 17:46:37 +0200 Subject: [PATCH 01/19] Add astoria NixOS host + Linux HM sub-bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit astoria: Dell XPS 13 9300 sofa-companion thin-client. Sway + Moonlight + LibreWolf on NixOS 26.11. Lanzaboote (Secure Boot) with TPM-sealed cryptswap, disko-managed LUKS + Btrfs, sops-nix for activation secrets, Restic-to-WebDAV backups. Refactor modules/home to a universal bundle: modules/home/default.nix self-selects the darwin or linux sub-bundle via a `hostSystem` specialArg (a `pkgs`-dependent check in the imports list recurses through `_module.args`). darwin + linux sub-bundles no longer back-reference ../default.nix — the recursive reimport overflows the stack before the module system's genericClosure dedup runs. hosts/{NB2123,makima}/home.nix swap to `imports = [../../modules/home]`. New modules/home/linux/: internet/librewolf, media/moonlight, thunar, window-manager/{sway,waybar,mako,fuzzel,swaylock}. Each gated by home.apps...enable, Tokyo Night palette pulled from theme.nix. Flake inputs: nixos-hardware, disko, sops-nix, nur, lanzaboote (v1.1.0 pinned tag). nixosConfigurations.astoria threads `inputs` through both system specialArgs AND home-manager.extraSpecialArgs (HM's inner scope doesn't inherit system specialArgs; shared modules like dev-shells and claude-code destructure `inputs` at pattern-match). .sops.yaml at repo root with two recipients (astoria host key + personal recovery age key); hosts/astoria/secrets/astoria.yaml encrypted to both. --- .sops.yaml | 15 + flake.lock | 256 +++++++- flake.nix | 97 ++- hosts/NB2123/home.nix | 2 +- hosts/astoria/README.md | 589 ++++++++++++++++++ hosts/astoria/default.nix | 215 +++++++ hosts/astoria/hardware.nix | 193 ++++++ hosts/astoria/home.nix | 58 ++ hosts/astoria/secrets.nix | 46 ++ hosts/astoria/secrets/astoria.yaml | 31 + hosts/astoria/verify-hardware.sh | 75 +++ hosts/makima/home.nix | 4 +- modules/home/darwin/default.nix | 24 +- modules/home/default.nix | 15 +- modules/home/fonts.nix | 3 + modules/home/linux/default.nix | 13 + modules/home/linux/internet/default.nix | 6 + modules/home/linux/internet/librewolf.nix | 38 ++ modules/home/linux/media/default.nix | 6 + modules/home/linux/media/moonlight.nix | 18 + modules/home/linux/thunar.nix | 25 + modules/home/linux/window-manager/default.nix | 12 + modules/home/linux/window-manager/fuzzel.nix | 48 ++ modules/home/linux/window-manager/mako.nix | 34 + modules/home/linux/window-manager/sway.nix | 116 ++++ .../home/linux/window-manager/swaylock.nix | 59 ++ modules/home/linux/window-manager/waybar.nix | 106 ++++ 27 files changed, 2077 insertions(+), 27 deletions(-) create mode 100644 .sops.yaml create mode 100644 hosts/astoria/README.md create mode 100644 hosts/astoria/default.nix create mode 100644 hosts/astoria/hardware.nix create mode 100644 hosts/astoria/home.nix create mode 100644 hosts/astoria/secrets.nix create mode 100644 hosts/astoria/secrets/astoria.yaml create mode 100755 hosts/astoria/verify-hardware.sh create mode 100644 modules/home/linux/default.nix create mode 100644 modules/home/linux/internet/default.nix create mode 100644 modules/home/linux/internet/librewolf.nix create mode 100644 modules/home/linux/media/default.nix create mode 100644 modules/home/linux/media/moonlight.nix create mode 100644 modules/home/linux/thunar.nix create mode 100644 modules/home/linux/window-manager/default.nix create mode 100644 modules/home/linux/window-manager/fuzzel.nix create mode 100644 modules/home/linux/window-manager/mako.nix create mode 100644 modules/home/linux/window-manager/sway.nix create mode 100644 modules/home/linux/window-manager/swaylock.nix create mode 100644 modules/home/linux/window-manager/waybar.nix diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..98d03a2 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,15 @@ +# sops config — maps files → recipients (age keys) for +# encryption/decryption of hosts/*/secrets/*.yaml. +# +# Adding more NixOS hosts later: add another `&` entry keyed to that +# host's SSH host key and another creation_rule scoped to its secrets dir. + +keys: + - &brutcha_recovery age1uvs9kqpzmdty57x5a4m5c2x08nhpfxwhxpy5dp4f5dckykncs49qm538tl + - &astoria age17c3ydfwz2etvrxfyfeemkna4ndgunaqszseyzufm9fmggjm0kdds2fw0mk + +creation_rules: + - path_regex: hosts/astoria/secrets/.*\.yaml$ + age: + - *brutcha_recovery + - *astoria diff --git a/flake.lock b/flake.lock index 56ac3a8..bf14ea7 100644 --- a/flake.lock +++ b/flake.lock @@ -33,6 +33,41 @@ "type": "github" } }, + "crane": { + "locked": { + "lastModified": 1781825982, + "narHash": "sha256-SlXKwIRIhrOSAcTjCB3ftPLzJWZStQIPS7J1FlZPnKk=", + "owner": "ipetkov", + "repo": "crane", + "rev": "469fd08d0bcf6926321fa973c6777fbc87785dd7", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "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" + } + }, "figma-plugin": { "flake": false, "locked": { @@ -49,6 +84,65 @@ "type": "github" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nur", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "lanzaboote", + "pre-commit", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "helium-flake": { "inputs": { "nixpkgs": [ @@ -89,6 +183,30 @@ "type": "github" } }, + "lanzaboote": { + "inputs": { + "crane": "crane", + "nixpkgs": [ + "nixpkgs" + ], + "pre-commit": "pre-commit", + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1782141370, + "narHash": "sha256-hqijVSEETttmo8Okql9/LG0Ua34hdciKW1a5zzlj8mU=", + "owner": "nix-community", + "repo": "lanzaboote", + "rev": "7c9a54a7f87b4539ddbd8bda09a8a5f5f9361aa9", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "v1.1.0", + "repo": "lanzaboote", + "type": "github" + } + }, "nix-darwin": { "inputs": { "nixpkgs": [ @@ -128,7 +246,39 @@ "type": "github" } }, + "nixos-hardware": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1784723954, + "narHash": "sha256-1CfD8ZUjCkTgjsneLZ/lxCHhgDfqxxE7/GX0MmsgiqA=", + "owner": "NixOS", + "repo": "nixos-hardware", + "rev": "a017f5b72210026af5b3ac5949f08d94380a6fbd", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "master", + "repo": "nixos-hardware", + "type": "github" + } + }, "nixpkgs": { + "locked": { + "lastModified": 1767892417, + "narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=", + "rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1784555310, "narHash": "sha256-/FCliTPgiuV1owejZFNx3Ch9irdvkOfOFl+HHZ+DrtM=", @@ -144,15 +294,119 @@ "type": "github" } }, + "nixpkgs_3": { + "locked": { + "lastModified": 1785090369, + "narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "624af665418d3c65d544145b4d34ad696439570e", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nur": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1785145543, + "narHash": "sha256-sYiHA2+r6JDT9/NvN+pwbPm/tr1VSx+a0x3HR5WllVc=", + "owner": "nix-community", + "repo": "NUR", + "rev": "71d05aabaf55a59606d38062a7c87b9c934ec851", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "pre-commit": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "lanzaboote", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781733627, + "narHash": "sha256-U3yTuGBnmXvXoQI3qkpfEDsn9RovQPAjN7ndRco+3u0=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "3bbec39bc90eadfa031e6f3b77272f3f60803e39", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, "root": { "inputs": { "claude-plugins-official": "claude-plugins-official", + "disko": "disko", "figma-plugin": "figma-plugin", "helium-flake": "helium-flake", "home-manager": "home-manager", + "lanzaboote": "lanzaboote", "nix-darwin": "nix-darwin", "nix-homebrew": "nix-homebrew", - "nixpkgs": "nixpkgs" + "nixos-hardware": "nixos-hardware", + "nixpkgs": "nixpkgs_2", + "nur": "nur", + "sops-nix": "sops-nix" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "lanzaboote", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782012058, + "narHash": "sha256-9mWUnReOUXfKjZuJAL/bAFH3LUyECTRtXgSNVjRw3UY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8534567325bd8a8d2928e6afd81e0a87d19efd3c", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "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" } } }, diff --git a/flake.nix b/flake.nix index 80f5caf..1973180 100644 --- a/flake.nix +++ b/flake.nix @@ -48,6 +48,41 @@ url = "github:amaanq/helium-flake"; inputs.nixpkgs.follows = "nixpkgs"; }; + + # --- NixOS-only inputs (used by astoria) --- + + # nixos-hardware — per-model hardware quirks (Dell XPS 13 9300) + # https://github.com/NixOS/nixos-hardware + nixos-hardware.url = "github:NixOS/nixos-hardware/master"; + + # disko — declarative disk partitioning (LUKS containers, Btrfs subvolumes) + # https://github.com/nix-community/disko + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # sops-nix — activation-time secrets, encrypted-in-repo + # https://github.com/Mic92/sops-nix + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # NUR — Nix User Repository, for rycee.firefox-addons (enhanced-h264ify, + # nextcloud-passwords). https://github.com/nix-community/NUR + nur.url = "github:nix-community/NUR"; + + # Lanzaboote — signed systemd-boot replacement (Secure Boot chain). Pinned + # to the release tag rather than main because it's a boot-chain-critical + # dependency and unpinned bumps can brick unattended updates. v1.1.0 + # (released 2026-06-22) is the first tag compatible with post-2026-06-09 + # nixpkgs where `boot.bootspec.enable` was removed. + # https://github.com/nix-community/lanzaboote + lanzaboote = { + url = "github:nix-community/lanzaboote/v1.1.0"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = inputs@{ self, nix-darwin, home-manager, nix-homebrew, nixpkgs, ... }: @@ -65,7 +100,7 @@ # Flake-only setup — drop the legacy channels path from NIX_PATH # (silences "Nix search path entry .../channels does not exist"). nix.channel.enable = false; - + # Allow unfree packages nixpkgs.config.allowUnfree = true; @@ -100,10 +135,10 @@ # into the project's own .gitignore. See dev-shells/default.nix for # the full mechanism, inheritance rules, and future privacy options. - # Helper function to create home-manager configuration for a user + # Helper function to create home-manager configuration for a user (darwin) # Creates a module list that integrates home-manager with the system configuration # and imports user-specific settings from hosts/${hostname}/home.nix - mkHomeConfig = { username, hostname, home, private ? null }: [ + mkHomeConfig = { username, hostname, home, system, private ? null }: [ home-manager.darwinModules.home-manager { # Set the user's home directory path @@ -114,8 +149,16 @@ # Install user packages to /etc/profiles instead of ~/.nix-profile. # `private` is always present (null on hosts without one) so modules # can pattern-match on it without triggering _module.args recursion. + # + # `hostSystem` is the Nix system string ("aarch64-darwin", + # "x86_64-linux", ...). Threaded via specialArgs so modules/home + # can decide platform sub-bundle imports (see modules/home/default.nix) + # without depending on `pkgs.stdenv.hostPlatform.*` at import-list + # eval time — that route hits `_module.args`->`config` recursion + # because pkgs isn't externally provided to HM's inner modules. home-manager.extraSpecialArgs = { inherit inputs private; + hostSystem = system; rootDir = self; utils = utils; }; @@ -200,7 +243,7 @@ configuration ./hosts/${hostname}/default.nix ] ++ mkHomeConfig { - inherit username hostname private; + inherit username hostname private system; home = "/Users/${username}"; } ++ mkHomebrewConfig { inherit username; @@ -246,14 +289,54 @@ configuration ./hosts/${hostname}/default.nix ] ++ mkHomeConfig { - inherit username hostname private; + inherit username hostname private system; home = "/Users/${username}"; } ++ mkHomebrewConfig { inherit username; autoMigrate = true; }; }; + + # NixOS system configuration for astoria (Dell XPS 13 9300 thin-client) + # + # `inputs` MUST be threaded into BOTH system specialArgs (for + # hosts/astoria/{default,hardware}.nix which pattern-match `{ inputs, ... }:`) + # AND `home-manager.extraSpecialArgs` (for shared HM modules like + # modules/home/development/dev-shells/default.nix and claude-code.nix which + # also eagerly destructure `{ inputs, ... }:`). System specialArgs do NOT + # propagate into HM's module scope — the second wiring below is not + # optional; without it HM eval fails "attribute 'inputs' missing" at + # pattern-match, before any lib.mkIf gate can fire. + # + # `private = null` is safe: the modules that consume it all use `private ? + # null` at pattern-match. Passing null explicitly keeps parity with the + # darwin wiring pattern so any future extraction/dedup stays trivial. + nixosConfigurations.astoria = + let + system = "x86_64-linux"; + private = null; + rootDir = self; + in + nixpkgs.lib.nixosSystem { + inherit system; + # NOTE: `utils` intentionally NOT in system specialArgs — NixOS's + # module framework injects its own `utils` (which carries + # `systemdUtils` used by nixos/modules/system/boot/systemd.nix); a + # user-supplied `utils` would shadow it and break systemd module + # eval with "undefined variable 'systemdUtils'". The custom `utils` + # from modules/lib/default.nix is still available to HM modules via + # extraSpecialArgs below (HM has no such internal `utils`). + specialArgs = { inherit inputs rootDir private; }; + modules = [ + configuration + ./hosts/astoria/default.nix + { + home-manager.extraSpecialArgs = { + inherit inputs private rootDir utils; + hostSystem = system; + }; + } + ]; + }; }; } - - diff --git a/hosts/NB2123/home.nix b/hosts/NB2123/home.nix index 25178c1..751d0e9 100644 --- a/hosts/NB2123/home.nix +++ b/hosts/NB2123/home.nix @@ -35,7 +35,7 @@ in home.stateVersion = "25.05"; imports = [ - ../../modules/home/darwin # darwin bundle (imports shared + darwin-only extras) + ../../modules/home ./registries.nix # NB2123-only npm/yarn corp registries ]; diff --git a/hosts/astoria/README.md b/hosts/astoria/README.md new file mode 100644 index 0000000..939175e --- /dev/null +++ b/hosts/astoria/README.md @@ -0,0 +1,589 @@ +# astoria — Dell XPS 13 9300 NixOS thin-client + +Sofa/bed/bath companion to a TV-connected Nobara desktop. Sway, Moonlight, +LibreWolf. TV inputs via AV receiver → single-scene Sunshine on the host. + +**Design plan**: `/Users/du234/.claude/plans/i-am-planning-to-fancy-lollipop.md` +— rationale, alternatives considered, threat model. This file is the runbook. + +--- + +## Prerequisites (one-time, before touching astoria) + +- [ ] **Password vault** reachable from a non-astoria device. astoria decrypts + its own secrets on first boot using its host SSH key — if that key ever + dies, recovery needs the age key stored in the vault. No non-astoria + vault access = no recovery. +- [ ] **Vault end-to-end / client-side encryption enabled** — set the vault's + unlock secret. Without it, the server operator can read every entry. + Verify the setting is active before storing recovery material. +- [ ] **WebDAV app-password** for astoria, from your WebDAV backend's web UI + (Devices/Sessions/Tokens section, label `"astoria-restic"`). DO NOT + reuse the main account password. +- [ ] **rclone-obscure the app-password** for use in rclone.conf (Phase 1 + step 7): + ``` + nix-shell -p rclone --run 'rclone obscure APP_PASSWORD' + ``` +- [ ] **WebDAV endpoint sanity check** — auth works, directory lists: + ``` + curl -X PROPFIND -H 'Depth: 0' -u USER:APP_PASSWORD https://WEBDAV_HOST/WEBDAV_ROOT/ + ``` + Success = `` XML. + +> **First install vs reinstall**: on the very first install, Phase 0 + Phase 1 +> were already done during scaffolding — skip to Phase 2. Phases 0/1 below are +> the runbook for reinstalls (host key rotation, HW replacement, disaster +> recovery). + +--- + +## Phase 0 — repo scaffolding (one-time) + +Create `.sops.yaml` at the repo root with placeholder recipients (Phase 1 step +1+2 fills in the real pubkeys). Commit + push. + +--- + +## Phase 1 — pre-generate on the dev machine + +Dev machine = any host you already trust with the repo checked out. + +**Placeholder convention** — anywhere you see `` below, substitute BEFORE +running. Bash treats `<` as an input-redirect metachar, so pasting `` as +part of a command silently misbehaves rather than erroring. + +**Cleanup discipline** — Phase 1 writes plaintext secrets to `/tmp` and shreds +them at exit. On macOS `/tmp` is APFS (persistent, `shred` unreliable); on +Linux usually tmpfs. A bash `trap` on EXIT/INT/TERM shreds on any clean exit. +The trap must be set INSIDE the nix-shell subshell — traps don't propagate +across exec into a fresh bash. For maximum hygiene, do Phase 1 in `/dev/shm` +(Linux) or an hdiutil RAM disk (macOS). + +### Enter the shell + +```bash +cd ~/git/dotfiles +nix-shell -p ssh-to-age age sops mkpasswd +``` + +Wait for the `[nix-shell:…]$` prompt, then paste FIRST: + +```bash +trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/dev/null || rm -Pf -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/dev/null' EXIT INT TERM +``` + +### Steps + +1. **Astoria host SSH key** + ``` + ssh-keygen -t ed25519 -f /tmp/astoria_host_key -N '' -C mail@brutcha.dev + ssh-to-age -i /tmp/astoria_host_key.pub + ``` + - Pubkey line → replace `age1astoriahostPUBKEY_TBD` in `.sops.yaml`. + - Private key → vault entry `"astoria SSH host key"`. If your vault's + Password field rejects multi-line PEM, base64 it to a single line first + (decoded back during Phase 3 step 3): + ``` + base64 -i /tmp/astoria_host_key | tr -d '\n' | pbcopy # macOS + base64 /tmp/astoria_host_key | tr -d '\n' | xclip -sel c # Linux (X11) + base64 /tmp/astoria_host_key | tr -d '\n' | wl-copy # Linux (Wayland) + ``` + +2. **Recovery age keypair** (once, ever) + ``` + age-keygen -o /tmp/recovery.txt + ``` + - Pubkey → replace `age1recoveryPUBKEY_TBD` in `.sops.yaml`. + - `AGE-SECRET-KEY-1…` line → vault entry `"astoria sops recovery"` + (single-line, no base64 needed). + +3. **Export recovery key for sops** + ``` + export SOPS_AGE_KEY_FILE=/tmp/recovery.txt + ``` + Step 7 encrypts fresh (only needs pubkeys). Step 8 (`sops updatekeys`) + decrypts + re-encrypts and needs the recovery key. + +4. **Login passphrase** → vault entry `"astoria login"`. Used for cryptroot + LUKS AND user login (same value; muscle memory). English-keyboard + typeable — the LUKS prompt uses US layout. + +4a. **Cryptswap fallback passphrase** → vault entry `"astoria cryptswap"`. + Different value from `"astoria login"`. Only ever typed when TPM + auto-unlock fails (BIOS updates, TPM state changes). + +5. **Restic repo password** → vault entry `"astoria restic repo"`. Strong + random: + ``` + openssl rand -base64 48 + ``` + +6. **User password hash** — via temp file to keep the hash off scrollback: + ``` + mkpasswd -m yescrypt > /tmp/astoria-hash + ``` + Enter the login passphrase from step 4 at the prompt. + +7. **Populate secrets YAML** + ``` + mkdir -p hosts/astoria/secrets # sops uses os.WriteFile — no MkdirAll + sops hosts/astoria/secrets/astoria.yaml + ``` + Top-level YAML keys must match what sops-nix looks up (see `secrets.nix`): + ```yaml + users: + brutcha: + hashed-password: PASTE_FROM_/tmp/astoria-hash + restic: + repo-password: PASTE_FROM_STEP_5 + rclone: + webdav.conf: | + [webdav] + type = webdav + url = https://WEBDAV_HOST/WEBDAV_ROOT + vendor = RCLONE_WEBDAV_VENDOR + user = WEBDAV_USER + pass = OBSCURED_FROM_PREREQUISITES + ``` + If ANY placeholder remains angle-bracketed on save, + `restic-backups-webdav.service` will fail on first boot. + +8. **Encrypt to both recipients** + ``` + sops updatekeys hosts/astoria/secrets/astoria.yaml + ``` + +9. **Commit + push**. + +10. **Shred + clear scrollback** + ``` + shred -u /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash + printf '\033c' + ``` + +--- + +## Phase 2 — physical machine prep + +### BIOS + +- [ ] **Flash latest firmware first** — Windows Dell Update or `fwupdmgr update` + from the installer. Doing this AFTER Phase 4c TPM enrollment invalidates + PCR seals (avoidable round trip). +- [ ] **Signs of Life off**. +- [ ] **BIOS admin password UNSET** (TLP charge thresholds require it). +- [ ] **Secure Boot: OFF, in Setup Mode** — Advanced Boot Options → Secure + Boot → "Reset to Setup Mode" / "Delete All Keys". Some Dell BIOSes + need an admin password to reach this menu: set → reset SB → unset. +- [ ] **TPM: On, and cleared** — Security → TPM → Clear TPM. + +### Installer + +- [ ] **Boot NixOS 26.11 minimal installer USB** (nixos-unstable ISO OK + while 26.11 is pre-release — flake pins nixpkgs to the same channel). +- [ ] **Wi-Fi**: + ``` + nmcli device wifi connect password + ``` +- [ ] **Hardware inventory** — adjust `hardware.nix` if output contradicts + the AX201 / Ice Lake assumptions: + ``` + USER_GH= + curl -sL "https://raw.githubusercontent.com/${USER_GH}/dotfiles/main/hosts/astoria/verify-hardware.sh" | sh + ``` + +--- + +## Phase 3 — install + +``` +GITHUB_USER= +ASTORIA_IP= # from `ip addr | grep 'inet '` on the installer +``` + +### 1. Clone the flake + +``` +nix-shell -p git +git clone "https://github.com/${GITHUB_USER}/dotfiles" /tmp/dotfiles +cd /tmp/dotfiles +``` + +### 2. Partition + format + +``` +sudo nix run 'github:nix-community/disko' \ + --extra-experimental-features 'nix-command flakes' \ + -- --mode destroy,format,mount --flake .#astoria +``` + +Why the flags: `sudo` because disko doesn't self-elevate; the +`--extra-experimental-features` inline because NixOS sudo drops `NIX_CONFIG` +env, so `export NIX_CONFIG=…` in the outer shell wouldn't survive. + +Prompts, in order: +1. `Type 'yes' to continue, anything else to abort:` — type literally `yes`. + **Don't paste a passphrase here** (anything except `yes` aborts before + formatting starts). Skip this prompt with `--yes-wipe-all-disks` if + re-running non-interactively. +2. **cryptswap** LUKS passphrase (2×) — from vault `"astoria cryptswap"`. +3. **cryptroot** LUKS passphrase (2×) — from vault `"astoria login"`. + +### 3. Transfer astoria's SSH host key from dev → installer + +On the installer, set a throwaway login password (the `nixos` user starts +empty, which blocks ssh): +``` +sudo passwd nixos +``` + +On the dev machine, pipe the base64'd key from clipboard through `base64 -d` +into the installer over ssh. `sudo install` (below) atomically creates the +target file with the right owner + 0600 mode — no umask window during which +the file would be world-readable. + +``` +# Option A — Linux dev (xclip): +xclip -o -selection clipboard | base64 -d | ssh "nixos@${ASTORIA_IP}" \ + 'sudo mkdir -p /mnt/etc/ssh && sudo install -m 600 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key' + +# Option B — macOS dev (pbpaste): +pbpaste | base64 -d | ssh "nixos@${ASTORIA_IP}" \ + 'sudo mkdir -p /mnt/etc/ssh && sudo install -m 600 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key' + +# Option C — heredoc paste (no clipboard tool available): +ssh "nixos@${ASTORIA_IP}" 'sudo mkdir -p /mnt/etc/ssh && base64 -d | sudo install -m 600 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key' <<'EOF' + +EOF +``` + +If you stored the key as raw PEM (not base64), drop the `| base64 -d` and +paste the PEM directly. + +On the installer, derive the .pub (strict openssh refuses to read a 0644 +private key without a matching .pub): +``` +sudo ssh-keygen -y -f /mnt/etc/ssh/ssh_host_ed25519_key | \ + sudo install -m 644 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key.pub +``` + +### 4. Generate sbctl Secure Boot keys + +`nixos-install` signs the bootloader via Lanzaboote's installHook using these +keys, so they must exist at `/mnt/var/lib/sbctl` before install. + +``` +sudo nix-shell -p sbctl --run 'sbctl create-keys --help' # confirm flag names first +sudo mkdir -p /mnt/var/lib/sbctl +sudo nix-shell -p sbctl --run 'sbctl create-keys --export /mnt/var/lib/sbctl/keys --database-path /mnt/var/lib/sbctl/GUID' +``` + +Gotchas: +- `--database-path` is a **file** path (writes a GUID file at that location), + not a directory. Passing a directory → EISDIR → no keys ever created. +- Older sbctl uses `--keydir` / `--pki-dir` instead. Check `--help` first. + +### 5. Install + +``` +sudo nixos-install --flake /tmp/dotfiles#astoria --no-root-passwd +``` + +During activation: +- sops decrypts `astoria.yaml` using the host key from step 3. Hashed password + materializes at `/run/secrets-for-users/…` before user creation, so greetd + accepts login on first boot. +- Lanzaboote signs bootloader + kernel + initrd with the sbctl keys. +- cryptswap TPM keyslot doesn't exist yet — first boot prompts for the + disko-set passphrase. TPM enrollment happens in Phase 4c. + +### 6. Reboot + +cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd +→ user login → Sway. + +--- + +## Phase 4 — first-boot + Secure Boot + TPM + +> **Single-sitting rule**: do 4a → 4b → 4c back-to-back, don't leave astoria +> unattended between install and 4c. Between install (SB off, ESP writable +> by any live USB) and Phase 4b's SB-on moment, an attacker with brief +> physical access could plant an unsigned payload on the ESP. When SB flips +> on, firmware measures that payload into PCR 7 as "trusted state"; Phase 4c +> then seals the TPM against that PCR 7. Post-4c the machine is protected. +> Roughly a 30-minute total sitting. + +### 4a. Bring-up checks + +- [ ] Cryptroot + cryptswap passphrases accept vault values. +- [ ] Sway starts (`Mod4+Return` → ghostty; `Mod4+Space` → fuzzel; Capslock + cycles us↔cz). +- [ ] `systemctl status sops-install-secrets` — active, exit 0. +- [ ] `ls /run/secrets-for-users/users/brutcha/` — hashed-password present. +- [ ] `ls /run/secrets/{restic,rclone}/` — secrets present. +- [ ] `lspci -k` — Wi-Fi chip matches Phase 2 hw-inventory. +- [ ] `cat /sys/power/mem_sleep` — `[s2idle]` bracketed. +- [ ] **Git-clone the flake** to `/home/brutcha/git/dotfiles` — otherwise + `environment.etc.nixos.source` is a dangling symlink and later + edit-then-rebuild has no on-machine flake to edit. +- [ ] **Add the dev-machine ssh pubkey** to + `users.users.brutcha.openssh.authorizedKeys.keys` (edit + `hosts/astoria/default.nix` in the just-cloned dir, commit, rebuild) + — required before first `--target-host` rebuild. +- [ ] Manual first Restic backup: + `sudo systemctl start restic-backups-webdav.service`; journal shows + success. +- [ ] LibreWolf `about:support` → `HARDWARE_VIDEO_DECODING = available`. +- [ ] Password-vault LibreWolf extension: paste server URL, log in with your + vault account, unlock with the challenge password on first launch + (one-time, manual). + +### 4b. Enroll Secure Boot keys + +Enroll BEFORE turning SB on in BIOS — first SB-enabled boot must verify +Lanzaboote against enrolled keys. + +``` +sudo sbctl status # Setup Mode: Enabled, Secure Boot: Disabled +sudo sbctl enroll-keys --microsoft # --microsoft: append MS certs to KEK+db (option ROMs) +sudo sbctl verify # every file: ✓ Signed +``` + +Reboot → BIOS → enable Secure Boot → save. Boots normally, still prompts for +both cryptroot + cryptswap passphrases (TPM not enrolled yet). + +Verify from the running system: +``` +bootctl status | grep 'Secure Boot' # enabled +sudo sbctl verify +``` + +### 4c. Enroll TPM keyslot for cryptswap + +``` +ls /sys/class/tpm/ # tpm0 should be there +sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7 /dev/nvme0n1p2 +``` + +Prompts for the cryptswap passphrase to authorize the new keyslot. PCRs 0/2/7 += firmware code / extended firmware code / SB state — binds key release to +firmware integrity + SB being on. + +Reboot to verify: cryptroot passphrase (always), cryptswap unlocks SILENTLY +via TPM (no prompt). + +**Recovery if enrollment failed mid-step** (tpm2-tss error, DA-lockout, PCR +read error): the passphrase keyslot is UNAFFECTED — cryptswap still opens +via passphrase. Clean up with: +``` +sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p2 # non-fatal if no slot +``` +Then retry the enroll command. If DA-locked: +``` +sudo tpm2_dictionarylockout --clear-lockout # blank owner pw on freshly-cleared TPM +``` + +Verify keyslots: +``` +sudo systemd-cryptenroll /dev/nvme0n1p2 # slot 0 (password) + slot N (tpm2) +``` + +### 4d. Lockdown check + +- [ ] `bootctl status` shows Secure Boot enabled, Setup Mode disabled. +- **BIOS admin password NOT recommended** — would prevent unauthorized + Setup-Mode re-entry but breaks TLP's Dell charge-threshold writes silently. + SB + PCR sealing already narrow the physical-access attack: any hostile + re-enrollment invalidates PCR 7 → cryptswap TPM auto-unlock breaks → + tamper visible on the next boot. + +--- + +## Operations reference (keep post-install) + +### Rebuild +- Preferred: `cd ~/git/dotfiles && sudo nixos-rebuild switch --flake .#astoria` +- Via /etc/nixos symlink: `sudo nixos-rebuild switch --flake /etc/nixos#astoria` +- From dev machine over LAN: `nixos-rebuild switch --flake .#astoria --target-host brutcha@astoria --use-remote-sudo` + +### Update +- Flake inputs: `nix flake update` (from dev machine, review lock diff, commit) +- Firmware: `fwupdmgr refresh && fwupdmgr get-updates && fwupdmgr update` + +### Rollback + +Two independent rollback mechanisms — use the right one for the failure mode. + +**NixOS generation rollback** — for reverting a `nixos-rebuild switch` that broke +something: +- Lanzaboote boot menu at boot → pick a previous generation +- Or from CLI (still-booted system): `sudo nixos-rebuild switch --rollback` + +**Snapper rootfs rollback** — for reverting non-`/nix/store` drift (files edited +outside the flake, corrupted state under `/etc`, `/var`, `/home`). Snapshots live +under `/.snapshots//snapshot` and are created at every boot by `snapper-boot.service`: +``` +sudo snapper -c root list # inspect snapshots + timestamps +sudo snapper -c root diff .. # peek at what would change +sudo snapper -c root undochange ..0 # restore from snapshot N +sudo snapper -c root rollback # nuke current @root, replace with snapshot N — reboots into it +``` +`snapper rollback` is destructive to the current @root (it swaps subvolumes); prefer +`undochange` for targeted recovery when only a few files are affected. + +Note: Snapper's SUBVOLUME is `/` (targets @root only). `/nix`, `/home`, `/.snapshots` +are separate subvolumes and NOT covered — `/nix/store` is already immutable + +content-addressed; `/home` is user data that Restic backs up. + +### Restore from backup +- Secrets already materialized on running astoria: `/run/secrets/rclone/webdav.conf` + + `/run/secrets/restic/repo-password` +- `sudo nix-shell -p restic rclone` — enters an interactive root shell with restic + + rclone on PATH. +- Inside that shell you are already root; rclone runs with `HOME=/root` and doesn't + find the sops-materialized config unless `RCLONE_CONFIG` is explicit. Same for + restic's password file. **Do NOT wrap these calls in another `sudo`** — inner sudo + triggers `env_reset` + PAM PATH reset, which drops the nix-shell's ephemeral PATH. + Use plain `env`: + ``` + env \ + RCLONE_CONFIG=/run/secrets/rclone/webdav.conf \ + RESTIC_PASSWORD_FILE=/run/secrets/restic/repo-password \ + restic -r rclone:webdav:restic-astoria snapshots + env \ + RCLONE_CONFIG=/run/secrets/rclone/webdav.conf \ + RESTIC_PASSWORD_FILE=/run/secrets/restic/repo-password \ + restic -r rclone:webdav:restic-astoria restore latest --target / + ``` + +### Password recovery (lost sudo password — machine still boots) +1. From a machine that has the recovery age key: `sops hosts/astoria/secrets/astoria.yaml` + — replace `users.brutcha.hashed-password` with a new `mkpasswd -m yescrypt` hash. + Commit + push. +2. On astoria: `sudo nixos-rebuild switch` (or `--target-host` from dev machine if + stuck at greetd). + +### Password recovery (fully bricked — installer rescue) +1. Boot NixOS installer USB. +2. `sudo cryptsetup luksOpen /dev/nvme0n1p3 cryptroot` # p3 = root; p2 = swap +3. `sudo mount -o subvol=@root /dev/mapper/cryptroot /mnt` +4. `sudo mount -o subvol=@nix /dev/mapper/cryptroot /mnt/nix` +5. `sudo mount -o subvol=@home /dev/mapper/cryptroot /mnt/home` +6. `sudo mount /dev/nvme0n1p1 /mnt/boot` +7. `sudo nixos-enter --root /mnt` +8. Do NOT `passwd brutcha` — `users.mutableUsers = false;` reverts it. Update sops + YAML from a trusted machine and rebuild. + +### Recovery age key — helper-device shred discipline + +The recovery age private key lives in your password vault. Any time you paste it out +of the vault onto a helper device to run `sops`, mirror Phase 1's shred discipline: + +1. On the helper device, mount a scratch tmpfs first: + - Linux: `SCRATCH=$(mktemp -d --tmpdir=/dev/shm astoria-recovery.XXXXX)` + - macOS: plain `~/.config/sops/age/keys.txt` lands on the boot APFS volume where + `rm` releases the inode but blocks remain until reclaimed, and Time Machine + local snapshots capture the file for ~24h. Use a RAM disk: + `hdiutil attach -nomount ram://8192 | xargs -I{} diskutil erasevolume APFS 'ARamDisk' {}; SCRATCH=/Volumes/ARamDisk` +2. Set the cleanup trap FIRST (before any paste): + ``` + trap 'shred -u -- "$SCRATCH"/keys.txt 2>/dev/null; rm -rf "$SCRATCH" 2>/dev/null; diskutil eject ARamDisk 2>/dev/null' EXIT INT TERM + ``` +3. Paste the recovery age private key to `"$SCRATCH"/keys.txt`; `chmod 600 "$SCRATCH"/keys.txt` immediately. +4. `SOPS_AGE_KEY_FILE="$SCRATCH"/keys.txt sops hosts/astoria/secrets/astoria.yaml` + — decrypt, edit, save, then `sops updatekeys …` to re-encrypt. +5. Commit + push. Exit the shell — the trap fires and shreds the temp key. +6. Verify: `ls -la "$SCRATCH"` should show "No such file". On macOS also verify + `tmutil listlocalsnapshots /` doesn't show a recent snapshot containing the file + (they roll off in ~24h; force-delete with `tmutil deletelocalsnapshots`). + +### LUKS passphrase change +``` +sudo cryptsetup luksChangeKey /dev/nvme0n1p3 # cryptroot — also update `"astoria login"` in the vault +sudo cryptsetup luksChangeKey /dev/nvme0n1p2 # cryptswap fallback — also update its vault entry +``` +The cryptswap TPM keyslot is separate from the passphrase keyslot; changing the +passphrase does NOT invalidate the TPM binding. Rotate TPM enrollment only if you +need to (BIOS updates, PCR changes). + +### TPM re-enrollment (after BIOS/firmware update — expected ~1-2× per year) + +Symptoms: first boot after a BIOS update prompts for the cryptswap passphrase +(fallback path) instead of TPM auto-unlock. `journalctl -b -u 'systemd-cryptsetup@luks\x2dswap.service'` +reports PCR mismatch. + +Fix: +``` +sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p2 +sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7 /dev/nvme0n1p2 +``` +Prompted for the cryptswap passphrase to authorize. Reboot to verify silent unlock. + +### Secure Boot state audit (occasional) +``` +sudo sbctl status # Setup Mode: Disabled, Secure Boot: Enabled +sudo sbctl verify # every EFI file: Signed +bootctl status | grep 'Secure' +``` +If `sbctl verify` shows unsigned files after a manual bootloader tweak: +`sudo sbctl sign -s ` per file, then `sudo nixos-rebuild switch`. + +### Full Secure Boot reset (rare — if keys get corrupted or you need to re-provision) + +1. Reboot into BIOS. Advanced Boot Options → Secure Boot → "Reset to Setup Mode" (or + "Delete All Keys"). +2. Boot back into NixOS. `sudo sbctl reset` — resets the UEFI PK/KEK/db variables to + Setup Mode by enrolling an empty signature DB. **`sbctl reset` does NOT touch + `/var/lib/sbctl`** — the local keypair on disk survives. +3. **Delete the local keypair** so step 4 doesn't silently no-op: + ``` + sudo rm -rf /var/lib/sbctl/keys /var/lib/sbctl/GUID + ``` + `sbctl create-keys` refuses to overwrite an existing keydir; skipping this rm + leaves the old keypair in place and the "reset" achieves nothing. +4. `sudo sbctl create-keys` — new keypair, now that the keydir is empty. +5. `sudo nixos-rebuild switch` — re-signs the bootloader + kernel + initrd with the + new keys. +6. `sudo sbctl enroll-keys --microsoft` — enroll into UEFI. +7. Reboot into BIOS → enable Secure Boot. +8. **TPM state also invalidates** (PCR 7 changes): follow "TPM re-enrollment" above. + +### Battery care +Charge thresholds 60/80 via TLP. Check with `tlp-stat -b`. Battery replacement +(~€60, iFixit rating 4/10) recommended if capacity drops below 60 %. + +### Known gotchas +- Wi-Fi tuning assumes AX201 — see `hardware.nix` comments for QCA6390 variant. +- iwlwifi power_save must stay off — turning it on drops Moonlight streams. +- `enhanced-h264ify` blocks AV1 on YouTube while leaving VP9 available; keeps + 4K/1440p playback that plain `h264ify` would silently drop. +- The inlined disko block at the bottom of `hardware.nix` owns the disk layout — + do NOT re-run `nixos-generate-config`. +- `users.mutableUsers = false;` — never fix passwords with `passwd`; always go + through sops. +- **DRM sites (Netflix / Prime / Disney+) don't work on astoria** — LibreWolf ships + Widevine disabled (default-off, user-toggleable). Netflix on Linux additionally + caps at ~720p. Watch DRM content from the Nobara desktop via Moonlight instead. +- The password-vault LibreWolf extension is declaratively installed but not + configured — set server URL, log in with your vault account, unlock with the + challenge password on first launch (manual, one-time). +- `PLATFORM_PROFILE_ON_BAT` in TLP may be a silent no-op if + `/sys/firmware/acpi/platform_profile_choices` is empty on this SKU — + verify-hardware.sh reports this; TLP tolerates. +- **If suspend-then-hibernate fails to wake at the scheduled time**: ADD + `rtc_cmos.use_acpi_alarm=1` to `boot.kernelParams`. Kernel auto-quirks the ACPI + SCI alarm path deterministically on Intel + BIOS≥2015 + HPET-on (XPS 13 9300 hits + all three), so the param is normally redundant and hardware.nix omits it. Kept as + a documented fallback in case a future BIOS revision breaks the auto-quirk. +- **Cryptswap prompts for a passphrase after a BIOS update**: expected — TPM PCRs + changed, sd-cryptsetup fell back to the passphrase keyslot. Re-run TPM enrollment + (see "TPM re-enrollment"). One-time-per-BIOS-bump friction. +- **`sbctl verify` shows unsigned files**: something touched the ESP outside + `nixos-rebuild switch`. `sudo sbctl sign -s ` and rebuild; check the ESP + wasn't manually edited. +- **Cannot enter Secure Boot Setup Mode from BIOS**: some Dell BIOSes require an + admin password to be set before Setup Mode is reachable. Set one first if BIOS + refuses; reset SB to Setup Mode; then UNSET the admin password (TLP charge + thresholds require it UNSET). diff --git a/hosts/astoria/default.nix b/hosts/astoria/default.nix new file mode 100644 index 0000000..57b40c1 --- /dev/null +++ b/hosts/astoria/default.nix @@ -0,0 +1,215 @@ +{ config, pkgs, lib, inputs, ... }: +# +# astoria — Dell XPS 13 9300 NixOS thin-client. Sway sole session, +# Moonlight-first, LibreWolf, greetd, no autologin. +# +# System composition + non-hardware config. Hardware knobs + disko live +# in ./hardware.nix. Sops secret declarations in ./secrets.nix. +# +{ + imports = [ + inputs.sops-nix.nixosModules.sops + inputs.home-manager.nixosModules.home-manager + ./hardware.nix + ./secrets.nix + ]; + + nixpkgs.config.allowUnfree = true; # required by hardware.enableAllFirmware + nixpkgs.overlays = [ inputs.nur.overlays.default ]; + + networking.hostName = "astoria"; + + # --- Locale / timezone / console --- + time.timeZone = "Europe/Prague"; + i18n.defaultLocale = "en_US.UTF-8"; + i18n.extraLocales = [ "cs_CZ.UTF-8/UTF-8" ]; + i18n.extraLocaleSettings = { + LC_TIME = "cs_CZ.UTF-8"; + LC_NUMERIC = "cs_CZ.UTF-8"; + }; + console.keyMap = "us"; # LUKS prompt reads from this + + # --- User (password from sops — see ./secrets.nix) --- + users.mutableUsers = false; + users.users.brutcha = { + isNormalUser = true; + description = "brutcha"; + extraGroups = [ "wheel" "video" "input" "render" "audio" "networkmanager" ]; + shell = pkgs.zsh; + hashedPasswordFile = config.sops.secrets."users/brutcha/hashed-password".path; + # Fill in Nobara's pubkey after first boot (README Phase 4a). + openssh.authorizedKeys.keys = [ + # "ssh-ed25519 AAAA... brutcha@nobara" + ]; + }; + programs.zsh.enable = true; # REQUIRED when user shell = pkgs.zsh + + # --- Sway via greetd tuigreet (no autologin — password muscle memory) --- + programs.sway = { + enable = true; + wrapperFeatures.gtk = true; + }; + services.greetd = { + enable = true; + settings.default_session = { + command = "${pkgs.greetd.tuigreet}/bin/tuigreet --time --remember --cmd sway"; + user = "greeter"; + }; + }; + + # --- XDG portals + polkit --- + # `common.default` catches processes that spawn before Sway exports + # XDG_CURRENT_DESKTOP — without it portal resolution is nondeterministic. + xdg.portal = { + enable = true; + wlr.enable = true; + extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; + config = { + sway.default = [ "wlr" "gtk" ]; + common.default = [ "wlr" "gtk" ]; + }; + }; + security.polkit.enable = true; + + # --- System packages --- + environment.systemPackages = with pkgs; [ + intel-gpu-tools # intel_gpu_top for VA-API verification + brightnessctl # sway XF86MonBrightness keybinds + sbctl # Secure Boot key management + tpm2-tools # TPM inspection + re-enrollment + pavucontrol # waybar pulseaudio click target + ]; + # brightnessctl's udev rule gives `video` group write access to + # /sys/class/backlight — services.udev scans only .packages (NOT + # environment.systemPackages), so it must be listed here explicitly. + services.udev.packages = [ pkgs.brightnessctl ]; + environment.sessionVariables = { + MOZ_ENABLE_WAYLAND = "1"; + NIXOS_OZONE_WL = "1"; + LIBVA_DRIVER_NAME = "iHD"; # Ice Lake Gen 11+ + }; + + # --- Audio (PipeWire) --- + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + wireplumber.enable = true; + extraConfig.pipewire."92-video-streaming"."context.properties" = { + "default.clock.rate" = 48000; + "default.clock.allowed-rates" = [ 44100 48000 ]; + "default.clock.quantum" = 512; + "default.clock.min-quantum" = 128; + "default.clock.max-quantum" = 2048; + }; + }; + + # --- Credential agents --- + # NOT setting programs.ssh.startAgent: gnome-keyring-daemon's ssh + # component already exports SSH_AUTH_SOCK; enabling both spawns two + # agents that race for the socket. + services.gnome.gnome-keyring.enable = true; + security.pam.services.login.enableGnomeKeyring = true; + + # --- SSH (for --target-host rebuilds from LAN) --- + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "no"; + }; + }; + + # --- Nix --- + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.trusted-users = [ "brutcha" ]; + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + nix.optimise.automatic = true; + + # --- Disk / filesystem maintenance --- + services.fstrim.enable = true; + services.btrfs.autoScrub.enable = true; + + services.journald.extraConfig = '' + SystemMaxUse=1G + MaxRetentionSec=1month + ''; + + # --- Sway ancillary services --- + services.upower.enable = true; + services.udisks2.enable = true; + services.gvfs.enable = true; + services.smartd = { + enable = true; + notifications.mail.enable = false; # no MTA — don't spam journal with sendmail failures + notifications.wall.enable = true; + }; + services.avahi = { enable = true; nssmdns4 = true; }; + + # --- Snapper (on-boot snapshot only, no timeline) --- + # Without snapshotRootOnBoot (or a timeline / activation script) the + # snapper module installs the daemon + cleanup timer but nothing creates + # snapshots — NUMBER_LIMIT would be meaningless. + services.snapper.snapshotRootOnBoot = true; + services.snapper.configs.root = { + SUBVOLUME = "/"; + ALLOW_USERS = [ "brutcha" ]; + TIMELINE_CREATE = false; + TIMELINE_CLEANUP = false; + NUMBER_CLEANUP = true; + NUMBER_LIMIT = 20; + }; + services.snapper.cleanupInterval = "1d"; + + programs.nix-index.enable = true; + programs.command-not-found.enable = false; + + # --- Restic backups (secrets from sops) --- + # Daily backup, weekly prune. `initialize = true` on prune too — idempotent + # (`restic cat config || restic init`), so prune doesn't fail-loop if the + # daily backup never bootstrapped the repo. + services.restic.backups.webdav = { + initialize = true; + repository = "rclone:webdav:restic-astoria"; + paths = [ + "/home/brutcha/.ssh" + "/home/brutcha/.local/share/keyrings" + "/home/brutcha/.zsh_history" + ]; + extraBackupArgs = [ "--one-file-system" ]; + passwordFile = config.sops.secrets."restic/repo-password".path; + rcloneConfigFile = config.sops.secrets."rclone/webdav.conf".path; + timerConfig = { OnCalendar = "daily"; Persistent = true; RandomizedDelaySec = "30m"; }; + }; + services.restic.backups.webdav-prune = { + initialize = true; + repository = "rclone:webdav:restic-astoria"; + passwordFile = config.sops.secrets."restic/repo-password".path; + rcloneConfigFile = config.sops.secrets."rclone/webdav.conf".path; + paths = []; + timerConfig = { OnCalendar = "weekly"; Persistent = true; RandomizedDelaySec = "1h"; }; + pruneOpts = [ "--keep-daily 7" "--keep-weekly 4" "--keep-monthly 12" "--keep-within 30d" ]; + }; + + # /home/brutcha/git/dotfiles is populated in README Phase 4a; broken + # symlink until then (cosmetic warning only). + environment.etc.nixos.source = "/home/brutcha/git/dotfiles"; + + # --- Home Manager --- + # `home-manager.extraSpecialArgs` (including `inputs` + `hostSystem`) is + # wired from flake.nix — shared HM modules destructure `{ inputs, ... }:` + # at pattern-match, and modules/home/default.nix uses `hostSystem` to + # select the platform sub-bundle. + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + home-manager.backupFileExtension = "backup"; + home-manager.users.brutcha = import ./home.nix; + + system.stateVersion = "26.11"; +} diff --git a/hosts/astoria/hardware.nix b/hosts/astoria/hardware.nix new file mode 100644 index 0000000..eb91620 --- /dev/null +++ b/hosts/astoria/hardware.nix @@ -0,0 +1,193 @@ +{ config, pkgs, lib, inputs, ... }: +# +# astoria hardware — Dell XPS 13 9300 (i7-1065G7 Ice Lake, 4K UHD+, 16 GB). +# +# Bootloader (Lanzaboote + Secure Boot), kernel + initrd + LUKS + resume, +# Wi-Fi, Bluetooth, TLP, thermal, sleep/hibernate, hardware.graphics, +# firmware, and the disko partition config inlined at the bottom. +# +{ + imports = [ + # https://github.com/NixOS/nixos-hardware/tree/master/dell/xps/13-9300 + inputs.nixos-hardware.nixosModules.dell-xps-13-9300 # psmouse blacklist + i2c-designware sleep-resume + QCA6390 fw + # https://github.com/nix-community/disko + inputs.disko.nixosModules.disko + # https://github.com/nix-community/lanzaboote + inputs.lanzaboote.nixosModules.lanzaboote + ]; + + # --- Bootloader (Lanzaboote / Secure Boot) --- + # Two-step key lifecycle (README Phase 3 step 5 + Phase 4b step 2): + # `sbctl create-keys` at install time → local keypair under pkiBundle; + # `sbctl enroll-keys --microsoft` at first boot → enroll into UEFI while + # in Setup Mode. --microsoft appends MS certs to KEK+db only (PK stays + # under our key), so signed option ROMs still load. + boot.loader.systemd-boot.enable = lib.mkForce false; # Lanzaboote sets boot.loader.external.enable = true + boot.loader.efi.canTouchEfiVariables = true; + boot.lanzaboote = { + enable = true; + pkiBundle = "/var/lib/sbctl"; + configurationLimit = 30; + }; + + # --- Kernel --- + boot.kernelPackages = pkgs.linuxPackages; + # nixos-hardware appends `mem_sleep_default=deep` unconditionally, but this + # box's firmware is s2idle-only → `systemctl suspend` returns -EINVAL. + # Kernel is last-wins for `mem_sleep_default=`, so `mkAfter` overrides. + # `mkMerge` (not two `boot.kernelParams = [...]` — Nix parser rejects + # duplicate attr paths before eval, so `mkAfter` on a second decl doesn't + # rescue it). + boot.kernelParams = lib.mkMerge [ + [ "resume=/dev/mapper/luks-swap" ] # matches disko mapper (see §disko) + (lib.mkAfter [ "mem_sleep_default=s2idle" ]) + ]; + + # --- Initrd (systemd stage 1) --- + boot.initrd.systemd.enable = true; + boot.initrd.availableKernelModules = [ + # No microSD reader on the 9300 (dropped after 9370). sd_mod stays — + # usb_storage routes external USB via SCSI. + "xhci_pci" "thunderbolt" "vmd" "nvme" "usb_storage" "sd_mod" + ]; + # 3840x2400 framebuffer text at ~6pt is unreadable at the LUKS prompt. + console.earlySetup = true; + console.font = "ter-v32b"; + console.packages = [ pkgs.terminus_font ]; + + # --- LUKS: cryptroot = passphrase, cryptswap = TPM auto-unlock --- + # `device` + `allowDiscards` come from disko; only the additive TPM opt + # goes here (list-merge). TPM keyslot enrolled post-install via + # `systemd-cryptenroll` (README Phase 4c); passphrase set at disko time + # remains as fallback. + boot.initrd.luks.devices."luks-swap" = { + crypttabExtraOpts = [ "tpm2-device=auto" ]; + }; + + # --- Wi-Fi (AX201-specific tuning) --- + # Alternative SKU: QCA6390 (ath11k). Drop the iwlwifi modprobe block if + # `lspci -k` reveals it; NM powersave = false applies either way. + boot.extraModprobeConfig = '' + options iwlwifi power_save=0 + ''; + networking.networkmanager.enable = true; + networking.networkmanager.wifi.powersave = false; + networking.networkmanager.wifi.backend = "wpa_supplicant"; + + # --- Bluetooth --- + hardware.bluetooth.enable = true; + hardware.bluetooth.powerOnBoot = false; + services.blueman.enable = true; + + # --- Power --- + services.power-profiles-daemon.enable = false; # TLP owns this + # https://linrunner.de/tlp/ + # TLP's Dell plugin writes `charge_types = Custom` via dell-smbios; that + # write silently fails when a BIOS admin password is set → thresholds + # never take effect. Keep BIOS admin password UNSET (README Phase 2). + services.tlp = { + enable = true; + settings = { + CPU_SCALING_GOVERNOR_ON_BAT = "powersave"; + CPU_ENERGY_PERF_POLICY_ON_BAT = "power"; + CPU_ENERGY_PERF_POLICY_ON_AC = "balance_performance"; + CPU_BOOST_ON_BAT = 0; + CPU_HWP_DYN_BOOST_ON_BAT = 0; + # Silent no-op if /sys/firmware/acpi/platform_profile_choices is empty + # (verify-hardware.sh reports). + PLATFORM_PROFILE_ON_BAT = "low-power"; + INTEL_GPU_MIN_FREQ_ON_BAT = 100; + INTEL_GPU_MAX_FREQ_ON_BAT = 750; + RUNTIME_PM_ON_BAT = "auto"; + PCIE_ASPM_ON_BAT = "powersupersave"; + WIFI_PWR_ON_BAT = "off"; + START_CHARGE_THRESH_BAT0 = 60; + STOP_CHARGE_THRESH_BAT0 = 80; + }; + }; + # nixos-hardware/dell/xps/13-9300 already mkDefault-enables thermald and + # fwupd; explicit `true` here for intent clarity (no-op override). + services.thermald.enable = true; + # https://github.com/erpalma/throttled — clears BIOS PL1/PL2 override on Ice Lake + services.throttled.enable = true; + + # --- Lid / suspend: suspend-then-hibernate (30 min) --- + services.logind.settings.Login = { + HandleLidSwitch = "suspend-then-hibernate"; + HandleLidSwitchExternalPower = "suspend-then-hibernate"; + HandleLidSwitchDocked = "ignore"; + HandlePowerKey = "suspend-then-hibernate"; + }; + # 26.11 removed `systemd.sleep.extraConfig` (mkRemovedOptionModule); attrs + # under `.settings.Sleep` become sleep.conf keys verbatim. + systemd.sleep.settings.Sleep = { + HibernateDelaySec = "30min"; + SuspendState = "mem"; + HibernateMode = "platform"; + }; + + # --- Graphics --- + # intel-compute-runtime meta says "12th Gen and newer" — Gen 11 unsupported. + hardware.graphics = { + enable = true; + extraPackages = with pkgs; [ intel-media-driver ]; + }; + + # --- Firmware --- + services.fwupd.enable = true; + hardware.enableAllFirmware = true; + + # --- Disk layout (disko, declarative) --- + # Inlined here so all HW-topology declarations live in one file. `device` + # on each LUKS entry, `allowDiscards`, and the auto-generated + # `boot.initrd.luks.devices.` entries come from disko — setting + # them again in boot.initrd.luks would conflict. + disko.devices.disk.main = { + device = "/dev/nvme0n1"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "4G"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + cryptswap = { + size = "20G"; + label = "cryptswap"; + content = { + type = "luks"; + name = "luks-swap"; # matches boot.initrd.luks.devices.luks-swap + resume= + settings.allowDiscards = true; + content.type = "swap"; + }; + }; + cryptroot = { + size = "100%"; # auto-priority 9001 → placed last + label = "cryptroot"; + content = { + type = "luks"; + name = "cryptroot"; + settings.allowDiscards = true; + content = { + type = "btrfs"; + extraArgs = [ "-L" "nixos" "-f" ]; + subvolumes = { + "@root" = { mountpoint = "/"; mountOptions = [ "compress=zstd:1" "noatime" ]; }; + "@nix" = { mountpoint = "/nix"; mountOptions = [ "compress=zstd:1" "noatime" ]; }; + "@home" = { mountpoint = "/home"; mountOptions = [ "compress=zstd:1" "noatime" ]; }; + "@snapshots" = { mountpoint = "/.snapshots"; mountOptions = [ "compress=zstd:1" "noatime" ]; }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/hosts/astoria/home.nix b/hosts/astoria/home.nix new file mode 100644 index 0000000..765b5d5 --- /dev/null +++ b/hosts/astoria/home.nix @@ -0,0 +1,58 @@ +{ config, pkgs, ... }: +# +# astoria — home-manager entry for `brutcha`. +# +# Toggles the fleet-standard `home.apps.*` options; modules/home/default.nix +# picks the linux sub-bundle (sway/waybar/mako/fuzzel/swaylock/thunar/ +# librewolf/moonlight) via `hostSystem`. +# +{ + imports = [ ../../modules/home ]; + + home.apps = { + development = { + ghostty.enable = true; + git.enable = true; # lazygit.enable auto-derives + }; + internet.librewolf.enable = true; + media.moonlight.enable = true; + filemanager.thunar.enable = true; + windowManager = { + sway.enable = true; + waybar.enable = true; + mako.enable = true; + fuzzel.enable = true; + swaylock.enable = true; + }; + }; + + home.stateVersion = "26.11"; + home.username = "brutcha"; + home.homeDirectory = "/home/brutcha"; + + # --- GTK theming --- + # Cross-cutting; not owned by any single app module. + # `tweakVariants = ["black"]` modifies the theme's internal palette but + # does NOT change the folder name (install.sh: "${name}${theme}${color}${size}${ctype}" + # → "Tokyonight-Dark"). + gtk = { + enable = true; + theme = { + name = "Tokyonight-Dark"; + package = pkgs.tokyonight-gtk-theme.override { + colorVariants = [ "dark" ]; + sizeVariants = [ "standard" ]; + tweakVariants = [ "black" ]; + }; + }; + iconTheme = { name = "Papirus-Dark"; package = pkgs.papirus-icon-theme; }; + cursorTheme = { name = "Bibata-Modern-Ice"; package = pkgs.bibata-cursors; }; + gtk3.extraConfig.gtk-application-prefer-dark-theme = 1; + }; + + # File-sync client. Server URL + creds set on first launch. + services.nextcloud-client = { + enable = true; + startInBackground = true; + }; +} diff --git a/hosts/astoria/secrets.nix b/hosts/astoria/secrets.nix new file mode 100644 index 0000000..6778be2 --- /dev/null +++ b/hosts/astoria/secrets.nix @@ -0,0 +1,46 @@ +{ config, lib, ... }: +# +# astoria — sops-nix (https://github.com/Mic92/sops-nix) secret declarations. +# +# `neededForUsers = true` materializes the hashed-password secret into +# /run/secrets-for-users BEFORE useradd runs. Without it, the user gets +# created with a locked shadow entry — silent soft-lock, recovery via +# installer USB only. +# +# The assertion below guards against enabling systemd-sysusers or userborn: +# on those paths sops-install-secrets-for-users' Before= ordering is soft, +# and a decrypt failure would let sysusers create the user with a `!` +# shadow entry (same soft-lock shape). +# +{ + assertions = [ + { + assertion = !(config.systemd.sysusers.enable or false) + && !((config.services.userborn or {}).enable or false); + message = '' + astoria: systemd.sysusers.enable and services.userborn.enable must both + stay false while users.mutableUsers = false and hashedPasswordFile is + sops-managed — the systemd-sysusers/userborn path uses soft ordering + that silently soft-locks the user on decrypt failure. See secrets.nix. + ''; + } + ]; + + sops = { + defaultSopsFile = ./secrets/astoria.yaml; + # age.sshKeyPaths defaults to services.openssh.hostKeys filtered to + # ed25519 (see sops-nix modules/sops/default.nix); openssh is enabled + # at the system level so no explicit setting needed. + + secrets = { + "users/brutcha/hashed-password" = { + neededForUsers = true; + }; + "restic/repo-password" = { }; + "rclone/webdav.conf" = { }; + # tailscale/authkey — add when mesh VPN goes in (plan §7). Declared + # here would require the yaml key to exist too, else sops-install-secrets + # aborts activation. + }; + }; +} diff --git a/hosts/astoria/secrets/astoria.yaml b/hosts/astoria/secrets/astoria.yaml new file mode 100644 index 0000000..075d21d --- /dev/null +++ b/hosts/astoria/secrets/astoria.yaml @@ -0,0 +1,31 @@ +users: + brutcha: + hashed-password: ENC[AES256_GCM,data:Hay99vmjDFBiC1tm/sm6sWryH6YpFRQiLGkfTdP5/OCQhvvxVbi2XkfHaJ8Gy+MuwZe+M2GcBefVayDUs3U4iQzi1lw8ekZ0aw==,iv:eqeNvbruvHlEbi5otjqBEjDvri2XY3n/4As83Op5sRw=,tag:bGNVbjA0lb9/3fAGlPrZGw==,type:str] +restic: + repo-password: ENC[AES256_GCM,data:+/b0PrsMTiX4CBNPppUCHQ8I7PdWfF492MiOfYXqFcqeYgN+jtgMBBQETKhVS0gdypFxnCGwbdgTLNFSwEqBJw==,iv:CYKmiDo3uOtXPTD90vGprEzzHfSI7hdWBuryd7jqQLw=,tag:uER1VsU304eLX986ELbj+g==,type:str] +rclone: + webdav.conf: ENC[AES256_GCM,data:xl/F8nqKSTOcmcmTXVZ/HNNhxR93RNdx6z7sHn366yo5PSRmTKMzdPBJfCWG8gx6Q0JWIhfQwCVI33iWz3kDI/o4fCxTBlu5ywGG2TiuocKm5wQ77sAPcS6Y1O6FzJ4XDBBTVa2cPOPyfqfze8ejBJ/7GQ7d9fWdZvAtjFwgaQfAmSVWquAFtABnVeJTH/eLRlha3bdakozElmuXpxqDD2H+Pf0qEAlk1zRh58yk9dEIq3M5sqlLBBst,iv:N4ut1wI0o8a6Sq6fAVz/ovdOVHr7US0+/0S5awEs/dk=,tag:r0a/kV1KAbtaUPzODReXfg==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYcURDMXNJSjFLNldraGVK + SWgvRmw2STU5Q2lHL0dSMjVZNzdxSVhUQjFzCnRUMmJGdXhvOUk3VFZ5VS8rME5Q + SmxCL3hrbjBXZkZSZmlIT1NNTjhINDQKLS0tIEV0SnUzYlNmRVZLTFFjK0lnVXhF + YVlXOVFHL0xUZzVFM1QxUVJlT2hnSG8KhcvI5joSSLbF4N+m/eJdIq03y4PpeEh9 + 5kfSxMsybh+pupPbmVdL1e+mTJ1f7bT4ztS43G/f6xbtriuhcxrCAQ== + -----END AGE ENCRYPTED FILE----- + recipient: age1uvs9kqpzmdty57x5a4m5c2x08nhpfxwhxpy5dp4f5dckykncs49qm538tl + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmRUViN2czN1hraWlHYjlY + SW9FN1lmR0Jwcmtnd0NHZk9LckdnRFcwRFFFClFZK3dZRGs4SXR0N1dObitMYy9v + MnlMY0EzSm8wRi9Hd1A4dlVucGVUL3cKLS0tIGR4VVcwdUFXMEtMK0dqODZxSjZu + eGU3NXkzVElyWTlSRU1SVFNUajROVFkKxlGi8PDn7peW1y4xwi2Rko9qrkVGgPnw + Kzl5f9Y6FNkwiq/WFmJHNvCIWPxc5KNicMDhoRUJw7TSE9Q8PIuQaQ== + -----END AGE ENCRYPTED FILE----- + recipient: age17c3ydfwz2etvrxfyfeemkna4ndgunaqszseyzufm9fmggjm0kdds2fw0mk + lastmodified: "2026-07-27T12:25:34Z" + mac: ENC[AES256_GCM,data:TfRB7zZA2TEuYJ027z0S3ds3gS2TgL0K69FaAc86vNfj7BnUi6elhWPU+YE0zv3IV5QjY0ShLeJeevrLEQd5hifT8Xcmh1SHYHSxCCR1CsiYIwIuLkQ0o9PXjRXZPJDEZC7K251I5pbb3hjQz/wrUk9uooqyYqiutAY5LECNmHA=,iv:U3xW7AIkidYalC1iBYOr+IFVMYPUkLlqXQXrwQmOUMo=,tag:QAzvrAigaeSBPZ9UgkHQjQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.2 diff --git a/hosts/astoria/verify-hardware.sh b/hosts/astoria/verify-hardware.sh new file mode 100755 index 0000000..99660ff --- /dev/null +++ b/hosts/astoria/verify-hardware.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env sh +# hosts/astoria/verify-hardware.sh — read-only hardware inventory for astoria. +# Prints values that hardware.nix currently assumes. Run during install +# (README Phase 2 step 5). If output contradicts hardware.nix, edit +# hardware.nix before / after nixos-install. + +set -u + +section() { printf '\n=== %s ===\n' "$1"; } + +section "Machine identity" +if [ -r /sys/class/dmi/id/product_name ]; then + printf 'DMI product: %s\n' "$(cat /sys/class/dmi/id/product_name)" +fi +printf 'CPU: %s\n' "$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2 | sed 's/^ *//')" +printf 'RAM: %s\n' "$(grep MemTotal /proc/meminfo | awk '{ printf "%.1f GiB\n", $2/1024/1024 }')" + +section "Wi-Fi chip" +# hardware.nix assumes Intel AX201 (iwlwifi). Alternative SKU: Qualcomm QCA6390 (ath11k). +lspci -k 2>/dev/null | grep -A3 -i 'network controller' | sed 's/^/ /' +echo " -> hardware.nix comment shows the alternate values if this is QCA6390." + +section "Sleep modes supported by firmware" +# hardware.nix expects s2idle only. If 'deep' appears bracketed, kernel may pick it. +if [ -r /sys/power/mem_sleep ]; then + printf ' /sys/power/mem_sleep: %s\n' "$(cat /sys/power/mem_sleep)" +else + echo ' /sys/power/mem_sleep: not present (kernel may be too old)' +fi + +section "Platform profile (TLP PLATFORM_PROFILE_ON_BAT)" +# If choices is empty/missing, PLATFORM_PROFILE_ON_BAT in TLP is a silent no-op. +if [ -r /sys/firmware/acpi/platform_profile_choices ]; then + printf ' choices: %s\n' "$(cat /sys/firmware/acpi/platform_profile_choices)" + printf ' active: %s\n' "$(cat /sys/firmware/acpi/platform_profile 2>/dev/null || echo unknown)" +else + echo ' not supported on this SKU — PLATFORM_PROFILE_ON_BAT will be a silent no-op' + echo ' -> OK to remove that TLP setting from hardware.nix (tidy)' +fi + +section "Fingerprint reader" +lsusb 2>/dev/null | grep -i '27c6' | sed 's/^/ /' || echo ' no Goodix device found' + +section "BIOS version" +if command -v fwupdmgr >/dev/null 2>&1; then + fwupdmgr get-devices 2>/dev/null | awk '/System Firmware/,/^$/' | head -20 | sed 's/^/ /' +else + echo ' fwupdmgr not available' +fi + +section "GPU / VA-API" +lspci -nnk 2>/dev/null | grep -A2 VGA | sed 's/^/ /' + +section "Battery health (approximate)" +if [ -r /sys/class/power_supply/BAT0/charge_full ] && [ -r /sys/class/power_supply/BAT0/charge_full_design ]; then + now=$(cat /sys/class/power_supply/BAT0/charge_full) + design=$(cat /sys/class/power_supply/BAT0/charge_full_design) + printf ' BAT0 charge_full=%s design=%s (health ~%d%%)\n' "$now" "$design" "$((100 * now / design))" +elif [ -r /sys/class/power_supply/BAT0/energy_full ] && [ -r /sys/class/power_supply/BAT0/energy_full_design ]; then + now=$(cat /sys/class/power_supply/BAT0/energy_full) + design=$(cat /sys/class/power_supply/BAT0/energy_full_design) + printf ' BAT0 energy_full=%s design=%s (health ~%d%%)\n' "$now" "$design" "$((100 * now / design))" +else + echo ' BAT0 counters not readable' +fi + +section "Notes" +cat <<'EOF' + - Thermal / throttled verification (CPU pinned at ~2.4 GHz under load) needs + stress-ng which isn't in the minimal ISO. Run post-install: + nix-shell -p stress-ng s-tui + stress-ng --cpu $(nproc) --timeout 5m & s-tui + - Any value above that contradicts hardware.nix -> edit hardware.nix before + nixos-install (README Phase 3 step 6), or in a post-install rebuild. +EOF diff --git a/hosts/makima/home.nix b/hosts/makima/home.nix index 464c354..061187d 100644 --- a/hosts/makima/home.nix +++ b/hosts/makima/home.nix @@ -11,8 +11,8 @@ # https://nix-community.github.io/home-manager/index.xhtml#sec-install-nix-darwin-module home.stateVersion = "25.05"; - # https://nix-community.github.io/home-manager/ - imports = [ ../../modules/home/darwin ]; + # reached via the universal bundle. + imports = [ ../../modules/home ]; home.apps = { development = { diff --git a/modules/home/darwin/default.nix b/modules/home/darwin/default.nix index 69aedd6..e4b35b1 100644 --- a/modules/home/darwin/default.nix +++ b/modules/home/darwin/default.nix @@ -1,22 +1,22 @@ { lib, pkgs, ... }: # -# Darwin bundle — imports the shared modules + darwin-specific extras + -# LaunchServices registration. Non-darwin hosts import ./default.nix directly. +# Darwin bundle — darwin-only categories. Shared modules +# (theme/fonts/shell/development) come from the universal bundle in +# ../default.nix; don't re-import from here — the cycle overflows the +# stack before `filterModules`'s dedup runs. # { imports = [ - ../default.nix # shared: theme, fonts, shell, development - ./development.nix # darwin dev extras: lazydocker, xcbuild - ./internet # davmail, helium - ./media # obs-studio (scene + profile) - ./security # keepass - ./window-manager # aerospace, sketchybar, jankyborders + ./development.nix + ./internet + ./media + ./security + ./window-manager ]; - # home-manager symlinks Mac .app bundles under ~/Applications/Home Manager Apps/, - # but LaunchServices doesn't recurse into that subdirectory. Re-register each - # bundle's resolved nix-store target so `open -a`, Spotlight, and Launchpad - # find them (`lsregister -f` needs a real path, not a symlink). + # LaunchServices doesn't recurse into ~/Applications/Home Manager Apps/; + # re-register each .app's real nix-store target so `open -a`, Spotlight, + # Launchpad find them. home.activation.registerNixApps = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin (lib.hm.dag.entryAfter [ "linkGeneration" ] '' diff --git a/modules/home/default.nix b/modules/home/default.nix index 6c8df44..b245637 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -1,7 +1,12 @@ -{ ... }: +{ lib, hostSystem, ... }: # -# Shared home-manager modules (platform-generic). Opt-in per app via -# home.apps...enable. Darwin-only extras live in ./darwin/. +# Universal home-manager bundle. Every host imports via +# `imports = [ ../../modules/home ];`; the bundle self-selects the platform +# sub-bundle based on `hostSystem` (threaded from flake.nix extraSpecialArgs). +# +# `hostSystem` (not `pkgs.stdenv.hostPlatform.*`) because the imports list +# must not depend on `pkgs` — HM resolves pkgs via `_module.args` → `config`, +# so touching it during imports triggers infinite recursion. # { imports = [ @@ -9,5 +14,7 @@ ./fonts.nix ./shell.nix ./development - ]; + ] + ++ lib.optionals (lib.hasSuffix "-darwin" hostSystem) [ ./darwin ] + ++ lib.optionals (lib.hasSuffix "-linux" hostSystem) [ ./linux ]; } diff --git a/modules/home/fonts.nix b/modules/home/fonts.nix index a0df378..2d2f29e 100644 --- a/modules/home/fonts.nix +++ b/modules/home/fonts.nix @@ -9,6 +9,9 @@ # https://www.nerdfonts.com/ home.packages = with pkgs; [ nerd-fonts.jetbrains-mono + dejavu_fonts + twemoji-color-font + noto-fonts-cjk-sans ]; # Enable fontconfig for proper font discovery and rendering across applications diff --git a/modules/home/linux/default.nix b/modules/home/linux/default.nix new file mode 100644 index 0000000..93b474e --- /dev/null +++ b/modules/home/linux/default.nix @@ -0,0 +1,13 @@ +{ ... }: +# +# Linux bundle — linux-only categories. Shared modules come via the +# universal bundle (see darwin/default.nix note on the removed back-ref). +# +{ + imports = [ + ./internet + ./media + ./thunar.nix + ./window-manager + ]; +} diff --git a/modules/home/linux/internet/default.nix b/modules/home/linux/internet/default.nix new file mode 100644 index 0000000..63ef1bf --- /dev/null +++ b/modules/home/linux/internet/default.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./librewolf.nix + ]; +} diff --git a/modules/home/linux/internet/librewolf.nix b/modules/home/linux/internet/librewolf.nix new file mode 100644 index 0000000..18e83fd --- /dev/null +++ b/modules/home/linux/internet/librewolf.nix @@ -0,0 +1,38 @@ +{ config, lib, pkgs, ... }: +# +# LibreWolf (https://librewolf.net) — hardened Firefox fork. +# +# Available options: +# - home.apps.internet.librewolf.enable +# +# `enhanced-h264ify` blocks AV1 on YouTube (Ice Lake has no HW AV1 decode) +# while leaving VP9 — which the Gen 11 media block DOES decode — available. +# Plain `h264ify` caps 4K/1440p (YouTube stopped H.264 encoding at those +# resolutions). +# +# extensions.packages symlinks profiles but does not enable them; the +# `autoDisableScopes = 0` pref is the standard HM idiom to auto-enable +# declaratively-installed extensions on first launch. +# +# NUR overlay wired at the system level (see hosts/astoria/default.nix). +# +let + cfg = config.home.apps.internet.librewolf; +in +{ + options.home.apps.internet.librewolf.enable = + lib.mkEnableOption "LibreWolf with enhanced-h264ify + nextcloud-passwords"; + + config = lib.mkIf cfg.enable { + programs.librewolf = { + enable = true; + profiles.default = { + settings."extensions.autoDisableScopes" = 0; + extensions.packages = with pkgs.nur.repos.rycee.firefox-addons; [ + enhanced-h264ify + nextcloud-passwords + ]; + }; + }; + }; +} diff --git a/modules/home/linux/media/default.nix b/modules/home/linux/media/default.nix new file mode 100644 index 0000000..83f87dc --- /dev/null +++ b/modules/home/linux/media/default.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./moonlight.nix + ]; +} diff --git a/modules/home/linux/media/moonlight.nix b/modules/home/linux/media/moonlight.nix new file mode 100644 index 0000000..6550f03 --- /dev/null +++ b/modules/home/linux/media/moonlight.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, ... }: +# +# Moonlight (https://moonlight-stream.org) — GameStream/Sunshine client. +# +# Available options: +# - home.apps.media.moonlight.enable +# +let + cfg = config.home.apps.media.moonlight; +in +{ + options.home.apps.media.moonlight.enable = + lib.mkEnableOption "Moonlight streaming client (moonlight-qt)"; + + config = lib.mkIf cfg.enable { + home.packages = [ pkgs.moonlight-qt ]; + }; +} diff --git a/modules/home/linux/thunar.nix b/modules/home/linux/thunar.nix new file mode 100644 index 0000000..a14657b --- /dev/null +++ b/modules/home/linux/thunar.nix @@ -0,0 +1,25 @@ +{ config, lib, pkgs, ... }: +# +# Thunar (https://docs.xfce.org/xfce/thunar/start) — GTK file manager. +# +# Available options: +# - home.apps.filemanager.thunar.enable +# +# No upstream HM module; install packages only. GTK theming inherits from +# the host's gtk = { ... }; block. +# +let + cfg = config.home.apps.filemanager.thunar; +in +{ + options.home.apps.filemanager.thunar.enable = + lib.mkEnableOption "Thunar file manager with volume automount"; + + config = lib.mkIf cfg.enable { + home.packages = with pkgs; [ + xfce.thunar + xfce.thunar-volman + xfce.thunar-archive-plugin + ]; + }; +} diff --git a/modules/home/linux/window-manager/default.nix b/modules/home/linux/window-manager/default.nix new file mode 100644 index 0000000..cae1eb8 --- /dev/null +++ b/modules/home/linux/window-manager/default.nix @@ -0,0 +1,12 @@ +# +# Linux window manager stack: Sway + Waybar + Mako + Fuzzel + Swaylock. +# +{ + imports = [ + ./sway.nix + ./waybar.nix + ./mako.nix + ./fuzzel.nix + ./swaylock.nix + ]; +} diff --git a/modules/home/linux/window-manager/fuzzel.nix b/modules/home/linux/window-manager/fuzzel.nix new file mode 100644 index 0000000..a674157 --- /dev/null +++ b/modules/home/linux/window-manager/fuzzel.nix @@ -0,0 +1,48 @@ +{ config, lib, ... }: +# +# Fuzzel (https://codeberg.org/dnkl/fuzzel) — dmenu-like Wayland launcher. +# +# Available options: +# - home.apps.windowManager.fuzzel.enable +# +let + cfg = config.home.apps.windowManager.fuzzel; + c = config.theme.dark; + # fuzzel wants rrggbbaa hex without the leading '#'. + stripHash = s: lib.removePrefix "#" s; +in +{ + options.home.apps.windowManager.fuzzel.enable = + lib.mkEnableOption "Fuzzel application launcher"; + + config = lib.mkIf cfg.enable { + programs.fuzzel = { + enable = true; + settings = { + main = { + font = "JetBrainsMonoNL Nerd Font:size=12"; + terminal = "ghostty"; + layer = "overlay"; + width = 40; + lines = 15; + horizontal-pad = 20; + vertical-pad = 12; + inner-pad = 8; + }; + colors = { + background = "${stripHash c.bg}f0"; + text = "${stripHash c.fg}ff"; + match = "${stripHash c.blue}ff"; + selection = "${stripHash c.bg_highlight}ff"; + selection-text = "${stripHash c.fg}ff"; + selection-match = "${stripHash c.blue}ff"; + border = "${stripHash c.blue}ff"; + }; + border = { + width = 2; + radius = 6; + }; + }; + }; + }; +} diff --git a/modules/home/linux/window-manager/mako.nix b/modules/home/linux/window-manager/mako.nix new file mode 100644 index 0000000..6b237a6 --- /dev/null +++ b/modules/home/linux/window-manager/mako.nix @@ -0,0 +1,34 @@ +{ config, lib, ... }: +# +# Mako (https://github.com/emersion/mako) — Wayland notification daemon. +# +# Available options: +# - home.apps.windowManager.mako.enable +# +let + cfg = config.home.apps.windowManager.mako; + c = config.theme.dark; +in +{ + options.home.apps.windowManager.mako.enable = + lib.mkEnableOption "Mako notification daemon"; + + config = lib.mkIf cfg.enable { + services.mako = { + enable = true; + settings = { + font = "JetBrainsMonoNL Nerd Font 10"; + background-color = c.bg; + text-color = c.fg; + border-color = c.blue; + border-size = 2; + border-radius = 6; + default-timeout = 6000; + anchor = "top-right"; + margin = "8"; + padding = "10"; + max-visible = 5; + }; + }; + }; +} diff --git a/modules/home/linux/window-manager/sway.nix b/modules/home/linux/window-manager/sway.nix new file mode 100644 index 0000000..a445d13 --- /dev/null +++ b/modules/home/linux/window-manager/sway.nix @@ -0,0 +1,116 @@ +{ config, lib, pkgs, ... }: +# +# Sway (https://swaywm.org) — wlroots-based tiling Wayland compositor. +# +# Available options: +# - home.apps.windowManager.sway.enable +# +# System-level `programs.sway.enable` in the host installs the compositor +# system-wide; this module owns user config (bindings, output, input, +# startup, idle chain, colors). +# +# grp:caps_toggle binds Capslock at the xkb layer to cycle us↔cz — no app +# can bind it and Capslock loses its latch function. +# +let + cfg = config.home.apps.windowManager.sway; +in +{ + options.home.apps.windowManager.sway.enable = + lib.mkEnableOption "Sway window manager (user-level config)"; + + config = lib.mkIf cfg.enable { + wayland.windowManager.sway = { + enable = true; + wrapperFeatures.gtk = true; + + config = rec { + modifier = "Mod4"; + terminal = "ghostty"; + menu = "fuzzel"; + + output."eDP-1" = { + mode = "3840x2400@60Hz"; + scale = "2"; + }; + + input."type:keyboard" = { + xkb_layout = "us,cz"; + xkb_variant = "altgr-intl,"; + xkb_options = "grp:caps_toggle"; + }; + input."type:touchpad" = { + tap = "enabled"; + natural_scroll = "enabled"; + }; + + keybindings = let mod = modifier; in { + "${mod}+Return" = "exec ${terminal}"; + "${mod}+space" = "exec ${menu}"; + "${mod}+w" = "kill"; + "${mod}+Shift+e" = "exit"; + + "${mod}+h" = "focus left"; + "${mod}+j" = "focus down"; + "${mod}+k" = "focus up"; + "${mod}+l" = "focus right"; + + "${mod}+Ctrl+h" = "move left"; + "${mod}+Ctrl+j" = "move down"; + "${mod}+Ctrl+k" = "move up"; + "${mod}+Ctrl+l" = "move right"; + + "${mod}+1" = "workspace number 1"; + "${mod}+2" = "workspace number 2"; + "${mod}+3" = "workspace number 3"; + "${mod}+4" = "workspace number 4"; + "${mod}+5" = "workspace number 5"; + + "${mod}+Ctrl+1" = "move container to workspace number 1"; + "${mod}+Ctrl+2" = "move container to workspace number 2"; + "${mod}+Ctrl+3" = "move container to workspace number 3"; + "${mod}+Ctrl+4" = "move container to workspace number 4"; + "${mod}+Ctrl+5" = "move container to workspace number 5"; + + "${mod}+e" = "layout toggle split"; + "${mod}+f" = "fullscreen"; + "${mod}+Shift+space" = "floating toggle"; + + "XF86MonBrightnessUp" = "exec brightnessctl set +5%"; + "XF86MonBrightnessDown" = "exec brightnessctl set 5%-"; + "XF86AudioRaiseVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"; + "XF86AudioLowerVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; + "XF86AudioMute" = "exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; + + "${mod}+Escape" = "exec swaylock"; + }; + + # Polkit auth agent — the system-level `security.polkit.enable` + # counterpart lives in the host default.nix. + startup = [ + { command = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"; } + ]; + + colors = let c = config.theme.dark; in { + focused = { border = c.blue; background = c.bg; text = c.fg; indicator = c.blue; childBorder = c.blue; }; + focusedInactive = { border = c.bg_dark; background = c.bg; text = c.fg_dark; indicator = c.bg_dark; childBorder = c.bg_dark; }; + unfocused = { border = c.bg_dark; background = c.bg; text = c.comment; indicator = c.bg_dark; childBorder = c.bg_dark; }; + urgent = { border = c.red; background = c.red; text = c.fg; indicator = c.red; childBorder = c.red; }; + }; + + fonts = { + names = [ "JetBrainsMonoNL Nerd Font" ]; + size = 10.0; + }; + }; + + extraConfig = '' + exec swayidle -w \ + timeout 180 'brightnessctl -s set 20%' resume 'brightnessctl -r' \ + timeout 300 'swaylock -f' \ + timeout 600 'swaymsg "output * dpms off"' resume 'swaymsg "output * dpms on"' \ + timeout 1200 'systemctl suspend' + ''; + }; + }; +} diff --git a/modules/home/linux/window-manager/swaylock.nix b/modules/home/linux/window-manager/swaylock.nix new file mode 100644 index 0000000..b3d34f7 --- /dev/null +++ b/modules/home/linux/window-manager/swaylock.nix @@ -0,0 +1,59 @@ +{ config, lib, ... }: +# +# Swaylock (https://github.com/swaywm/swaylock) — Sway screen locker. +# +# Available options: +# - home.apps.windowManager.swaylock.enable +# +# Invoked by swayidle timeout chain (see sway.nix) and Mod+Escape. +# +let + cfg = config.home.apps.windowManager.swaylock; + c = config.theme.dark; + # swaylock reads rrggbb hex, no leading '#'. + stripHash = s: lib.removePrefix "#" s; +in +{ + options.home.apps.windowManager.swaylock.enable = + lib.mkEnableOption "Swaylock screen locker"; + + config = lib.mkIf cfg.enable { + programs.swaylock = { + enable = true; + settings = { + color = stripHash c.bg; + font = "JetBrainsMonoNL Nerd Font"; + font-size = 24; + + indicator-radius = 100; + indicator-thickness = 10; + + ring-color = stripHash c.bg_dark; + ring-clear-color = stripHash c.orange; + ring-caps-lock-color = stripHash c.yellow; + ring-ver-color = stripHash c.blue; + ring-wrong-color = stripHash c.red; + + key-hl-color = stripHash c.blue; + bs-hl-color = stripHash c.red; + text-color = stripHash c.fg; + text-clear-color = stripHash c.orange; + text-ver-color = stripHash c.blue; + text-wrong-color = stripHash c.red; + text-caps-lock-color = stripHash c.yellow; + + inside-color = stripHash c.bg; + inside-clear-color = stripHash c.bg; + inside-ver-color = stripHash c.bg; + inside-wrong-color = stripHash c.bg; + inside-caps-lock-color = stripHash c.bg; + + line-color = stripHash c.bg; + + show-failed-attempts = true; + daemonize = true; + ignore-empty-password = true; + }; + }; + }; +} diff --git a/modules/home/linux/window-manager/waybar.nix b/modules/home/linux/window-manager/waybar.nix new file mode 100644 index 0000000..c45c02a --- /dev/null +++ b/modules/home/linux/window-manager/waybar.nix @@ -0,0 +1,106 @@ +{ config, lib, pkgs, ... }: +# +# Waybar (https://github.com/Alexays/Waybar) — Sway status bar. +# +# Available options: +# - home.apps.windowManager.waybar.enable +# +# The `tray` module is required for blueman-applet (StatusNotifierItem); +# without it the applet runs invisibly. +# +let + cfg = config.home.apps.windowManager.waybar; + c = config.theme.dark; +in +{ + options.home.apps.windowManager.waybar.enable = + lib.mkEnableOption "Waybar status bar for Sway"; + + config = lib.mkIf cfg.enable { + programs.waybar = { + enable = true; + systemd.enable = true; + + settings.mainBar = { + layer = "top"; + position = "top"; + height = 28; + spacing = 6; + modules-left = [ "sway/workspaces" "sway/mode" ]; + modules-center = [ "sway/window" ]; + modules-right = [ "tray" "pulseaudio" "network" "battery" "clock" ]; + + "sway/workspaces" = { + disable-scroll = true; + all-outputs = true; + }; + + tray = { spacing = 10; }; + + clock = { + format = "{:%a %d %b %H:%M}"; + tooltip-format = "{:%Y-%m-%d}\n{calendar}"; + }; + + battery = { + format = "{capacity}% {icon}"; + format-icons = [ "" "" "" "" "" ]; + format-charging = "{capacity}% "; + states = { warning = 30; critical = 15; }; + }; + + network = { + format-wifi = "{essid} ({signalStrength}%) "; + format-ethernet = "{ifname} "; + format-disconnected = "disconnected ⚠"; + tooltip-format = "{ifname}: {ipaddr}/{cidr}"; + }; + + pulseaudio = { + format = "{volume}% {icon}"; + format-muted = "muted "; + format-icons = { default = [ "" "" "" ]; }; + on-click = "${pkgs.pavucontrol}/bin/pavucontrol"; + }; + }; + + style = '' + * { + font-family: "JetBrainsMonoNL Nerd Font", monospace; + font-size: 12px; + border: none; + border-radius: 0; + min-height: 0; + } + + window#waybar { + background: ${c.bg}; + color: ${c.fg}; + } + + #workspaces button { + padding: 0 8px; + color: ${c.fg_dark}; + background: transparent; + border-bottom: 2px solid transparent; + } + #workspaces button.focused { + color: ${c.fg}; + border-bottom: 2px solid ${c.blue}; + } + #workspaces button.urgent { + color: ${c.red}; + border-bottom: 2px solid ${c.red}; + } + + #clock, #battery, #network, #pulseaudio, #tray, #mode { + padding: 0 10px; + color: ${c.fg}; + } + + #battery.warning { color: ${c.orange}; } + #battery.critical { color: ${c.red}; } + ''; + }; + }; +} From 32cf30bb408a828517e9baca8b9b1a222bb06e66 Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 27 Jul 2026 18:23:32 +0200 Subject: [PATCH 02/19] Harden astoria install docs + verify-hardware.sh --- hosts/astoria/README.md | 24 ++++++++++++----- hosts/astoria/verify-hardware.sh | 46 +++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/hosts/astoria/README.md b/hosts/astoria/README.md index 939175e..fbb59d6 100644 --- a/hosts/astoria/README.md +++ b/hosts/astoria/README.md @@ -3,8 +3,10 @@ Sofa/bed/bath companion to a TV-connected Nobara desktop. Sway, Moonlight, LibreWolf. TV inputs via AV receiver → single-scene Sunshine on the host. -**Design plan**: `/Users/du234/.claude/plans/i-am-planning-to-fancy-lollipop.md` -— rationale, alternatives considered, threat model. This file is the runbook. +Design rationale (why Lanzaboote, why TPM-sealed cryptswap, why sops-nix over a +private flake, threat model) lives in the author's working notes outside the +repo. In-file: each module's header comment carries the "why" for its own +choices; this file is the operator's runbook. --- @@ -25,9 +27,11 @@ LibreWolf. TV inputs via AV receiver → single-scene Sunshine on the host. ``` nix-shell -p rclone --run 'rclone obscure APP_PASSWORD' ``` -- [ ] **WebDAV endpoint sanity check** — auth works, directory lists: +- [ ] **WebDAV endpoint sanity check** — auth works, directory lists. + `-u USER` (no colon) makes curl prompt for the password on tty rather + than leaving it in argv / scrollback / `ps`: ``` - curl -X PROPFIND -H 'Depth: 0' -u USER:APP_PASSWORD https://WEBDAV_HOST/WEBDAV_ROOT/ + curl -X PROPFIND -H 'Depth: 0' -u USER https://WEBDAV_HOST/WEBDAV_ROOT/ ``` Success = `` XML. @@ -187,11 +191,19 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ nmcli device wifi connect password ``` - [ ] **Hardware inventory** — adjust `hardware.nix` if output contradicts - the AX201 / Ice Lake assumptions: + the AX201 / Ice Lake assumptions. Fetch to a file first (never pipe a + raw HTTP response straight into `sh` — a compromised repo tag or an + MITM'd CDN response would execute unreviewed), inspect if you like, + then run the local copy: ``` USER_GH= - curl -sL "https://raw.githubusercontent.com/${USER_GH}/dotfiles/main/hosts/astoria/verify-hardware.sh" | sh + curl -sLo /tmp/verify-hardware.sh "https://raw.githubusercontent.com/${USER_GH}/dotfiles/main/hosts/astoria/verify-hardware.sh" + less /tmp/verify-hardware.sh # optional inspection + sh /tmp/verify-hardware.sh ``` + Alternatively, if you already cloned the flake in Phase 3 step 1 + (see below), just run `sh /tmp/dotfiles/hosts/astoria/verify-hardware.sh` + from that git-verified copy. --- diff --git a/hosts/astoria/verify-hardware.sh b/hosts/astoria/verify-hardware.sh index 99660ff..f0d48cd 100755 --- a/hosts/astoria/verify-hardware.sh +++ b/hosts/astoria/verify-hardware.sh @@ -29,17 +29,27 @@ else fi section "Platform profile (TLP PLATFORM_PROFILE_ON_BAT)" -# If choices is empty/missing, PLATFORM_PROFILE_ON_BAT in TLP is a silent no-op. -if [ -r /sys/firmware/acpi/platform_profile_choices ]; then - printf ' choices: %s\n' "$(cat /sys/firmware/acpi/platform_profile_choices)" +# The file may exist and be readable but empty on SKUs that expose the ACPI +# node without any profiles — treat empty as unsupported (silent no-op for TLP). +pp_choices=$(cat /sys/firmware/acpi/platform_profile_choices 2>/dev/null) +if [ -n "$pp_choices" ]; then + printf ' choices: %s\n' "$pp_choices" printf ' active: %s\n' "$(cat /sys/firmware/acpi/platform_profile 2>/dev/null || echo unknown)" else echo ' not supported on this SKU — PLATFORM_PROFILE_ON_BAT will be a silent no-op' echo ' -> OK to remove that TLP setting from hardware.nix (tidy)' fi +unset pp_choices section "Fingerprint reader" -lsusb 2>/dev/null | grep -i '27c6' | sed 's/^/ /' || echo ' no Goodix device found' +# grep's exit status must drive the fallback, not sed's (sed returns 0 on +# empty input). Buffer the grep output first so we can test it directly. +if goodix=$(lsusb 2>/dev/null | grep -i '27c6') && [ -n "$goodix" ]; then + printf '%s\n' "$goodix" | sed 's/^/ /' +else + echo ' no Goodix device found' +fi +unset goodix section "BIOS version" if command -v fwupdmgr >/dev/null 2>&1; then @@ -52,14 +62,30 @@ section "GPU / VA-API" lspci -nnk 2>/dev/null | grep -A2 VGA | sed 's/^/ /' section "Battery health (approximate)" +# Guard the divisions: sysfs can return 0 or blank on uncalibrated / hot-swap / +# corrupt states, and `$((… / 0))` aborts the script under `set -u`. Read both +# counters, verify they're positive integers, then compute. +report_health() { + # $1 = human label ("charge_full" | "energy_full") + # $2 = current, $3 = design. + # `[ "$X" -ge 0 ]` errors (silently, via stderr redirect) on non-numeric or + # empty input → `if` branch not taken → we fall to the "skipped" printf. + # Both operands need this guard: `$((… / 0))` and `$(( / … ))` both abort + # the arithmetic under `set -u`. + if [ "$2" -ge 0 ] 2>/dev/null && [ "$3" -gt 0 ] 2>/dev/null; then + printf ' BAT0 %s=%s design=%s (health ~%d%%)\n' "$1" "$2" "$3" "$((100 * $2 / $3))" + else + printf ' BAT0 %s=%s design=%s (health: skipped — counter invalid or design zero)\n' "$1" "$2" "$3" + fi +} if [ -r /sys/class/power_supply/BAT0/charge_full ] && [ -r /sys/class/power_supply/BAT0/charge_full_design ]; then - now=$(cat /sys/class/power_supply/BAT0/charge_full) - design=$(cat /sys/class/power_supply/BAT0/charge_full_design) - printf ' BAT0 charge_full=%s design=%s (health ~%d%%)\n' "$now" "$design" "$((100 * now / design))" + report_health charge_full \ + "$(cat /sys/class/power_supply/BAT0/charge_full)" \ + "$(cat /sys/class/power_supply/BAT0/charge_full_design)" elif [ -r /sys/class/power_supply/BAT0/energy_full ] && [ -r /sys/class/power_supply/BAT0/energy_full_design ]; then - now=$(cat /sys/class/power_supply/BAT0/energy_full) - design=$(cat /sys/class/power_supply/BAT0/energy_full_design) - printf ' BAT0 energy_full=%s design=%s (health ~%d%%)\n' "$now" "$design" "$((100 * now / design))" + report_health energy_full \ + "$(cat /sys/class/power_supply/BAT0/energy_full)" \ + "$(cat /sys/class/power_supply/BAT0/energy_full_design)" else echo ' BAT0 counters not readable' fi From a9b5957722636e10c52cb8b27403633dd2450861 Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 27 Jul 2026 19:01:56 +0200 Subject: [PATCH 03/19] Run verify-hardware.sh from cloned repo --- hosts/astoria/README.md | 44 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/hosts/astoria/README.md b/hosts/astoria/README.md index fbb59d6..9ecaee5 100644 --- a/hosts/astoria/README.md +++ b/hosts/astoria/README.md @@ -87,7 +87,7 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ - Pubkey line → replace `age1astoriahostPUBKEY_TBD` in `.sops.yaml`. - Private key → vault entry `"astoria SSH host key"`. If your vault's Password field rejects multi-line PEM, base64 it to a single line first - (decoded back during Phase 3 step 3): + (decoded back during Phase 3 step 4): ``` base64 -i /tmp/astoria_host_key | tr -d '\n' | pbcopy # macOS base64 /tmp/astoria_host_key | tr -d '\n' | xclip -sel c # Linux (X11) @@ -190,20 +190,6 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ ``` nmcli device wifi connect password ``` -- [ ] **Hardware inventory** — adjust `hardware.nix` if output contradicts - the AX201 / Ice Lake assumptions. Fetch to a file first (never pipe a - raw HTTP response straight into `sh` — a compromised repo tag or an - MITM'd CDN response would execute unreviewed), inspect if you like, - then run the local copy: - ``` - USER_GH= - curl -sLo /tmp/verify-hardware.sh "https://raw.githubusercontent.com/${USER_GH}/dotfiles/main/hosts/astoria/verify-hardware.sh" - less /tmp/verify-hardware.sh # optional inspection - sh /tmp/verify-hardware.sh - ``` - Alternatively, if you already cloned the flake in Phase 3 step 1 - (see below), just run `sh /tmp/dotfiles/hosts/astoria/verify-hardware.sh` - from that git-verified copy. --- @@ -222,7 +208,23 @@ git clone "https://github.com/${GITHUB_USER}/dotfiles" /tmp/dotfiles cd /tmp/dotfiles ``` -### 2. Partition + format +### 2. Hardware inventory + +Run the verify script from the just-cloned (git-verified) copy — never +pipe a raw HTTP response into `sh`; a compromised repo tag or an MITM'd +CDN response would execute unreviewed. `hardware.nix` assumes AX201 / +Ice Lake; adjust it if the output disagrees BEFORE the install step. + +``` +sh hosts/astoria/verify-hardware.sh +``` + +If it flags a QCA6390 Wi-Fi chip, unavailable `platform_profile`, +non-`s2idle` sleep mode, etc., edit `hosts/astoria/hardware.nix` now +(see the in-file comments for the alternate values) — the install +step below picks up the changes. + +### 3. Partition + format ``` sudo nix run 'github:nix-community/disko' \ @@ -242,7 +244,7 @@ Prompts, in order: 2. **cryptswap** LUKS passphrase (2×) — from vault `"astoria cryptswap"`. 3. **cryptroot** LUKS passphrase (2×) — from vault `"astoria login"`. -### 3. Transfer astoria's SSH host key from dev → installer +### 4. Transfer astoria's SSH host key from dev → installer On the installer, set a throwaway login password (the `nixos` user starts empty, which blocks ssh): @@ -280,7 +282,7 @@ sudo ssh-keygen -y -f /mnt/etc/ssh/ssh_host_ed25519_key | \ sudo install -m 644 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key.pub ``` -### 4. Generate sbctl Secure Boot keys +### 5. Generate sbctl Secure Boot keys `nixos-install` signs the bootloader via Lanzaboote's installHook using these keys, so they must exist at `/mnt/var/lib/sbctl` before install. @@ -296,7 +298,7 @@ Gotchas: not a directory. Passing a directory → EISDIR → no keys ever created. - Older sbctl uses `--keydir` / `--pki-dir` instead. Check `--help` first. -### 5. Install +### 6. Install ``` sudo nixos-install --flake /tmp/dotfiles#astoria --no-root-passwd @@ -310,7 +312,7 @@ During activation: - cryptswap TPM keyslot doesn't exist yet — first boot prompts for the disko-set passphrase. TPM enrollment happens in Phase 4c. -### 6. Reboot +### 7. Reboot cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd → user login → Sway. @@ -335,7 +337,7 @@ cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd - [ ] `systemctl status sops-install-secrets` — active, exit 0. - [ ] `ls /run/secrets-for-users/users/brutcha/` — hashed-password present. - [ ] `ls /run/secrets/{restic,rclone}/` — secrets present. -- [ ] `lspci -k` — Wi-Fi chip matches Phase 2 hw-inventory. +- [ ] `lspci -k` — Wi-Fi chip matches Phase 3 hw-inventory output. - [ ] `cat /sys/power/mem_sleep` — `[s2idle]` bracketed. - [ ] **Git-clone the flake** to `/home/brutcha/git/dotfiles` — otherwise `environment.etc.nixos.source` is a dangling symlink and later From 7cc05692fd2da41798c674208f1a7c18780042ef Mon Sep 17 00:00:00 2001 From: brutcha Date: Tue, 28 Jul 2026 09:48:24 +0200 Subject: [PATCH 04/19] Label bash code fences in astoria README --- hosts/astoria/README.md | 68 ++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/hosts/astoria/README.md b/hosts/astoria/README.md index 9ecaee5..a481596 100644 --- a/hosts/astoria/README.md +++ b/hosts/astoria/README.md @@ -24,13 +24,13 @@ choices; this file is the operator's runbook. reuse the main account password. - [ ] **rclone-obscure the app-password** for use in rclone.conf (Phase 1 step 7): - ``` + ```bash nix-shell -p rclone --run 'rclone obscure APP_PASSWORD' ``` - [ ] **WebDAV endpoint sanity check** — auth works, directory lists. `-u USER` (no colon) makes curl prompt for the password on tty rather than leaving it in argv / scrollback / `ps`: - ``` + ```bash curl -X PROPFIND -H 'Depth: 0' -u USER https://WEBDAV_HOST/WEBDAV_ROOT/ ``` Success = `` XML. @@ -80,7 +80,7 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ ### Steps 1. **Astoria host SSH key** - ``` + ```bash ssh-keygen -t ed25519 -f /tmp/astoria_host_key -N '' -C mail@brutcha.dev ssh-to-age -i /tmp/astoria_host_key.pub ``` @@ -88,14 +88,14 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ - Private key → vault entry `"astoria SSH host key"`. If your vault's Password field rejects multi-line PEM, base64 it to a single line first (decoded back during Phase 3 step 4): - ``` + ```bash base64 -i /tmp/astoria_host_key | tr -d '\n' | pbcopy # macOS base64 /tmp/astoria_host_key | tr -d '\n' | xclip -sel c # Linux (X11) base64 /tmp/astoria_host_key | tr -d '\n' | wl-copy # Linux (Wayland) ``` 2. **Recovery age keypair** (once, ever) - ``` + ```bash age-keygen -o /tmp/recovery.txt ``` - Pubkey → replace `age1recoveryPUBKEY_TBD` in `.sops.yaml`. @@ -103,7 +103,7 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ (single-line, no base64 needed). 3. **Export recovery key for sops** - ``` + ```bash export SOPS_AGE_KEY_FILE=/tmp/recovery.txt ``` Step 7 encrypts fresh (only needs pubkeys). Step 8 (`sops updatekeys`) @@ -119,18 +119,18 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ 5. **Restic repo password** → vault entry `"astoria restic repo"`. Strong random: - ``` + ```bash openssl rand -base64 48 ``` 6. **User password hash** — via temp file to keep the hash off scrollback: - ``` + ```bash mkpasswd -m yescrypt > /tmp/astoria-hash ``` Enter the login passphrase from step 4 at the prompt. 7. **Populate secrets YAML** - ``` + ```bash mkdir -p hosts/astoria/secrets # sops uses os.WriteFile — no MkdirAll sops hosts/astoria/secrets/astoria.yaml ``` @@ -154,14 +154,14 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ `restic-backups-webdav.service` will fail on first boot. 8. **Encrypt to both recipients** - ``` + ```bash sops updatekeys hosts/astoria/secrets/astoria.yaml ``` 9. **Commit + push**. 10. **Shred + clear scrollback** - ``` + ```bash shred -u /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash printf '\033c' ``` @@ -187,7 +187,7 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ - [ ] **Boot NixOS 26.11 minimal installer USB** (nixos-unstable ISO OK while 26.11 is pre-release — flake pins nixpkgs to the same channel). - [ ] **Wi-Fi**: - ``` + ```bash nmcli device wifi connect password ``` @@ -195,14 +195,14 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ ## Phase 3 — install -``` +```bash GITHUB_USER= ASTORIA_IP= # from `ip addr | grep 'inet '` on the installer ``` ### 1. Clone the flake -``` +```bash nix-shell -p git git clone "https://github.com/${GITHUB_USER}/dotfiles" /tmp/dotfiles cd /tmp/dotfiles @@ -215,7 +215,7 @@ pipe a raw HTTP response into `sh`; a compromised repo tag or an MITM'd CDN response would execute unreviewed. `hardware.nix` assumes AX201 / Ice Lake; adjust it if the output disagrees BEFORE the install step. -``` +```bash sh hosts/astoria/verify-hardware.sh ``` @@ -226,7 +226,7 @@ step below picks up the changes. ### 3. Partition + format -``` +```bash sudo nix run 'github:nix-community/disko' \ --extra-experimental-features 'nix-command flakes' \ -- --mode destroy,format,mount --flake .#astoria @@ -248,7 +248,7 @@ Prompts, in order: On the installer, set a throwaway login password (the `nixos` user starts empty, which blocks ssh): -``` +```bash sudo passwd nixos ``` @@ -257,7 +257,7 @@ into the installer over ssh. `sudo install` (below) atomically creates the target file with the right owner + 0600 mode — no umask window during which the file would be world-readable. -``` +```bash # Option A — Linux dev (xclip): xclip -o -selection clipboard | base64 -d | ssh "nixos@${ASTORIA_IP}" \ 'sudo mkdir -p /mnt/etc/ssh && sudo install -m 600 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key' @@ -277,7 +277,7 @@ paste the PEM directly. On the installer, derive the .pub (strict openssh refuses to read a 0644 private key without a matching .pub): -``` +```bash sudo ssh-keygen -y -f /mnt/etc/ssh/ssh_host_ed25519_key | \ sudo install -m 644 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key.pub ``` @@ -287,7 +287,7 @@ sudo ssh-keygen -y -f /mnt/etc/ssh/ssh_host_ed25519_key | \ `nixos-install` signs the bootloader via Lanzaboote's installHook using these keys, so they must exist at `/mnt/var/lib/sbctl` before install. -``` +```bash sudo nix-shell -p sbctl --run 'sbctl create-keys --help' # confirm flag names first sudo mkdir -p /mnt/var/lib/sbctl sudo nix-shell -p sbctl --run 'sbctl create-keys --export /mnt/var/lib/sbctl/keys --database-path /mnt/var/lib/sbctl/GUID' @@ -300,7 +300,7 @@ Gotchas: ### 6. Install -``` +```bash sudo nixos-install --flake /tmp/dotfiles#astoria --no-root-passwd ``` @@ -359,7 +359,7 @@ cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd Enroll BEFORE turning SB on in BIOS — first SB-enabled boot must verify Lanzaboote against enrolled keys. -``` +```bash sudo sbctl status # Setup Mode: Enabled, Secure Boot: Disabled sudo sbctl enroll-keys --microsoft # --microsoft: append MS certs to KEK+db (option ROMs) sudo sbctl verify # every file: ✓ Signed @@ -369,14 +369,14 @@ Reboot → BIOS → enable Secure Boot → save. Boots normally, still prompts f both cryptroot + cryptswap passphrases (TPM not enrolled yet). Verify from the running system: -``` +```bash bootctl status | grep 'Secure Boot' # enabled sudo sbctl verify ``` ### 4c. Enroll TPM keyslot for cryptswap -``` +```bash ls /sys/class/tpm/ # tpm0 should be there sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7 /dev/nvme0n1p2 ``` @@ -391,16 +391,16 @@ via TPM (no prompt). **Recovery if enrollment failed mid-step** (tpm2-tss error, DA-lockout, PCR read error): the passphrase keyslot is UNAFFECTED — cryptswap still opens via passphrase. Clean up with: -``` +```bash sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p2 # non-fatal if no slot ``` Then retry the enroll command. If DA-locked: -``` +```bash sudo tpm2_dictionarylockout --clear-lockout # blank owner pw on freshly-cleared TPM ``` Verify keyslots: -``` +```bash sudo systemd-cryptenroll /dev/nvme0n1p2 # slot 0 (password) + slot N (tpm2) ``` @@ -438,7 +438,7 @@ something: **Snapper rootfs rollback** — for reverting non-`/nix/store` drift (files edited outside the flake, corrupted state under `/etc`, `/var`, `/home`). Snapshots live under `/.snapshots//snapshot` and are created at every boot by `snapper-boot.service`: -``` +```bash sudo snapper -c root list # inspect snapshots + timestamps sudo snapper -c root diff .. # peek at what would change sudo snapper -c root undochange ..0 # restore from snapshot N @@ -461,7 +461,7 @@ content-addressed; `/home` is user data that Restic backs up. restic's password file. **Do NOT wrap these calls in another `sudo`** — inner sudo triggers `env_reset` + PAM PATH reset, which drops the nix-shell's ephemeral PATH. Use plain `env`: - ``` + ```bash env \ RCLONE_CONFIG=/run/secrets/rclone/webdav.conf \ RESTIC_PASSWORD_FILE=/run/secrets/restic/repo-password \ @@ -502,7 +502,7 @@ of the vault onto a helper device to run `sops`, mirror Phase 1's shred discipli local snapshots capture the file for ~24h. Use a RAM disk: `hdiutil attach -nomount ram://8192 | xargs -I{} diskutil erasevolume APFS 'ARamDisk' {}; SCRATCH=/Volumes/ARamDisk` 2. Set the cleanup trap FIRST (before any paste): - ``` + ```bash trap 'shred -u -- "$SCRATCH"/keys.txt 2>/dev/null; rm -rf "$SCRATCH" 2>/dev/null; diskutil eject ARamDisk 2>/dev/null' EXIT INT TERM ``` 3. Paste the recovery age private key to `"$SCRATCH"/keys.txt`; `chmod 600 "$SCRATCH"/keys.txt` immediately. @@ -514,7 +514,7 @@ of the vault onto a helper device to run `sops`, mirror Phase 1's shred discipli (they roll off in ~24h; force-delete with `tmutil deletelocalsnapshots`). ### LUKS passphrase change -``` +```bash sudo cryptsetup luksChangeKey /dev/nvme0n1p3 # cryptroot — also update `"astoria login"` in the vault sudo cryptsetup luksChangeKey /dev/nvme0n1p2 # cryptswap fallback — also update its vault entry ``` @@ -529,14 +529,14 @@ Symptoms: first boot after a BIOS update prompts for the cryptswap passphrase reports PCR mismatch. Fix: -``` +```bash sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme0n1p2 sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7 /dev/nvme0n1p2 ``` Prompted for the cryptswap passphrase to authorize. Reboot to verify silent unlock. ### Secure Boot state audit (occasional) -``` +```bash sudo sbctl status # Setup Mode: Disabled, Secure Boot: Enabled sudo sbctl verify # every EFI file: Signed bootctl status | grep 'Secure' @@ -552,7 +552,7 @@ If `sbctl verify` shows unsigned files after a manual bootloader tweak: Setup Mode by enrolling an empty signature DB. **`sbctl reset` does NOT touch `/var/lib/sbctl`** — the local keypair on disk survives. 3. **Delete the local keypair** so step 4 doesn't silently no-op: - ``` + ```bash sudo rm -rf /var/lib/sbctl/keys /var/lib/sbctl/GUID ``` `sbctl create-keys` refuses to overwrite an existing keydir; skipping this rm From 834503bcfd4b94b0742757afb8121a661f37024d Mon Sep 17 00:00:00 2001 From: brutcha Date: Tue, 28 Jul 2026 11:30:16 +0200 Subject: [PATCH 05/19] Astoria: 0600 hash, stdin rclone-obscure, pinned disko --- flake.nix | 6 ++++++ hosts/astoria/README.md | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index 1973180..ef8d6e9 100644 --- a/flake.nix +++ b/flake.nix @@ -212,6 +212,12 @@ ]; in { + # Re-export disko's CLI at the flake's own package output so the + # astoria README can invoke it as `sudo nix run '.#disko' -- ...` + # from the cloned repo — that hits our lock-pinned commit rather + # than `github:nix-community/disko` HEAD. + packages.x86_64-linux.disko = inputs.disko.packages.x86_64-linux.disko; + # macOS system configuration for makima (personal MacBook) darwinConfigurations.makima = let diff --git a/hosts/astoria/README.md b/hosts/astoria/README.md index a481596..383c863 100644 --- a/hosts/astoria/README.md +++ b/hosts/astoria/README.md @@ -23,9 +23,11 @@ choices; this file is the operator's runbook. (Devices/Sessions/Tokens section, label `"astoria-restic"`). DO NOT reuse the main account password. - [ ] **rclone-obscure the app-password** for use in rclone.conf (Phase 1 - step 7): + step 7). Passing the password as an argv positional would leak it to + `ps` and shell history; `read -rs` prompts on tty and never puts the + value in argv: ```bash - nix-shell -p rclone --run 'rclone obscure APP_PASSWORD' + nix-shell -p rclone --run 'IFS= read -rsp "app-password: " pw; echo; rclone obscure "$pw"; unset pw' ``` - [ ] **WebDAV endpoint sanity check** — auth works, directory lists. `-u USER` (no colon) makes curl prompt for the password on tty rather @@ -123,9 +125,11 @@ trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/ openssl rand -base64 48 ``` -6. **User password hash** — via temp file to keep the hash off scrollback: +6. **User password hash** — via temp file to keep the hash off scrollback. + The subshell-scoped `umask 077` forces the redirected file to be created + as 0600 in a single syscall (no umask-races, no leak into the outer shell): ```bash - mkpasswd -m yescrypt > /tmp/astoria-hash + ( umask 077 && mkpasswd -m yescrypt > /tmp/astoria-hash ) ``` Enter the login passphrase from step 4 at the prompt. @@ -227,12 +231,17 @@ step below picks up the changes. ### 3. Partition + format ```bash -sudo nix run 'github:nix-community/disko' \ +sudo nix run '.#disko' \ --extra-experimental-features 'nix-command flakes' \ -- --mode destroy,format,mount --flake .#astoria ``` -Why the flags: `sudo` because disko doesn't self-elevate; the +`.#disko` (rather than `github:nix-community/disko`) resolves the disko +binary through the flake's own `packages.x86_64-linux.disko` re-export, +which is pinned via `flake.lock` — no unpinned github ref, no MITM-able +fetch, exact same code as everything else the flake produces. + +Why the other flags: `sudo` because disko doesn't self-elevate; the `--extra-experimental-features` inline because NixOS sudo drops `NIX_CONFIG` env, so `export NIX_CONFIG=…` in the outer shell wouldn't survive. From 5abc49d46e5c069cd974b2db2873ae35835e5558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Bo=C4=8Dek?= Date: Sun, 2 Aug 2026 11:28:05 +0200 Subject: [PATCH 06/19] change low-power profile name --- hosts/astoria/hardware.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/astoria/hardware.nix b/hosts/astoria/hardware.nix index eb91620..1ab538b 100644 --- a/hosts/astoria/hardware.nix +++ b/hosts/astoria/hardware.nix @@ -95,7 +95,7 @@ CPU_HWP_DYN_BOOST_ON_BAT = 0; # Silent no-op if /sys/firmware/acpi/platform_profile_choices is empty # (verify-hardware.sh reports). - PLATFORM_PROFILE_ON_BAT = "low-power"; + PLATFORM_PROFILE_ON_BAT = "cool"; INTEL_GPU_MIN_FREQ_ON_BAT = 100; INTEL_GPU_MAX_FREQ_ON_BAT = 750; RUNTIME_PM_ON_BAT = "auto"; From 68414481443a1e0a5c3cc845b6af7b7efa19825e Mon Sep 17 00:00:00 2001 From: brutcha Date: Sun, 2 Aug 2026 10:29:08 +0000 Subject: [PATCH 07/19] change &astoria sops key --- .sops.yaml | 2 +- hosts/astoria/secrets/astoria.yaml | 34 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.sops.yaml b/.sops.yaml index 98d03a2..0ab01d7 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -6,7 +6,7 @@ keys: - &brutcha_recovery age1uvs9kqpzmdty57x5a4m5c2x08nhpfxwhxpy5dp4f5dckykncs49qm538tl - - &astoria age17c3ydfwz2etvrxfyfeemkna4ndgunaqszseyzufm9fmggjm0kdds2fw0mk + - &astoria age1jzg3lvcacyrkhdqeu87fplqrsx6glrgfhjy9xtvapca8k22a5g9s6r22pl creation_rules: - path_regex: hosts/astoria/secrets/.*\.yaml$ diff --git a/hosts/astoria/secrets/astoria.yaml b/hosts/astoria/secrets/astoria.yaml index 075d21d..f9e00dd 100644 --- a/hosts/astoria/secrets/astoria.yaml +++ b/hosts/astoria/secrets/astoria.yaml @@ -1,31 +1,31 @@ users: brutcha: - hashed-password: ENC[AES256_GCM,data:Hay99vmjDFBiC1tm/sm6sWryH6YpFRQiLGkfTdP5/OCQhvvxVbi2XkfHaJ8Gy+MuwZe+M2GcBefVayDUs3U4iQzi1lw8ekZ0aw==,iv:eqeNvbruvHlEbi5otjqBEjDvri2XY3n/4As83Op5sRw=,tag:bGNVbjA0lb9/3fAGlPrZGw==,type:str] + hashed-password: ENC[AES256_GCM,data:nnIbGx4cYYgD13nuz7Gqc8qst2h2MrvFrvPWH9MFxGRYKxOcOuNvvMdIDskaTsdoVP7GVqBOQv15rDp4yXQoSss7E6EUp5ToRg==,iv:sxgMcvB9jvNboXj28HNglHiHe2+gnlMqveiV8JVws/A=,tag:WhkixZLIw+r7t8EB10LWdw==,type:str] restic: - repo-password: ENC[AES256_GCM,data:+/b0PrsMTiX4CBNPppUCHQ8I7PdWfF492MiOfYXqFcqeYgN+jtgMBBQETKhVS0gdypFxnCGwbdgTLNFSwEqBJw==,iv:CYKmiDo3uOtXPTD90vGprEzzHfSI7hdWBuryd7jqQLw=,tag:uER1VsU304eLX986ELbj+g==,type:str] + repo-password: ENC[AES256_GCM,data:GBLQNrOIbqFXn2dkTW1c3nCyHs6TQslroeTINz9V6foYRPtuyZyOWGJjTdfPlVMe3/N1MJpdSfOUE+Oo+ToySw==,iv:1OFCFLJE41XZsnY8p5lu/pPsA/YDYrLuArUn4YBAJcM=,tag:08uZznAEbcdsuWmzbSHQ5Q==,type:str] rclone: - webdav.conf: ENC[AES256_GCM,data:xl/F8nqKSTOcmcmTXVZ/HNNhxR93RNdx6z7sHn366yo5PSRmTKMzdPBJfCWG8gx6Q0JWIhfQwCVI33iWz3kDI/o4fCxTBlu5ywGG2TiuocKm5wQ77sAPcS6Y1O6FzJ4XDBBTVa2cPOPyfqfze8ejBJ/7GQ7d9fWdZvAtjFwgaQfAmSVWquAFtABnVeJTH/eLRlha3bdakozElmuXpxqDD2H+Pf0qEAlk1zRh58yk9dEIq3M5sqlLBBst,iv:N4ut1wI0o8a6Sq6fAVz/ovdOVHr7US0+/0S5awEs/dk=,tag:r0a/kV1KAbtaUPzODReXfg==,type:str] + webdav.conf: ENC[AES256_GCM,data:ZlYE5N1B0+tvHu8R1LeyPt52oLUJRD61NaN8NVAqTrGo4g8xUTz2tYNujV3jU20D73GslILnFxb5OFWkaNmPrwABIcDvDKINbKd+lIQIvJ1ls9P/IA6PVrbuTYJoNtqw19+Mv9AzyPSbKN63jOBUDJ0QMbXZeJuUN7OE3eIDWnIjTAP0PXxrXCv5VN4UANaG6NeJsljA25vC3V00E91XlyY2vvuewuJ5JS63cvMnpzrPcdO4w0Fk99lo,iv:E04kboQaKlLGE6B483RJO7FCxhuR5m7JW0kwo+mBCko=,tag:ScpYo2W2BeSC5+4k+9YoKg==,type:str] sops: age: - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYcURDMXNJSjFLNldraGVK - SWgvRmw2STU5Q2lHL0dSMjVZNzdxSVhUQjFzCnRUMmJGdXhvOUk3VFZ5VS8rME5Q - SmxCL3hrbjBXZkZSZmlIT1NNTjhINDQKLS0tIEV0SnUzYlNmRVZLTFFjK0lnVXhF - YVlXOVFHL0xUZzVFM1QxUVJlT2hnSG8KhcvI5joSSLbF4N+m/eJdIq03y4PpeEh9 - 5kfSxMsybh+pupPbmVdL1e+mTJ1f7bT4ztS43G/f6xbtriuhcxrCAQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQWm1iRjB0RXhERENTNFNJ + eGdSeHJqd2QvTEtnbWEvMXBPZ0QrN0xBS0FVCnBqS2dnbkZVSkIrMEtHMG5oWUgx + WTl2Vkc1KzhzSEw0ZUg4ZlBnZVFXNHcKLS0tIEFmSUFNR1JSbzZJaTlJRXFNRmRR + ZitmeDgyODFwQzZ0TW1pbVVDOWkyY28KQS1g4hjAU1F1mVExFiq9cOQMtnkK6BAg + rDQzYfMVYXqQVGhVeZ02dCiO3BWj5w89mQh7+MFl/85rFDJhkDSQQQ== -----END AGE ENCRYPTED FILE----- recipient: age1uvs9kqpzmdty57x5a4m5c2x08nhpfxwhxpy5dp4f5dckykncs49qm538tl - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmRUViN2czN1hraWlHYjlY - SW9FN1lmR0Jwcmtnd0NHZk9LckdnRFcwRFFFClFZK3dZRGs4SXR0N1dObitMYy9v - MnlMY0EzSm8wRi9Hd1A4dlVucGVUL3cKLS0tIGR4VVcwdUFXMEtMK0dqODZxSjZu - eGU3NXkzVElyWTlSRU1SVFNUajROVFkKxlGi8PDn7peW1y4xwi2Rko9qrkVGgPnw - Kzl5f9Y6FNkwiq/WFmJHNvCIWPxc5KNicMDhoRUJw7TSE9Q8PIuQaQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzdU05YWFlK0ZSMEZ3Wk1L + em1hQzFuQ3hWTXpLdy9DSnBmZkNPZU4wWmdBCjEyWkJ6RVJkdzJNUExpSm5UQ0M1 + RERhN2VrM3pMWDRsL21saWdmNUFyQmsKLS0tIFJvLzJTeEVpbzlKcUlJWlVtQkMx + d01DZFBzZVhTOGxxUFhrVlB3YlRxZk0KA+X0VEYqtSMWoqS1ItjK6A/GUxdzbklW + KZiocG6UyP9LB38qPpUuU39hFvfuszqs57wK73zEPpju7a21GXyNaA== -----END AGE ENCRYPTED FILE----- - recipient: age17c3ydfwz2etvrxfyfeemkna4ndgunaqszseyzufm9fmggjm0kdds2fw0mk - lastmodified: "2026-07-27T12:25:34Z" - mac: ENC[AES256_GCM,data:TfRB7zZA2TEuYJ027z0S3ds3gS2TgL0K69FaAc86vNfj7BnUi6elhWPU+YE0zv3IV5QjY0ShLeJeevrLEQd5hifT8Xcmh1SHYHSxCCR1CsiYIwIuLkQ0o9PXjRXZPJDEZC7K251I5pbb3hjQz/wrUk9uooqyYqiutAY5LECNmHA=,iv:U3xW7AIkidYalC1iBYOr+IFVMYPUkLlqXQXrwQmOUMo=,tag:QAzvrAigaeSBPZ9UgkHQjQ==,type:str] + recipient: age1jzg3lvcacyrkhdqeu87fplqrsx6glrgfhjy9xtvapca8k22a5g9s6r22pl + lastmodified: "2026-08-02T10:26:49Z" + mac: ENC[AES256_GCM,data:PDqPrKiVibGhZG7Crs0MZFA89LDDe2fRYlqk6puaXWwLFEj4BMSHhBOvrZJ5dKUZiupPlhBrsZbAUblrpsmyI+no/zZekOE+VqliU2YfHA2JpFbhdfR/ipGjCvGQUii3GhUUhAxCLdTjDlPpn6BFDf8UwEM+gdEY4HSHvQMgttA=,iv:kz1VNg5eY7ZtKoNB28LVX5fp4Z3JJKVMoMw4yYwFJwA=,tag:OOkOvBE2ZPcdNhakW2yY5A==,type:str] unencrypted_suffix: _unencrypted - version: 3.13.2 + version: 3.13.3 From 2182aae4c8dbf57b4e84754d3a72b926e0342c2f Mon Sep 17 00:00:00 2001 From: brutcha Date: Sun, 2 Aug 2026 10:54:33 +0000 Subject: [PATCH 08/19] fix tuigreed pkg path --- hosts/astoria/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/astoria/default.nix b/hosts/astoria/default.nix index 57b40c1..c216370 100644 --- a/hosts/astoria/default.nix +++ b/hosts/astoria/default.nix @@ -52,7 +52,7 @@ services.greetd = { enable = true; settings.default_session = { - command = "${pkgs.greetd.tuigreet}/bin/tuigreet --time --remember --cmd sway"; + command = "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd sway"; user = "greeter"; }; }; From 77d888d829d73b3fcb21aace72befa07332a05e9 Mon Sep 17 00:00:00 2001 From: brutcha Date: Sun, 2 Aug 2026 10:58:00 +0000 Subject: [PATCH 09/19] fix experimental-features in flake.nix --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index ef8d6e9..e431b49 100644 --- a/flake.nix +++ b/flake.nix @@ -95,7 +95,7 @@ configuration = { pkgs, ... }: { # Enable flakes support globally to use nix flake commands # https://github.com/NixOS/nix/blob/master/doc/manual/rl-next.md - nix.settings.experimental-features = "nix-command flakes"; + nix.settings.experimental-features = ["nix-command" "flakes"]; # Flake-only setup — drop the legacy channels path from NIX_PATH # (silences "Nix search path entry .../channels does not exist"). From df3b7caa58010e47a159dc25ce3da16937f9bcc3 Mon Sep 17 00:00:00 2001 From: brutcha Date: Sun, 2 Aug 2026 11:01:26 +0000 Subject: [PATCH 10/19] force config.sway.default to use wlr --- hosts/astoria/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/astoria/default.nix b/hosts/astoria/default.nix index c216370..f5ef717 100644 --- a/hosts/astoria/default.nix +++ b/hosts/astoria/default.nix @@ -65,7 +65,7 @@ wlr.enable = true; extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; config = { - sway.default = [ "wlr" "gtk" ]; + sway.default = lib.mkForce [ "wlr" "gtk" ]; common.default = [ "wlr" "gtk" ]; }; }; From 1d056f5dd4b9c966f58b5bb8824a39f75f62041c Mon Sep 17 00:00:00 2001 From: brutcha Date: Sun, 2 Aug 2026 20:38:28 +0200 Subject: [PATCH 11/19] astoria: finish post-install fixes, add tooling, fix theming README: - fold in the post-install fix list (tuigreet path, WiFi comment, SATA/RAID BIOS gotcha, Secure Boot key-reset gotcha) - restructure: promote Rebuild/Update/Rollback and the rest of Operations reference to flat top-level sections, condense the one-time install phases (secrets setup, dev-machine key generation) into short reference docs pointing at secrets.nix and upstream tooling docs - rework the install flow for nix-community/nixos-images (flakes enabled by default, root SSH from boot) instead of the official minimal ISO - add a References section hardware.nix: resolve the now-confirmed AX201 WiFi hedge comment, add the missing CPU_BOOST_ON_AC TLP setting default.nix: enable programs.dconf (portal dark-mode signal backing), wire in sudoers.nix sudoers.nix: NOPASSWD sudo for `nixos-rebuild switch` screenshot.nix: grim/slurp/swappy region + full-screen capture imv.nix: default image viewer, wired as default app for common image mimetypes thunar.nix: drop the deprecated xfce.* package namespace librewolf.nix: fix GitHub login not persisting across restarts (clearOnShutdown_v2, LibreWolf migrated off the old clearOnShutdown.* prefs), fix prefers-color-scheme being forced to light for all sites (swap resistFingerprinting for fingerprintingProtection with a CSSPrefersColorScheme carve-out) waybar.nix/sway.nix/home.nix: SketchyBar-inspired restyle (workspace pills, icons, matching GTK font), nmtui/pavucontrol quick-launch on click, dconf dark-mode key, smart_borders on single-window workspaces nvim: fix treesitter build hook double-invocation --- config/nvim/lua/plugins/init.lua | 2 +- hosts/astoria/README.md | 442 ++++++------------ hosts/astoria/default.nix | 8 + hosts/astoria/hardware.nix | 7 +- hosts/astoria/home.nix | 33 +- hosts/astoria/sudoers.nix | 9 + hosts/astoria/verify-hardware.sh | 101 ---- modules/home/development/git.nix | 9 +- modules/home/linux/internet/librewolf.nix | 16 +- modules/home/linux/media/default.nix | 1 + modules/home/linux/media/imv.nix | 28 ++ modules/home/linux/thunar.nix | 6 +- modules/home/linux/window-manager/default.nix | 1 + .../home/linux/window-manager/screenshot.nix | 48 ++ modules/home/linux/window-manager/sway.nix | 46 +- modules/home/linux/window-manager/waybar.nix | 47 +- 16 files changed, 357 insertions(+), 447 deletions(-) create mode 100644 hosts/astoria/sudoers.nix delete mode 100755 hosts/astoria/verify-hardware.sh create mode 100644 modules/home/linux/media/imv.nix create mode 100644 modules/home/linux/window-manager/screenshot.nix diff --git a/config/nvim/lua/plugins/init.lua b/config/nvim/lua/plugins/init.lua index e02f72a..56511ce 100644 --- a/config/nvim/lua/plugins/init.lua +++ b/config/nvim/lua/plugins/init.lua @@ -44,7 +44,7 @@ return { "nvim-treesitter/nvim-treesitter", priority = 52, build = function() - require("nvim-treesitter.install").update({ with_sync = true })() + require("nvim-treesitter.install").update({ with_sync = true }) end, }, { diff --git a/hosts/astoria/README.md b/hosts/astoria/README.md index 383c863..42943ae 100644 --- a/hosts/astoria/README.md +++ b/hosts/astoria/README.md @@ -10,228 +10,128 @@ choices; this file is the operator's runbook. --- -## Prerequisites (one-time, before touching astoria) - -- [ ] **Password vault** reachable from a non-astoria device. astoria decrypts - its own secrets on first boot using its host SSH key — if that key ever - dies, recovery needs the age key stored in the vault. No non-astoria - vault access = no recovery. -- [ ] **Vault end-to-end / client-side encryption enabled** — set the vault's - unlock secret. Without it, the server operator can read every entry. - Verify the setting is active before storing recovery material. -- [ ] **WebDAV app-password** for astoria, from your WebDAV backend's web UI - (Devices/Sessions/Tokens section, label `"astoria-restic"`). DO NOT - reuse the main account password. -- [ ] **rclone-obscure the app-password** for use in rclone.conf (Phase 1 - step 7). Passing the password as an argv positional would leak it to - `ps` and shell history; `read -rs` prompts on tty and never puts the - value in argv: - ```bash - nix-shell -p rclone --run 'IFS= read -rsp "app-password: " pw; echo; rclone obscure "$pw"; unset pw' - ``` -- [ ] **WebDAV endpoint sanity check** — auth works, directory lists. - `-u USER` (no colon) makes curl prompt for the password on tty rather - than leaving it in argv / scrollback / `ps`: - ```bash - curl -X PROPFIND -H 'Depth: 0' -u USER https://WEBDAV_HOST/WEBDAV_ROOT/ - ``` - Success = `` XML. - -> **First install vs reinstall**: on the very first install, Phase 0 + Phase 1 -> were already done during scaffolding — skip to Phase 2. Phases 0/1 below are -> the runbook for reinstalls (host key rotation, HW replacement, disaster -> recovery). - ---- - -## Phase 0 — repo scaffolding (one-time) - -Create `.sops.yaml` at the repo root with placeholder recipients (Phase 1 step -1+2 fills in the real pubkeys). Commit + push. - ---- - -## Phase 1 — pre-generate on the dev machine - -Dev machine = any host you already trust with the repo checked out. - -**Placeholder convention** — anywhere you see `` below, substitute BEFORE -running. Bash treats `<` as an input-redirect metachar, so pasting `` as -part of a command silently misbehaves rather than erroring. +## Rebuild +- Preferred: `cd ~/git/dotfiles && sudo nixos-rebuild switch --flake .#astoria` +- Via /etc/nixos symlink: `sudo nixos-rebuild switch --flake /etc/nixos#astoria` +- From dev machine over LAN: `nixos-rebuild switch --flake .#astoria --target-host brutcha@astoria --use-remote-sudo` -**Cleanup discipline** — Phase 1 writes plaintext secrets to `/tmp` and shreds -them at exit. On macOS `/tmp` is APFS (persistent, `shred` unreliable); on -Linux usually tmpfs. A bash `trap` on EXIT/INT/TERM shreds on any clean exit. -The trap must be set INSIDE the nix-shell subshell — traps don't propagate -across exec into a fresh bash. For maximum hygiene, do Phase 1 in `/dev/shm` -(Linux) or an hdiutil RAM disk (macOS). +## Update +- Flake inputs: `nix flake update` (from dev machine, review lock diff, commit) +- Firmware: `fwupdmgr refresh && fwupdmgr get-updates && fwupdmgr update` -### Enter the shell +## Rollback -```bash -cd ~/git/dotfiles -nix-shell -p ssh-to-age age sops mkpasswd -``` +Two independent rollback mechanisms — use the right one for the failure mode. -Wait for the `[nix-shell:…]$` prompt, then paste FIRST: +**NixOS generation rollback** — for reverting a `nixos-rebuild switch` that broke +something: +- Lanzaboote boot menu at boot → pick a previous generation +- Or from CLI (still-booted system): `sudo nixos-rebuild switch --rollback` +**Snapper rootfs rollback** — for reverting non-`/nix/store` drift (files edited +outside the flake, corrupted state under `/etc`, `/var`, `/home`). Snapshots live +under `/.snapshots//snapshot` and are created at every boot by `snapper-boot.service`: ```bash -trap 'shred -u -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/dev/null || rm -Pf -- /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash 2>/dev/null' EXIT INT TERM +sudo snapper -c root list # inspect snapshots + timestamps +sudo snapper -c root diff .. # peek at what would change +sudo snapper -c root undochange ..0 # restore from snapshot N +sudo snapper -c root rollback # nuke current @root, replace with snapshot N — reboots into it ``` +`snapper rollback` is destructive to the current @root (it swaps subvolumes); prefer +`undochange` for targeted recovery when only a few files are affected. -### Steps - -1. **Astoria host SSH key** - ```bash - ssh-keygen -t ed25519 -f /tmp/astoria_host_key -N '' -C mail@brutcha.dev - ssh-to-age -i /tmp/astoria_host_key.pub - ``` - - Pubkey line → replace `age1astoriahostPUBKEY_TBD` in `.sops.yaml`. - - Private key → vault entry `"astoria SSH host key"`. If your vault's - Password field rejects multi-line PEM, base64 it to a single line first - (decoded back during Phase 3 step 4): - ```bash - base64 -i /tmp/astoria_host_key | tr -d '\n' | pbcopy # macOS - base64 /tmp/astoria_host_key | tr -d '\n' | xclip -sel c # Linux (X11) - base64 /tmp/astoria_host_key | tr -d '\n' | wl-copy # Linux (Wayland) - ``` - -2. **Recovery age keypair** (once, ever) - ```bash - age-keygen -o /tmp/recovery.txt - ``` - - Pubkey → replace `age1recoveryPUBKEY_TBD` in `.sops.yaml`. - - `AGE-SECRET-KEY-1…` line → vault entry `"astoria sops recovery"` - (single-line, no base64 needed). - -3. **Export recovery key for sops** - ```bash - export SOPS_AGE_KEY_FILE=/tmp/recovery.txt - ``` - Step 7 encrypts fresh (only needs pubkeys). Step 8 (`sops updatekeys`) - decrypts + re-encrypts and needs the recovery key. - -4. **Login passphrase** → vault entry `"astoria login"`. Used for cryptroot - LUKS AND user login (same value; muscle memory). English-keyboard - typeable — the LUKS prompt uses US layout. - -4a. **Cryptswap fallback passphrase** → vault entry `"astoria cryptswap"`. - Different value from `"astoria login"`. Only ever typed when TPM - auto-unlock fails (BIOS updates, TPM state changes). - -5. **Restic repo password** → vault entry `"astoria restic repo"`. Strong - random: - ```bash - openssl rand -base64 48 - ``` - -6. **User password hash** — via temp file to keep the hash off scrollback. - The subshell-scoped `umask 077` forces the redirected file to be created - as 0600 in a single syscall (no umask-races, no leak into the outer shell): - ```bash - ( umask 077 && mkpasswd -m yescrypt > /tmp/astoria-hash ) - ``` - Enter the login passphrase from step 4 at the prompt. - -7. **Populate secrets YAML** - ```bash - mkdir -p hosts/astoria/secrets # sops uses os.WriteFile — no MkdirAll - sops hosts/astoria/secrets/astoria.yaml - ``` - Top-level YAML keys must match what sops-nix looks up (see `secrets.nix`): - ```yaml - users: - brutcha: - hashed-password: PASTE_FROM_/tmp/astoria-hash - restic: - repo-password: PASTE_FROM_STEP_5 - rclone: - webdav.conf: | - [webdav] - type = webdav - url = https://WEBDAV_HOST/WEBDAV_ROOT - vendor = RCLONE_WEBDAV_VENDOR - user = WEBDAV_USER - pass = OBSCURED_FROM_PREREQUISITES - ``` - If ANY placeholder remains angle-bracketed on save, - `restic-backups-webdav.service` will fail on first boot. - -8. **Encrypt to both recipients** - ```bash - sops updatekeys hosts/astoria/secrets/astoria.yaml - ``` - -9. **Commit + push**. - -10. **Shred + clear scrollback** - ```bash - shred -u /tmp/astoria_host_key* /tmp/recovery.txt /tmp/astoria-hash - printf '\033c' - ``` +Note: Snapper's SUBVOLUME is `/` (targets @root only). `/nix`, `/home`, `/.snapshots` +are separate subvolumes and NOT covered — `/nix/store` is already immutable + +content-addressed; `/home` is user data that Restic backs up. --- -## Phase 2 — physical machine prep +## Secrets (sops) -### BIOS +astoria decrypts `hosts/astoria/secrets/astoria.yaml` at boot using its SSH +host key (converted to age) plus a recovery age key — both listed as +recipients under the `&astoria` / `&brutcha_recovery` anchors in `.sops.yaml` +at the repo root. The secret schema sops-nix expects in that YAML is +declared in `hosts/astoria/secrets.nix`. -- [ ] **Flash latest firmware first** — Windows Dell Update or `fwupdmgr update` - from the installer. Doing this AFTER Phase 4c TPM enrollment invalidates - PCR seals (avoidable round trip). -- [ ] **Signs of Life off**. -- [ ] **BIOS admin password UNSET** (TLP charge thresholds require it). -- [ ] **Secure Boot: OFF, in Setup Mode** — Advanced Boot Options → Secure - Boot → "Reset to Setup Mode" / "Delete All Keys". Some Dell BIOSes - need an admin password to reach this menu: set → reset SB → unset. -- [ ] **TPM: On, and cleared** — Security → TPM → Clear TPM. +Tooling docs: [sops-nix](https://github.com/Mic92/sops-nix) (the NixOS +module wiring `.sops.yaml` → `secrets.nix` → `/run/secrets`), +[sops](https://github.com/getsops/sops) (the encryption CLI), and +[age](https://github.com/FiloSottile/age) (the key format). -### Installer +--- -- [ ] **Boot NixOS 26.11 minimal installer USB** (nixos-unstable ISO OK - while 26.11 is pre-release — flake pins nixpkgs to the same channel). -- [ ] **Wi-Fi**: - ```bash - nmcli device wifi connect password - ``` +## Dev-machine secrets (only needed for host-key rotation / disaster recovery) + +Generating astoria's secrets from scratch needs, once: +- An SSH host key for astoria, converted to age via `ssh-to-age` — its + pubkey becomes the `&astoria` recipient in `.sops.yaml`; the private key + goes to `/etc/ssh/ssh_host_ed25519_key` on the machine (Phase 3 step 3) + and to the vault (`"astoria SSH host key"`) for recovery. +- A recovery age keypair (`age-keygen`, generated once, ever, reused across + hosts) → pubkey is the `&brutcha_recovery` anchor; private key → vault + (`"astoria sops recovery"`). Follow the shred discipline in "Recovery age + key — helper-device shred discipline" below whenever it leaves the vault. +- Login/cryptswap LUKS passphrases, a restic repo password, and a + `mkpasswd -m yescrypt` password hash — each in its own vault entry + (`"astoria login"`, `"astoria cryptswap"`, `"astoria restic repo"`), then + assembled into `hosts/astoria/secrets/astoria.yaml` matching the schema + declared in `secrets.nix`, and `sops updatekeys + hosts/astoria/secrets/astoria.yaml`, commit, push. + +Tooling docs: [ssh-to-age](https://github.com/Mic92/ssh-to-age), +[age](https://github.com/FiloSottile/age), +[sops](https://github.com/getsops/sops). --- ## Phase 3 — install +Boot the [nix-community/nixos-images](https://github.com/nix-community/nixos-images) +unstable installer ISO. Grab the IP + root password from its on-screen +QR/JSON, then SSH in as root — everything below runs inside that one +session (no `sudo` needed anywhere in this phase, you're already root), +except the host-key transfer, which is run from the dev machine. + ```bash GITHUB_USER= -ASTORIA_IP= # from `ip addr | grep 'inet '` on the installer +ASTORIA_IP= # from the installer's on-screen/QR/clipboard info +ssh "root@${ASTORIA_IP}" ``` ### 1. Clone the flake ```bash -nix-shell -p git -git clone "https://github.com/${GITHUB_USER}/dotfiles" /tmp/dotfiles +nix --extra-experimental-features 'nix-command flakes' run nixpkgs#git -- \ + clone "https://github.com/${GITHUB_USER}/dotfiles" /tmp/dotfiles cd /tmp/dotfiles ``` -### 2. Hardware inventory - -Run the verify script from the just-cloned (git-verified) copy — never -pipe a raw HTTP response into `sh`; a compromised repo tag or an MITM'd -CDN response would execute unreviewed. `hardware.nix` assumes AX201 / -Ice Lake; adjust it if the output disagrees BEFORE the install step. - +The minimal ISO's closure is intentionally bare, and flakes aren't enabled +by default on it either — anything else needed mid-install has to be +fetched on-demand the same way: ```bash -sh hosts/astoria/verify-hardware.sh +nix --extra-experimental-features 'nix-command flakes' run nixpkgs# -- ... ``` - -If it flags a QCA6390 Wi-Fi chip, unavailable `platform_profile`, -non-`s2idle` sleep mode, etc., edit `hosts/astoria/hardware.nix` now -(see the in-file comments for the alternate values) — the install -step below picks up the changes. - -### 3. Partition + format +Packages that come up needing this during install: `sops`, `ssh-to-age`, +`sbctl`, `pciutils` (for `lspci`), `stress-ng`, `s-tui`, +`linuxPackages.turbostat`. `sbctl` also needs `--disable-landlock` when +writing outside its expected paths (e.g. `--export /mnt/var/lib/sbctl/keys` +in step 4 below), since it sandboxes itself with Landlock by default. + +### 2. Partition + format + +This model defaults to **RAID (Intel RST) mode** in BIOS, which hides the +NVMe drive from normal enumeration (`lspci` shows a `RAID bus controller` +instead of a `Non-Volatile memory controller`; `dmesg` shows `ahci ...: +Found 1 remapped NVMe devices`). Fix before running disko: BIOS → System +Configuration → **SATA Operation: AHCI** (switch from RAID). No config +changes needed once this is set correctly — `nvme` in +`boot.initrd.availableKernelModules` is already sufficient once the drive +enumerates natively. ```bash -sudo nix run '.#disko' \ +nix run '.#disko' \ --extra-experimental-features 'nix-command flakes' \ -- --mode destroy,format,mount --flake .#astoria ``` @@ -241,10 +141,6 @@ binary through the flake's own `packages.x86_64-linux.disko` re-export, which is pinned via `flake.lock` — no unpinned github ref, no MITM-able fetch, exact same code as everything else the flake produces. -Why the other flags: `sudo` because disko doesn't self-elevate; the -`--extra-experimental-features` inline because NixOS sudo drops `NIX_CONFIG` -env, so `export NIX_CONFIG=…` in the outer shell wouldn't survive. - Prompts, in order: 1. `Type 'yes' to continue, anything else to abort:` — type literally `yes`. **Don't paste a passphrase here** (anything except `yes` aborts before @@ -253,53 +149,34 @@ Prompts, in order: 2. **cryptswap** LUKS passphrase (2×) — from vault `"astoria cryptswap"`. 3. **cryptroot** LUKS passphrase (2×) — from vault `"astoria login"`. -### 4. Transfer astoria's SSH host key from dev → installer +### 3. Transfer astoria's SSH host key from dev → installer -On the installer, set a throwaway login password (the `nixos` user starts -empty, which blocks ssh): +From the dev machine (a separate terminal from the root SSH session above), +scp the raw private key over, then move it into place with the right +owner/mode and shred the transient copy: ```bash -sudo passwd nixos +scp /tmp/astoria_host_key "root@${ASTORIA_IP}:/tmp/astoria_host_key" +ssh "root@${ASTORIA_IP}" 'mkdir -p /mnt/etc/ssh && \ + install -m 600 -o root -g root /tmp/astoria_host_key /mnt/etc/ssh/ssh_host_ed25519_key && \ + shred -u /tmp/astoria_host_key' ``` -On the dev machine, pipe the base64'd key from clipboard through `base64 -d` -into the installer over ssh. `sudo install` (below) atomically creates the -target file with the right owner + 0600 mode — no umask window during which -the file would be world-readable. - +Back in the installer session, derive the .pub (strict openssh refuses to +read a 0644 private key without a matching .pub): ```bash -# Option A — Linux dev (xclip): -xclip -o -selection clipboard | base64 -d | ssh "nixos@${ASTORIA_IP}" \ - 'sudo mkdir -p /mnt/etc/ssh && sudo install -m 600 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key' - -# Option B — macOS dev (pbpaste): -pbpaste | base64 -d | ssh "nixos@${ASTORIA_IP}" \ - 'sudo mkdir -p /mnt/etc/ssh && sudo install -m 600 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key' - -# Option C — heredoc paste (no clipboard tool available): -ssh "nixos@${ASTORIA_IP}" 'sudo mkdir -p /mnt/etc/ssh && base64 -d | sudo install -m 600 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key' <<'EOF' - -EOF +ssh-keygen -y -f /mnt/etc/ssh/ssh_host_ed25519_key | \ + install -m 644 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key.pub ``` -If you stored the key as raw PEM (not base64), drop the `| base64 -d` and -paste the PEM directly. - -On the installer, derive the .pub (strict openssh refuses to read a 0644 -private key without a matching .pub): -```bash -sudo ssh-keygen -y -f /mnt/etc/ssh/ssh_host_ed25519_key | \ - sudo install -m 644 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key.pub -``` - -### 5. Generate sbctl Secure Boot keys +### 4. Generate sbctl Secure Boot keys `nixos-install` signs the bootloader via Lanzaboote's installHook using these keys, so they must exist at `/mnt/var/lib/sbctl` before install. ```bash -sudo nix-shell -p sbctl --run 'sbctl create-keys --help' # confirm flag names first -sudo mkdir -p /mnt/var/lib/sbctl -sudo nix-shell -p sbctl --run 'sbctl create-keys --export /mnt/var/lib/sbctl/keys --database-path /mnt/var/lib/sbctl/GUID' +nix --extra-experimental-features 'nix-command flakes' run nixpkgs#sbctl -- create-keys --help # confirm flag names first +mkdir -p /mnt/var/lib/sbctl +nix --extra-experimental-features 'nix-command flakes' run nixpkgs#sbctl -- create-keys --export /mnt/var/lib/sbctl/keys --database-path /mnt/var/lib/sbctl/GUID ``` Gotchas: @@ -307,10 +184,10 @@ Gotchas: not a directory. Passing a directory → EISDIR → no keys ever created. - Older sbctl uses `--keydir` / `--pki-dir` instead. Check `--help` first. -### 6. Install +### 5. Install ```bash -sudo nixos-install --flake /tmp/dotfiles#astoria --no-root-passwd +nixos-install --flake /tmp/dotfiles#astoria --no-root-passwd ``` During activation: @@ -321,7 +198,7 @@ During activation: - cryptswap TPM keyslot doesn't exist yet — first boot prompts for the disko-set passphrase. TPM enrollment happens in Phase 4c. -### 7. Reboot +### 6. Reboot cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd → user login → Sway. @@ -340,17 +217,8 @@ cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd ### 4a. Bring-up checks -- [ ] Cryptroot + cryptswap passphrases accept vault values. -- [ ] Sway starts (`Mod4+Return` → ghostty; `Mod4+Space` → fuzzel; Capslock - cycles us↔cz). -- [ ] `systemctl status sops-install-secrets` — active, exit 0. -- [ ] `ls /run/secrets-for-users/users/brutcha/` — hashed-password present. -- [ ] `ls /run/secrets/{restic,rclone}/` — secrets present. -- [ ] `lspci -k` — Wi-Fi chip matches Phase 3 hw-inventory output. -- [ ] `cat /sys/power/mem_sleep` — `[s2idle]` bracketed. -- [ ] **Git-clone the flake** to `/home/brutcha/git/dotfiles` — otherwise - `environment.etc.nixos.source` is a dangling symlink and later - edit-then-rebuild has no on-machine flake to edit. +Still open as of the last session: + - [ ] **Add the dev-machine ssh pubkey** to `users.users.brutcha.openssh.authorizedKeys.keys` (edit `hosts/astoria/default.nix` in the just-cloned dir, commit, rebuild) @@ -358,7 +226,6 @@ cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd - [ ] Manual first Restic backup: `sudo systemctl start restic-backups-webdav.service`; journal shows success. -- [ ] LibreWolf `about:support` → `HARDWARE_VIDEO_DECODING = available`. - [ ] Password-vault LibreWolf extension: paste server URL, log in with your vault account, unlock with the challenge password on first launch (one-time, manual). @@ -424,43 +291,7 @@ sudo systemd-cryptenroll /dev/nvme0n1p2 # slot 0 (password) --- -## Operations reference (keep post-install) - -### Rebuild -- Preferred: `cd ~/git/dotfiles && sudo nixos-rebuild switch --flake .#astoria` -- Via /etc/nixos symlink: `sudo nixos-rebuild switch --flake /etc/nixos#astoria` -- From dev machine over LAN: `nixos-rebuild switch --flake .#astoria --target-host brutcha@astoria --use-remote-sudo` - -### Update -- Flake inputs: `nix flake update` (from dev machine, review lock diff, commit) -- Firmware: `fwupdmgr refresh && fwupdmgr get-updates && fwupdmgr update` - -### Rollback - -Two independent rollback mechanisms — use the right one for the failure mode. - -**NixOS generation rollback** — for reverting a `nixos-rebuild switch` that broke -something: -- Lanzaboote boot menu at boot → pick a previous generation -- Or from CLI (still-booted system): `sudo nixos-rebuild switch --rollback` - -**Snapper rootfs rollback** — for reverting non-`/nix/store` drift (files edited -outside the flake, corrupted state under `/etc`, `/var`, `/home`). Snapshots live -under `/.snapshots//snapshot` and are created at every boot by `snapper-boot.service`: -```bash -sudo snapper -c root list # inspect snapshots + timestamps -sudo snapper -c root diff .. # peek at what would change -sudo snapper -c root undochange ..0 # restore from snapshot N -sudo snapper -c root rollback # nuke current @root, replace with snapshot N — reboots into it -``` -`snapper rollback` is destructive to the current @root (it swaps subvolumes); prefer -`undochange` for targeted recovery when only a few files are affected. - -Note: Snapper's SUBVOLUME is `/` (targets @root only). `/nix`, `/home`, `/.snapshots` -are separate subvolumes and NOT covered — `/nix/store` is already immutable + -content-addressed; `/home` is user data that Restic backs up. - -### Restore from backup +## Restore from backup - Secrets already materialized on running astoria: `/run/secrets/rclone/webdav.conf` + `/run/secrets/restic/repo-password` - `sudo nix-shell -p restic rclone` — enters an interactive root shell with restic + @@ -481,14 +312,14 @@ content-addressed; `/home` is user data that Restic backs up. restic -r rclone:webdav:restic-astoria restore latest --target / ``` -### Password recovery (lost sudo password — machine still boots) +## Password recovery (lost sudo password — machine still boots) 1. From a machine that has the recovery age key: `sops hosts/astoria/secrets/astoria.yaml` — replace `users.brutcha.hashed-password` with a new `mkpasswd -m yescrypt` hash. Commit + push. 2. On astoria: `sudo nixos-rebuild switch` (or `--target-host` from dev machine if stuck at greetd). -### Password recovery (fully bricked — installer rescue) +## Password recovery (fully bricked — installer rescue) 1. Boot NixOS installer USB. 2. `sudo cryptsetup luksOpen /dev/nvme0n1p3 cryptroot` # p3 = root; p2 = swap 3. `sudo mount -o subvol=@root /dev/mapper/cryptroot /mnt` @@ -499,10 +330,10 @@ content-addressed; `/home` is user data that Restic backs up. 8. Do NOT `passwd brutcha` — `users.mutableUsers = false;` reverts it. Update sops YAML from a trusted machine and rebuild. -### Recovery age key — helper-device shred discipline +## Recovery age key — helper-device shred discipline The recovery age private key lives in your password vault. Any time you paste it out -of the vault onto a helper device to run `sops`, mirror Phase 1's shred discipline: +of the vault onto a helper device to run `sops`, follow this discipline: 1. On the helper device, mount a scratch tmpfs first: - Linux: `SCRATCH=$(mktemp -d --tmpdir=/dev/shm astoria-recovery.XXXXX)` @@ -522,7 +353,7 @@ of the vault onto a helper device to run `sops`, mirror Phase 1's shred discipli `tmutil listlocalsnapshots /` doesn't show a recent snapshot containing the file (they roll off in ~24h; force-delete with `tmutil deletelocalsnapshots`). -### LUKS passphrase change +## LUKS passphrase change ```bash sudo cryptsetup luksChangeKey /dev/nvme0n1p3 # cryptroot — also update `"astoria login"` in the vault sudo cryptsetup luksChangeKey /dev/nvme0n1p2 # cryptswap fallback — also update its vault entry @@ -531,7 +362,7 @@ The cryptswap TPM keyslot is separate from the passphrase keyslot; changing the passphrase does NOT invalidate the TPM binding. Rotate TPM enrollment only if you need to (BIOS updates, PCR changes). -### TPM re-enrollment (after BIOS/firmware update — expected ~1-2× per year) +## TPM re-enrollment (after BIOS/firmware update — expected ~1-2× per year) Symptoms: first boot after a BIOS update prompts for the cryptswap passphrase (fallback path) instead of TPM auto-unlock. `journalctl -b -u 'systemd-cryptsetup@luks\x2dswap.service'` @@ -544,7 +375,7 @@ sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7 /dev/nvme0n1p2 ``` Prompted for the cryptswap passphrase to authorize. Reboot to verify silent unlock. -### Secure Boot state audit (occasional) +## Secure Boot state audit (occasional) ```bash sudo sbctl status # Setup Mode: Disabled, Secure Boot: Enabled sudo sbctl verify # every EFI file: Signed @@ -553,7 +384,7 @@ bootctl status | grep 'Secure' If `sbctl verify` shows unsigned files after a manual bootloader tweak: `sudo sbctl sign -s ` per file, then `sudo nixos-rebuild switch`. -### Full Secure Boot reset (rare — if keys get corrupted or you need to re-provision) +## Full Secure Boot reset (rare — if keys get corrupted or you need to re-provision) 1. Reboot into BIOS. Advanced Boot Options → Secure Boot → "Reset to Setup Mode" (or "Delete All Keys"). @@ -573,12 +404,11 @@ If `sbctl verify` shows unsigned files after a manual bootloader tweak: 7. Reboot into BIOS → enable Secure Boot. 8. **TPM state also invalidates** (PCR 7 changes): follow "TPM re-enrollment" above. -### Battery care +## Battery care Charge thresholds 60/80 via TLP. Check with `tlp-stat -b`. Battery replacement (~€60, iFixit rating 4/10) recommended if capacity drops below 60 %. -### Known gotchas -- Wi-Fi tuning assumes AX201 — see `hardware.nix` comments for QCA6390 variant. +## Known gotchas - iwlwifi power_save must stay off — turning it on drops Moonlight streams. - `enhanced-h264ify` blocks AV1 on YouTube while leaving VP9 available; keeps 4K/1440p playback that plain `h264ify` would silently drop. @@ -592,9 +422,6 @@ Charge thresholds 60/80 via TLP. Check with `tlp-stat -b`. Battery replacement - The password-vault LibreWolf extension is declaratively installed but not configured — set server URL, log in with your vault account, unlock with the challenge password on first launch (manual, one-time). -- `PLATFORM_PROFILE_ON_BAT` in TLP may be a silent no-op if - `/sys/firmware/acpi/platform_profile_choices` is empty on this SKU — - verify-hardware.sh reports this; TLP tolerates. - **If suspend-then-hibernate fails to wake at the scheduled time**: ADD `rtc_cmos.use_acpi_alarm=1` to `boot.kernelParams`. Kernel auto-quirks the ACPI SCI alarm path deterministically on Intel + BIOS≥2015 + HPET-on (XPS 13 9300 hits @@ -606,7 +433,28 @@ Charge thresholds 60/80 via TLP. Check with `tlp-stat -b`. Battery replacement - **`sbctl verify` shows unsigned files**: something touched the ESP outside `nixos-rebuild switch`. `sudo sbctl sign -s ` and rebuild; check the ESP wasn't manually edited. -- **Cannot enter Secure Boot Setup Mode from BIOS**: some Dell BIOSes require an - admin password to be set before Setup Mode is reachable. Set one first if BIOS - refuses; reset SB to Setup Mode; then UNSET the admin password (TLP charge - thresholds require it UNSET). +- **Dell Expert Key Management: "Reset All Keys" vs "Delete All Keys"** — these + are two similarly-labeled, opposite actions. **Reset All Keys** restores + factory Microsoft/OEM keys (NOT what you want here). **Delete All Keys** + clears everything and enters Setup Mode (correct). Also: the page's "N + changes were made" counter does NOT track Delete All Keys — it's an + immediate action, not a queued setting. Trust `sbctl status` after + rebooting, not the counter. Also: enabling Secure Boot while the firmware + has no PK enrolled (Setup Mode + SB on simultaneously) can produce a "no + bootable devices" SupportAssist screen on some BIOS revisions — leave SB + off through key deletion + enrollment, and only re-enable it last, once + `sbctl enroll-keys` has succeeded. + +## References + +- [NixOS](https://nixos.org) +- [home-manager](https://github.com/nix-community/home-manager) +- [sops-nix](https://github.com/Mic92/sops-nix) +- [disko](https://github.com/nix-community/disko) +- [lanzaboote](https://github.com/nix-community/lanzaboote) +- [snapper](https://github.com/openSUSE/snapper) +- [TLP](https://linrunner.de/tlp/) +- [sbctl](https://github.com/Foxboron/sbctl) +- [nix-community/nixos-images](https://github.com/nix-community/nixos-images) +- [Moonlight](https://moonlight-stream.org) +- [LibreWolf](https://librewolf.net) diff --git a/hosts/astoria/default.nix b/hosts/astoria/default.nix index f5ef717..546f3d5 100644 --- a/hosts/astoria/default.nix +++ b/hosts/astoria/default.nix @@ -12,6 +12,7 @@ inputs.home-manager.nixosModules.home-manager ./hardware.nix ./secrets.nix + ./sudoers.nix ]; nixpkgs.config.allowUnfree = true; # required by hardware.enableAllFirmware @@ -69,6 +70,13 @@ common.default = [ "wlr" "gtk" ]; }; }; + # dconf/D-Bus service — xdg-desktop-portal-gtk's Settings backend (above) + # answers org.freedesktop.appearance queries by reading gsettings, which + # is backed by dconf. Without this, apps that query the portal for + # color-scheme (Ghostty, LibreWolf) never see a preference and fall back + # to light, even with gtk-application-prefer-dark-theme set (that ini + # setting only affects GTK3 apps' own rendering, not the portal signal). + programs.dconf.enable = true; security.polkit.enable = true; # --- System packages --- diff --git a/hosts/astoria/hardware.nix b/hosts/astoria/hardware.nix index 1ab538b..5f1ce03 100644 --- a/hosts/astoria/hardware.nix +++ b/hosts/astoria/hardware.nix @@ -17,7 +17,7 @@ ]; # --- Bootloader (Lanzaboote / Secure Boot) --- - # Two-step key lifecycle (README Phase 3 step 5 + Phase 4b step 2): + # Two-step key lifecycle (README Phase 3 step 4 + Phase 4b step 2): # `sbctl create-keys` at install time → local keypair under pkiBundle; # `sbctl enroll-keys --microsoft` at first boot → enroll into UEFI while # in Setup Mode. --microsoft appends MS certs to KEK+db only (PK stays @@ -64,9 +64,7 @@ crypttabExtraOpts = [ "tpm2-device=auto" ]; }; - # --- Wi-Fi (AX201-specific tuning) --- - # Alternative SKU: QCA6390 (ath11k). Drop the iwlwifi modprobe block if - # `lspci -k` reveals it; NM powersave = false applies either way. + # --- Wi-Fi (AX201 iwlwifi tuning, confirmed via lspci -nn / dmesg) --- boot.extraModprobeConfig = '' options iwlwifi power_save=0 ''; @@ -92,6 +90,7 @@ CPU_ENERGY_PERF_POLICY_ON_BAT = "power"; CPU_ENERGY_PERF_POLICY_ON_AC = "balance_performance"; CPU_BOOST_ON_BAT = 0; + CPU_BOOST_ON_AC = 1; CPU_HWP_DYN_BOOST_ON_BAT = 0; # Silent no-op if /sys/firmware/acpi/platform_profile_choices is empty # (verify-hardware.sh reports). diff --git a/hosts/astoria/home.nix b/hosts/astoria/home.nix index 765b5d5..361dab9 100644 --- a/hosts/astoria/home.nix +++ b/hosts/astoria/home.nix @@ -3,8 +3,8 @@ # astoria — home-manager entry for `brutcha`. # # Toggles the fleet-standard `home.apps.*` options; modules/home/default.nix -# picks the linux sub-bundle (sway/waybar/mako/fuzzel/swaylock/thunar/ -# librewolf/moonlight) via `hostSystem`. +# picks the linux sub-bundle (sway/waybar/mako/fuzzel/swaylock/screenshot/ +# thunar/librewolf/moonlight/imv) via `hostSystem`. # { imports = [ ../../modules/home ]; @@ -12,17 +12,20 @@ home.apps = { development = { ghostty.enable = true; - git.enable = true; # lazygit.enable auto-derives + git.enable = true; + claude-code.enable = true; }; internet.librewolf.enable = true; media.moonlight.enable = true; + media.imv.enable = true; filemanager.thunar.enable = true; windowManager = { - sway.enable = true; - waybar.enable = true; - mako.enable = true; - fuzzel.enable = true; - swaylock.enable = true; + sway.enable = true; + waybar.enable = true; + mako.enable = true; + fuzzel.enable = true; + swaylock.enable = true; + screenshot.enable = true; }; }; @@ -47,9 +50,23 @@ }; iconTheme = { name = "Papirus-Dark"; package = pkgs.papirus-icon-theme; }; cursorTheme = { name = "Bibata-Modern-Ice"; package = pkgs.bibata-cursors; }; + # Matches the Nerd Font used across sway/waybar/mako/fuzzel (fonts.nix + # installs the package already) — GTK chrome font, so LibreWolf's + # address bar/tabs and other GTK apps' UI text follow it too. + font = { name = "JetBrainsMonoNL Nerd Font"; size = 10; }; gtk3.extraConfig.gtk-application-prefer-dark-theme = 1; }; + # Portal-facing appearance signal (org.freedesktop.appearance + # color-scheme). xdg-desktop-portal-gtk reads this via gsettings/dconf — + # it's what Ghostty's dark:/light: auto-theme and LibreWolf's + # prefers-color-scheme detection actually consult on Wayland, unlike the + # GTK3-only setting above. Static "prefer-dark", matching the static + # Tokyonight-Dark GTK theme choice (no day/night auto-switch here). + dconf.settings."org/gnome/desktop/interface" = { + color-scheme = "prefer-dark"; + }; + # File-sync client. Server URL + creds set on first launch. services.nextcloud-client = { enable = true; diff --git a/hosts/astoria/sudoers.nix b/hosts/astoria/sudoers.nix new file mode 100644 index 0000000..a09e83c --- /dev/null +++ b/hosts/astoria/sudoers.nix @@ -0,0 +1,9 @@ +{ lib, ... }: +# NOPASSWD for the single nixos-rebuild switch invocation, so it can run +# from Claude Code / scripts without an OS prompt. All other sudo actions +# still prompt normally. +{ + security.sudo.extraConfig = lib.mkAfter '' + brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch * + ''; +} diff --git a/hosts/astoria/verify-hardware.sh b/hosts/astoria/verify-hardware.sh deleted file mode 100755 index f0d48cd..0000000 --- a/hosts/astoria/verify-hardware.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env sh -# hosts/astoria/verify-hardware.sh — read-only hardware inventory for astoria. -# Prints values that hardware.nix currently assumes. Run during install -# (README Phase 2 step 5). If output contradicts hardware.nix, edit -# hardware.nix before / after nixos-install. - -set -u - -section() { printf '\n=== %s ===\n' "$1"; } - -section "Machine identity" -if [ -r /sys/class/dmi/id/product_name ]; then - printf 'DMI product: %s\n' "$(cat /sys/class/dmi/id/product_name)" -fi -printf 'CPU: %s\n' "$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2 | sed 's/^ *//')" -printf 'RAM: %s\n' "$(grep MemTotal /proc/meminfo | awk '{ printf "%.1f GiB\n", $2/1024/1024 }')" - -section "Wi-Fi chip" -# hardware.nix assumes Intel AX201 (iwlwifi). Alternative SKU: Qualcomm QCA6390 (ath11k). -lspci -k 2>/dev/null | grep -A3 -i 'network controller' | sed 's/^/ /' -echo " -> hardware.nix comment shows the alternate values if this is QCA6390." - -section "Sleep modes supported by firmware" -# hardware.nix expects s2idle only. If 'deep' appears bracketed, kernel may pick it. -if [ -r /sys/power/mem_sleep ]; then - printf ' /sys/power/mem_sleep: %s\n' "$(cat /sys/power/mem_sleep)" -else - echo ' /sys/power/mem_sleep: not present (kernel may be too old)' -fi - -section "Platform profile (TLP PLATFORM_PROFILE_ON_BAT)" -# The file may exist and be readable but empty on SKUs that expose the ACPI -# node without any profiles — treat empty as unsupported (silent no-op for TLP). -pp_choices=$(cat /sys/firmware/acpi/platform_profile_choices 2>/dev/null) -if [ -n "$pp_choices" ]; then - printf ' choices: %s\n' "$pp_choices" - printf ' active: %s\n' "$(cat /sys/firmware/acpi/platform_profile 2>/dev/null || echo unknown)" -else - echo ' not supported on this SKU — PLATFORM_PROFILE_ON_BAT will be a silent no-op' - echo ' -> OK to remove that TLP setting from hardware.nix (tidy)' -fi -unset pp_choices - -section "Fingerprint reader" -# grep's exit status must drive the fallback, not sed's (sed returns 0 on -# empty input). Buffer the grep output first so we can test it directly. -if goodix=$(lsusb 2>/dev/null | grep -i '27c6') && [ -n "$goodix" ]; then - printf '%s\n' "$goodix" | sed 's/^/ /' -else - echo ' no Goodix device found' -fi -unset goodix - -section "BIOS version" -if command -v fwupdmgr >/dev/null 2>&1; then - fwupdmgr get-devices 2>/dev/null | awk '/System Firmware/,/^$/' | head -20 | sed 's/^/ /' -else - echo ' fwupdmgr not available' -fi - -section "GPU / VA-API" -lspci -nnk 2>/dev/null | grep -A2 VGA | sed 's/^/ /' - -section "Battery health (approximate)" -# Guard the divisions: sysfs can return 0 or blank on uncalibrated / hot-swap / -# corrupt states, and `$((… / 0))` aborts the script under `set -u`. Read both -# counters, verify they're positive integers, then compute. -report_health() { - # $1 = human label ("charge_full" | "energy_full") - # $2 = current, $3 = design. - # `[ "$X" -ge 0 ]` errors (silently, via stderr redirect) on non-numeric or - # empty input → `if` branch not taken → we fall to the "skipped" printf. - # Both operands need this guard: `$((… / 0))` and `$(( / … ))` both abort - # the arithmetic under `set -u`. - if [ "$2" -ge 0 ] 2>/dev/null && [ "$3" -gt 0 ] 2>/dev/null; then - printf ' BAT0 %s=%s design=%s (health ~%d%%)\n' "$1" "$2" "$3" "$((100 * $2 / $3))" - else - printf ' BAT0 %s=%s design=%s (health: skipped — counter invalid or design zero)\n' "$1" "$2" "$3" - fi -} -if [ -r /sys/class/power_supply/BAT0/charge_full ] && [ -r /sys/class/power_supply/BAT0/charge_full_design ]; then - report_health charge_full \ - "$(cat /sys/class/power_supply/BAT0/charge_full)" \ - "$(cat /sys/class/power_supply/BAT0/charge_full_design)" -elif [ -r /sys/class/power_supply/BAT0/energy_full ] && [ -r /sys/class/power_supply/BAT0/energy_full_design ]; then - report_health energy_full \ - "$(cat /sys/class/power_supply/BAT0/energy_full)" \ - "$(cat /sys/class/power_supply/BAT0/energy_full_design)" -else - echo ' BAT0 counters not readable' -fi - -section "Notes" -cat <<'EOF' - - Thermal / throttled verification (CPU pinned at ~2.4 GHz under load) needs - stress-ng which isn't in the minimal ISO. Run post-install: - nix-shell -p stress-ng s-tui - stress-ng --cpu $(nproc) --timeout 5m & s-tui - - Any value above that contradicts hardware.nix -> edit hardware.nix before - nixos-install (README Phase 3 step 6), or in a post-install rebuild. -EOF diff --git a/modules/home/development/git.nix b/modules/home/development/git.nix index baec140..00edc29 100644 --- a/modules/home/development/git.nix +++ b/modules/home/development/git.nix @@ -26,7 +26,14 @@ in ".emdash.json" ]; settings = { - credential.helper = "manager"; + credential = { + helper = "manager"; + } // lib.optionalAttrs pkgs.stdenv.isLinux { + # GCM needs an explicit store on Linux (no single obvious default, + # unlike macOS Keychain). gnome-keyring's Secret Service is already + # enabled system-wide (services.gnome.gnome-keyring.enable). + credentialStore = "secretservice"; + }; }; includes = [ { diff --git a/modules/home/linux/internet/librewolf.nix b/modules/home/linux/internet/librewolf.nix index 18e83fd..c3aa043 100644 --- a/modules/home/linux/internet/librewolf.nix +++ b/modules/home/linux/internet/librewolf.nix @@ -27,7 +27,21 @@ in programs.librewolf = { enable = true; profiles.default = { - settings."extensions.autoDisableScopes" = 0; + settings = { + "extensions.autoDisableScopes" = 0; + "privacy.clearOnShutdown_v2.cookiesAndStorage" = false; + "privacy.clearOnShutdown_v2.cache" = false; + # resistFingerprinting forces prefers-color-scheme to "light" for + # all sites; fingerprintingProtection covers everything else RFP + # did except that, so sites see the real dark/light preference. + "privacy.resistFingerprinting" = false; + "privacy.fingerprintingProtection" = true; + "privacy.fingerprintingProtection.overrides" = "+AllTargets,-CSSPrefersColorScheme"; + "sidebar.revamp" = true; + "sidebar.verticalTabs" = true; + "sidebar.position_start" = false; + }; + extensions.packages = with pkgs.nur.repos.rycee.firefox-addons; [ enhanced-h264ify nextcloud-passwords diff --git a/modules/home/linux/media/default.nix b/modules/home/linux/media/default.nix index 83f87dc..847c371 100644 --- a/modules/home/linux/media/default.nix +++ b/modules/home/linux/media/default.nix @@ -2,5 +2,6 @@ { imports = [ ./moonlight.nix + ./imv.nix ]; } diff --git a/modules/home/linux/media/imv.nix b/modules/home/linux/media/imv.nix new file mode 100644 index 0000000..75d4f9a --- /dev/null +++ b/modules/home/linux/media/imv.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: +# +# imv (https://sr.ht/~exec64/imv/) — lightweight Wayland-native image viewer. +# +# Available options: +# - home.apps.media.imv.enable +# +let + cfg = config.home.apps.media.imv; +in +{ + options.home.apps.media.imv.enable = + lib.mkEnableOption "imv image viewer"; + + config = lib.mkIf cfg.enable { + home.packages = [ pkgs.imv ]; + + xdg.mimeApps.enable = true; + xdg.mimeApps.defaultApplications = { + "image/png" = [ "imv.desktop" ]; + "image/jpeg" = [ "imv.desktop" ]; + "image/gif" = [ "imv.desktop" ]; + "image/webp" = [ "imv.desktop" ]; + "image/bmp" = [ "imv.desktop" ]; + "image/tiff" = [ "imv.desktop" ]; + }; + }; +} diff --git a/modules/home/linux/thunar.nix b/modules/home/linux/thunar.nix index a14657b..0b47bf7 100644 --- a/modules/home/linux/thunar.nix +++ b/modules/home/linux/thunar.nix @@ -17,9 +17,9 @@ in config = lib.mkIf cfg.enable { home.packages = with pkgs; [ - xfce.thunar - xfce.thunar-volman - xfce.thunar-archive-plugin + thunar + thunar-volman + thunar-archive-plugin ]; }; } diff --git a/modules/home/linux/window-manager/default.nix b/modules/home/linux/window-manager/default.nix index cae1eb8..10ae1a5 100644 --- a/modules/home/linux/window-manager/default.nix +++ b/modules/home/linux/window-manager/default.nix @@ -8,5 +8,6 @@ ./mako.nix ./fuzzel.nix ./swaylock.nix + ./screenshot.nix ]; } diff --git a/modules/home/linux/window-manager/screenshot.nix b/modules/home/linux/window-manager/screenshot.nix new file mode 100644 index 0000000..0333148 --- /dev/null +++ b/modules/home/linux/window-manager/screenshot.nix @@ -0,0 +1,48 @@ +{ config, lib, pkgs, ... }: +# +# Screenshots — grim (capture) + slurp (region select) + swappy (annotate). +# Sway-native via wlr-screencopy; spectacle/flameshot don't target wlroots +# compositors well. +# +# Available options: +# - home.apps.windowManager.screenshot.enable +# +let + cfg = config.home.apps.windowManager.screenshot; +in +{ + options.home.apps.windowManager.screenshot.enable = + lib.mkEnableOption "grim/slurp/swappy screenshot tooling"; + + config = lib.mkIf cfg.enable { + home.packages = [ + # Region select, opens swappy for annotate/save/copy. + (pkgs.writeShellScriptBin "screenshot-region" '' + exec ${pkgs.grim}/bin/grim -g "$(${pkgs.slurp}/bin/slurp)" - | ${pkgs.swappy}/bin/swappy -f - + '') + # Full screen, saved straight to disk + clipboard, no editor. + (pkgs.writeShellScriptBin "screenshot-full" '' + set -euo pipefail + dir="$HOME/Pictures/Screenshots" + mkdir -p "$dir" + f="$dir/screenshot-$(date +%Y%m%d-%H%M%S).png" + ${pkgs.grim}/bin/grim "$f" + ${pkgs.wl-clipboard}/bin/wl-copy < "$f" + ${pkgs.libnotify}/bin/notify-send "Screenshot saved" "$f" + '') + ]; + + xdg.configFile."swappy/config".text = '' + [Default] + save_dir=$HOME/Pictures/Screenshots + save_filename_format=screenshot-%Y%m%d-%H%M%S.png + show_panel=false + line_size=5 + text_size=20 + text_font=JetBrainsMonoNL Nerd Font + paint_mode=brush + early_exit=true + fill_shape=false + ''; + }; +} diff --git a/modules/home/linux/window-manager/sway.nix b/modules/home/linux/window-manager/sway.nix index a445d13..51b8154 100644 --- a/modules/home/linux/window-manager/sway.nix +++ b/modules/home/linux/window-manager/sway.nix @@ -20,6 +20,16 @@ in lib.mkEnableOption "Sway window manager (user-level config)"; config = lib.mkIf cfg.enable { + home.pointerCursor = { + enable = true; + package = pkgs.capitaine-cursors; + name = "capitaine-cursors"; + size = 24; + gtk.enable = true; + x11.enable = true; + sway.enable = true; + }; + wayland.windowManager.sway = { enable = true; wrapperFeatures.gtk = true; @@ -28,6 +38,11 @@ in modifier = "Mod4"; terminal = "ghostty"; menu = "fuzzel"; + bars = [ ]; + + window.titlebar = false; + floating.titlebar = false; + gaps.smartBorders = "on"; output."eDP-1" = { mode = "3840x2400@60Hz"; @@ -45,10 +60,10 @@ in }; keybindings = let mod = modifier; in { - "${mod}+Return" = "exec ${terminal}"; - "${mod}+space" = "exec ${menu}"; - "${mod}+w" = "kill"; - "${mod}+Shift+e" = "exit"; + "${mod}+Return" = "exec ${terminal}"; + "${mod}+space" = "exec ${menu}"; + "${mod}+w" = "kill"; + "${mod}+Shift+e" = "exit"; "${mod}+h" = "focus left"; "${mod}+j" = "focus down"; @@ -72,17 +87,20 @@ in "${mod}+Ctrl+4" = "move container to workspace number 4"; "${mod}+Ctrl+5" = "move container to workspace number 5"; - "${mod}+e" = "layout toggle split"; - "${mod}+f" = "fullscreen"; + "${mod}+e" = "layout toggle split"; + "${mod}+f" = "fullscreen"; "${mod}+Shift+space" = "floating toggle"; - "XF86MonBrightnessUp" = "exec brightnessctl set +5%"; + "XF86MonBrightnessUp" = "exec brightnessctl set +5%"; "XF86MonBrightnessDown" = "exec brightnessctl set 5%-"; - "XF86AudioRaiseVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"; - "XF86AudioLowerVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; - "XF86AudioMute" = "exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; + "XF86AudioRaiseVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"; + "XF86AudioLowerVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; + "XF86AudioMute" = "exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; "${mod}+Escape" = "exec swaylock"; + + "Print" = "exec screenshot-region"; + "Shift+Print" = "exec screenshot-full"; }; # Polkit auth agent — the system-level `security.polkit.enable` @@ -92,10 +110,10 @@ in ]; colors = let c = config.theme.dark; in { - focused = { border = c.blue; background = c.bg; text = c.fg; indicator = c.blue; childBorder = c.blue; }; + focused = { border = c.blue; background = c.bg; text = c.fg; indicator = c.blue; childBorder = c.blue; }; focusedInactive = { border = c.bg_dark; background = c.bg; text = c.fg_dark; indicator = c.bg_dark; childBorder = c.bg_dark; }; - unfocused = { border = c.bg_dark; background = c.bg; text = c.comment; indicator = c.bg_dark; childBorder = c.bg_dark; }; - urgent = { border = c.red; background = c.red; text = c.fg; indicator = c.red; childBorder = c.red; }; + unfocused = { border = c.bg_dark; background = c.bg; text = c.comment; indicator = c.bg_dark; childBorder = c.bg_dark; }; + urgent = { border = c.red; background = c.red; text = c.fg; indicator = c.red; childBorder = c.red; }; }; fonts = { @@ -105,6 +123,8 @@ in }; extraConfig = '' + seat seat0 xcursor_theme capitaine-cursors 24 + exec swayidle -w \ timeout 180 'brightnessctl -s set 20%' resume 'brightnessctl -r' \ timeout 300 'swaylock -f' \ diff --git a/modules/home/linux/window-manager/waybar.nix b/modules/home/linux/window-manager/waybar.nix index c45c02a..93adf3f 100644 --- a/modules/home/linux/window-manager/waybar.nix +++ b/modules/home/linux/window-manager/waybar.nix @@ -38,28 +38,29 @@ in tray = { spacing = 10; }; clock = { - format = "{:%a %d %b %H:%M}"; + format = " {:%a %d %b %H:%M}"; tooltip-format = "{:%Y-%m-%d}\n{calendar}"; }; battery = { - format = "{capacity}% {icon}"; - format-icons = [ "" "" "" "" "" ]; - format-charging = "{capacity}% "; + format = "{icon} {capacity}%"; + format-icons = [ "󰂎" "󰁻" "󰁾" "󰂀" "󰂂" ]; + format-charging = "󰂄 {capacity}%"; states = { warning = 30; critical = 15; }; }; network = { - format-wifi = "{essid} ({signalStrength}%) "; - format-ethernet = "{ifname} "; - format-disconnected = "disconnected ⚠"; + format-wifi = "󰤨 {essid} ({signalStrength}%)"; + format-ethernet = "󰈀 {ifname}"; + format-disconnected = "󰌙 disconnected"; tooltip-format = "{ifname}: {ipaddr}/{cidr}"; + on-click = "${pkgs.ghostty}/bin/ghostty --command=nmtui"; }; pulseaudio = { - format = "{volume}% {icon}"; - format-muted = "muted "; - format-icons = { default = [ "" "" "" ]; }; + format = "{icon} {volume}%"; + format-muted = "󰖁 muted"; + format-icons = { default = [ "󰕿" "󰖀" "󰕾" ]; }; on-click = "${pkgs.pavucontrol}/bin/pavucontrol"; }; }; @@ -78,19 +79,24 @@ in color: ${c.fg}; } + #workspaces { + margin: 4px 6px; + } #workspaces button { - padding: 0 8px; - color: ${c.fg_dark}; - background: transparent; - border-bottom: 2px solid transparent; + padding: 0; + margin: 2px 4px; + font-weight: bold; + color: alpha(${c.fg}, 0.53); + background: alpha(${c.blue}, 0.2); + border-radius: 6px; } #workspaces button.focused { - color: ${c.fg}; - border-bottom: 2px solid ${c.blue}; + color: ${c.black}; + background: ${c.purple}; } #workspaces button.urgent { - color: ${c.red}; - border-bottom: 2px solid ${c.red}; + color: ${c.fg}; + background: ${c.red}; } #clock, #battery, #network, #pulseaudio, #tray, #mode { @@ -98,6 +104,11 @@ in color: ${c.fg}; } + #network:hover, #pulseaudio:hover { + background: ${c.bg_highlight}; + border-radius: 6px; + } + #battery.warning { color: ${c.orange}; } #battery.critical { color: ${c.red}; } ''; From 1180d8a4c9aff44c67e587b3b458eb3ef1a0e7b7 Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 3 Aug 2026 07:36:05 +0200 Subject: [PATCH 12/19] astoria: harden install/secrets docs, pin sudoers, trim docs and comments Security/correctness fixes from an review: - sudoers.nix: pin exact nixos-rebuild invocations, no wildcard (was a passwordless arbitrary-flake privesc surface) - README: scp the SSH host key's .pub from the vault instead of deriving it on the installer (removes an unguarded pipe), shred the dev-machine's temp copy, fix disko's nix run arg order, add missing sbctl --disable-landlock, correct PCR 7 (policy state, not payload measurement) in two spots, make shred failures visible instead of swallowed, fix a cleanup check referencing an already-exited shell's var - screenshot.nix: screenshot-region now exits cleanly on a cancelled/empty slurp selection instead of feeding grim garbage geometry --- hosts/astoria/README.md | 217 ++++++++---------- hosts/astoria/default.nix | 8 +- hosts/astoria/home.nix | 12 +- hosts/astoria/sudoers.nix | 8 +- modules/home/development/git.nix | 5 +- .../home/linux/window-manager/screenshot.nix | 10 +- 6 files changed, 117 insertions(+), 143 deletions(-) diff --git a/hosts/astoria/README.md b/hosts/astoria/README.md index 42943ae..1152464 100644 --- a/hosts/astoria/README.md +++ b/hosts/astoria/README.md @@ -21,38 +21,35 @@ choices; this file is the operator's runbook. ## Rollback -Two independent rollback mechanisms — use the right one for the failure mode. +Two independent mechanisms — use the right one for the failure mode. -**NixOS generation rollback** — for reverting a `nixos-rebuild switch` that broke -something: +**NixOS generation rollback** — for a `nixos-rebuild switch` that broke something: - Lanzaboote boot menu at boot → pick a previous generation - Or from CLI (still-booted system): `sudo nixos-rebuild switch --rollback` -**Snapper rootfs rollback** — for reverting non-`/nix/store` drift (files edited -outside the flake, corrupted state under `/etc`, `/var`, `/home`). Snapshots live -under `/.snapshots//snapshot` and are created at every boot by `snapper-boot.service`: +**Snapper rootfs rollback** — for non-`/nix/store` drift (files edited outside +the flake, corrupted state under `/etc`, `/var`, `/home`). Snapshots live under +`/.snapshots//snapshot`, created at every boot by `snapper-boot.service`: ```bash sudo snapper -c root list # inspect snapshots + timestamps sudo snapper -c root diff .. # peek at what would change sudo snapper -c root undochange ..0 # restore from snapshot N sudo snapper -c root rollback # nuke current @root, replace with snapshot N — reboots into it ``` -`snapper rollback` is destructive to the current @root (it swaps subvolumes); prefer -`undochange` for targeted recovery when only a few files are affected. +`rollback` swaps subvolumes, destroying current @root — use `undochange` +instead for a few files. -Note: Snapper's SUBVOLUME is `/` (targets @root only). `/nix`, `/home`, `/.snapshots` -are separate subvolumes and NOT covered — `/nix/store` is already immutable + -content-addressed; `/home` is user data that Restic backs up. +Note: Snapper's SUBVOLUME is `/` (@root only). `/nix`, `/home`, `/.snapshots` +are separate subvolumes, not covered — `/nix/store` is already immutable + +content-addressed; `/home` is user data Restic backs up. --- ## Secrets (sops) -astoria decrypts `hosts/astoria/secrets/astoria.yaml` at boot using its SSH -host key (converted to age) plus a recovery age key — both listed as -recipients under the `&astoria` / `&brutcha_recovery` anchors in `.sops.yaml` -at the repo root. The secret schema sops-nix expects in that YAML is -declared in `hosts/astoria/secrets.nix`. +astoria decrypts `hosts/astoria/secrets/astoria.yaml` at boot with its SSH +host key (as age, `&astoria` in `.sops.yaml`) or the off-host recovery key +(`&brutcha_recovery`) — either works alone. Schema: `hosts/astoria/secrets.nix`. Tooling docs: [sops-nix](https://github.com/Mic92/sops-nix) (the NixOS module wiring `.sops.yaml` → `secrets.nix` → `/run/secrets`), @@ -64,20 +61,18 @@ module wiring `.sops.yaml` → `secrets.nix` → `/run/secrets`), ## Dev-machine secrets (only needed for host-key rotation / disaster recovery) Generating astoria's secrets from scratch needs, once: -- An SSH host key for astoria, converted to age via `ssh-to-age` — its - pubkey becomes the `&astoria` recipient in `.sops.yaml`; the private key - goes to `/etc/ssh/ssh_host_ed25519_key` on the machine (Phase 3 step 3) - and to the vault (`"astoria SSH host key"`) for recovery. -- A recovery age keypair (`age-keygen`, generated once, ever, reused across - hosts) → pubkey is the `&brutcha_recovery` anchor; private key → vault - (`"astoria sops recovery"`). Follow the shred discipline in "Recovery age - key — helper-device shred discipline" below whenever it leaves the vault. +- SSH host key (age via `ssh-to-age`, `&astoria` recipient) — both halves to + `/etc/ssh/ssh_host_ed25519_key`(`.pub`) on the machine (Phase 3 step 3) and + to the vault (`"astoria SSH host key"`). +- Recovery age keypair (`age-keygen`, once ever, shared fleet-wide — rotate + with a new keypair + `sops updatekeys` on every host) → `&brutcha_recovery`; + private key to vault (`"astoria sops recovery"`). Shred discipline below. - Login/cryptswap LUKS passphrases, a restic repo password, and a - `mkpasswd -m yescrypt` password hash — each in its own vault entry - (`"astoria login"`, `"astoria cryptswap"`, `"astoria restic repo"`), then - assembled into `hosts/astoria/secrets/astoria.yaml` matching the schema - declared in `secrets.nix`, and `sops updatekeys - hosts/astoria/secrets/astoria.yaml`, commit, push. + `mkpasswd -m yescrypt` password hash — each its own vault entry + (`"astoria login"`, `"astoria cryptswap"`, `"astoria restic repo"`), + assembled into `hosts/astoria/secrets/astoria.yaml` per `secrets.nix`'s + schema, then `sops updatekeys hosts/astoria/secrets/astoria.yaml`, commit, + push. Tooling docs: [ssh-to-age](https://github.com/Mic92/ssh-to-age), [age](https://github.com/FiloSottile/age), @@ -88,10 +83,9 @@ Tooling docs: [ssh-to-age](https://github.com/Mic92/ssh-to-age), ## Phase 3 — install Boot the [nix-community/nixos-images](https://github.com/nix-community/nixos-images) -unstable installer ISO. Grab the IP + root password from its on-screen -QR/JSON, then SSH in as root — everything below runs inside that one -session (no `sudo` needed anywhere in this phase, you're already root), -except the host-key transfer, which is run from the dev machine. +unstable installer ISO, grab IP + root password from its QR/JSON, SSH in as +root. Everything below runs in that session (already root, no `sudo`) except +the host-key transfer, run from the dev machine. ```bash GITHUB_USER= @@ -107,39 +101,33 @@ nix --extra-experimental-features 'nix-command flakes' run nixpkgs#git -- \ cd /tmp/dotfiles ``` -The minimal ISO's closure is intentionally bare, and flakes aren't enabled -by default on it either — anything else needed mid-install has to be -fetched on-demand the same way: +The minimal ISO's closure is bare and flakes aren't enabled by default — +fetch anything else the same way: ```bash nix --extra-experimental-features 'nix-command flakes' run nixpkgs# -- ... ``` Packages that come up needing this during install: `sops`, `ssh-to-age`, `sbctl`, `pciutils` (for `lspci`), `stress-ng`, `s-tui`, -`linuxPackages.turbostat`. `sbctl` also needs `--disable-landlock` when -writing outside its expected paths (e.g. `--export /mnt/var/lib/sbctl/keys` -in step 4 below), since it sandboxes itself with Landlock by default. +`linuxPackages.turbostat`. `sbctl` also needs `--disable-landlock` writing +outside its expected paths (step 4's `--export`) — it sandboxes itself with +Landlock by default. ### 2. Partition + format -This model defaults to **RAID (Intel RST) mode** in BIOS, which hides the -NVMe drive from normal enumeration (`lspci` shows a `RAID bus controller` -instead of a `Non-Volatile memory controller`; `dmesg` shows `ahci ...: -Found 1 remapped NVMe devices`). Fix before running disko: BIOS → System -Configuration → **SATA Operation: AHCI** (switch from RAID). No config -changes needed once this is set correctly — `nvme` in -`boot.initrd.availableKernelModules` is already sufficient once the drive -enumerates natively. +This model defaults to **RAID (Intel RST)** in BIOS, hiding the NVMe drive +(`lspci` shows `RAID bus controller`, `dmesg` shows `Found 1 remapped NVMe +devices`). Fix: BIOS → System Configuration → **SATA Operation: AHCI**. No +config changes needed after — `nvme` in `boot.initrd.availableKernelModules` +already covers it. ```bash -nix run '.#disko' \ - --extra-experimental-features 'nix-command flakes' \ +nix --extra-experimental-features 'nix-command flakes' run '.#disko' \ -- --mode destroy,format,mount --flake .#astoria ``` -`.#disko` (rather than `github:nix-community/disko`) resolves the disko -binary through the flake's own `packages.x86_64-linux.disko` re-export, -which is pinned via `flake.lock` — no unpinned github ref, no MITM-able -fetch, exact same code as everything else the flake produces. +`.#disko` resolves through the flake's own pinned +`packages.x86_64-linux.disko` re-export — no unpinned github ref, same code +as the rest of the flake. Prompts, in order: 1. `Type 'yes' to continue, anything else to abort:` — type literally `yes`. @@ -151,21 +139,16 @@ Prompts, in order: ### 3. Transfer astoria's SSH host key from dev → installer -From the dev machine (a separate terminal from the root SSH session above), -scp the raw private key over, then move it into place with the right -owner/mode and shred the transient copy: +From the dev machine (separate terminal from the root SSH session above), +scp both halves of the key over, install them, shred every transient copy +on both ends: ```bash -scp /tmp/astoria_host_key "root@${ASTORIA_IP}:/tmp/astoria_host_key" +scp /tmp/astoria_host_key /tmp/astoria_host_key.pub "root@${ASTORIA_IP}:/tmp/" ssh "root@${ASTORIA_IP}" 'mkdir -p /mnt/etc/ssh && \ install -m 600 -o root -g root /tmp/astoria_host_key /mnt/etc/ssh/ssh_host_ed25519_key && \ - shred -u /tmp/astoria_host_key' -``` - -Back in the installer session, derive the .pub (strict openssh refuses to -read a 0644 private key without a matching .pub): -```bash -ssh-keygen -y -f /mnt/etc/ssh/ssh_host_ed25519_key | \ - install -m 644 -o root -g root /dev/stdin /mnt/etc/ssh/ssh_host_ed25519_key.pub + install -m 644 -o root -g root /tmp/astoria_host_key.pub /mnt/etc/ssh/ssh_host_ed25519_key.pub && \ + shred -u /tmp/astoria_host_key /tmp/astoria_host_key.pub' +shred -u /tmp/astoria_host_key /tmp/astoria_host_key.pub ``` ### 4. Generate sbctl Secure Boot keys @@ -176,12 +159,12 @@ keys, so they must exist at `/mnt/var/lib/sbctl` before install. ```bash nix --extra-experimental-features 'nix-command flakes' run nixpkgs#sbctl -- create-keys --help # confirm flag names first mkdir -p /mnt/var/lib/sbctl -nix --extra-experimental-features 'nix-command flakes' run nixpkgs#sbctl -- create-keys --export /mnt/var/lib/sbctl/keys --database-path /mnt/var/lib/sbctl/GUID +nix --extra-experimental-features 'nix-command flakes' run nixpkgs#sbctl -- --disable-landlock create-keys --export /mnt/var/lib/sbctl/keys --database-path /mnt/var/lib/sbctl/GUID ``` Gotchas: -- `--database-path` is a **file** path (writes a GUID file at that location), - not a directory. Passing a directory → EISDIR → no keys ever created. +- `--database-path` is a **file** path (writes a GUID file there), not a + directory. Passing a directory → EISDIR → no keys ever created. - Older sbctl uses `--keydir` / `--pki-dir` instead. Check `--help` first. ### 5. Install @@ -207,13 +190,12 @@ cryptroot passphrase → cryptswap passphrase (once, until Phase 4c) → greetd ## Phase 4 — first-boot + Secure Boot + TPM -> **Single-sitting rule**: do 4a → 4b → 4c back-to-back, don't leave astoria -> unattended between install and 4c. Between install (SB off, ESP writable -> by any live USB) and Phase 4b's SB-on moment, an attacker with brief -> physical access could plant an unsigned payload on the ESP. When SB flips -> on, firmware measures that payload into PCR 7 as "trusted state"; Phase 4c -> then seals the TPM against that PCR 7. Post-4c the machine is protected. -> Roughly a 30-minute total sitting. +> **Single-sitting rule**: do 4a → 4b → 4c back-to-back — SB is off and the +> ESP is writable by any live USB until 4b. PCR 7 tracks Secure Boot +> **policy state** (enrolled PK/KEK/db), not ESP file contents, so this +> window is about limiting physical-access time, not PCR 7 catching +> tampering. Once SB is on with only the legit keys enrolled, unsigned +> payloads won't boot anyway. ~30 minutes total. ### 4a. Bring-up checks @@ -257,9 +239,8 @@ ls /sys/class/tpm/ # tpm0 should be there sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7 /dev/nvme0n1p2 ``` -Prompts for the cryptswap passphrase to authorize the new keyslot. PCRs 0/2/7 -= firmware code / extended firmware code / SB state — binds key release to -firmware integrity + SB being on. +Prompts for the cryptswap passphrase. PCRs 0/2/7 = firmware code / extended +firmware code / SB state — key release requires all three unchanged. Reboot to verify: cryptroot passphrase (always), cryptswap unlocks SILENTLY via TPM (no prompt). @@ -283,11 +264,10 @@ sudo systemd-cryptenroll /dev/nvme0n1p2 # slot 0 (password) ### 4d. Lockdown check - [ ] `bootctl status` shows Secure Boot enabled, Setup Mode disabled. -- **BIOS admin password NOT recommended** — would prevent unauthorized - Setup-Mode re-entry but breaks TLP's Dell charge-threshold writes silently. - SB + PCR sealing already narrow the physical-access attack: any hostile - re-enrollment invalidates PCR 7 → cryptswap TPM auto-unlock breaks → - tamper visible on the next boot. +- **BIOS admin password**: trade-off. Unset keeps TLP's charge-threshold + writes; set blocks Setup-Mode re-entry but may break them — test first. + PCR sealing only gates *TPM key release*: a hostile re-enrollment breaks + the seal → passphrase fallback next boot (a signal, not tamper detection). --- @@ -296,10 +276,9 @@ sudo systemd-cryptenroll /dev/nvme0n1p2 # slot 0 (password) + `/run/secrets/restic/repo-password` - `sudo nix-shell -p restic rclone` — enters an interactive root shell with restic + rclone on PATH. -- Inside that shell you are already root; rclone runs with `HOME=/root` and doesn't - find the sops-materialized config unless `RCLONE_CONFIG` is explicit. Same for - restic's password file. **Do NOT wrap these calls in another `sudo`** — inner sudo - triggers `env_reset` + PAM PATH reset, which drops the nix-shell's ephemeral PATH. +- Already root inside; rclone needs `RCLONE_CONFIG` explicit (`HOME=/root` + won't find the sops config). Same for restic's password file. **Don't + wrap in another `sudo`** — it resets PATH and drops the nix-shell env. Use plain `env`: ```bash env \ @@ -337,19 +316,28 @@ of the vault onto a helper device to run `sops`, follow this discipline: 1. On the helper device, mount a scratch tmpfs first: - Linux: `SCRATCH=$(mktemp -d --tmpdir=/dev/shm astoria-recovery.XXXXX)` - - macOS: plain `~/.config/sops/age/keys.txt` lands on the boot APFS volume where - `rm` releases the inode but blocks remain until reclaimed, and Time Machine - local snapshots capture the file for ~24h. Use a RAM disk: + - macOS: the default path lands on the boot APFS volume — `rm` frees the + inode but blocks linger, and Time Machine snapshots keep it ~24h. Use a + RAM disk: `hdiutil attach -nomount ram://8192 | xargs -I{} diskutil erasevolume APFS 'ARamDisk' {}; SCRATCH=/Volumes/ARamDisk` -2. Set the cleanup trap FIRST (before any paste): +2. Confirm `shred` is available, then set the cleanup trap (before any + paste) — failures are reported, not silently swallowed: ```bash - trap 'shred -u -- "$SCRATCH"/keys.txt 2>/dev/null; rm -rf "$SCRATCH" 2>/dev/null; diskutil eject ARamDisk 2>/dev/null' EXIT INT TERM + command -v shred || { echo "shred not found — do not paste yet" >&2; exit 1; } + trap ' + shred -u -- "$SCRATCH"/keys.txt \ + || printf "WARNING: shred failed — %s/keys.txt may still be readable\n" "$SCRATCH" >&2 + rm -rf "$SCRATCH" 2>/dev/null + diskutil eject ARamDisk 2>/dev/null + ' EXIT INT TERM ``` 3. Paste the recovery age private key to `"$SCRATCH"/keys.txt`; `chmod 600 "$SCRATCH"/keys.txt` immediately. 4. `SOPS_AGE_KEY_FILE="$SCRATCH"/keys.txt sops hosts/astoria/secrets/astoria.yaml` — decrypt, edit, save, then `sops updatekeys …` to re-encrypt. 5. Commit + push. Exit the shell — the trap fires and shreds the temp key. -6. Verify: `ls -la "$SCRATCH"` should show "No such file". On macOS also verify +6. Verify, from a fresh shell (`$SCRATCH` no longer exists once you've + exited the one that defined it): `ls -la /dev/shm/astoria-recovery.*` + (Linux) should show "No such file or directory". On macOS also verify `tmutil listlocalsnapshots /` doesn't show a recent snapshot containing the file (they roll off in ~24h; force-delete with `tmutil deletelocalsnapshots`). @@ -358,9 +346,9 @@ of the vault onto a helper device to run `sops`, follow this discipline: sudo cryptsetup luksChangeKey /dev/nvme0n1p3 # cryptroot — also update `"astoria login"` in the vault sudo cryptsetup luksChangeKey /dev/nvme0n1p2 # cryptswap fallback — also update its vault entry ``` -The cryptswap TPM keyslot is separate from the passphrase keyslot; changing the -passphrase does NOT invalidate the TPM binding. Rotate TPM enrollment only if you -need to (BIOS updates, PCR changes). +TPM keyslot is separate from the passphrase keyslot — changing the +passphrase doesn't touch the TPM binding. Rotate TPM only if needed (BIOS +updates, PCR changes). ## TPM re-enrollment (after BIOS/firmware update — expected ~1-2× per year) @@ -388,15 +376,15 @@ If `sbctl verify` shows unsigned files after a manual bootloader tweak: 1. Reboot into BIOS. Advanced Boot Options → Secure Boot → "Reset to Setup Mode" (or "Delete All Keys"). -2. Boot back into NixOS. `sudo sbctl reset` — resets the UEFI PK/KEK/db variables to - Setup Mode by enrolling an empty signature DB. **`sbctl reset` does NOT touch - `/var/lib/sbctl`** — the local keypair on disk survives. +2. Boot back into NixOS. `sudo sbctl reset` — resets UEFI PK/KEK/db to Setup + Mode via an empty signature DB. **Doesn't touch `/var/lib/sbctl`** — + local keypair survives. 3. **Delete the local keypair** so step 4 doesn't silently no-op: ```bash sudo rm -rf /var/lib/sbctl/keys /var/lib/sbctl/GUID ``` - `sbctl create-keys` refuses to overwrite an existing keydir; skipping this rm - leaves the old keypair in place and the "reset" achieves nothing. + `sbctl create-keys` won't overwrite an existing keydir — skip this and + the "reset" is a no-op. 4. `sudo sbctl create-keys` — new keypair, now that the keydir is empty. 5. `sudo nixos-rebuild switch` — re-signs the bootloader + kernel + initrd with the new keys. @@ -422,28 +410,23 @@ Charge thresholds 60/80 via TLP. Check with `tlp-stat -b`. Battery replacement - The password-vault LibreWolf extension is declaratively installed but not configured — set server URL, log in with your vault account, unlock with the challenge password on first launch (manual, one-time). -- **If suspend-then-hibernate fails to wake at the scheduled time**: ADD - `rtc_cmos.use_acpi_alarm=1` to `boot.kernelParams`. Kernel auto-quirks the ACPI - SCI alarm path deterministically on Intel + BIOS≥2015 + HPET-on (XPS 13 9300 hits - all three), so the param is normally redundant and hardware.nix omits it. Kept as - a documented fallback in case a future BIOS revision breaks the auto-quirk. +- **If suspend-then-hibernate fails to wake on schedule**: add + `rtc_cmos.use_acpi_alarm=1` to `boot.kernelParams`. Redundant on this + hardware (kernel auto-quirks it), so hardware.nix omits it — documented + fallback if a BIOS update breaks the auto-quirk. - **Cryptswap prompts for a passphrase after a BIOS update**: expected — TPM PCRs changed, sd-cryptsetup fell back to the passphrase keyslot. Re-run TPM enrollment (see "TPM re-enrollment"). One-time-per-BIOS-bump friction. - **`sbctl verify` shows unsigned files**: something touched the ESP outside `nixos-rebuild switch`. `sudo sbctl sign -s ` and rebuild; check the ESP wasn't manually edited. -- **Dell Expert Key Management: "Reset All Keys" vs "Delete All Keys"** — these - are two similarly-labeled, opposite actions. **Reset All Keys** restores - factory Microsoft/OEM keys (NOT what you want here). **Delete All Keys** - clears everything and enters Setup Mode (correct). Also: the page's "N - changes were made" counter does NOT track Delete All Keys — it's an - immediate action, not a queued setting. Trust `sbctl status` after - rebooting, not the counter. Also: enabling Secure Boot while the firmware - has no PK enrolled (Setup Mode + SB on simultaneously) can produce a "no - bootable devices" SupportAssist screen on some BIOS revisions — leave SB - off through key deletion + enrollment, and only re-enable it last, once - `sbctl enroll-keys` has succeeded. +- **Dell Expert Key Management: "Reset All Keys" vs "Delete All Keys"** — + opposite actions. **Reset** restores factory MS/OEM keys (wrong). + **Delete** clears everything into Setup Mode (correct). The "N changes" + counter doesn't track Delete — trust `sbctl status` after reboot instead. + Also: SB-on with no PK enrolled can throw a "no bootable devices" screen + on some BIOS revisions — keep SB off until `sbctl enroll-keys` succeeds, + then enable it last. ## References diff --git a/hosts/astoria/default.nix b/hosts/astoria/default.nix index 546f3d5..49907ab 100644 --- a/hosts/astoria/default.nix +++ b/hosts/astoria/default.nix @@ -70,12 +70,8 @@ common.default = [ "wlr" "gtk" ]; }; }; - # dconf/D-Bus service — xdg-desktop-portal-gtk's Settings backend (above) - # answers org.freedesktop.appearance queries by reading gsettings, which - # is backed by dconf. Without this, apps that query the portal for - # color-scheme (Ghostty, LibreWolf) never see a preference and fall back - # to light, even with gtk-application-prefer-dark-theme set (that ini - # setting only affects GTK3 apps' own rendering, not the portal signal). + # Backs the portal's color-scheme signal (Ghostty/LibreWolf dark mode) — + # gtk-application-prefer-dark-theme alone doesn't reach it. programs.dconf.enable = true; security.polkit.enable = true; diff --git a/hosts/astoria/home.nix b/hosts/astoria/home.nix index 361dab9..20558d4 100644 --- a/hosts/astoria/home.nix +++ b/hosts/astoria/home.nix @@ -50,19 +50,13 @@ }; iconTheme = { name = "Papirus-Dark"; package = pkgs.papirus-icon-theme; }; cursorTheme = { name = "Bibata-Modern-Ice"; package = pkgs.bibata-cursors; }; - # Matches the Nerd Font used across sway/waybar/mako/fuzzel (fonts.nix - # installs the package already) — GTK chrome font, so LibreWolf's - # address bar/tabs and other GTK apps' UI text follow it too. + # Matches the Nerd Font used across sway/waybar/mako/fuzzel. font = { name = "JetBrainsMonoNL Nerd Font"; size = 10; }; gtk3.extraConfig.gtk-application-prefer-dark-theme = 1; }; - # Portal-facing appearance signal (org.freedesktop.appearance - # color-scheme). xdg-desktop-portal-gtk reads this via gsettings/dconf — - # it's what Ghostty's dark:/light: auto-theme and LibreWolf's - # prefers-color-scheme detection actually consult on Wayland, unlike the - # GTK3-only setting above. Static "prefer-dark", matching the static - # Tokyonight-Dark GTK theme choice (no day/night auto-switch here). + # Portal color-scheme signal — what Ghostty/LibreWolf actually consult + # for dark mode on Wayland, unlike the GTK3-only setting above. dconf.settings."org/gnome/desktop/interface" = { color-scheme = "prefer-dark"; }; diff --git a/hosts/astoria/sudoers.nix b/hosts/astoria/sudoers.nix index a09e83c..504c759 100644 --- a/hosts/astoria/sudoers.nix +++ b/hosts/astoria/sudoers.nix @@ -1,9 +1,9 @@ { lib, ... }: -# NOPASSWD for the single nixos-rebuild switch invocation, so it can run -# from Claude Code / scripts without an OS prompt. All other sudo actions -# still prompt normally. +# NOPASSWD for these two exact rebuild commands only — no wildcard, so a +# passwordless call can't smuggle in an arbitrary --flake path. { security.sudo.extraConfig = lib.mkAfter '' - brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch * + brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch --flake .#astoria + brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch --flake /etc/nixos#astoria ''; } diff --git a/modules/home/development/git.nix b/modules/home/development/git.nix index 00edc29..287fcfa 100644 --- a/modules/home/development/git.nix +++ b/modules/home/development/git.nix @@ -29,9 +29,8 @@ in credential = { helper = "manager"; } // lib.optionalAttrs pkgs.stdenv.isLinux { - # GCM needs an explicit store on Linux (no single obvious default, - # unlike macOS Keychain). gnome-keyring's Secret Service is already - # enabled system-wide (services.gnome.gnome-keyring.enable). + # GCM needs an explicit credential store on Linux; gnome-keyring's + # Secret Service is already enabled system-wide. credentialStore = "secretservice"; }; }; diff --git a/modules/home/linux/window-manager/screenshot.nix b/modules/home/linux/window-manager/screenshot.nix index 0333148..280d0cd 100644 --- a/modules/home/linux/window-manager/screenshot.nix +++ b/modules/home/linux/window-manager/screenshot.nix @@ -1,8 +1,7 @@ { config, lib, pkgs, ... }: # -# Screenshots — grim (capture) + slurp (region select) + swappy (annotate). -# Sway-native via wlr-screencopy; spectacle/flameshot don't target wlroots -# compositors well. +# Screenshots — grim/slurp/swappy; not spectacle/flameshot (don't target +# wlroots well). # # Available options: # - home.apps.windowManager.screenshot.enable @@ -18,7 +17,10 @@ in home.packages = [ # Region select, opens swappy for annotate/save/copy. (pkgs.writeShellScriptBin "screenshot-region" '' - exec ${pkgs.grim}/bin/grim -g "$(${pkgs.slurp}/bin/slurp)" - | ${pkgs.swappy}/bin/swappy -f - + set -euo pipefail + geometry="$(${pkgs.slurp}/bin/slurp)" || exit 0 + [ -n "$geometry" ] || exit 0 + exec ${pkgs.grim}/bin/grim -g "$geometry" - | ${pkgs.swappy}/bin/swappy -f - '') # Full screen, saved straight to disk + clipboard, no editor. (pkgs.writeShellScriptBin "screenshot-full" '' From c768e5442858318530687889cc93990040039fd7 Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 3 Aug 2026 08:49:04 +0200 Subject: [PATCH 13/19] Disable swaylock, adjust power settings, fix sudoers --- hosts/astoria/home.nix | 5 +-- hosts/astoria/sudoers.nix | 4 +- modules/home/linux/window-manager/sway.nix | 48 +++++++++++++++++++--- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/hosts/astoria/home.nix b/hosts/astoria/home.nix index 20558d4..cf09e17 100644 --- a/hosts/astoria/home.nix +++ b/hosts/astoria/home.nix @@ -3,8 +3,8 @@ # astoria — home-manager entry for `brutcha`. # # Toggles the fleet-standard `home.apps.*` options; modules/home/default.nix -# picks the linux sub-bundle (sway/waybar/mako/fuzzel/swaylock/screenshot/ -# thunar/librewolf/moonlight/imv) via `hostSystem`. +# picks the linux sub-bundle (sway/waybar/mako/fuzzel/screenshot/thunar/ +# librewolf/moonlight/imv) via `hostSystem`. # { imports = [ ../../modules/home ]; @@ -24,7 +24,6 @@ waybar.enable = true; mako.enable = true; fuzzel.enable = true; - swaylock.enable = true; screenshot.enable = true; }; }; diff --git a/hosts/astoria/sudoers.nix b/hosts/astoria/sudoers.nix index 504c759..6f546a3 100644 --- a/hosts/astoria/sudoers.nix +++ b/hosts/astoria/sudoers.nix @@ -3,7 +3,7 @@ # passwordless call can't smuggle in an arbitrary --flake path. { security.sudo.extraConfig = lib.mkAfter '' - brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch --flake .#astoria - brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch --flake /etc/nixos#astoria + brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch --flake .\#astoria + brutcha ALL=(root) NOPASSWD: /run/current-system/sw/bin/nixos-rebuild switch --flake /etc/nixos\#astoria ''; } diff --git a/modules/home/linux/window-manager/sway.nix b/modules/home/linux/window-manager/sway.nix index 51b8154..1f30985 100644 --- a/modules/home/linux/window-manager/sway.nix +++ b/modules/home/linux/window-manager/sway.nix @@ -20,6 +20,44 @@ in lib.mkEnableOption "Sway window manager (user-level config)"; config = lib.mkIf cfg.enable { + # Power-aware swayidle stages — swayidle itself has no AC/battery + # awareness, so each stage checks power state at fire time via + # /sys/class/power_supply/AC/online (this machine's actual AC device). + home.packages = [ + (pkgs.writeShellScriptBin "on-ac" '' + [ "$(cat /sys/class/power_supply/AC/online)" = "1" ] + '') + # 3min: dim on battery only (AC's dim is deferred to stage 2). + (pkgs.writeShellScriptBin "idle-stage1" '' + on-ac || brightnessctl -s set 20% + '') + # 5min: dim on AC, screen off on battery. + (pkgs.writeShellScriptBin "idle-stage2" '' + if on-ac; then + brightnessctl -s set 20% + else + swaymsg 'output * dpms off' + fi + '') + # 10min: screen off on AC, RAM-suspend on battery. + (pkgs.writeShellScriptBin "idle-stage3" '' + if on-ac; then + swaymsg 'output * dpms off' + else + systemctl suspend + fi + '') + # 20min: suspend-then-hibernate on AC (auto-hibernates ~30min later + # if left unplugged and unresumed), explicit hibernate on battery. + (pkgs.writeShellScriptBin "idle-stage4" '' + if on-ac; then + systemctl suspend-then-hibernate + else + systemctl hibernate + fi + '') + ]; + home.pointerCursor = { enable = true; package = pkgs.capitaine-cursors; @@ -97,8 +135,6 @@ in "XF86AudioLowerVolume" = "exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; "XF86AudioMute" = "exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; - "${mod}+Escape" = "exec swaylock"; - "Print" = "exec screenshot-region"; "Shift+Print" = "exec screenshot-full"; }; @@ -126,10 +162,10 @@ in seat seat0 xcursor_theme capitaine-cursors 24 exec swayidle -w \ - timeout 180 'brightnessctl -s set 20%' resume 'brightnessctl -r' \ - timeout 300 'swaylock -f' \ - timeout 600 'swaymsg "output * dpms off"' resume 'swaymsg "output * dpms on"' \ - timeout 1200 'systemctl suspend' + timeout 180 idle-stage1 resume 'brightnessctl -r' \ + timeout 300 idle-stage2 resume 'swaymsg "output * dpms on"; brightnessctl -r' \ + timeout 600 idle-stage3 resume 'swaymsg "output * dpms on"' \ + timeout 1200 idle-stage4 ''; }; }; From 86357624144c3abec90af3dc65e177a41c9df502 Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 3 Aug 2026 11:18:41 +0200 Subject: [PATCH 14/19] Update flake inputs, vendor tokyonight-gtk-theme, rename utils to helpers, tweak waybar nixpkgs-unstable removed tokyonight-gtk-theme after the flake bump (its package.nix still depended on the now-dead gtk-engine-murrine), so it's vendored locally in pkgs/tokyonight-gtk-theme with that dependency dropped. Renamed the shared `utils` lib to `helpers` so it no longer collides with NixOS's internal `utils` module arg, letting astoria pull in the same pkgs/ overlay darwin hosts already use instead of a one-off overlay. Also switched claude-code's `plugins` to an attrset so plugin directory names stay stable instead of deriving from a store hash. Waybar: tighter module padding, and a sway/language module to show the active us/cz keyboard layout. --- flake.lock | 68 +++++----- flake.nix | 28 ++-- hosts/astoria/default.nix | 4 +- .../darwin/window-manager/jankyborders.nix | 6 +- .../home/darwin/window-manager/sketchybar.nix | 4 +- modules/home/development/claude-code.nix | 8 +- modules/home/lib.nix | 6 +- modules/home/linux/window-manager/waybar.nix | 10 +- modules/lib/default.nix | 6 +- pkgs/affinity/default.nix | 4 +- pkgs/alt-tab-macos/default.nix | 4 +- pkgs/android-studio/default.nix | 4 +- pkgs/better-touch-tool/default.nix | 4 +- pkgs/blurred/default.nix | 4 +- pkgs/default.nix | 36 ++--- pkgs/docker-desktop/default.nix | 4 +- pkgs/emdash/default.nix | 4 +- pkgs/insync/default.nix | 4 +- pkgs/karabiner-elements/default.nix | 4 +- pkgs/keepassxc/default.nix | 4 +- pkgs/microsoft-outlook/default.nix | 4 +- pkgs/microsoft-teams/default.nix | 4 +- pkgs/obs-studio/default.nix | 4 +- pkgs/orbstack/default.nix | 4 +- pkgs/raycast/default.nix | 4 +- pkgs/tokyonight-gtk-theme/default.nix | 125 ++++++++++++++++++ pkgs/ungoogled-chromium/default.nix | 4 +- pkgs/zed-editor/default.nix | 4 +- 28 files changed, 248 insertions(+), 121 deletions(-) create mode 100644 pkgs/tokyonight-gtk-theme/default.nix diff --git a/flake.lock b/flake.lock index bf14ea7..7072f29 100644 --- a/flake.lock +++ b/flake.lock @@ -3,16 +3,16 @@ "brew-src": { "flake": false, "locked": { - "lastModified": 1784068757, - "narHash": "sha256-7KnV7rTlMpys+2J+TFVxUDDyKPLXFs4wIMtckMC72VM=", + "lastModified": 1785146564, + "narHash": "sha256-Sa7/HrfB04H32OJ7/ofxXjiZEbkWtCNOriONYYTL1OA=", "owner": "Homebrew", "repo": "brew", - "rev": "6bd951d96e7ebc54787799dba77bfb26ec956c4c", + "rev": "b2cfc03346d482f79886de108fee5dc49a6efc10", "type": "github" }, "original": { "owner": "Homebrew", - "ref": "6.0.11", + "ref": "6.0.13", "repo": "brew", "type": "github" } @@ -20,11 +20,11 @@ "claude-plugins-official": { "flake": false, "locked": { - "lastModified": 1784766795, - "narHash": "sha256-04vjOPpnieiLri1muM+h1/GHxne6XrPjP0lp62nxNY4=", + "lastModified": 1785715405, + "narHash": "sha256-m4bUT3bxxnhsayFsirHPd1sxhzPEGkG9yrQTYuULGN0=", "owner": "anthropics", "repo": "claude-plugins-official", - "rev": "e3e378cbbb205673a5d7254ded32679cafa6179d", + "rev": "909649d9b178d142201000c76715b5fc952818e3", "type": "github" }, "original": { @@ -71,11 +71,11 @@ "figma-plugin": { "flake": false, "locked": { - "lastModified": 1783964166, - "narHash": "sha256-ZkdCdDPNLPPKXa9bTIOw58jyTSv5hjMzx0kvygO6kxE=", + "lastModified": 1785413628, + "narHash": "sha256-metUsjmCvt5t2er2FWhKUGMFF/kLCS3PGlDwCLqMnL0=", "owner": "figma", "repo": "mcp-server-guide", - "rev": "07316dd2920d61303ca0e52812b31f5f341e7b15", + "rev": "ef474d181a6eca44b37722f839e8a7eb58d644ec", "type": "github" }, "original": { @@ -150,11 +150,11 @@ ] }, "locked": { - "lastModified": 1784798039, - "narHash": "sha256-WwMugZH8Uy5vrtBeQypxqEuwPzMtQjGuAtt1qKpev9g=", + "lastModified": 1785461417, + "narHash": "sha256-p+AD3rx7jsoSWIUkexums+IjK6E/5njDlRTJ3ofl5TA=", "owner": "amaanq", "repo": "helium-flake", - "rev": "11e0f53dd6538e562fd3e085db34fb0503af9371", + "rev": "b968c3aa5c58b438a6b5ba4c9ea4fcdc2002fc42", "type": "github" }, "original": { @@ -170,11 +170,11 @@ ] }, "locked": { - "lastModified": 1784789275, - "narHash": "sha256-cgRTWuG+u1tBPhm01JrId2k0Bni09phVJ1Q0BZlRd7M=", + "lastModified": 1785531816, + "narHash": "sha256-vkMnV0JIyw+g/NmcfoajlGaAO+9a0ezia+FZohQJrik=", "owner": "nix-community", "repo": "home-manager", - "rev": "3b0e6bbd65869af1beadf5963a99befc179d209f", + "rev": "bf9ce9fec78f95f374e8dd3b503863a3ec128ebe", "type": "github" }, "original": { @@ -214,11 +214,11 @@ ] }, "locked": { - "lastModified": 1784500460, - "narHash": "sha256-UvORnAxTRHax7RG74W8Z2t4GvIkX6AjJ5kk0QlwZomo=", + "lastModified": 1785389976, + "narHash": "sha256-0tLW8Ff5yt8AH97jw4ZpFJ0OCJ122zIlgWGDmOfU/VU=", "owner": "nix-darwin", "repo": "nix-darwin", - "rev": "57a3171f94705599a2499248ca5758d5eb47c0e0", + "rev": "15abb8c98f336cd8bd840d71059adebabe60bf04", "type": "github" }, "original": { @@ -233,11 +233,11 @@ "brew-src": "brew-src" }, "locked": { - "lastModified": 1784159664, - "narHash": "sha256-I/B6YoRLImHEqNWh8bs+tPjEDeteACeFhcLyoSMo1GE=", + "lastModified": 1785544760, + "narHash": "sha256-qV6OoNuly4ntqpCg7esIeJjUboxSnQNlLPxz+y5h9/o=", "owner": "zhaofengli", "repo": "nix-homebrew", - "rev": "842eeb863ecca0eeb463f7a814cdc51e1d925776", + "rev": "937ce52c7d046310571f3a070713804ead496843", "type": "github" }, "original": { @@ -251,11 +251,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1784723954, - "narHash": "sha256-1CfD8ZUjCkTgjsneLZ/lxCHhgDfqxxE7/GX0MmsgiqA=", + "lastModified": 1785232496, + "narHash": "sha256-65EQYIRRpTdpH8lUiB6Mvo5uBkG60aBIzAJuALfx+O0=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "a017f5b72210026af5b3ac5949f08d94380a6fbd", + "rev": "2e790b0a6be8ec2b76174ac0931b8ff11919ec98", "type": "github" }, "original": { @@ -280,11 +280,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1784555310, - "narHash": "sha256-/FCliTPgiuV1owejZFNx3Ch9irdvkOfOFl+HHZ+DrtM=", + "lastModified": 1785715447, + "narHash": "sha256-4K0IGmbAOJnd/Hx2+CNEvx3ECrtYnsoaf4grI6gvZ/Q=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "421eebfd0ec7bccd4abe826ce62d7e6e83129493", + "rev": "243895692ae2a2fbd08a05141462c0cc0d3ca10f", "type": "github" }, "original": { @@ -296,11 +296,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1785090369, - "narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=", + "lastModified": 1785692966, + "narHash": "sha256-vUfIeBEfpbAfZ5zjgIkYk7eHBeVfCYVjLbWnMkseYnk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "624af665418d3c65d544145b4d34ad696439570e", + "rev": "643809054d65fdd466a63e3155b8c498cb483c04", "type": "github" }, "original": { @@ -316,11 +316,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1785145543, - "narHash": "sha256-sYiHA2+r6JDT9/NvN+pwbPm/tr1VSx+a0x3HR5WllVc=", + "lastModified": 1785747989, + "narHash": "sha256-uzfXp4xdXd+N7ISja44OD6UE7LsvoDtBeedLgFS5trk=", "owner": "nix-community", "repo": "NUR", - "rev": "71d05aabaf55a59606d38062a7c87b9c934ec851", + "rev": "9c2bcb321cb4a65cd9791fdfae7ff99787e9c22c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e431b49..1af0360 100644 --- a/flake.nix +++ b/flake.nix @@ -87,8 +87,9 @@ outputs = inputs@{ self, nix-darwin, home-manager, nix-homebrew, nixpkgs, ... }: let - # Custom utilities available globally as 'utils' - utils = import ./modules/lib/default.nix { lib = nixpkgs.lib; }; + # Custom utilities available globally as 'helpers'. Must not be named + # `utils` — NixOS's module framework injects its own internal `utils` arg. + helpers = import ./modules/lib/default.nix { lib = nixpkgs.lib; }; # Base configuration shared across all systems # Enables flakes and sets up fundamental packages @@ -160,7 +161,7 @@ inherit inputs private; hostSystem = system; rootDir = self; - utils = utils; + helpers = helpers; }; home-manager.useUserPackages = true; @@ -236,14 +237,14 @@ (final: prev: { fish = prev.fish.overrideAttrs (old: { doCheck = false; }); }) - (import ./pkgs { inherit utils; }) + (import ./pkgs { inherit helpers; }) ]; }; rootDir = self; in nix-darwin.lib.darwinSystem { inherit system; - specialArgs = { inherit utils pkgs rootDir private; }; + specialArgs = { inherit helpers pkgs rootDir private; }; modules = [ configuration @@ -281,15 +282,15 @@ doCheck = false; }); }) - (import ./pkgs { inherit utils; }) + (import ./pkgs { inherit helpers; }) ]; }; rootDir = self; in nix-darwin.lib.darwinSystem { inherit system; - # Add utils to the nix flake specialArgs, make helpers like toARGB available in each module - specialArgs = { inherit utils pkgs rootDir private; }; + # Add helpers to the nix flake specialArgs, make helpers like toARGB available in each module + specialArgs = { inherit helpers pkgs rootDir private; }; modules = [ configuration @@ -325,20 +326,13 @@ in nixpkgs.lib.nixosSystem { inherit system; - # NOTE: `utils` intentionally NOT in system specialArgs — NixOS's - # module framework injects its own `utils` (which carries - # `systemdUtils` used by nixos/modules/system/boot/systemd.nix); a - # user-supplied `utils` would shadow it and break systemd module - # eval with "undefined variable 'systemdUtils'". The custom `utils` - # from modules/lib/default.nix is still available to HM modules via - # extraSpecialArgs below (HM has no such internal `utils`). - specialArgs = { inherit inputs rootDir private; }; + specialArgs = { inherit inputs rootDir private helpers; }; modules = [ configuration ./hosts/astoria/default.nix { home-manager.extraSpecialArgs = { - inherit inputs private rootDir utils; + inherit inputs private rootDir helpers; hostSystem = system; }; } diff --git a/hosts/astoria/default.nix b/hosts/astoria/default.nix index 49907ab..020c17c 100644 --- a/hosts/astoria/default.nix +++ b/hosts/astoria/default.nix @@ -1,4 +1,4 @@ -{ config, pkgs, lib, inputs, ... }: +{ config, pkgs, lib, inputs, helpers, ... }: # # astoria — Dell XPS 13 9300 NixOS thin-client. Sway sole session, # Moonlight-first, LibreWolf, greetd, no autologin. @@ -16,7 +16,7 @@ ]; nixpkgs.config.allowUnfree = true; # required by hardware.enableAllFirmware - nixpkgs.overlays = [ inputs.nur.overlays.default ]; + nixpkgs.overlays = [ inputs.nur.overlays.default (import ../../pkgs { inherit helpers; }) ]; networking.hostName = "astoria"; diff --git a/modules/home/darwin/window-manager/jankyborders.nix b/modules/home/darwin/window-manager/jankyborders.nix index 39f851b..3456600 100644 --- a/modules/home/darwin/window-manager/jankyborders.nix +++ b/modules/home/darwin/window-manager/jankyborders.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, utils, ... }: +{ config, lib, pkgs, helpers, ... }: # # JankyBorders — window borders (https://github.com/FelixKratz/JankyBorders) # @@ -16,8 +16,8 @@ in services.jankyborders = { enable = true; settings = { - active_color = utils.colors.toARGB config.theme.dark.blue 1; - inactive_color = utils.colors.toARGB config.theme.dark.blue 0; + active_color = helpers.colors.toARGB config.theme.dark.blue 1; + inactive_color = helpers.colors.toARGB config.theme.dark.blue 0; width = 8.0; }; }; diff --git a/modules/home/darwin/window-manager/sketchybar.nix b/modules/home/darwin/window-manager/sketchybar.nix index 74720d0..abfeda3 100644 --- a/modules/home/darwin/window-manager/sketchybar.nix +++ b/modules/home/darwin/window-manager/sketchybar.nix @@ -1,4 +1,4 @@ -{ pkgs, config, lib, utils, ... }: +{ pkgs, config, lib, helpers, ... }: # # SketchyBar — status bar for macOS, AeroSpace-integrated (Tokyo Night) # https://felixkratz.github.io/SketchyBar/setup @@ -43,7 +43,7 @@ in "sketchybar/colors.lua".text = let c = config.theme.dark; - toARGB = utils.colors.toARGB; + toARGB = helpers.colors.toARGB; in '' #!/usr/bin/env lua diff --git a/modules/home/development/claude-code.nix b/modules/home/development/claude-code.nix index 109648a..77b7f81 100644 --- a/modules/home/development/claude-code.nix +++ b/modules/home/development/claude-code.nix @@ -42,10 +42,10 @@ in enableMcpIntegration = true; # Bump with `nix flake update `. - plugins = [ - inputs.figma-plugin - "${inputs.claude-plugins-official}/plugins/typescript-lsp" - ]; + plugins = { + figma-plugin = inputs.figma-plugin; + typescript-lsp = "${inputs.claude-plugins-official}/plugins/typescript-lsp"; + }; # User-global MCP servers (project-scoped ones live in each project's .mcp.json). mcpServers = { diff --git a/modules/home/lib.nix b/modules/home/lib.nix index 5bdfa6d..75137a6 100644 --- a/modules/home/lib.nix +++ b/modules/home/lib.nix @@ -1,4 +1,4 @@ -{ utils, ... }: +{ helpers, ... }: # # Home-manager library utilities # @@ -6,7 +6,7 @@ # These utilities are made available via the `config.lib` attribute. # # Available utilities: -# - lib.colors: Color manipulation functions (from utils.colors) +# - lib.colors: Color manipulation functions (from helpers.colors) # - toARGB: Convert hex color and alpha to ARGB format # # Usage in home-manager modules: @@ -17,6 +17,6 @@ # { config = { - lib.colors = utils.colors; + lib.colors = helpers.colors; }; } diff --git a/modules/home/linux/window-manager/waybar.nix b/modules/home/linux/window-manager/waybar.nix index 93adf3f..8359b78 100644 --- a/modules/home/linux/window-manager/waybar.nix +++ b/modules/home/linux/window-manager/waybar.nix @@ -28,13 +28,17 @@ in spacing = 6; modules-left = [ "sway/workspaces" "sway/mode" ]; modules-center = [ "sway/window" ]; - modules-right = [ "tray" "pulseaudio" "network" "battery" "clock" ]; + modules-right = [ "tray" "sway/language" "pulseaudio" "network" "battery" "clock" ]; "sway/workspaces" = { disable-scroll = true; all-outputs = true; }; + "sway/language" = { + format = "󰌌 {short}"; + }; + tray = { spacing = 10; }; clock = { @@ -99,8 +103,8 @@ in background: ${c.red}; } - #clock, #battery, #network, #pulseaudio, #tray, #mode { - padding: 0 10px; + #clock, #battery, #network, #pulseaudio, #tray, #mode, #language { + padding: 0 6px; color: ${c.fg}; } diff --git a/modules/lib/default.nix b/modules/lib/default.nix index b6dca6b..98e01fa 100644 --- a/modules/lib/default.nix +++ b/modules/lib/default.nix @@ -2,7 +2,7 @@ # # Custom utility functions # -# Global utilities available across the entire configuration via the `utils` special arg. +# Global utilities available across the entire configuration via the `helpers` special arg. # Imported in flake.nix and passed to all modules via specialArgs. # # These utilities are system-wide and available to both nix-darwin and home-manager modules. @@ -56,7 +56,7 @@ # A derivation that installs the macOS application # # Example: - # utils.darwin.mkDmgApp { + # helpers.darwin.mkDmgApp { # inherit stdenv fetchurl; # pname = "insync"; # version = "3.8.7.50505"; @@ -103,7 +103,7 @@ # A marker derivation with the cask name in passthru # # Example: - # utils.darwin.mkBrewCask { caskName = "karabiner-elements"; } + # helpers.darwin.mkBrewCask { caskName = "karabiner-elements"; } # # Usage in modules: # When a package created with mkBrewCask is added to environment.systemPackages, diff --git a/pkgs/affinity/default.nix b/pkgs/affinity/default.nix index e9e1f97..0ef9c45 100644 --- a/pkgs/affinity/default.nix +++ b/pkgs/affinity/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Affinity # for macOS # # Image editing and design software for macOS. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "affinity"; } +helpers.darwin.mkBrewCask { caskName = "affinity"; } diff --git a/pkgs/alt-tab-macos/default.nix b/pkgs/alt-tab-macos/default.nix index 1809c47..a49fcf7 100644 --- a/pkgs/alt-tab-macos/default.nix +++ b/pkgs/alt-tab-macos/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Alt-Tab for macOS # # Windows-style alt-tab window switcher for macOS. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "alt-tab"; } +helpers.darwin.mkBrewCask { caskName = "alt-tab"; } diff --git a/pkgs/android-studio/default.nix b/pkgs/android-studio/default.nix index ef1a1f5..0787b6e 100644 --- a/pkgs/android-studio/default.nix +++ b/pkgs/android-studio/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Android Studio for macOS # @@ -9,4 +9,4 @@ # Android Studio is darwin-only in this overlay; the SDK Manager still runs # inside the app to fetch platform-tools/emulator/system-images at first run. # -utils.darwin.mkBrewCask { caskName = "android-studio"; } +helpers.darwin.mkBrewCask { caskName = "android-studio"; } diff --git a/pkgs/better-touch-tool/default.nix b/pkgs/better-touch-tool/default.nix index 4490ebb..b1c2f94 100644 --- a/pkgs/better-touch-tool/default.nix +++ b/pkgs/better-touch-tool/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # BetterTouchTool for macOS # # Custom gesture and touchpad configuration tool for macOS. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "bettertouchtool"; } \ No newline at end of file +helpers.darwin.mkBrewCask { caskName = "bettertouchtool"; } \ No newline at end of file diff --git a/pkgs/blurred/default.nix b/pkgs/blurred/default.nix index a4ee229..a03c5f9 100644 --- a/pkgs/blurred/default.nix +++ b/pkgs/blurred/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Blurred for macOS # # Utility to dim background/inactive windows and content. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "blurred"; } +helpers.darwin.mkBrewCask { caskName = "blurred"; } diff --git a/pkgs/default.nix b/pkgs/default.nix index e954993..40222a3 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -20,56 +20,60 @@ # - obs-studio: Screen recorder/streamer (brew cask on macOS — nixpkgs is Linux-only) # - keepassxc: Password manager (brew cask on macOS — sidesteps qtmacextras # linker crash; Linux uses nixpkgs) +# - tokyonight-gtk-theme: GTK theme (nixpkgs dropped it over a dead GTK2 +# dependency the theme doesn't need — see pkgs/tokyonight-gtk-theme) # # This overlay is applied in flake.nix when creating the pkgs instance. # -{ utils }: +{ helpers }: final: prev: { keepassxc = if prev.stdenv.isDarwin - then prev.callPackage ./keepassxc { inherit utils; } + then prev.callPackage ./keepassxc { inherit helpers; } else prev.keepassxc; insync = if prev.stdenv.isDarwin - then prev.callPackage ./insync { inherit utils; } + then prev.callPackage ./insync { inherit helpers; } else prev.insync; - blurred = prev.callPackage ./blurred { inherit utils; }; + blurred = prev.callPackage ./blurred { inherit helpers; }; - docker-desktop = prev.callPackage ./docker-desktop { inherit utils; }; + docker-desktop = prev.callPackage ./docker-desktop { inherit helpers; }; - orbstack = prev.callPackage ./orbstack { inherit utils; }; + orbstack = prev.callPackage ./orbstack { inherit helpers; }; ungoogled-chromium = if prev.stdenv.isDarwin - then prev.callPackage ./ungoogled-chromium { inherit utils; } + then prev.callPackage ./ungoogled-chromium { inherit helpers; } else prev.ungoogled-chromium; - better-touch-tool = prev.callPackage ./better-touch-tool { inherit utils; }; + better-touch-tool = prev.callPackage ./better-touch-tool { inherit helpers; }; raycast = if prev.stdenv.isDarwin - then prev.callPackage ./raycast { inherit utils; } + then prev.callPackage ./raycast { inherit helpers; } else prev.raycast; android-studio = if prev.stdenv.isDarwin - then prev.callPackage ./android-studio { inherit utils; } + then prev.callPackage ./android-studio { inherit helpers; } else prev.android-studio; - emdash = prev.callPackage ./emdash { inherit utils; }; + emdash = prev.callPackage ./emdash { inherit helpers; }; zed-editor = if prev.stdenv.isDarwin - then prev.callPackage ./zed-editor { inherit utils; } + then prev.callPackage ./zed-editor { inherit helpers; } else prev.zed-editor; - microsoft-teams = prev.callPackage ./microsoft-teams { inherit utils; }; + microsoft-teams = prev.callPackage ./microsoft-teams { inherit helpers; }; - microsoft-outlook = prev.callPackage ./microsoft-outlook { inherit utils; }; + microsoft-outlook = prev.callPackage ./microsoft-outlook { inherit helpers; }; - affinity = prev.callPackage ./affinity { inherit utils; }; + affinity = prev.callPackage ./affinity { inherit helpers; }; - obs-studio = prev.callPackage ./obs-studio { inherit utils; }; + obs-studio = prev.callPackage ./obs-studio { inherit helpers; }; + + tokyonight-gtk-theme = prev.callPackage ./tokyonight-gtk-theme { }; } diff --git a/pkgs/docker-desktop/default.nix b/pkgs/docker-desktop/default.nix index ee8e4e8..9a0b5e8 100644 --- a/pkgs/docker-desktop/default.nix +++ b/pkgs/docker-desktop/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Docker Desktop for macOS # # Complete Docker environment with GUI and daemon. # Installed via Homebrew cask for easier updates. VM configuration managed by Docker Desktop. # -utils.darwin.mkBrewCask { caskName = "docker-desktop"; } +helpers.darwin.mkBrewCask { caskName = "docker-desktop"; } diff --git a/pkgs/emdash/default.nix b/pkgs/emdash/default.nix index 32d858e..fbac50d 100644 --- a/pkgs/emdash/default.nix +++ b/pkgs/emdash/default.nix @@ -1,4 +1,4 @@ -{ utils, lib, stdenv, fetchurl, appimageTools, makeWrapper }: +{ helpers, lib, stdenv, fetchurl, appimageTools, makeWrapper }: # # Emdash — multi-agent dev environment (https://emdash.sh) # @@ -15,7 +15,7 @@ # https://github.com/generalaction/emdash/releases/download/v/emdash-x86_64.AppImage # let - cfg = utils.darwin; + cfg = helpers.darwin; in if stdenv.hostPlatform.isDarwin then cfg.mkBrewCask { caskName = "emdash"; } diff --git a/pkgs/insync/default.nix b/pkgs/insync/default.nix index 1aab261..93ba8a6 100644 --- a/pkgs/insync/default.nix +++ b/pkgs/insync/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Insync for macOS # @@ -6,4 +6,4 @@ # Installed via Homebrew cask on macOS for easier updates. # Linux uses the nixpkgs version (handled by overlay in pkgs/default.nix). # -utils.darwin.mkBrewCask { caskName = "insync"; } +helpers.darwin.mkBrewCask { caskName = "insync"; } diff --git a/pkgs/karabiner-elements/default.nix b/pkgs/karabiner-elements/default.nix index 6a0b6ab..571829b 100644 --- a/pkgs/karabiner-elements/default.nix +++ b/pkgs/karabiner-elements/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # Karabiner-Elements for macOS # # Keyboard customizer that allows complex key remapping and shortcuts. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "karabiner-elements"; } +helpers.darwin.mkBrewCask { caskName = "karabiner-elements"; } diff --git a/pkgs/keepassxc/default.nix b/pkgs/keepassxc/default.nix index 693f9bc..376a856 100644 --- a/pkgs/keepassxc/default.nix +++ b/pkgs/keepassxc/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # KeePassXC — https://keepassxc.org # @@ -9,7 +9,7 @@ # /Applications/... prefix. Reference as `pkgs.keepassxc.passthru.cli`. # let - marker = utils.darwin.mkBrewCask { caskName = "keepassxc"; }; + marker = helpers.darwin.mkBrewCask { caskName = "keepassxc"; }; appMacOS = "/Applications/KeePassXC.app/Contents/MacOS"; in marker // { diff --git a/pkgs/microsoft-outlook/default.nix b/pkgs/microsoft-outlook/default.nix index 09b8d82..0da30e4 100644 --- a/pkgs/microsoft-outlook/default.nix +++ b/pkgs/microsoft-outlook/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Microsoft Outlook for macOS # @@ -6,4 +6,4 @@ # Exchange accounts. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "microsoft-outlook"; } +helpers.darwin.mkBrewCask { caskName = "microsoft-outlook"; } diff --git a/pkgs/microsoft-teams/default.nix b/pkgs/microsoft-teams/default.nix index 429df46..73d2b4e 100644 --- a/pkgs/microsoft-teams/default.nix +++ b/pkgs/microsoft-teams/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Microsoft Teams for macOS # @@ -6,4 +6,4 @@ # used for corporate/work communication. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "microsoft-teams"; } +helpers.darwin.mkBrewCask { caskName = "microsoft-teams"; } diff --git a/pkgs/obs-studio/default.nix b/pkgs/obs-studio/default.nix index 27df3ca..4f5cd4b 100644 --- a/pkgs/obs-studio/default.nix +++ b/pkgs/obs-studio/default.nix @@ -1,4 +1,4 @@ -{ utils, stdenv, obs-studio }: +{ helpers, stdenv, obs-studio }: # # OBS Studio — https://obsproject.com # @@ -7,6 +7,6 @@ # System Settings → Privacy & Security after first "Start Virtual Camera". # if stdenv.hostPlatform.isDarwin then - utils.darwin.mkBrewCask { caskName = "obs"; } + helpers.darwin.mkBrewCask { caskName = "obs"; } else obs-studio diff --git a/pkgs/orbstack/default.nix b/pkgs/orbstack/default.nix index 6c73424..9dde1f8 100644 --- a/pkgs/orbstack/default.nix +++ b/pkgs/orbstack/default.nix @@ -1,8 +1,8 @@ -{ utils }: +{ helpers }: # # OrbStack for macOS # # Fast, lightweight Docker and Linux VM alternative to Docker Desktop. # Installed via Homebrew cask for easier updates and maintenance. # -utils.darwin.mkBrewCask { caskName = "orbstack"; } +helpers.darwin.mkBrewCask { caskName = "orbstack"; } diff --git a/pkgs/raycast/default.nix b/pkgs/raycast/default.nix index 4c7ec4a..60fb9b3 100644 --- a/pkgs/raycast/default.nix +++ b/pkgs/raycast/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Raycast for macOS # @@ -12,4 +12,4 @@ # # Raycast is darwin-only (no Linux build); the overlay leaves Linux alone. # -utils.darwin.mkBrewCask { caskName = "raycast"; } +helpers.darwin.mkBrewCask { caskName = "raycast"; } diff --git a/pkgs/tokyonight-gtk-theme/default.nix b/pkgs/tokyonight-gtk-theme/default.nix new file mode 100644 index 0000000..19dfdb1 --- /dev/null +++ b/pkgs/tokyonight-gtk-theme/default.nix @@ -0,0 +1,125 @@ +{ + lib, + stdenvNoCC, + fetchFromGitHub, + gnome-shell, + sassc, + gnome-themes-extra, + colorVariants ? [ ], + sizeVariants ? [ ], + themeVariants ? [ ], + tweakVariants ? [ ], + iconVariants ? [ ], +}: +# +# Vendored copy of nixpkgs's pkgs/by-name/to/tokyonight-gtk-theme — +# nixpkgs removed the package because its propagatedUserEnvPkgs referenced +# gtk-engine-murrine, itself removed for depending on GTK2. The theme's +# CSS-based rendering never actually needed that engine, so this drops the +# gtk-engine-murrine arg/propagatedUserEnvPkgs line and keeps everything +# else identical. Delete this and go back to +# `pkgs.tokyonight-gtk-theme.override {...}` once nixpkgs restores it. +# +let + pname = "tokyonight-gtk-theme"; + colorVariantList = [ + "dark" + "light" + ]; + sizeVariantList = [ + "compact" + "standard" + ]; + themeVariantList = [ + "default" + "green" + "grey" + "orange" + "pink" + "purple" + "red" + "teal" + "yellow" + "all" + ]; + tweakVariantList = [ + "moon" + "storm" + "black" + "float" + "outline" + "macos" + ]; + iconVariantList = [ + "Dark-Cyan" + "Dark" + "Light" + "Moon" + ]; +in +lib.checkListOfEnum "${pname}: colorVariants" colorVariantList colorVariants lib.checkListOfEnum + "${pname}: sizeVariants" + sizeVariantList + sizeVariants + lib.checkListOfEnum + "${pname}: themeVariants" + themeVariantList + themeVariants + lib.checkListOfEnum + "${pname}: tweakVariants" + tweakVariantList + tweakVariants + lib.checkListOfEnum + "${pname}: iconVariants" + iconVariantList + iconVariants + + stdenvNoCC.mkDerivation + { + inherit pname; + version = "0-unstable-2025-10-23"; + + src = fetchFromGitHub { + owner = "Fausto-Korpsvart"; + repo = "Tokyonight-GTK-Theme"; + rev = "6c340e058e84c1975a038a8e5d1e384477225dc0"; + hash = "sha256-7H2n9wTaW8Db1RejWK071ITV1j5KIuzfql0Tx9WT6zM="; + }; + + nativeBuildInputs = [ + gnome-shell + sassc + ]; + buildInputs = [ gnome-themes-extra ]; + + dontBuild = true; + + postPatch = '' + patchShebangs themes/install.sh + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out/share/themes + cd themes + ./install.sh -n Tokyonight \ + ${lib.optionalString (colorVariants != [ ]) "-c " + toString colorVariants} \ + ${lib.optionalString (sizeVariants != [ ]) "-s " + toString sizeVariants} \ + ${lib.optionalString (themeVariants != [ ]) "-t " + toString themeVariants} \ + ${lib.optionalString (tweakVariants != [ ]) "--tweaks " + toString tweakVariants} \ + -d "$out/share/themes" + cd ../icons + ${lib.optionalString (iconVariants != [ ]) '' + mkdir -p $out/share/icons + cp -a ${toString (map (v: "Tokyonight-${v}") iconVariants)} $out/share/icons/ + ''} + runHook postInstall + ''; + + meta = { + description = "GTK theme based on the Tokyo Night colour palette"; + homepage = "https://github.com/Fausto-Korpsvart/Tokyonight-GTK-Theme"; + license = lib.licenses.gpl3Plus; + platforms = lib.platforms.unix; + }; + } diff --git a/pkgs/ungoogled-chromium/default.nix b/pkgs/ungoogled-chromium/default.nix index d978f17..9fbf82d 100644 --- a/pkgs/ungoogled-chromium/default.nix +++ b/pkgs/ungoogled-chromium/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Ungoogled Chromium for macOS # @@ -6,4 +6,4 @@ # Installed via Homebrew cask on macOS for easier updates. # Linux uses the nixpkgs version (handled by overlay in pkgs/default.nix). # -utils.darwin.mkBrewCask { caskName = "ungoogled-chromium"; } +helpers.darwin.mkBrewCask { caskName = "ungoogled-chromium"; } diff --git a/pkgs/zed-editor/default.nix b/pkgs/zed-editor/default.nix index 47eb2bf..0170ad8 100644 --- a/pkgs/zed-editor/default.nix +++ b/pkgs/zed-editor/default.nix @@ -1,4 +1,4 @@ -{ utils }: +{ helpers }: # # Zed editor for macOS # @@ -13,4 +13,4 @@ # nixpkgs source : https://github.com/NixOS/nixpkgs/tree/master/pkgs/by-name/ze/zed-editor # upstream cache tracking issue: https://github.com/zed-industries/zed/issues/26277 # -utils.darwin.mkBrewCask { caskName = "zed"; } +helpers.darwin.mkBrewCask { caskName = "zed"; } From e6a93b3c8f364b1ab70c76893c872f800b4b9358 Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 3 Aug 2026 18:38:32 +0200 Subject: [PATCH 15/19] default workspace layout for librewolf/ghostty/moonlight, always-visible waybar workspaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LibreWolf, Ghostty, and Moonlight are persistently assigned to workspaces 1, 2, and 3 respectively, and for_window focus rules make opening any of the three (from fuzzel or a keybinding) jump straight to its workspace. None of them autostart at login — they're placed correctly whenever you launch them yourself. Moonlight is additionally launched lazily via mod+3, which checks the Sway window tree (its wrapped process name doesn't match a simple pgrep) so repeated presses don't spawn duplicate instances. waybar now shows workspaces 1-5 at all times via persistent-workspaces, with a new dim .empty style so empty workspaces read distinctly from ones with an unfocused app open. --- modules/home/linux/window-manager/sway.nix | 18 +++++++++++++++--- modules/home/linux/window-manager/waybar.nix | 11 +++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/modules/home/linux/window-manager/sway.nix b/modules/home/linux/window-manager/sway.nix index 1f30985..8705147 100644 --- a/modules/home/linux/window-manager/sway.nix +++ b/modules/home/linux/window-manager/sway.nix @@ -115,7 +115,9 @@ in "${mod}+1" = "workspace number 1"; "${mod}+2" = "workspace number 2"; - "${mod}+3" = "workspace number 3"; + # `pgrep -x moonlight` doesn't work here: the persistent process's comm + # is `.moonlight-wrap` (a truncated Nix wrapper name) + "${mod}+3" = ''exec "swaymsg -t get_tree | grep -q com.moonlight_stream.Moonlight || moonlight"; workspace number 3''; "${mod}+4" = "workspace number 4"; "${mod}+5" = "workspace number 5"; @@ -139,12 +141,22 @@ in "Shift+Print" = "exec screenshot-full"; }; - # Polkit auth agent — the system-level `security.polkit.enable` - # counterpart lives in the host default.nix. startup = [ { command = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"; } ]; + assigns = { + "1" = [ { app_id = "librewolf"; } ]; + "2" = [ { app_id = "com.mitchellh.ghostty"; } ]; + "3" = [ { app_id = "com.moonlight_stream.Moonlight"; } ]; + }; + + window.commands = [ + { command = "focus"; criteria = { app_id = "librewolf"; }; } + { command = "focus"; criteria = { app_id = "com.mitchellh.ghostty"; }; } + { command = "focus"; criteria = { app_id = "com.moonlight_stream.Moonlight"; }; } + ]; + colors = let c = config.theme.dark; in { focused = { border = c.blue; background = c.bg; text = c.fg; indicator = c.blue; childBorder = c.blue; }; focusedInactive = { border = c.bg_dark; background = c.bg; text = c.fg_dark; indicator = c.bg_dark; childBorder = c.bg_dark; }; diff --git a/modules/home/linux/window-manager/waybar.nix b/modules/home/linux/window-manager/waybar.nix index 8359b78..02565f9 100644 --- a/modules/home/linux/window-manager/waybar.nix +++ b/modules/home/linux/window-manager/waybar.nix @@ -33,6 +33,13 @@ in "sway/workspaces" = { disable-scroll = true; all-outputs = true; + persistent-workspaces = { + "1" = [ ]; + "2" = [ ]; + "3" = [ ]; + "4" = [ ]; + "5" = [ ]; + }; }; "sway/language" = { @@ -94,6 +101,10 @@ in background: alpha(${c.blue}, 0.2); border-radius: 6px; } + #workspaces button.empty { + color: alpha(${c.fg}, 0.3); + background: ${c.bg_dark}; + } #workspaces button.focused { color: ${c.black}; background: ${c.purple}; From ba655d50a8410acca5e5c2386769775d89221edb Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 3 Aug 2026 19:06:31 +0200 Subject: [PATCH 16/19] wire flake.nix to use the extracted darwin-hosts helpers Missed staging this alongside the previous commit: flake.nix now calls darwinHosts.mkHomeConfig/mkHomebrewConfig/mkDarwinPkgs instead of the inline definitions that used to live here. --- modules/lib/darwin-hosts.nix | 159 +++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 modules/lib/darwin-hosts.nix diff --git a/modules/lib/darwin-hosts.nix b/modules/lib/darwin-hosts.nix new file mode 100644 index 0000000..190a4f6 --- /dev/null +++ b/modules/lib/darwin-hosts.nix @@ -0,0 +1,159 @@ +{ lib }: +# +# Darwin host-wiring helpers +# +# Functions that assemble nix-darwin module lists and pkgs instantiations for +# a host's `darwinConfigurations.` block in flake.nix. Unlike +# modules/lib/default.nix (whose `colors`/`darwin` helpers are threaded into +# module bodies via the `helpers` specialArg), these functions are consumed +# ONLY by flake.nix itself, before any specialArgs wiring happens. +# +# Because they build module lists and pkgs sets rather than being pure config +# utilities, every value they'd otherwise close over (home-manager, +# nix-homebrew, nixpkgs, inputs, rootDir, helpers) is passed in explicitly by +# the caller in flake.nix — including `helpers` itself, since this file +# cannot reference the `helpers` binding it is a sibling of. +# +{ + # Build the home-manager module list for a darwin host + # + # Integrates home-manager with the system configuration and imports + # user-specific settings from hosts/${hostname}/home.nix. + # + # Arguments: + # home-manager - The home-manager flake input (darwinModules.home-manager) + # inputs - The flake's full `inputs` attrset, threaded into extraSpecialArgs + # rootDir - The flake's `self`, threaded into extraSpecialArgs and used + # to build an absolute path to hosts/${hostname}/home.nix + # (a relative `./hosts/...` path here would resolve against + # this file's own directory, not the flake root) + # helpers - The `helpers` value from modules/lib/default.nix, threaded + # into extraSpecialArgs + # username - The user account this home-manager config applies to + # hostname - Host directory name under hosts/, used to locate home.nix + # home - Absolute path to the user's home directory + # system - Nix system string (e.g. "aarch64-darwin"), threaded as hostSystem + # private - Per-host private values (default null) + # + # Returns: + # A list of modules to append to a darwinSystem's `modules` + # + # Example: + # darwinHosts.mkHomeConfig { + # inherit home-manager inputs rootDir helpers; + # username = "pavla"; hostname = "makima"; system = "aarch64-darwin"; + # home = "/Users/pavla"; + # } + # + mkHomeConfig = { home-manager, inputs, rootDir, helpers, username, hostname, home, system, private ? null }: [ + home-manager.darwinModules.home-manager + { + # Set the user's home directory path + users.users.${username}.home = lib.mkDefault home; + + # Use the system's nixpkgs instance for home-manager + home-manager.useGlobalPkgs = true; + # Install user packages to /etc/profiles instead of ~/.nix-profile. + # `private` is always present (null on hosts without one) so modules + # can pattern-match on it without triggering _module.args recursion. + # + # `hostSystem` is the Nix system string ("aarch64-darwin", + # "x86_64-linux", ...). Threaded via specialArgs so modules/home + # can decide platform sub-bundle imports (see modules/home/default.nix) + # without depending on `pkgs.stdenv.hostPlatform.*` at import-list + # eval time — that route hits `_module.args`->`config` recursion + # because pkgs isn't externally provided to HM's inner modules. + home-manager.extraSpecialArgs = { + inherit inputs private helpers; + hostSystem = system; + rootDir = rootDir; + }; + + home-manager.useUserPackages = true; + + # When a file home-manager wants to manage already exists (e.g. + # KeePassXC writes its own keepassxc.ini before we declare it), + # move the existing file to `.backup` instead of aborting. + home-manager.backupFileExtension = "backup"; + + home-manager.users.${username} = { + home.username = username; + + imports = [ + (rootDir + "/hosts/${hostname}/home.nix") + ]; + }; + } + ]; + + # Build the nix-homebrew module list for a darwin host + # + # Creates a module list that integrates nix-homebrew with the system + # configuration. nix-homebrew manages Homebrew installation itself, while + # nix-darwin's homebrew module manages packages declaratively. + # + # Arguments: + # nix-homebrew - The nix-homebrew flake input (darwinModules.nix-homebrew) + # username - The user who owns the Homebrew installation + # taps - Optional attrset of Homebrew taps to manage declaratively + # autoMigrate - Whether to automatically migrate an existing Homebrew + # installation + # + # Returns: + # A list of modules to append to a darwinSystem's `modules` + # + # Example: + # darwinHosts.mkHomebrewConfig { inherit nix-homebrew; username = "pavla"; } + # + mkHomebrewConfig = { nix-homebrew, username, taps ? { }, autoMigrate ? true }: [ + nix-homebrew.darwinModules.nix-homebrew + { + # Set the primary user for nix-darwin + system.primaryUser = username; + + nix-homebrew = { + enable = true; + enableRosetta = true; + user = username; + taps = taps; + mutableTaps = true; + autoMigrate = autoMigrate; + }; + } + ]; + + # Build the pkgs instantiation for a darwin host + # + # Wraps `import nixpkgs { ... }` with the overlays every darwin host needs: + # a fish `doCheck = false` override (fish's test suite doesn't pass in our + # darwin build environment) and our own ./pkgs overlay. + # + # Arguments: + # nixpkgs - The nixpkgs flake input + # system - Nix system string (e.g. "aarch64-darwin") + # helpers - The `helpers` value from modules/lib/default.nix, passed + # through to the ./pkgs overlay + # rootDir - The flake's `self`, used to build an absolute path to + # ./pkgs (a relative path here would resolve against this + # file's own directory, not the flake root) + # allowUnfree - Whether to allow unfree packages (default true) + # overlays - Extra host-specific overlays, applied after the shared ones + # + # Returns: + # An instantiated nixpkgs set + # + # Example: + # darwinHosts.mkDarwinPkgs { inherit nixpkgs system helpers rootDir; } + # + mkDarwinPkgs = { nixpkgs, system, helpers, rootDir, allowUnfree ? true, overlays ? [ ] }: + import nixpkgs { + inherit system; + config = { inherit allowUnfree; }; + overlays = [ + (final: prev: { + fish = prev.fish.overrideAttrs (old: { doCheck = false; }); + }) + (import (rootDir + "/pkgs") { inherit helpers; }) + ] ++ overlays; + }; +} From af2af582e054831040648a94790b65789829f43a Mon Sep 17 00:00:00 2001 From: brutcha Date: Mon, 3 Aug 2026 19:06:57 +0200 Subject: [PATCH 17/19] wire flake.nix to use the extracted darwin-hosts helpers Missed staging this alongside the previous commit: flake.nix now calls darwinHosts.mkHomeConfig/mkHomebrewConfig/mkDarwinPkgs instead of the inline definitions that used to live here. --- flake.nix | 119 +++++++----------------------------------------------- 1 file changed, 14 insertions(+), 105 deletions(-) diff --git a/flake.nix b/flake.nix index 1af0360..15fce52 100644 --- a/flake.nix +++ b/flake.nix @@ -91,6 +91,10 @@ # `utils` — NixOS's module framework injects its own internal `utils` arg. helpers = import ./modules/lib/default.nix { lib = nixpkgs.lib; }; + # Darwin host-wiring helpers (module-list and pkgs builders used by + # darwinConfigurations.* below) — see modules/lib/darwin-hosts.nix + darwinHosts = import ./modules/lib/darwin-hosts.nix { lib = nixpkgs.lib; }; + # Base configuration shared across all systems # Enables flakes and sets up fundamental packages configuration = { pkgs, ... }: { @@ -136,81 +140,6 @@ # into the project's own .gitignore. See dev-shells/default.nix for # the full mechanism, inheritance rules, and future privacy options. - # Helper function to create home-manager configuration for a user (darwin) - # Creates a module list that integrates home-manager with the system configuration - # and imports user-specific settings from hosts/${hostname}/home.nix - mkHomeConfig = { username, hostname, home, system, private ? null }: [ - home-manager.darwinModules.home-manager - { - # Set the user's home directory path - users.users.${username}.home = nixpkgs.lib.mkDefault home; - - # Use the system's nixpkgs instance for home-manager - home-manager.useGlobalPkgs = true; - # Install user packages to /etc/profiles instead of ~/.nix-profile. - # `private` is always present (null on hosts without one) so modules - # can pattern-match on it without triggering _module.args recursion. - # - # `hostSystem` is the Nix system string ("aarch64-darwin", - # "x86_64-linux", ...). Threaded via specialArgs so modules/home - # can decide platform sub-bundle imports (see modules/home/default.nix) - # without depending on `pkgs.stdenv.hostPlatform.*` at import-list - # eval time — that route hits `_module.args`->`config` recursion - # because pkgs isn't externally provided to HM's inner modules. - home-manager.extraSpecialArgs = { - inherit inputs private; - hostSystem = system; - rootDir = self; - helpers = helpers; - }; - - home-manager.useUserPackages = true; - - # When a file home-manager wants to manage already exists (e.g. - # KeePassXC writes its own keepassxc.ini before we declare it), - # move the existing file to `.backup` instead of aborting. - home-manager.backupFileExtension = "backup"; - - home-manager.users.${username} = { - home.username = username; - - imports = [ - ./hosts/${hostname}/home.nix - ]; - }; - } - ]; - - # Helper function to create nix-homebrew configuration for a user - # Creates a module list that integrates nix-homebrew with the system configuration - # nix-homebrew manages Homebrew installation itself, while nix-darwin's homebrew - # module manages packages declaratively. - # - # Parameters: - # - username: The user who owns the Homebrew installation - # - taps: Optional attribute set of Homebrew taps to manage declaratively - # - autoMigrate: Whether to automatically migrate existing Homebrew installations - # - # Homebrew integration approach: - # - Uses nix-homebrew to manage Homebrew installation itself - # - Uses nix-darwin's homebrew.* options to manage packages declaratively - # - Works with existing Homebrew installations via autoMigrate - mkHomebrewConfig = { username, taps ? { }, autoMigrate ? true }: [ - nix-homebrew.darwinModules.nix-homebrew - { - # Set the primary user for nix-darwin - system.primaryUser = username; - - nix-homebrew = { - enable = true; - enableRosetta = true; - user = username; - taps = taps; - mutableTaps = true; - autoMigrate = autoMigrate; - }; - } - ]; in { # Re-export disko's CLI at the flake's own package output so the @@ -230,16 +159,7 @@ # (nix's pathExists under sudo is unreliable; let `import` fail with a # clearer file-not-found message if the file is missing) private = import "/Users/${username}/.config/dotfiles/private.nix"; - pkgs = import nixpkgs { - inherit system; - config = { allowUnfree = true; }; - overlays = [ - (final: prev: { - fish = prev.fish.overrideAttrs (old: { doCheck = false; }); - }) - (import ./pkgs { inherit helpers; }) - ]; - }; + pkgs = darwinHosts.mkDarwinPkgs { inherit nixpkgs system helpers rootDir; }; rootDir = self; in nix-darwin.lib.darwinSystem { @@ -249,11 +169,12 @@ modules = [ configuration ./hosts/${hostname}/default.nix - ] ++ mkHomeConfig { + ] ++ darwinHosts.mkHomeConfig { + inherit home-manager inputs rootDir helpers; inherit username hostname private system; home = "/Users/${username}"; - } ++ mkHomebrewConfig { - inherit username; + } ++ darwinHosts.mkHomebrewConfig { + inherit nix-homebrew username; autoMigrate = true; }; }; @@ -271,20 +192,7 @@ # (nix's pathExists under sudo is unreliable; let `import` fail with a # clearer file-not-found message if the file is missing) private = import "/Users/${username}/.config/dotfiles/private.nix"; - pkgs = import nixpkgs { - inherit system; - config = { - allowUnfree = true; - }; - overlays = [ - (final: prev: { - fish = prev.fish.overrideAttrs (old: { - doCheck = false; - }); - }) - (import ./pkgs { inherit helpers; }) - ]; - }; + pkgs = darwinHosts.mkDarwinPkgs { inherit nixpkgs system helpers rootDir; }; rootDir = self; in nix-darwin.lib.darwinSystem { @@ -295,11 +203,12 @@ modules = [ configuration ./hosts/${hostname}/default.nix - ] ++ mkHomeConfig { + ] ++ darwinHosts.mkHomeConfig { + inherit home-manager inputs rootDir helpers; inherit username hostname private system; home = "/Users/${username}"; - } ++ mkHomebrewConfig { - inherit username; + } ++ darwinHosts.mkHomebrewConfig { + inherit nix-homebrew username; autoMigrate = true; }; }; From cd5a662757eb64d064ab78981a3194d32a037c87 Mon Sep 17 00:00:00 2001 From: brutcha Date: Tue, 4 Aug 2026 09:33:01 +0200 Subject: [PATCH 18/19] NB2123: define corpPlugins as an attrset so it merges with the base module --- hosts/NB2123/home.nix | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/hosts/NB2123/home.nix b/hosts/NB2123/home.nix index 751d0e9..1f781ce 100644 --- a/hosts/NB2123/home.nix +++ b/hosts/NB2123/home.nix @@ -24,12 +24,18 @@ let CORP_FIGMA_ACCESS_TOKEN = "corp/figma-access-token"; }; - # Corp Claude Code marketplaces + plugin picks, sourced from private.nix + # Corp Claude Code marketplaces + plugin picks, sourced from private.nix. + # `programs.claude-code.plugins` is either-attrset-or-list; the base module + # in modules/home/development/claude-code.nix defines an attrset, so this + # override must too — the two forms can't merge. corpMarketplaces = lib.mapAttrs (_: cfg: fetchGit cfg) (private.claude.marketplaces or { }); - corpPlugins = map - (p: "${corpMarketplaces.${p.marketplace}}/${p.path}") - (private.claude.plugins or [ ]); + corpPlugins = lib.listToAttrs (map + (p: { + name = p.name or (baseNameOf p.path); + value = "${corpMarketplaces.${p.marketplace}}/${p.path}"; + }) + (private.claude.plugins or [ ])); in { home.stateVersion = "25.05"; From 36b265174808f83a275221087ddf50b7817e6634 Mon Sep 17 00:00:00 2001 From: brutcha Date: Tue, 4 Aug 2026 09:44:27 +0200 Subject: [PATCH 19/19] NB2123: guard corpPlugins against duplicate derived names --- hosts/NB2123/home.nix | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/hosts/NB2123/home.nix b/hosts/NB2123/home.nix index 1f781ce..04f0a2e 100644 --- a/hosts/NB2123/home.nix +++ b/hosts/NB2123/home.nix @@ -30,12 +30,18 @@ let # override must too — the two forms can't merge. corpMarketplaces = lib.mapAttrs (_: cfg: fetchGit cfg) (private.claude.marketplaces or { }); - corpPlugins = lib.listToAttrs (map + corpPluginList = map (p: { name = p.name or (baseNameOf p.path); value = "${corpMarketplaces.${p.marketplace}}/${p.path}"; }) - (private.claude.plugins or [ ])); + (private.claude.plugins or [ ]); + corpPluginNames = map (p: p.name) corpPluginList; + corpPlugins = + if lib.length corpPluginNames != lib.length (lib.unique corpPluginNames) then + throw "corpPlugins: duplicate plugin name(s) in private.claude.plugins — set an explicit `name` on entries whose `path` basename collides" + else + lib.listToAttrs corpPluginList; in { home.stateVersion = "25.05";