My nix-darwin and NixOS config
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: REWRITE

+2541 -2700
+6 -4
.gitignore
··· 16 16 # 1. Ignore everything in the secrets directory by default 17 17 /secrets/* 18 18 19 - # 2. Whitelist specific Ragenix/Config files that MUST be in Git 20 - !/secrets/age/ 21 - !/secrets/age/*.age 22 - !/secrets/secrets.nix 19 + # 2. Whitelist sops-encrypted secret files and the sops config itself 20 + !.sops.yaml 21 + !/secrets/*.env 22 + !/secrets/*.json 23 + !/secrets/*.yaml 24 + !/secrets/*.tar.gz 23 25 !/secrets/setup.sh 24 26 25 27 # 3. Explicitly block sensitive patterns everywhere just in case
+51
.sops.yaml
··· 1 + # sops configuration — defines which age keys can decrypt each secret file. 2 + # 3 + # Key derivation: 4 + # User key : age-keygen (stored at ~/.config/age/keys.txt) 5 + # Host keys : derived from each host's SSH ed25519 host key at deploy time: 6 + # ssh-keyscan <host> | ssh-to-age 7 + # 8 + # To add a new host: 9 + # 1. nix-shell -p ssh-to-age --run 'ssh-keyscan <host-ip> | ssh-to-age' 10 + # 2. Add the result below and uncomment the server reference in creation_rules. 11 + # 3. Re-encrypt all secrets: sops updatekeys secrets/<file> 12 + # 13 + # To encrypt a new secret: 14 + # sops --encrypt secrets/mysecret.env > secrets/mysecret.env (in-place) 15 + # or: sops secrets/mysecret.env (opens $EDITOR) 16 + 17 + keys: 18 + # User key — always included so secrets can be edited from any machine. 19 + - &ewan age1xl8ptkqm03skrdadqgprnez3trrc0k9t0ex052lweewqre2zc9qq7ljm3z 20 + 21 + # Host keys — derived from /etc/ssh/ssh_host_ed25519_key on each machine. 22 + # sops-nix decrypts at activation time using the host's own key. 23 + - &macmini age10ysmz3603uupz0043mpznchtnh6jsnk5cu3eg05xalma4xjacppsgupgvj 24 + - &laptop age1s4exn5venvd2rkrvw9g6g9rua05quut62m6le8k79st0dryhcy3qq4n55k 25 + # - &server age1... # uncomment after: ssh-keyscan <server> | ssh-to-age 26 + 27 + creation_rules: 28 + # ── Secrets available on all machines ────────────────────────────────────── 29 + - path_regex: secrets/(wifi-home|ssh-passphrase|docker-config\.json|claude\.json|duckdns\.tar\.gz)$ 30 + key_groups: 31 + - age: 32 + - *ewan 33 + - *macmini 34 + - *laptop 35 + # - *server 36 + 37 + # ── Server-only secrets ───────────────────────────────────────────────────── 38 + - path_regex: secrets/(pds\.env|matrix\.env|cloudflare\.token|cf-tunnel\.json|forgejo\.env)$ 39 + key_groups: 40 + - age: 41 + - *ewan 42 + # - *server # uncomment once the server age key is known 43 + 44 + # ── Fallback: any other file under secrets/ ───────────────────────────────── 45 + - path_regex: secrets/.* 46 + key_groups: 47 + - age: 48 + - *ewan 49 + - *macmini 50 + - *laptop 51 + # - *server
+33 -21
docs/REFERENCE.md
··· 4 4 ``` 5 5 ├── flake.nix 6 6 ├── flake.lock 7 + ├── .sops.yaml # sops age key and creation rules 7 8 ├── hosts/ 8 9 │ ├── laptop/ # NixOS desktop (Dell Inspiron 3501) 9 10 │ ├── server/ # NixOS headless server 10 11 │ └── macmini/ # macOS (nix-darwin) 11 12 ├── modules/ 13 + │ ├── options.nix # ⭐ All option declarations + defaults 12 14 │ ├── common.nix # Base NixOS settings 13 15 │ ├── desktop.nix # KDE Plasma 6 + SDDM 14 16 │ ├── packages.nix # Desktop applications ··· 17 19 │ ├── users.nix # User accounts 18 20 │ ├── caddy.nix # Caddy web server 19 21 │ ├── pds.nix # Bluesky PDS service 22 + │ ├── matrix.nix # Matrix Synapse 23 + │ ├── forgejo.nix # Forgejo git forge 24 + │ ├── cloudflare-tunnel.nix # Cloudflare tunnel 20 25 │ ├── ssh-keys.nix # Public key registry 21 26 │ ├── server/ # Headless server modules 22 - │ │ ├── default.nix 23 27 │ │ ├── firewall.nix 24 28 │ │ ├── intrusion.nix # fail2ban 25 29 │ │ ├── ssh.nix ··· 33 37 │ ├── server-base.nix 34 38 │ └── server-hardened.nix 35 39 ├── home/ 36 - │ ├── home.nix 40 + │ ├── default.nix # ⭐ Home-manager entry point (all hosts) 37 41 │ ├── programs/ # git, zsh, ssh, starship, vscode, kde, ... 38 42 │ └── scripts/ # verify-tailscale-ssh, update-all, update-everything, relts 39 - ├── lib/ 40 - │ ├── default.nix # cfgLib helpers 41 - │ └── USAGE.md 42 43 ├── secrets/ 43 - │ ├── secrets.nix # age public key mappings 44 44 │ ├── setup.sh # Key management helper 45 - │ └── age/*.age # Encrypted secret files 45 + │ └── *.env / *.json / ... # sops-encrypted secret files (safe to commit) 46 46 ├── settings/ 47 - │ ├── config.nix # Entry point 48 - │ ├── config/ # ⭐ Edit here — one file per domain 49 - │ ├── darwin/ # macOS system defaults 47 + │ ├── darwin/ # macOS system.defaults (Dock, Finder, trackpad, etc.) 50 48 │ └── plasma/ # KDE Plasma declarative settings 51 49 ├── tools/ # Rust maintenance tools 52 50 │ └── src/bin/ # health-check, flake-bump, gen-diff ··· 60 58 | `nrs` | Rebuild and switch (shell alias) | 61 59 | `nrt` | Test build without switching | 62 60 | `nrb` | Build for next boot (NixOS only) | 63 - | `update` | Update flake inputs + rebuild | 61 + | `update-all` | Update flake inputs + rebuild | 64 62 | `cleanup` | Garbage collect old generations | 65 63 | `health-check` | Pre-build validation | 66 64 | `gen-diff` | Compare generations | ··· 78 76 79 77 | What | Where | 80 78 |---|---| 81 - | Username / email | `settings/config/user.nix` | 82 - | Add package (Linux) | `settings/config/packages.nix` | 83 - | Add package (macOS) | `settings/config/darwin.nix` → `packages` | 84 - | Add Homebrew cask | `settings/config/darwin.nix` → `homebrew.casks` | 85 - | Theme / fonts | `settings/config/desktop.nix` | 79 + | Username / email | `modules/options.nix` → `user.*` defaults | 80 + | Timezone / locale | `modules/options.nix` → `timeZone` / `locale` | 81 + | Add package (Linux) | `modules/options.nix` → `packages.common` or `packages.desktop` | 82 + | Add package (macOS) | `modules/options.nix` → `packages.darwin` | 83 + | Add Homebrew cask | `modules/options.nix` → `darwin.homebrew.casks` | 84 + | Theme / fonts | `modules/options.nix` → `desktop.*` | 86 85 | KDE Plasma settings | `settings/plasma/default.nix` + `home/programs/kde.nix` | 87 - | Shell aliases | `settings/config/shell.nix` | 88 - | Git settings | `settings/config/git.nix` | 89 - | VS Code | `settings/config/development.nix` | 90 - | Wallpaper | `wallpapers/wallpaper.jpg` | 91 - | Firewall ports | `settings/config/server.nix` | 86 + | macOS system defaults | `settings/darwin/default.nix` | 87 + | Toggle desktop mode | `hosts/<n>/default.nix` → `myConfig.isDesktop = true` | 88 + | Enable gaming | `hosts/laptop/default.nix` → `myConfig.gaming.enable = true` | 89 + | Enable server service | `hosts/server/default.nix` → `myConfig.services.<n>.enable = true` | 90 + | Firewall ports | `modules/options.nix` → `server.firewall.allowedTCPPorts` | 92 91 | SSH hosts | `home/programs/ssh.nix` → `internalHosts` | 93 92 | SSH public keys | `modules/ssh-keys.nix` | 93 + | Wallpaper | `wallpapers/wallpaper.jpg` | 94 94 95 95 ## Hardware (laptop) 96 96 - **Model**: Dell Inspiron 3501 ··· 116 116 ``` 117 117 118 118 macOS binary path: `/Applications/Tailscale.app/Contents/MacOS/Tailscale` 119 + 120 + ## Secrets 121 + 122 + Secrets are encrypted with [sops](https://github.com/getsops/sops) using age keys. Encrypted files live in `secrets/` and are safe to commit. The key inventory and creation rules are in `.sops.yaml`. 123 + 124 + ```bash 125 + sops secrets/pds.env # Edit a secret 126 + sops --encrypt secrets/new.env > secrets/new.env # Create a new secret 127 + sops updatekeys secrets/pds.env # Re-encrypt after adding a host key 128 + ``` 129 + 130 + See [secrets.md](secrets.md) for full documentation. 119 131 120 132 ## Emergency Recovery 121 133
+32 -35
docs/hosts-laptop.md
··· 193 193 194 194 ## Customization 195 195 196 - All laptop-specific customization should be done through `settings/config/` files, not by editing the host file directly. 196 + For values shared across all hosts, edit the defaults in `modules/options.nix`. For laptop-only behaviour, add `myConfig.*` overrides in `hosts/laptop/default.nix`. 197 197 198 198 ### Common Customizations 199 199 200 200 | What to change | Where to edit | 201 201 |---|---| 202 - | Username / email | `settings/config/user.nix` | 203 - | Desktop theme | `settings/config/desktop.nix` → `theme` / `iconTheme` | 204 - | Fonts | `settings/config/desktop.nix` → `monoFont` | 205 - | Add applications | `settings/config/packages.nix` → `desktop` list | 206 - | Gaming enable/disable | `settings/config/gaming.nix` → `enable` | 207 - | Audio backend | `settings/config/audio.nix` → `backend` | 208 - | VSCode extensions | `settings/config/development.nix` → `vscode.extensions` | 209 - | Shell aliases | `settings/config/shell.nix` → `aliases` | 210 - | Git settings | `settings/config/git.nix` | 202 + | Username / email | `modules/options.nix` → `user.*` defaults | 203 + | Desktop theme | `modules/options.nix` → `desktop.theme` / `desktop.iconTheme` | 204 + | Fonts | `modules/options.nix` → `desktop.monoFontBase` | 205 + | Add applications | `modules/options.nix` → `packages.desktop` list | 206 + | Gaming enable/disable | `hosts/laptop/default.nix` → `myConfig.gaming.enable` | 207 + | Audio backend | `modules/options.nix` → `audio.backend` | 208 + | VSCode extensions | `home/programs/vscode.nix` | 209 + | Shell aliases | `home/programs/zsh.nix` → `shellAliases` | 210 + | Git settings | `modules/options.nix` → `git.*` or `home/programs/git.nix` | 211 211 | Plasma layout | `settings/plasma/default.nix` | 212 212 | Konsole theme | `home/programs/kde.nix` | 213 213 ··· 215 215 216 216 **System-wide packages** (available to all users): 217 217 ```nix 218 - # settings/config/packages.nix 219 - desktop = [ 220 - "firefox" 221 - "vlc" 222 - "gimp" 223 - # Add your package here 224 - ]; 218 + # modules/options.nix 219 + packages.desktop = mkOption { 220 + default = [ 221 + # ... existing list ... 222 + "my-new-package" # add here 223 + ]; 224 + }; 225 225 ``` 226 226 227 - **User packages** (just for your home): 227 + **Linux-only user packages**: 228 228 ```nix 229 - # settings/config/packages.nix 230 - linux = [ 231 - "htop" 232 - "neofetch" 233 - # Add your package here 234 - ]; 229 + # modules/options.nix 230 + packages.linux = mkOption { 231 + default = [ 232 + "vlc" 233 + "my-linux-tool" # add here 234 + ]; 235 + }; 235 236 ``` 236 237 237 238 ### Disabling Gaming 238 239 239 - If you don't game, disable the gaming module: 240 + Remove or comment out the override in the laptop host file: 240 241 ```nix 241 - # settings/config/gaming.nix 242 - { 243 - enable = false; # Change to false 244 - # ... rest stays the same 245 - } 242 + # hosts/laptop/default.nix 243 + # myConfig.gaming.enable = true; ← remove this line 246 244 ``` 247 245 246 + The default in `modules/options.nix` is `false`, so removing the override disables it. 247 + 248 248 ### Changing Audio Backend 249 249 250 250 To switch from PipeWire to PulseAudio: 251 251 ```nix 252 - # settings/config/audio.nix 253 - { 254 - enable = true; 255 - backend = "pulseaudio"; # Change from "pipewire" 256 - } 252 + # modules/options.nix (changes all hosts) — or override in hosts/laptop/default.nix 253 + myConfig.audio.backend = "pulseaudio"; 257 254 ``` 258 255 259 256 ### KDE Plasma Customization
+19 -18
docs/hosts-macmini.md
··· 70 70 71 71 ``` 72 72 hosts/macmini/ 73 - └── default.nix 73 + └── default.nix # host-specific imports and myConfig.* overrides 74 74 75 75 modules/darwin/ 76 - ├── common.nix # Shared macOS nix settings (gc, flakes, zsh) 77 - ├── packages.nix # Nix-managed CLI tools 78 - ├── homebrew.nix # Homebrew formulae and casks 79 - └── system.nix # macOS system settings 76 + ├── common.nix # Shared macOS nix settings (gc, flakes, zsh) 77 + ├── packages.nix # Nix-managed CLI tools 78 + ├── homebrew.nix # Homebrew formulae and casks 79 + └── system.nix # macOS system settings + Time Machine activation 80 + 81 + modules/options.nix # ⭐ All darwin.* option values live here 80 82 81 83 settings/darwin/ 82 - └── default.nix # macOS system.defaults (Dock, Finder, login window, etc.) 83 - 84 - settings/config/darwin.nix # All darwin values — edit here 84 + └── default.nix # macOS system.defaults (Dock, Finder, login window, etc.) 85 85 ``` 86 86 87 87 ## Package Management 88 88 89 - ### What goes in Nix (`settings/config/darwin.nix` → `packages`) 89 + ### What goes in Nix (`modules/options.nix` → `packages.darwin`) 90 90 CLI tools, development tools, languages — anything with good Nix packaging. 91 91 92 - ### What goes in Homebrew (`settings/config/darwin.nix` → `homebrew`) 93 - - **Casks** — GUI apps (VLC, OrbStack, etc.) 94 - - **Brews** — Complex media codecs and libraries that work better via brew 92 + ### What goes in Homebrew (`modules/options.nix` → `darwin.homebrew`) 93 + - **`casks`** — GUI apps 94 + - **`brews`** — Complex media codecs and libraries that work better via brew 95 + - **`masApps`** — Mac App Store apps 95 96 96 - Edit `settings/config/darwin.nix` to add packages to either list. 97 + Edit `modules/options.nix` (the `darwin.homebrew` defaults) to add packages to either list. 97 98 98 99 ## System Settings 99 100 100 - Controlled via `settings/config/darwin.nix`: 101 - - `keyboard` — key mapping, Caps Lock 102 - - `startup.chime` — boot chime 103 - - `security.touchIdForSudo` — Touch ID for sudo 101 + High-level toggles are options in `modules/options.nix`: 102 + - `darwin.keyboard.*` — key mapping, Caps Lock 103 + - `darwin.startup.chime` — boot chime 104 + - `darwin.security.touchIdForSudo` — Touch ID for sudo 104 105 105 - Fine-grained defaults (Dock, Finder, trackpad, login window, etc.) live in `settings/darwin/default.nix`. Edit them directly in Nix rather than exporting from the GUI. 106 + Fine-grained defaults (Dock, Finder, trackpad, login window, etc.) live in `settings/darwin/default.nix`. Edit them directly in Nix rather than exporting from System Settings. 106 107 107 108 ## Architecture 108 109
+60 -57
docs/hosts-overview.md
··· 120 120 121 121 ``` 122 122 ┌─────────────────────────────────────────┐ 123 - │ settings/config/*.nix │ ← Global values (DRY) 124 - │ (user, packages, theme, git, etc.) │ 123 + │ modules/options.nix │ ← All option declarations + defaults 125 124 └─────────────────────────────────────────┘ 126 125 127 126 ┌─────────────────────────────────────────┐ 128 127 │ modules/*.nix │ ← Reusable components 129 - │ (common, desktop, gaming, services) │ 128 + │ (common, desktop, gaming, services) │ read via config.myConfig.* 130 129 └─────────────────────────────────────────┘ 131 130 132 131 ┌──────────────┬──────────────┬───────────┐ 133 - │ laptop/ │ server/ │ macmini/ │ ← Host-specific 134 - │ default.nix │ default.nix │default.nix│ (imports + overrides) 132 + │ laptop/ │ server/ │ macmini/ │ ← Per-host overrides 133 + │ default.nix │ default.nix │default.nix│ myConfig.isDesktop = true; etc. 135 134 └──────────────┴──────────────┴───────────┘ 136 135 ``` 137 136 ··· 156 155 | `darwin/homebrew.nix` | ❌ | ❌ | ✅ | Homebrew management | 157 156 | `darwin/system.nix` | ❌ | ❌ | ✅ | macOS system defaults | 158 157 159 - ## Settings Scope 158 + ## Option Scope 160 159 161 - How `settings/config/` values are used across hosts: 160 + Which `myConfig.*` option categories are active on each host: 162 161 163 - | Setting File | laptop | server | macmini | Notes | 162 + | Option category | laptop | server | macmini | Notes | 164 163 |---|:---:|:---:|:---:|---| 165 - | `user.nix` | ✅ | ✅ | ✅ | Used everywhere | 166 - | `system.nix` | ✅ | ✅ | Partial | NixOS-specific | 167 - | `nix.nix` | ✅ | ✅ | ✅ | Nix itself | 168 - | `packages.nix` | ✅ | Minimal | ❌ | Linux packages | 169 - | `git.nix` | ✅ | ✅ | ✅ | Via home-manager | 170 - | `shell.nix` | ✅ | ✅ | ✅ | Via home-manager | 171 - | `desktop.nix` | ✅ | ❌ | ❌ | Desktop-only | 172 - | `ssh.nix` | ✅ | ✅ | ✅ | Via home-manager | 173 - | `audio.nix` | ✅ | ❌ | ❌ | Desktop-only | 174 - | `gaming.nix` | ✅ | ❌ | ❌ | Desktop-only | 175 - | `server.nix` | ❌ | ✅ | ❌ | Server-only | 176 - | `darwin.nix` | ❌ | ❌ | ✅ | macOS-only | 177 - | `secrets.nix` | ✅ | ✅ | ✅ | All (via age) | 178 - | `development.nix` | ✅ | ❌ | ✅ | Development hosts | 164 + | `user.*` | ✅ | ✅ | ✅ | Used everywhere | 165 + | `stateVersion`, `timeZone`, `locale` | ✅ | ✅ | Partial | NixOS-specific | 166 + | `packages.common` / `.development` | ✅ | ✅ | ✅ | All hosts | 167 + | `packages.desktop` / `.linux` | ✅ | ❌ | ❌ | `isDesktop = true` hosts | 168 + | `packages.darwin` | ❌ | ❌ | ✅ | macOS only | 169 + | `desktop.*` | ✅ | ❌ | ❌ | Desktop-only | 170 + | `audio.*` | ✅ | ❌ | ❌ | Desktop-only | 171 + | `gaming.*` | ✅ | ❌ | ❌ | `gaming.enable = true` on laptop | 172 + | `server.*` | ❌ | ✅ | ❌ | Server-only | 173 + | `services.*` | ❌ | ✅ | ❌ | Toggled in `hosts/server/default.nix` | 174 + | `darwin.*` | ❌ | ❌ | ✅ | macOS-only | 175 + | `secrets.*` | ✅ | ✅ | ✅ | All hosts (via sops-nix) | 176 + | `development.vscode` | ✅ | ❌ | ✅ | Development hosts | 179 177 180 178 ## Network Architecture 181 179 ··· 221 219 222 220 Platform-specific modules are conditionally imported: 223 221 ```nix 224 - # home/home.nix 222 + # home/default.nix 225 223 imports = [ 226 224 ./programs/git.nix # All platforms 227 225 ./programs/zsh.nix # All platforms ··· 229 227 ./programs/starship.nix # All platforms 230 228 ./programs/vscode.nix # All platforms 231 229 ] ++ lib.optionals (!isDarwin) [ 232 - ./programs/kde.nix # Linux only 230 + ./programs/terminal.nix # Konsole — Linux only 231 + ] ++ lib.optionals (cfg.isDesktop && !isDarwin) [ 232 + ./programs/kde.nix # KDE Plasma — Linux desktop only 233 233 ]; 234 234 ``` 235 235 ··· 238 238 ### Scenario 1: Change Username Everywhere 239 239 240 240 ```bash 241 - # Edit once (on macmini, your primary computer) 242 - vim settings/config/user.nix 243 - # Change: username = "newname"; 241 + # Edit the default in modules/options.nix 242 + vim modules/options.nix 243 + # Change: username = mkOption { ... default = "newname"; }; 244 244 245 245 # Apply to macmini (local) 246 - darwin-rebuild switch --flake .#macmini 246 + nrs # alias for: sudo darwin-rebuild switch --flake .#macmini 247 247 248 248 # Apply to laptop (when you use it) 249 - ssh laptop sudo nixos-rebuild switch --flake .#laptop 249 + ssh laptop 'cd ~/.config/nix-config && nrs' 250 250 251 251 # Apply to server (when deployed) 252 - ssh server sudo nixos-rebuild switch --flake .#server 252 + ssh server 'cd ~/.config/nix-config && nrs' 253 253 ``` 254 254 255 255 ### Scenario 2: Add Package to macOS (Primary) 256 256 257 257 ```bash 258 - # Edit on macmini 259 - vim settings/config/darwin.nix 260 - # Add to "packages" or "homebrew.casks" 258 + # Edit modules/options.nix 259 + vim modules/options.nix 260 + # Add to packages.darwin or darwin.homebrew.casks list 261 261 262 262 # Apply immediately 263 - darwin-rebuild switch --flake .#macmini 263 + nrs 264 264 265 265 # Applies to: macmini ✅, laptop ❌, server ❌ 266 266 ``` ··· 268 268 ### Scenario 3: Add Package to Linux Hosts Only 269 269 270 270 ```bash 271 - # Edit on macmini 272 - vim settings/config/packages.nix 273 - # Add to "linux" or "desktop" list 271 + # Edit modules/options.nix 272 + vim modules/options.nix 273 + # Add to packages.linux or packages.desktop list 274 274 275 275 # Apply to laptop (when you use it) 276 - ssh laptop sudo nixos-rebuild switch --flake .#laptop 276 + ssh laptop 'cd ~/.config/nix-config && nrs' 277 277 278 278 # Applies to: laptop ✅, server ✅ (when deployed), macmini ❌ 279 279 ``` ··· 281 281 ### Scenario 4: Test Config on Secondary Before Primary 282 282 283 283 ```bash 284 - # Make a risky change on macmini 285 - vim settings/config/packages.nix 284 + # Make a risky change 285 + vim modules/options.nix 286 286 287 287 # Test on laptop first (secondary, less critical) 288 288 ssh laptop sudo nixos-rebuild test --flake .#laptop 289 289 290 290 # If it works, apply to macmini (primary) 291 - darwin-rebuild switch --flake .#macmini 291 + nrs 292 292 ``` 293 293 294 - ### Scenario 5: Change Shell Alias Everywhere 294 + ### Scenario 5: Add a Shell Alias Everywhere 295 295 296 296 ```bash 297 - # Edit once on macmini (primary) 298 - vim settings/config/shell.nix 299 - # Add alias to "aliases" 297 + # Shell aliases live in home/programs/zsh.nix 298 + vim home/programs/zsh.nix 299 + # Add to shellAliases 300 300 301 - # Apply to macmini immediately (you're using it now) 302 - darwin-rebuild switch --flake .#macmini 301 + # Apply to macmini immediately 302 + nrs 303 303 304 304 # Apply to laptop next time you use it 305 - ssh laptop sudo nixos-rebuild switch --flake .#laptop 305 + ssh laptop 'cd ~/.config/nix-config && nrs' 306 306 307 307 # Propagates via home-manager to all hosts 308 308 ``` ··· 367 367 368 368 **NixOS hosts** (laptop, server): 369 369 ```bash 370 - # Auto-runs weekly (configured in settings/config/nix.nix) 370 + # Auto-runs weekly (configured in modules/common.nix) 371 371 sudo nix-collect-garbage -d 372 372 373 373 # Manual cleanup ··· 384 384 ### Updates 385 385 386 386 **Automated** (laptop, server): 387 - - Configured in `settings/config/maintenance.nix` 387 + - Configured in `modules/common.nix` via `system.autoUpgrade` 388 388 - Daily auto-upgrades (if enabled) 389 389 - Weekly garbage collection 390 390 391 391 **Manual** (macmini): 392 - - Update when needed 393 - - No auto-upgrade configured (macOS best practice) 392 + - `nix flake update && nrs` 393 + - No auto-upgrade in nix-darwin (macOS best practice) 394 394 395 395 ### Health Checks 396 396 ··· 509 509 510 510 ### Secrets Not Available on Host 511 511 512 - Secrets are managed via ragenix. Ensure the secret is enabled in `settings/config/secrets.nix` and that the host's age key is in `secrets/secrets.nix`. Check activation logs for decryption errors. 512 + Secrets are managed via sops-nix. Check that: 513 + - The host's age key is listed in `.sops.yaml` as a recipient for that secret 514 + - The secret has been re-encrypted with `sops updatekeys secrets/<file>` after adding the key 515 + - Check activation logs: `journalctl -b | grep sops` 513 516 514 517 ## Best Practices 515 518 516 - 1. **Keep hosts/*/default.nix minimal** — just imports and overrides 517 - 2. **Use settings/config/ for shared values** — edit once, apply everywhere 519 + 1. **Keep hosts/*/default.nix minimal** — just imports and `myConfig.*` overrides 520 + 2. **Change defaults in modules/options.nix** — shared values live there, not in host files 518 521 3. **Test on one host before deploying to all** — laptop → test → others 519 522 4. **Document host-specific quirks** — in host file comments 520 523 5. **Use version control** — commit after working changes 521 - 6. **Keep secrets separate** — never commit unencrypted secrets 522 - 7. **Regular backups** — especially of ~/.config/nix-config 524 + 6. **Never commit unencrypted secrets** — always encrypt with `sops` first 525 + 7. **Re-encrypt after adding a new host** — `sops updatekeys secrets/<file>` for every affected secret 523 526 8. **Monitor all hosts** — check logs after rebuild 524 527 525 528 ## Resources
+40 -37
docs/hosts-server.md
··· 28 28 29 29 ### 1. Generate PDS secrets 30 30 31 - If you haven't already done this (check whether `secrets/age/pds.env.age` is 32 - populated with real secrets — not a placeholder): 31 + If `secrets/pds.env` doesn't exist yet or contains placeholder values: 33 32 34 33 ```bash 35 34 # Generate each secret separately — do NOT reuse values ··· 38 37 PDS_PLC_ROTATION_KEY=$(openssl ecparam --name secp256k1 --genkey --noout \ 39 38 --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32) 40 39 41 - # Edit the secret file (ragenix opens $EDITOR): 42 - nix run github:yaxitech/ragenix -- \ 43 - --rules secrets/secrets.nix \ 44 - --editor "code --wait" \ 45 - -e secrets/age/pds.env.age 40 + # Write plaintext to a temp file, then encrypt with sops 41 + cat > /tmp/pds.env << EOF 42 + PDS_JWT_SECRET=${PDS_JWT_SECRET} 43 + PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD} 44 + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${PDS_PLC_ROTATION_KEY} 45 + PDS_EMAIL_SMTP_URL=smtps://resend:<api-key>@smtp.resend.com:465/ 46 + PDS_EMAIL_FROM_ADDRESS=pds@ewancroft.uk 47 + EOF 48 + 49 + sops --encrypt /tmp/pds.env > secrets/pds.env 50 + rm /tmp/pds.env 51 + git add secrets/pds.env 46 52 ``` 47 53 48 - The file should contain (one per line): 49 - 50 - ``` 51 - PDS_JWT_SECRET=<value> 52 - PDS_ADMIN_PASSWORD=<value> 53 - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=<value> 54 - PDS_EMAIL_SMTP_URL=smtps://resend:<api-key>@smtp.resend.com:465/ 55 - PDS_EMAIL_FROM_ADDRESS=pds@ewancroft.uk 54 + To edit an existing secret: 55 + ```bash 56 + sops secrets/pds.env 56 57 ``` 57 58 58 59 ### 2. Create the Cloudflare tunnel ··· 83 84 The JSON credentials file is at `~/.cloudflared/<UUID>.json` after step 2: 84 85 85 86 ```bash 86 - cp ~/.cloudflared/<UUID>.json /tmp/cf-tunnel-pds.json 87 - 88 - nix run github:yaxitech/ragenix -- \ 89 - --rules secrets/secrets.nix \ 90 - --editor "code --wait" \ 91 - -e secrets/age/cf-tunnel-pds.json.age 92 - 93 - # Paste the JSON file contents into the editor, save and close. 94 - # Delete the plaintext copy: 95 - rm /tmp/cf-tunnel-pds.json 87 + # Encrypt directly with sops (reads .sops.yaml for recipients) 88 + sops --encrypt ~/.cloudflared/<UUID>.json > secrets/cf-tunnel.json 89 + git add secrets/cf-tunnel.json 96 90 ``` 97 91 98 92 ### 5. Add the DNS CNAME in Cloudflare ··· 129 123 nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age' 130 124 ``` 131 125 132 - Paste the result into `secrets/secrets.nix`: 126 + Paste the result into `.sops.yaml` under `keys:` and add it to the relevant `creation_rules`: 133 127 134 - ```nix 135 - systems = { 128 + ```yaml 129 + keys: 136 130 # ... 137 - server = "age1..."; # ← paste here 138 - }; 131 + - &server age1... # ← paste here 132 + 133 + creation_rules: 134 + - path_regex: secrets/(pds\.env|matrix\.env|...) 135 + key_groups: 136 + - age: 137 + - *ewan 138 + - *server # ← uncomment / add 139 139 ``` 140 140 141 - Also change `pdsKeys` from `[ users.ewan ]` to `[ users.ewan systems.server ]`. 141 + ### 3. Re-encrypt secrets for the server 142 142 143 - ### 3. Rekey secrets for the server 144 - 145 - From your macmini or laptop (you need your private age key): 143 + From your macmini or laptop (you need your personal age key `~/.config/age/keys.txt`): 146 144 147 145 ```bash 148 146 cd ~/.config/nix-config 149 - nix run github:yaxitech/ragenix -- --rules secrets/secrets.nix --rekey 150 - git add secrets/age/ secrets/secrets.nix 151 - git commit -m "secrets: add server key and rekey PDS secrets" 147 + # Re-encrypt each server secret with the new key added 148 + sops updatekeys secrets/pds.env 149 + sops updatekeys secrets/matrix.env 150 + sops updatekeys secrets/cf-tunnel.json 151 + sops updatekeys secrets/cloudflare.token 152 + sops updatekeys secrets/forgejo.env 153 + git add .sops.yaml secrets/ 154 + git commit -m "secrets: add server age key, re-encrypt server secrets" 152 155 git push 153 156 ``` 154 157
+106 -63
docs/secrets.md
··· 1 1 # Secrets Management 2 2 3 - Encrypted secrets managed with [ragenix](https://github.com/yaxitech/ragenix) (age encryption). Secrets are decrypted at runtime to `/run/agenix/<name>` and referenced via `config.age.secrets.<name>.path`. 3 + Encrypted secrets are managed with [sops-nix](https://github.com/Mic92/sops-nix) using [age](https://age-encryption.org/) as the encryption backend. Secrets are decrypted at activation time and referenced via `config.sops.secrets.<name>.path` (system-level) or `config.sops.secrets.<name>.path` inside home-manager. 4 4 5 - ## Quick Start 5 + ## How it works 6 6 7 - ```bash 8 - bash ./secrets/setup.sh 9 - ``` 7 + - Each secret file is committed to the repo **already encrypted** — it is useless without the private key. 8 + - `sops` uses the rules in `.sops.yaml` at the repo root to know which age keys can decrypt each file. 9 + - On NixOS hosts, `sops-nix` decrypts secrets at activation using the host's `/etc/ssh/ssh_host_ed25519_key` (automatically converted to an age key). No separate key file is needed on the system itself. 10 + - On macOS, your personal age key (`~/.config/age/keys.txt`) is used. 10 11 11 - **What the script does:** 12 - 1. Manages `~/.config/age/keys.txt` (your master identity — copy this to every machine) 13 - 2. Converts the machine's SSH host key to an age key and adds it to the `systems` block in `secrets/secrets.nix` 14 - 3. Additively updates `secrets.nix` without removing existing entries 15 - 4. Validates Nix syntax and re-encrypts (rekeys) secrets for the new hardware 12 + ## Key inventory 16 13 17 - ## Adding a New Secret 14 + Keys are declared in `.sops.yaml`: 18 15 19 - ### 1. Register it in `settings/config/secrets.nix` 16 + | Name | Type | Location | 17 + |---|---|---| 18 + | `ewan` | User (personal) | `~/.config/age/keys.txt` | 19 + | `macmini` | Host | `/etc/ssh/ssh_host_ed25519_key` on macmini | 20 + | `laptop` | Host | `/etc/ssh/ssh_host_ed25519_key` on laptop | 21 + | `server` | Host | `/etc/ssh/ssh_host_ed25519_key` on server *(add after first boot)* | 20 22 21 - ```nix 22 - { 23 - files = [ 24 - "ssh-passphrase" 25 - "wifi-home" 26 - "my-new-secret" # add here 27 - ]; 28 - } 29 - ``` 23 + ## Quick reference 30 24 31 - `modules/secrets.nix` automatically generates `age.secrets` entries for every file in this list. 25 + ```bash 26 + # Edit an existing secret (opens $EDITOR with decrypted content) 27 + sops secrets/pds.env 32 28 33 - ### 2. Encrypt the secret 29 + # Encrypt a new file in-place 30 + sops --encrypt secrets/new-secret.env > secrets/new-secret.env 34 31 35 - ```nix 36 - # Using ragenix (recommended) 37 - nix run github:yaxitech/ragenix -- \ 38 - --rules secrets/secrets.nix \ 39 - --editor "code --wait" \ 40 - -e secrets/age/my-new-secret.age 32 + # Re-encrypt all secrets after adding a new key to .sops.yaml 33 + sops updatekeys secrets/pds.env 41 34 ``` 42 35 43 - Or encrypt directly with rage: 36 + ## Adding a new secret 37 + 38 + ### 1. Create and encrypt the file 44 39 45 40 ```bash 46 - rage -e -r "$(cat ~/.ssh/id_ed25519.pub)" my-secret.txt > secrets/age/my-new-secret.age 41 + # Create plaintext in /tmp, never in the repo 42 + cat > /tmp/my-secret.env << 'EOF' 43 + MY_KEY=some-value 44 + EOF 45 + 46 + # Encrypt it into the secrets directory 47 + sops --encrypt /tmp/my-secret.env > secrets/my-secret.env 48 + rm /tmp/my-secret.env 47 49 ``` 48 50 49 - ### 3. Register the public key in `secrets/secrets.nix` 51 + `sops` reads `.sops.yaml` automatically and encrypts for the correct recipients based on the filename. 52 + 53 + ### 2. Declare it in the NixOS module that uses it 50 54 51 55 ```nix 52 - "age/my-new-secret.age".publicKeys = all; # or a subset 56 + # e.g. modules/my-service.nix 57 + sops.secrets."my-secret.env" = { 58 + sopsFile = ../secrets/my-secret.env; 59 + format = "binary"; # for env files / raw content 60 + owner = "my-service"; 61 + mode = "0400"; 62 + }; 53 63 ``` 54 64 55 - ### 4. Rebuild 65 + For structured files (YAML/JSON/dotenv), you can also extract individual keys: 56 66 57 - The secret is now available at `config.age.secrets.my-new-secret.path`. 67 + ```nix 68 + sops.secrets."my-service/api-key" = { 69 + sopsFile = ../secrets/my-service.yaml; 70 + # sops-nix extracts the "my-service/api-key" key automatically 71 + }; 72 + ``` 58 73 59 - ## Using Secrets in Config 74 + ### 3. Reference the decrypted path 60 75 61 76 ```nix 62 - # Service password file 63 - services.someService.passwordFile = config.age.secrets.my-secret.path; 64 - 65 - # Environment variable 66 - systemd.services.myservice.environment.TOKEN_FILE = 67 - config.age.secrets.api-token.path; 77 + # In a systemd service 78 + systemd.services.my-service.serviceConfig.EnvironmentFile = 79 + config.sops.secrets."my-secret.env".path; 68 80 69 81 # In a script 70 82 script = '' 71 - TOKEN=$(cat ${config.age.secrets.api-token.path}) 83 + TOKEN=$(cat ${config.sops.secrets."my-service/api-key".path}) 72 84 ''; 73 85 ``` 74 86 75 - ## Rekeying (after adding a new machine) 87 + ### 4. Home-manager secrets 76 88 77 - ```bash 78 - nix run github:yaxitech/ragenix -- --rules secrets/secrets.nix --rekey 89 + Home-manager secrets use the same `sops-nix` module (via `sops-nix.homeManagerModules.sops`): 90 + 91 + ```nix 92 + # home/default.nix 93 + sops.secrets."claude-config" = { 94 + sopsFile = ../secrets/claude.json; 95 + path = "${config.home.homeDirectory}/.claude.json"; 96 + mode = "0600"; 97 + }; 79 98 ``` 80 99 81 - ## File Structure 100 + The `path` field places the decrypted file at a specific location rather than `/run/user/<uid>/secrets/`. 101 + 102 + ## Adding a new host 82 103 83 - ``` 84 - secrets/ 85 - ├── secrets.nix # Public key mappings — safe to commit 86 - ├── setup.sh # Key management automation 87 - └── age/ 88 - ├── *.age # Encrypted secrets — safe to commit 89 - └── ... 104 + When a new machine is provisioned, its SSH host key must be added to `.sops.yaml` so it can decrypt the secrets it needs. 90 105 91 - ~/.config/age/keys.txt # ⚠️ Private master key — NEVER commit 106 + ```bash 107 + # 1. Get the host's age public key from its SSH host key 108 + ssh-keyscan <host-ip> | ssh-to-age 109 + 110 + # 2. Add the result to .sops.yaml under `keys:` 111 + # - &server age1... 112 + 113 + # 3. Reference it in the relevant creation_rules 114 + 115 + # 4. Re-encrypt every secret the host needs 116 + sops updatekeys secrets/pds.env 117 + sops updatekeys secrets/cf-tunnel.json 118 + # ... etc 92 119 ``` 93 120 94 - ## Security Rules 121 + ## Existing secrets 95 122 96 - 1. `~/.config/age/keys.txt` is your master private key — treat it like your SSH private key 97 - 2. Sync `keys.txt` to other machines via `scp` over Tailscale (never via git) 98 - 3. `.age` files are safe to commit — they are useless without the private key 99 - 4. **UI preferences are NOT secrets** — they live in `settings/gnome/` and `settings/darwin/` 123 + | File | Purpose | Accessible by | 124 + |---|---|---| 125 + | `secrets/wifi-home` | Home WiFi passphrase | all hosts | 126 + | `secrets/ssh-passphrase` | SSH private key passphrase | all hosts | 127 + | `secrets/docker-config.json` | Docker Hub credentials | all hosts | 128 + | `secrets/claude.json` | Claude API / config | all hosts | 129 + | `secrets/duckdns.tar.gz` | DuckDNS config bundle | all hosts | 130 + | `secrets/pds.env` | Bluesky PDS runtime secrets | ewan + server | 131 + | `secrets/matrix.env` | Matrix Synapse secrets | ewan + server | 132 + | `secrets/forgejo.env` | Forgejo `SECRET_KEY` etc. | ewan + server | 133 + | `secrets/cloudflare.token` | Cloudflare API token | ewan + server | 134 + | `secrets/cf-tunnel.json` | Cloudflare tunnel credentials | ewan + server | 135 + 136 + ## Security rules 137 + 138 + 1. `~/.config/age/keys.txt` is your personal private key — treat it like an SSH private key. Never commit it. 139 + 2. Sync it to other machines via `scp` over Tailscale: `scp ~/.config/age/keys.txt ewan@laptop:~/.config/age/keys.txt` 140 + 3. Encrypted secret files (in `secrets/`) **are** committed to git — they are useless without a matching private key. 141 + 4. Host keys are derived from the host's SSH `ed25519` host key and are never stored anywhere beyond the key itself. 100 142 101 143 ## Troubleshooting 102 144 103 145 | Error | Cause | Fix | 104 146 |---|---|---| 105 - | "No rule for file" | `.age` file not in `secrets.nix` | Add it to `secrets/secrets.nix` | 106 - | "Decryption failed" | New system key added but not rekeyed | Run `--rekey` from a machine that has access | 107 - | Path errors | Running from wrong directory | Pass `--rules secrets/secrets.nix` explicitly | 147 + | `no matching keys` | Secret not encrypted for this key | Add key to `.sops.yaml`, run `sops updatekeys <file>` | 148 + | `key not found` | Missing `~/.config/age/keys.txt` or host SSH key | Restore key or re-derive host key | 149 + | `failed to decrypt` | Wrong key or corrupted file | Verify key with `age-keygen --to-public-key` | 150 + | Secret path is empty | sops-nix activation failed | Check `journalctl -b | grep sops` |
+127 -80
docs/settings-config.md
··· 1 - # Configuration Directory — Single Source of Truth 1 + # Configuration Options Reference 2 + 3 + All configurable values are declared in `modules/options.nix` as typed NixOS module options. Defaults are set there; per-host overrides go in `hosts/<n>/default.nix`. 4 + 5 + > This file was previously the reference for the now-removed `settings/config/` directory. That directory and `settings/config.nix` have been deleted — see [settings.md](settings.md) for the current approach. 2 6 3 - Every configurable value for the entire NixOS / nix-darwin setup lives here, split into focused files. **No module or host file should contain a hardcoded value that belongs here.** 7 + ## Option map 4 8 5 - ## File Map 9 + ### User (`myConfig.user`) 6 10 7 - | File | What it controls | 11 + | Option | Default | Description | 12 + |---|---|---| 13 + | `username` | `"ewan"` | Unix username | 14 + | `fullName` | `"Ewan Croft"` | Display name for Git, etc. | 15 + | `email` | `"git@ewancroft.uk"` | Email for Git commits | 16 + 17 + ### System 18 + 19 + | Option | Default | Description | 20 + |---|---|---| 21 + | `stateVersion` | `"25.11"` | NixOS / home-manager state version | 22 + | `timeZone` | `"Europe/London"` | System timezone | 23 + | `locale` | `"en_GB.UTF-8"` | Default locale | 24 + | `isDesktop` | `false` | Whether this is an interactive desktop — set `true` in the host file | 25 + 26 + ### Audio (`myConfig.audio`) 27 + 28 + | Option | Default | 8 29 |---|---| 9 - | `user.nix` | Username, full name, email, shell | 10 - | `system.nix` | State version, timezone, locale, boot, kernel, network | 11 - | `nix.nix` | Experimental features, store optimisation, garbage collection | 12 - | `packages.nix` | Package lists — common, fonts, linux, desktop, gaming, server | 13 - | `git.nix` | Branch, editor, LFS, commit signing, aliases, global gitignore | 14 - | `shell.nix` | Aliases, git shortcuts, platform aliases, history | 15 - | `desktop.nix` | Theme, icon theme, mono fonts, display manager, KDE Plasma settings | 16 - | `ssh.nix` | Key file path, SSH agent | 17 - | `audio.nix` | Backend (pipewire / pulseaudio) | 18 - | `gaming.nix` | Enable flag, Steam, Gamemode | 19 - | `server.nix` | sshd, fail2ban, firewall | 20 - | `darwin.nix` | Homebrew brews/casks, nixpkgs packages, keyboard, startup, security | 21 - | `secrets.nix` | Age key path, secret file list | 22 - | `development.nix` | Languages, VS Code theme/fonts/extensions | 23 - | `maintenance.nix` | Auto-upgrade, backup | 24 - | `paths.nix` | Config repo path, home-manager path | 30 + | `enable` | `true` | 31 + | `backend` | `"pipewire"` | 25 32 26 - ## Usage 33 + ### Gaming (`myConfig.gaming`) 27 34 28 - ```nix 29 - let 30 - cfg = import ../../settings/config.nix; 31 - in { 32 - home.username = cfg.user.username; 33 - programs.git.userEmail = cfg.user.email; 34 - home.stateVersion = cfg.system.stateVersion; 35 - environment.systemPackages = map (p: pkgs.${p}) cfg.packages.common; 36 - programs.vscode.profiles.default.extensions = 37 - map toExt cfg.development.vscode.extensions; 38 - } 39 - ``` 35 + | Option | Default | Notes | 36 + |---|---|---| 37 + | `enable` | `false` | Set `true` in `hosts/laptop/default.nix` | 38 + | `steam.enable` | `true` | | 39 + | `steam.openFirewall` | `false` | | 40 + 41 + ### Packages (`myConfig.packages`) 42 + 43 + | Option | Description | 44 + |---|---| 45 + | `common` | CLI tools on every host | 46 + | `development` | Languages and tooling (laptop + macmini) | 47 + | `fonts` | Nerd Font names to install via home-manager | 48 + | `linux` | Linux-only GUI extras (e.g. `vlc`) | 49 + | `desktop` | Linux desktop GUI apps | 50 + | `gaming` | Gaming packages | 51 + | `server` | Server-only extras | 52 + | `darwin` | macOS-specific Nix packages | 53 + 54 + ### Desktop (`myConfig.desktop`) 55 + 56 + | Option | Default | 57 + |---|---| 58 + | `environment` | `"plasma6"` | 59 + | `displayManager` | `"sddm"` | 60 + | `uiFont` / `uiFontSize` | `"Noto Sans"` / `10` | 61 + | `monoFontBase` | `"FiraCode"` | 62 + | `monoFontFamily` | `"FiraCode Nerd Font Mono"` | 63 + | `monoFontSize` | `11` | 64 + | `theme` | `"Catppuccin-Mocha-Standard-Green-Dark"` | 65 + | `iconTheme` | `"Papirus-Dark"` | 66 + | `plasma.colorScheme` | `"CatppuccinMochaGreen"` | 67 + | `plasma.desktopTheme` | `"breeze-dark"` | 68 + | `plasma.excludePackages` | `["oxygen" "elisa"]` | 69 + 70 + ### Git (`myConfig.git`) 71 + 72 + | Option | Default | 73 + |---|---| 74 + | `defaultBranch` | `"main"` | 75 + | `editor` | `"code --wait"` | 76 + | `lfs.enable` | `true` | 77 + | `signing.enabled` | `true` | 78 + | `signing.format` | `"ssh"` | 40 79 41 - ## Quick-edit cheatsheet 80 + ### Development / VS Code (`myConfig.development.vscode`) 42 81 43 - | I want to change… | Edit | 82 + | Option | Default | 44 83 |---|---| 45 - | Username / email | `user.nix` | 46 - | Timezone / locale | `system.nix` | 47 - | Add a package (Linux) | `packages.nix` → `common` or `desktop` | 48 - | Add a package (macOS) | `darwin.nix` → `packages` | 49 - | Add a Homebrew cask | `darwin.nix` → `homebrew.casks` | 50 - | Git alias | `git.nix` → `aliases` | 51 - | Shell alias | `shell.nix` → `aliases` | 52 - | Theme / icon theme | `desktop.nix` → `theme` / `iconTheme` | 53 - | Monospace font | `desktop.nix` → `monoFont` / `monoFontConsole` | 54 - | KDE Plasma packages | `desktop.nix` → `plasma.excludePackages` | 55 - | VS Code extensions | `development.nix` → `vscode.extensions` | 56 - | VS Code font | `development.nix` → `vscode.fontFamily` | 57 - | Enable gaming | `gaming.nix` → `enable = true` | 58 - | SSH port | `server.nix` → `sshd.port` | 59 - | Firewall ports | `server.nix` → `firewall.allowedTCPPorts` | 60 - | Auto-upgrade | `maintenance.nix` → `autoUpgrade.enable` | 61 - | Add a secret | `secrets.nix` → `files` list, then create the `.age` file | 62 - | macOS Touch ID sudo | `darwin.nix` → `security.touchIdForSudo` | 63 - | macOS startup chime | `darwin.nix` → `startup.chime` | 84 + | `enable` | `true` | 85 + | `colorTheme` | `"Catppuccin Mocha"` | 86 + | `iconTheme` | `"catppuccin-vsc-icons"` | 87 + | `fontSize` | `14` | 88 + | `terminalFontSize` | `13` | 89 + | `lineHeight` | `22` | 90 + | `fontLigatures` | `true` | 64 91 65 - ## Adding a new secret 92 + ### Secrets (`myConfig.secrets`) 66 93 67 - ```bash 68 - # 1. Encrypt it 69 - rage -e -r "$(cat ~/.ssh/id_ed25519.pub)" my-secret.txt > secrets/age/my-secret.age 94 + | Option | Default | What it enables | 95 + |---|---|---| 96 + | `docker.enable` | `true` | `~/.docker/config.json` | 97 + | `claude.enable` | `true` | `~/.claude.json` | 98 + | `duckdns.enable` | `false` | `~/.duckdns/` bundle | 70 99 71 - # 2. Register in settings/config/secrets.nix 72 - # files = [ "ssh-passphrase" "wifi-home" "my-secret" ]; 100 + ### Server services (`myConfig.services`) 101 + 102 + | Option | Default | Set in | 103 + |---|---|---| 104 + | `forgejo.enable` | `false` | `hosts/server/default.nix` | 105 + | `pds.enable` | `false` | `hosts/server/default.nix` | 106 + | `matrix.enable` | `false` | `hosts/server/default.nix` | 107 + | `cloudflare.enable` | `false` | `hosts/server/default.nix` | 108 + 109 + ### Server SSH (`myConfig.server.sshd`) 73 110 74 - # 3. Rebuild — available at config.age.secrets.my-secret.path 75 - ``` 111 + | Option | Default | 112 + |---|---| 113 + | `enable` | `true` | 114 + | `permitRootLogin` | `"no"` | 115 + | `passwordAuthentication` | `false` | 116 + | `port` | `22` | 117 + | `maxAuthTries` | `3` | 118 + | `x11Forwarding` | `false` | 76 119 77 - ## Adding a new settings category 120 + ### Firewall (`myConfig.server.firewall`) 78 121 79 - ```bash 80 - # 1. Create the file 81 - cat > settings/config/monitoring.nix << 'EOF' 82 - { 83 - prometheus = { enable = false; port = 9090; }; 84 - grafana = { enable = false; port = 3000; }; 85 - } 86 - EOF 122 + | Option | Default | 123 + |---|---| 124 + | `enable` | `true` | 125 + | `allowPing` | `true` | 126 + | `allowedTCPPorts` | `[22]` | 127 + | `allowedUDPPorts` | `[]` | 87 128 88 - # 2. Register in default.nix 89 - # monitoring = import ./monitoring.nix; 129 + ### Darwin (`myConfig.darwin`) 90 130 91 - # 3. Use anywhere 92 - # cfg.monitoring.prometheus.enable 93 - ``` 131 + | Option | Default | 132 + |---|---| 133 + | `keyboard.enableKeyMapping` | `true` | 134 + | `keyboard.remapCapsLockToControl` | `false` | 135 + | `startup.chime` | `true` | 136 + | `security.touchIdForSudo` | `true` | 137 + | `homebrew.enable` | `true` | 138 + | `homebrew.taps` | `[]` | 139 + | `homebrew.brews` | *(media codec list — see options.nix)* | 140 + | `homebrew.casks` | *(GUI app list — see options.nix)* | 141 + | `homebrew.masApps` | *(Mac App Store apps — see options.nix)* | 94 142 95 - ## Further Reading 143 + ## Further reading 96 144 97 - - [settings.md](settings.md) — overview and export workflow 98 - - [settings-structure.md](settings-structure.md) — why the config is modular 99 - - [REFERENCE.md](REFERENCE.md) — quick-reference command card 145 + - [settings.md](settings.md) — how to make changes 146 + - [REFERENCE.md](REFERENCE.md) — command reference
+61 -65
docs/settings-structure.md
··· 1 - # Settings Structure 1 + # Configuration Structure 2 2 3 - ## Why split into modules? 3 + > **Note**: The `settings/config/` directory and `settings/config.nix` have been removed. This document describes the current structure. 4 4 5 - A single monolithic `config.nix` becomes hard to navigate as it grows. Splitting by domain gives each setting a clear home and keeps files small. 5 + ## How configuration is organised 6 + 7 + All options are declared once in `modules/options.nix` with typed defaults. Hosts override what they need in their own `default.nix`. There is no separate config file layer, no custom abstraction, and no manual dependency injection. 6 8 7 9 ``` 8 - settings/ 9 - ├── config.nix # 3 lines — just imports config/ 10 - └── config/ 11 - ├── default.nix # Combines all modules into one attrset 12 - ├── user.nix # Username, email, shell 13 - ├── system.nix # Timezone, locale, boot, kernel 14 - ├── nix.nix # Flakes, store optimisation, GC 15 - ├── packages.nix # Package lists per context 16 - ├── git.nix # Branch, editor, signing, aliases 17 - ├── shell.nix # Aliases, history 18 - ├── desktop.nix # Theme, fonts, KDE Plasma settings 19 - ├── ssh.nix # Key file, agent 20 - ├── audio.nix # Backend (pipewire / pulseaudio) 21 - ├── gaming.nix # Enable flag, Steam 22 - ├── server.nix # sshd, fail2ban, firewall 23 - ├── darwin.nix # Homebrew, nixpkgs packages, keyboard, security 24 - ├── secrets.nix # Age key path, secret file list 25 - ├── development.nix # Languages, VS Code 26 - ├── maintenance.nix # Auto-upgrade, backup 27 - └── paths.nix # Config repo and home-manager paths 10 + modules/options.nix # Single source of truth for all option declarations + defaults 11 + hosts/<n>/default.nix # Per-host overrides using the module system 28 12 ``` 29 13 30 - ## Usage 14 + ## Why this approach? 15 + 16 + The previous approach imported a plain Nix attrset (`settings/config.nix`) and threaded it through a custom `cfgLib` helper. This had several downsides: 31 17 32 - The API is unchanged from a flat file — everything is accessed via `cfg.<domain>.<key>`: 18 + - No type checking — typos and wrong types silently produced bad configs 19 + - No documentation — `nix-option` couldn't introspect the values 20 + - Manual wiring — every module had to receive the config as an argument 21 + - Duplication — defaults lived in `settings/config/` *and* had to be mirrored in `modules/options.nix` definitions 33 22 34 - ```nix 35 - let 36 - cfg = import ../settings/config.nix; 37 - in { 38 - home.username = cfg.user.username; 39 - programs.git.userEmail = cfg.user.email; 40 - home.packages = map (p: pkgs.${p}) cfg.packages.common; 41 - time.timeZone = cfg.system.timeZone; 42 - } 23 + Using the NixOS module system directly gives type checking, proper `mkDefault`/`mkForce` priority, and means every module gets `config.myConfig` automatically — no wiring needed. 24 + 25 + ## Directory layout 26 + 27 + ``` 28 + settings/ 29 + ├── darwin/ # macOS system.defaults — a plain NixOS module 30 + │ └── default.nix # Dock, Finder, NSGlobalDomain, trackpad, etc. 31 + └── plasma/ # KDE Plasma declarative settings (plasma-manager) 32 + └── default.nix 43 33 ``` 34 + 35 + Both remaining directories in `settings/` are standard NixOS modules imported by their respective platform modules (`modules/darwin/system.nix` and `home/programs/kde.nix`). They are **not** part of any custom abstraction — they're just modules. 44 36 45 37 ## Edit frequency guide 46 38 47 - | File | Edit frequency | 48 - |---|---| 49 - | `user.nix` | 🔴 Rare | 50 - | `system.nix` | 🔴 Rare | 51 - | `nix.nix` | 🔴 Rare | 52 - | `ssh.nix` | 🔴 Rare | 53 - | `audio.nix` | 🔴 Rare | 54 - | `paths.nix` | 🔴 Rare | 55 - | `secrets.nix` | 🔴 Per-secret | 56 - | `server.nix` | 🔴 Per-host | 57 - | `gaming.nix` | 🔴 Per-host | 58 - | `packages.nix` | 🟡 Occasional | 59 - | `git.nix` | 🟡 Occasional | 60 - | `desktop.nix` | 🟡 Occasional | 61 - | `darwin.nix` | 🟡 Occasional | 62 - | `development.nix` | 🟡 Occasional | 63 - | `maintenance.nix` | 🟡 Occasional | 64 - | `shell.nix` | 🟢 Frequent | 39 + | File | Edit frequency | When | 40 + |---|---|---| 41 + | `modules/options.nix` | 🟡 Occasional | Adding/changing global defaults | 42 + | `hosts/laptop/default.nix` | 🔴 Rare | Laptop-specific overrides | 43 + | `hosts/server/default.nix` | 🔴 Rare | Service toggles, server-specific config | 44 + | `hosts/macmini/default.nix` | 🔴 Rare | macOS-specific overrides | 45 + | `settings/darwin/default.nix` | 🟡 Occasional | macOS UI defaults (Dock, Finder, etc.) | 46 + | `settings/plasma/default.nix` | 🟡 Occasional | KDE Plasma layout and behaviour | 47 + | `home/programs/kde.nix` | 🟡 Occasional | KDE fonts, theme, Konsole | 65 48 66 - ## Adding a new category 49 + ## Adding a new option 50 + 51 + ```nix 52 + # 1. Declare it in modules/options.nix 53 + myNewThing = { 54 + enable = mkOption { 55 + type = bool; 56 + default = false; 57 + description = "Enable the new thing."; 58 + }; 59 + port = mkOption { 60 + type = int; 61 + default = 9000; 62 + }; 63 + }; 67 64 68 - ```bash 69 - # 1. Create the file 70 - cat > settings/config/monitoring.nix << 'EOF' 71 - { 72 - prometheus = { enable = false; port = 9090; }; 73 - grafana = { enable = false; port = 3000; }; 65 + # 2. Use it in a module 66 + lib.mkIf config.myConfig.myNewThing.enable { 67 + # ... 74 68 } 75 - EOF 76 69 77 - # 2. Register in default.nix 78 - # monitoring = import ./monitoring.nix; 70 + # 3. Override per-host if needed 71 + # hosts/server/default.nix 72 + myConfig.myNewThing.enable = true; 73 + ``` 74 + 75 + ## Further reading 79 76 80 - # 3. Use anywhere 81 - # cfg.monitoring.prometheus.enable 82 - ``` 77 + - [settings.md](settings.md) — practical how-to guide 78 + - [settings-config.md](settings-config.md) — full option reference
+93 -39
docs/settings.md
··· 1 - # Settings — Central Configuration 1 + # Configuration — How It Works 2 2 3 - `settings/config.nix` is the **single source of truth** for all configuration values across the NixOS and nix-darwin setup. Every module and host reads from here; nothing is hardcoded elsewhere. 3 + All configurable values are expressed as **NixOS module options** in `modules/options.nix`. Every option has a typed default. Per-host overrides are set in the host's `default.nix` using the standard NixOS module system — no separate config file, no custom abstraction layer. 4 4 5 - ## Structure 5 + ## Where settings live 6 6 7 7 ``` 8 - settings/ 9 - ├── config.nix # Entry point — imports config/ 10 - ├── config/ # All configurable values (one file per domain) 11 - │ ├── default.nix # Imports all sub-modules 12 - │ ├── user.nix 13 - │ ├── system.nix 14 - │ ├── packages.nix 15 - │ ├── desktop.nix 16 - │ ├── darwin.nix 17 - │ └── ... 18 - ├── plasma/ # KDE Plasma declarative settings 19 - │ └── default.nix 20 - └── darwin/ # macOS system defaults (Dock, Finder, trackpad, etc.) 21 - └── default.nix 8 + modules/options.nix # Declares all options with defaults 9 + hosts/<name>/default.nix # Per-host overrides (myConfig.* = ...) 22 10 ``` 23 11 24 - ## Usage 12 + ## Accessing settings in modules 25 13 14 + System-level modules (`modules/*.nix`): 26 15 ```nix 27 - let 28 - cfg = import ../settings/config.nix; 29 - in { 30 - home.username = cfg.user.username; 16 + { config, ... }: 17 + let cfg = config.myConfig; in 18 + { 19 + time.timeZone = cfg.timeZone; 20 + users.users.${cfg.user.username} = { ... }; 21 + } 22 + ``` 23 + 24 + Home-manager modules (`home/**/*.nix`): 25 + ```nix 26 + { osConfig, ... }: 27 + let cfg = osConfig.myConfig; in 28 + { 31 29 programs.git.userEmail = cfg.user.email; 32 - home.stateVersion = cfg.system.stateVersion; 33 30 } 34 31 ``` 35 32 36 - ## Benefits 33 + ## Option categories 37 34 38 - - **Single source of truth** — change one value, updates everywhere 39 - - **DRY** — no duplication across modules or hosts 40 - - **Discoverable** — clear file names, each file focused on one domain 41 - - **Safe** — impossible to have inconsistent settings across hosts 35 + | Category | Key options | 36 + |---|---| 37 + | `myConfig.user` | `username`, `fullName`, `email` | 38 + | `myConfig` | `stateVersion`, `timeZone`, `locale`, `isDesktop` | 39 + | `myConfig.audio` | `enable`, `backend` | 40 + | `myConfig.gaming` | `enable`, `steam.*` | 41 + | `myConfig.packages` | `common`, `development`, `fonts`, `linux`, `desktop`, `darwin` | 42 + | `myConfig.desktop` | `environment`, `displayManager`, fonts, theme, KDE Plasma settings | 43 + | `myConfig.ssh` | `keyFile` | 44 + | `myConfig.git` | `defaultBranch`, `editor`, `lfs`, `signing` | 45 + | `myConfig.development.vscode` | `enable`, theme, font, size settings | 46 + | `myConfig.secrets` | `docker.enable`, `claude.enable`, `duckdns.enable` | 47 + | `myConfig.services` | `forgejo.enable`, `pds.enable`, `matrix.enable`, `cloudflare.enable` | 48 + | `myConfig.server` | `sshd.*`, `fail2ban.*`, `firewall.*`, `timemachine.*`, … | 49 + | `myConfig.darwin` | `keyboard.*`, `startup.*`, `security.*`, `homebrew.*` | 50 + | `myConfig.forgejo` | `hostname`, `port`, `appName`, … | 51 + | `myConfig.pds` | `hostname`, `port`, `adminEmail`, `crawlers`, … | 52 + | `myConfig.matrix` | `hostname`, `serverName`, `port`, … | 53 + | `myConfig.cloudflare` | `tunnelId` | 42 54 43 - ## Exporting GUI Settings 55 + ## Making a change 44 56 45 - ### KDE Plasma (Linux) 57 + ### Change a value that has a suitable default 46 58 47 - KDE Plasma settings are managed declaratively via `plasma-manager`. Instead of exporting settings from the GUI, you should: 59 + Most values default to something sensible in `modules/options.nix`. You rarely need to touch anything — just rebuild. 48 60 49 - 1. Edit `settings/plasma/default.nix` for desktop layout and behavior 50 - 2. Edit `home/programs/kde.nix` for user-level Plasma configuration 61 + ### Override a value for one host 51 62 52 - Changes are applied automatically on next Home Manager rebuild. This ensures your configuration is reproducible and version-controlled. 63 + ```nix 64 + # hosts/laptop/default.nix 65 + { 66 + myConfig.isDesktop = true; 67 + myConfig.gaming.enable = true; 68 + } 69 + ``` 53 70 54 - ### macOS 71 + ### Change a default that applies to all hosts 55 72 56 - macOS system defaults are managed declaratively in `settings/darwin/default.nix`. Edit the Nix values directly rather than exporting from System Settings — this ensures the config is reproducible and version-controlled. 73 + Edit the `default = ...` in `modules/options.nix`: 57 74 58 - ## Further Reading 75 + ```nix 76 + # modules/options.nix 77 + timeZone = mkOption { 78 + type = str; 79 + default = "Europe/London"; # ← change here 80 + }; 81 + ``` 59 82 60 - - [settings-config.md](settings-config.md) — full per-file reference and quick-edit map 61 - - [settings-structure.md](settings-structure.md) — why the config is split into modules 83 + ## Quick-edit cheatsheet 84 + 85 + | I want to change… | Where | 86 + |---|---| 87 + | Username / email | `modules/options.nix` → `user.*` defaults | 88 + | Timezone / locale | `modules/options.nix` → `timeZone` / `locale` | 89 + | Add a package (Linux) | `modules/options.nix` → `packages.common` or `packages.desktop` | 90 + | Add a package (macOS) | `modules/options.nix` → `packages.darwin` | 91 + | Add a Homebrew cask | `modules/options.nix` → `darwin.homebrew.casks` | 92 + | Toggle desktop mode | `hosts/<n>/default.nix` → `myConfig.isDesktop = true` | 93 + | Enable gaming | `hosts/<n>/default.nix` → `myConfig.gaming.enable = true` | 94 + | Theme / icon theme | `modules/options.nix` → `desktop.theme` / `desktop.iconTheme` | 95 + | Monospace font | `modules/options.nix` → `desktop.monoFontBase` | 96 + | VS Code font / theme | `modules/options.nix` → `development.vscode.*` | 97 + | SSH port (server) | `modules/options.nix` → `server.sshd.port` | 98 + | Firewall ports | `modules/options.nix` → `server.firewall.allowedTCPPorts` | 99 + | Enable a server service | `hosts/server/default.nix` → `myConfig.services.<n>.enable = true` | 100 + | macOS Touch ID sudo | `modules/options.nix` → `darwin.security.touchIdForSudo` | 101 + | macOS startup chime | `modules/options.nix` → `darwin.startup.chime` | 102 + 103 + ## macOS system.defaults 104 + 105 + Fine-grained macOS defaults (Dock, Finder, trackpad, login window, etc.) live in `settings/darwin/default.nix`. This is a regular NixOS module imported by `modules/darwin/system.nix`. Edit it directly in Nix rather than exporting from System Settings — this ensures reproducibility. 106 + 107 + ## KDE Plasma settings 108 + 109 + Plasma settings are managed declaratively via `plasma-manager`. Edit `settings/plasma/default.nix` for desktop layout and behaviour, and `home/programs/kde.nix` for user-level Plasma config. Changes are applied on the next `nixos-rebuild switch`. 110 + 111 + ## Further reading 112 + 113 + - [REFERENCE.md](REFERENCE.md) — quick-reference command card 114 + - [hosts-overview.md](hosts-overview.md) — how the three hosts relate to each other 115 + - [secrets.md](secrets.md) — secrets management with sops-nix
+78 -79
flake.nix
··· 1 1 { 2 - description = "NixOS configuration"; 2 + description = "NixOS / nix-darwin configuration"; 3 3 4 4 inputs = { 5 5 nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; 6 - nixpkgs-darwin.url = "github:nixos/nixpkgs/nixpkgs-25.11-darwin"; 7 6 8 7 home-manager = { 9 8 url = "github:nix-community/home-manager/release-25.11"; ··· 12 11 13 12 nix-darwin = { 14 13 url = "github:LnL7/nix-darwin/nix-darwin-25.11"; 15 - inputs.nixpkgs.follows = "nixpkgs-darwin"; 14 + inputs.nixpkgs.follows = "nixpkgs"; 16 15 }; 17 16 18 - ragenix = { 19 - url = "github:yaxitech/ragenix"; 17 + sops-nix = { 18 + url = "github:Mic92/sops-nix"; 20 19 inputs.nixpkgs.follows = "nixpkgs"; 21 20 }; 22 21 ··· 36 35 mac-app-util.url = "github:hraban/mac-app-util"; 37 36 }; 38 37 39 - outputs = { self, nixpkgs, nixpkgs-darwin, home-manager, nix-darwin, ragenix, nix-vscode-extensions, catppuccin, mac-app-util, plasma-manager, ... }: 40 - let 41 - # generic lib from the main nixpkgs input 42 - lib = nixpkgs.lib; 43 - 44 - # Central configuration - single source of truth 45 - config = import ./settings/config.nix; 46 - userConfig = config.user; 47 - 48 - # Custom library with helpers to reduce repetition 49 - cfgLib = import ./lib { inherit lib; }; 50 - 51 - # helper that returns the value to use as the home manager user module 52 - homeUser = { pkgsFor, isDarwin, isDesktop, hostName }: import ./home/home.nix { 53 - pkgs = pkgsFor; 54 - lib = lib; 55 - isDarwin = isDarwin; 56 - isDesktop = isDesktop; 57 - hostName = hostName; 58 - }; 38 + outputs = 39 + { 40 + self, 41 + nixpkgs, 42 + home-manager, 43 + nix-darwin, 44 + sops-nix, 45 + nix-vscode-extensions, 46 + catppuccin, 47 + mac-app-util, 48 + plasma-manager, 49 + ... 50 + }: 51 + let 52 + # Shared home-manager modules used by every host. 53 + sharedHMModules = [ 54 + catppuccin.homeModules.catppuccin 55 + sops-nix.homeManagerModules.sops 56 + ]; 59 57 60 - # DRY NixOS builder: compute pkgsForSystem and pass it explicitly into homeUser 61 - mkNixOS = { system, hostFile, hostName, isDesktop ? true }: let 62 - pkgsForSystem = import nixpkgs { 63 - inherit system; 64 - config = { allowUnfree = config.packages.allowUnfree; }; 65 - overlays = [ nix-vscode-extensions.overlays.default ]; 66 - }; 67 - in nixpkgs.lib.nixosSystem { 68 - inherit system; 69 - pkgs = pkgsForSystem; 70 - specialArgs = { inherit self cfgLib; }; 71 - modules = [ 72 - hostFile 73 - ragenix.nixosModules.default 58 + # Modules common to every NixOS host. 59 + nixosModules = [ 60 + ./modules/options.nix 61 + ./modules/common.nix 62 + sops-nix.nixosModules.sops 74 63 home-manager.nixosModules.home-manager 75 64 { 65 + nixpkgs.config.allowUnfree = true; 66 + nixpkgs.overlays = [ nix-vscode-extensions.overlays.default ]; 67 + } 68 + { 76 69 home-manager.useGlobalPkgs = true; 77 70 home-manager.useUserPackages = true; 78 - home-manager.sharedModules = [ 79 - catppuccin.homeModules.catppuccin 71 + home-manager.sharedModules = sharedHMModules ++ [ 80 72 plasma-manager.homeModules.plasma-manager 81 - ragenix.homeManagerModules.default 82 73 ]; 83 - home-manager.extraSpecialArgs = { inherit cfgLib; }; 84 - home-manager.users.${userConfig.username} = homeUser { pkgsFor = pkgsForSystem; isDarwin = false; inherit isDesktop hostName; }; 85 - # Automatically handle backup collisions 74 + home-manager.users.ewan = ./home/default.nix; 86 75 home-manager.backupFileExtension = "hm-bak"; 87 76 home-manager.overwriteBackup = true; 88 77 } 89 78 ]; 90 - }; 91 79 92 - # DRY Darwin builder: compute pkgs for Darwin and pass into homeUser 93 - mkDarwin = { system, hostFile, hostName, isDesktop ? false }: let 94 - pkgsForDarwin = import nixpkgs-darwin { 95 - inherit system; 96 - config = { allowUnfree = config.packages.allowUnfree; }; 97 - overlays = [ nix-vscode-extensions.overlays.default ]; 98 - }; 99 - in nix-darwin.lib.darwinSystem { 100 - inherit system; 101 - pkgs = pkgsForDarwin; 102 - specialArgs = { inherit cfgLib; }; 103 - modules = [ 104 - hostFile 105 - ragenix.darwinModules.default 80 + # Modules common to every nix-darwin host. 81 + darwinModules = [ 82 + ./modules/options.nix 106 83 mac-app-util.darwinModules.default 107 84 home-manager.darwinModules.home-manager 108 85 { 86 + nixpkgs.config.allowUnfree = true; 87 + nixpkgs.overlays = [ nix-vscode-extensions.overlays.default ]; 88 + } 89 + { 109 90 home-manager.useGlobalPkgs = true; 110 91 home-manager.useUserPackages = true; 111 - home-manager.sharedModules = [ 112 - catppuccin.homeModules.catppuccin 92 + home-manager.sharedModules = sharedHMModules ++ [ 113 93 mac-app-util.homeManagerModules.default 114 - ragenix.homeManagerModules.default 115 94 ]; 116 - home-manager.extraSpecialArgs = { inherit cfgLib; }; 117 - home-manager.users.${userConfig.username} = homeUser { pkgsFor = pkgsForDarwin; isDarwin = true; inherit isDesktop hostName; }; 118 - # Automatically handle backup collisions 95 + home-manager.users.ewan = ./home/default.nix; 119 96 home-manager.backupFileExtension = "hm-bak"; 120 97 home-manager.overwriteBackup = true; 121 98 } 122 99 ]; 123 - }; 124 - in { 125 - nixosConfigurations = rec { 126 - default = mkNixOS { system = "x86_64-linux"; hostFile = ./hosts/laptop; hostName = "laptop"; }; 127 - laptop = default; 128 - 129 - # Server configurations for different architectures 130 - server = mkNixOS { system = "x86_64-linux"; hostFile = ./hosts/server; hostName = "server"; isDesktop = false; }; 131 - server-arm = mkNixOS { system = "aarch64-linux"; hostFile = ./hosts/server; hostName = "server"; isDesktop = false; }; 100 + 101 + forAllSystems = 102 + f: nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ] (system: f system); 103 + in 104 + { 105 + formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixfmt-rfc-style); 106 + 107 + nixosConfigurations = { 108 + laptop = nixpkgs.lib.nixosSystem { 109 + specialArgs = { inherit self; }; 110 + modules = nixosModules ++ [ ./hosts/laptop ]; 111 + }; 112 + 113 + server = nixpkgs.lib.nixosSystem { 114 + specialArgs = { inherit self; }; 115 + modules = nixosModules ++ [ 116 + ./hosts/server 117 + { nixpkgs.hostPlatform = "x86_64-linux"; } 118 + ]; 119 + }; 120 + 121 + server-arm = nixpkgs.lib.nixosSystem { 122 + specialArgs = { inherit self; }; 123 + modules = nixosModules ++ [ 124 + ./hosts/server 125 + { nixpkgs.hostPlatform = "aarch64-linux"; } 126 + ]; 127 + }; 132 128 }; 133 129 134 - darwinConfigurations = { 135 - macmini = mkDarwin { system = "aarch64-darwin"; hostFile = ./hosts/macmini; hostName = "macmini"; isDesktop = true; }; 130 + darwinConfigurations = { 131 + macmini = nix-darwin.lib.darwinSystem { 132 + specialArgs = { inherit self; }; 133 + modules = darwinModules ++ [ ./hosts/macmini ]; 134 + }; 135 + }; 136 136 }; 137 - }; 138 - } 137 + }
+166
home/default.nix
··· 1 + # Home-manager configuration — all hosts. 2 + # 3 + # Access system-level options via `osConfig.myConfig.*`. 4 + # Platform detection uses `pkgs.stdenv.isDarwin` — no flags passed as args. 5 + { 6 + config, 7 + pkgs, 8 + lib, 9 + osConfig, 10 + ... 11 + }: 12 + let 13 + cfg = osConfig.myConfig; 14 + isDarwin = pkgs.stdenv.isDarwin; 15 + 16 + # Custom scripts from home/scripts/ — available on PATH on both platforms. 17 + myScripts = pkgs.stdenv.mkDerivation { 18 + name = "my-scripts"; 19 + src = ./scripts; 20 + installPhase = '' 21 + mkdir -p $out/bin 22 + cp -r * $out/bin/ 23 + chmod +x $out/bin/* 24 + ''; 25 + }; 26 + 27 + allKeys = import ../modules/ssh-keys.nix; 28 + in 29 + { 30 + imports = 31 + [ 32 + ./programs/git.nix 33 + ./programs/zsh.nix 34 + ./programs/ssh.nix 35 + ./programs/starship.nix 36 + ./programs/fastfetch.nix 37 + ./programs/vscode.nix 38 + ] 39 + ++ lib.optionals (!isDarwin) [ 40 + ./programs/terminal.nix # Konsole profile — all non-Darwin hosts 41 + ] 42 + ++ lib.optionals (cfg.isDesktop && !isDarwin) [ 43 + ./programs/kde.nix # KDE Plasma settings — Linux desktop only 44 + ]; 45 + 46 + home = { 47 + username = cfg.user.username; 48 + homeDirectory = if isDarwin then "/Users/${cfg.user.username}" else "/home/${cfg.user.username}"; 49 + stateVersion = cfg.stateVersion; 50 + 51 + packages = 52 + [ myScripts ] 53 + ++ map (font: pkgs.nerd-fonts.${font}) cfg.packages.fonts 54 + ++ lib.optionals (!isDarwin) ( 55 + map (pkg: pkgs.${pkg}) cfg.packages.linux 56 + ); 57 + 58 + # SSH authorised keys — all machines except this one. 59 + # Filter by hostname so each host does not authorise its own key. 60 + file.".ssh/authorized_keys".text = 61 + let 62 + hostName = osConfig.networking.hostName; 63 + filteredKeys = lib.attrValues ( 64 + lib.filterAttrs (name: _: name != hostName) allKeys 65 + ); 66 + in 67 + builtins.concatStringsSep "\n" filteredKeys; 68 + 69 + file.".ssh/allowed_signers".text = 70 + let 71 + entries = lib.mapAttrsToList (_: key: "${cfg.user.email} ${key}") allKeys; 72 + validEntries = lib.filter (e: !(lib.hasInfix "REPLACE_WITH" e)) (lib.unique entries); 73 + in 74 + builtins.concatStringsSep "\n" validEntries + "\n"; 75 + 76 + file.".gitignore_global".text = builtins.concatStringsSep "\n" [ 77 + ".DS_Store" 78 + ".DS_Store?" 79 + "._*" 80 + ".Spotlight-V100" 81 + ".Trashes" 82 + "ehthumbs.db" 83 + "Thumbs.db" 84 + ".vscode/" 85 + ".idea/" 86 + "*.swp" 87 + "*.swo" 88 + "*~" 89 + "*.tmp" 90 + "*.bak" 91 + "*.log" 92 + ]; 93 + }; 94 + 95 + programs.home-manager.enable = true; 96 + 97 + fonts.fontconfig.enable = true; 98 + 99 + # ── Linux-only theming ──────────────────────────────────────────────────── 100 + gtk = lib.mkIf (!isDarwin) { 101 + enable = true; 102 + theme = { 103 + name = cfg.desktop.theme; 104 + package = pkgs.catppuccin-gtk.override { 105 + accents = [ "green" ]; 106 + variant = "mocha"; 107 + }; 108 + }; 109 + iconTheme.name = cfg.desktop.iconTheme; 110 + }; 111 + 112 + qt = lib.mkIf (!isDarwin) { 113 + enable = true; 114 + platformTheme.name = "kvantum"; 115 + style.name = "kvantum"; 116 + }; 117 + 118 + catppuccin = lib.mkIf (!isDarwin) { 119 + enable = true; 120 + flavor = "mocha"; 121 + accent = "green"; 122 + starship.enable = false; 123 + }; 124 + 125 + # ── macOS: wallpaper via desktoppr ─────────────────────────────────────── 126 + programs.desktoppr = lib.mkIf isDarwin { 127 + enable = true; 128 + settings.picture = "${../wallpapers/wallpaper.jpg}"; 129 + }; 130 + 131 + # ── Encrypted secrets (sops-nix) ───────────────────────────────────────── 132 + sops.secrets = lib.mkMerge [ 133 + (lib.mkIf cfg.secrets.docker.enable { 134 + "docker-config" = { 135 + sopsFile = ../secrets/docker-config.json; 136 + path = "${config.home.homeDirectory}/.docker/config.json"; 137 + mode = "0600"; 138 + }; 139 + }) 140 + 141 + (lib.mkIf cfg.secrets.claude.enable { 142 + "claude-config" = { 143 + sopsFile = ../secrets/claude.json; 144 + path = "${config.home.homeDirectory}/.claude.json"; 145 + mode = "0600"; 146 + }; 147 + }) 148 + 149 + (lib.mkIf cfg.secrets.duckdns.enable { 150 + "duckdns" = { 151 + sopsFile = ../secrets/duckdns.tar.gz; 152 + }; 153 + }) 154 + ]; 155 + 156 + # Extract DuckDNS tarball on activation. 157 + home.activation.setupDuckDNS = lib.mkIf cfg.secrets.duckdns.enable ( 158 + lib.hm.dag.entryAfter [ "writeBoundary" ] '' 159 + if [ -f "${config.sops.secrets.duckdns.path}" ]; then 160 + $DRY_RUN_CMD mkdir -p "${config.home.homeDirectory}/.duckdns" 161 + $DRY_RUN_CMD tar -xzf "${config.sops.secrets.duckdns.path}" \ 162 + -C "${config.home.homeDirectory}" 163 + fi 164 + '' 165 + ); 166 + }
-154
home/home.nix
··· 1 - { pkgs, lib, isDarwin, isDesktop, hostName, extraSpecialArgs ? {}, ... }: 2 - { config, cfgLib, ... }: 3 - 4 - let 5 - cfg = cfgLib.cfg; 6 - userConfig = cfg.user; 7 - homeDir = extraSpecialArgs.homeDirectory or ( 8 - if isDarwin then "/Users/${userConfig.username}" 9 - else "/home/${userConfig.username}" 10 - ); 11 - 12 - # Custom scripts from home/scripts/ — available on PATH on both platforms. 13 - myScripts = pkgs.stdenv.mkDerivation { 14 - name = "my-scripts"; 15 - src = ./scripts; 16 - installPhase = '' 17 - mkdir -p $out/bin 18 - cp -r * $out/bin/ 19 - chmod +x $out/bin/* 20 - ''; 21 - }; 22 - in 23 - { 24 - imports = [ 25 - ./programs/git.nix 26 - (import ./programs/zsh.nix { inherit hostName isDarwin; }) 27 - (import ./programs/ssh.nix { inherit isDarwin isDesktop; }) 28 - ./programs/starship.nix 29 - ./programs/fastfetch.nix 30 - ./programs/vscode.nix 31 - ] ++ lib.optionals (!isDarwin) [ 32 - ./programs/terminal.nix # Konsole profile — all non-Darwin hosts 33 - ] ++ lib.optionals (isDesktop && !isDarwin) [ 34 - ./programs/kde.nix # KDE Plasma settings — Linux desktop only 35 - ]; 36 - 37 - home = { 38 - username = userConfig.username; 39 - homeDirectory = homeDir; 40 - stateVersion = cfg.system.stateVersion; 41 - 42 - packages = 43 - # Custom scripts from home/scripts/ 44 - [ myScripts ] 45 - # Nerd Fonts (home-manager is the canonical place for fonts) 46 - ++ map (font: pkgs.nerd-fonts.${font}) cfg.packages.fonts 47 - # Linux-only packages 48 - ++ lib.optionals (!isDarwin) (map (pkg: pkgs.${pkg}) cfg.packages.linux); 49 - 50 - # SSH authorised keys — all machines except this one (so each host can SSH 51 - # to the others without a password prompt). 52 - file.".ssh/authorized_keys" = { 53 - text = 54 - let 55 - allKeys = import ../modules/ssh-keys.nix; 56 - filteredKeys = lib.attrValues (lib.filterAttrs (name: _: name != hostName) allKeys); 57 - in 58 - builtins.concatStringsSep "\n" filteredKeys; 59 - }; 60 - 61 - file.".ssh/allowed_signers".text = 62 - let 63 - allKeys = import ../modules/ssh-keys.nix; 64 - entries = lib.mapAttrsToList (_: key: "${cfg.user.email} ${key}") allKeys; 65 - validEntries = lib.filter (e: !(lib.hasInfix "REPLACE_WITH" e)) (lib.unique entries); 66 - in 67 - builtins.concatStringsSep "\n" validEntries + "\n"; 68 - 69 - file.".gitignore_global".text = builtins.concatStringsSep "\n" cfg.git.globalIgnore; 70 - }; 71 - 72 - programs.home-manager.enable = true; 73 - 74 - fonts.fontconfig.enable = true; 75 - 76 - # ─── Linux-only theming ─────────────────────────────────────────────────────── 77 - 78 - gtk = lib.mkIf (!isDarwin) { 79 - enable = true; 80 - theme = { 81 - name = cfg.desktop.theme; 82 - package = pkgs.catppuccin-gtk.override { 83 - accents = [ "green" ]; 84 - variant = "mocha"; 85 - }; 86 - }; 87 - iconTheme.name = cfg.desktop.iconTheme; 88 - }; 89 - 90 - qt = lib.mkIf (!isDarwin) { 91 - enable = true; 92 - platformTheme.name = "kvantum"; 93 - style.name = "kvantum"; 94 - }; 95 - 96 - # Catppuccin module (shared modules in flake.nix) — Linux only. 97 - # Starship keeps its own custom forest_dark theme; disable the override. 98 - catppuccin = lib.mkIf (!isDarwin) { 99 - enable = true; 100 - flavor = "mocha"; 101 - accent = "green"; 102 - starship.enable = false; 103 - }; 104 - 105 - # ─── macOS-only: wallpaper via desktoppr ────────────────────────────────────── 106 - # KDE wallpaper is set in home/programs/kde.nix via plasma-manager. 107 - programs.desktoppr = lib.mkIf isDarwin { 108 - enable = true; 109 - settings.picture = "${../wallpapers/wallpaper.jpg}"; 110 - }; 111 - 112 - # ─── Encrypted secrets ──────────────────────────────────────────────────────── 113 - # Each block is only active when the corresponding flag is set in 114 - # settings/config/secrets.nix AND the .age file exists in secrets/age/. 115 - # Flip enable = false → true only after running the migration script. 116 - 117 - age.secrets = lib.mkMerge [ 118 - 119 - (lib.mkIf cfg.secrets.docker.enable { 120 - "docker-config" = { 121 - file = ../secrets/age/docker-config.json.age; 122 - path = "${config.home.homeDirectory}/.docker/config.json"; 123 - mode = "0600"; 124 - }; 125 - }) 126 - 127 - (lib.mkIf cfg.secrets.claude.enable { 128 - "claude-config" = { 129 - file = ../secrets/age/claude.json.age; 130 - path = "${config.home.homeDirectory}/.claude.json"; 131 - mode = "0600"; 132 - }; 133 - }) 134 - 135 - (lib.mkIf cfg.secrets.duckdns.enable { 136 - "duckdns" = { 137 - file = ../secrets/age/duckdns.tar.gz.age; 138 - }; 139 - }) 140 - 141 - ]; 142 - 143 - # Extract DuckDNS tarball on activation. 144 - # Only runs when duckdns secret is enabled (server/Linux only by default). 145 - home.activation.setupDuckDNS = lib.mkIf cfg.secrets.duckdns.enable ( 146 - lib.hm.dag.entryAfter [ "writeBoundary" ] '' 147 - if [ -f "${config.age.secrets.duckdns.path}" ]; then 148 - $DRY_RUN_CMD mkdir -p "${config.home.homeDirectory}/.duckdns" 149 - $DRY_RUN_CMD tar -xzf "${config.age.secrets.duckdns.path}" \ 150 - -C "${config.home.homeDirectory}" 151 - fi 152 - '' 153 - ); 154 - }
+15 -8
home/programs/git.nix
··· 1 - { config, pkgs, lib, osConfig, cfgLib, ... }: 2 - 1 + # Git configuration. 2 + { 3 + pkgs, 4 + lib, 5 + osConfig, 6 + ... 7 + }: 3 8 let 4 - cfg = cfgLib.cfg; 9 + cfg = osConfig.myConfig; 5 10 in 6 11 { 7 12 programs.git = { ··· 10 15 11 16 settings = { 12 17 user = { 13 - name = cfg.user.fullName; 18 + name = cfg.user.fullName; 14 19 email = cfg.user.email; 15 20 }; 16 21 17 - # /etc/nixos only exists on NixOS; skip on macOS/Darwin. 18 22 safe.directory = lib.mkIf (!pkgs.stdenv.isDarwin) "/etc/nixos"; 19 23 20 24 core = { ··· 24 28 25 29 init.defaultBranch = cfg.git.defaultBranch; 26 30 27 - # SSH commit signing 28 31 gpg.format = cfg.git.signing.format; 29 32 user.signingkey = "${cfg.ssh.keyFile}.pub"; 30 33 commit.gpgsign = cfg.git.signing.enabled; 31 34 gpg.ssh.allowedSignersFile = "~/.ssh/allowed_signers"; 32 35 33 - alias = cfg.git.aliases; 36 + alias = { 37 + la = "log --all --graph --pretty=format:'%C(auto)%h%d %s %C(bold black)(%ar by <%aN>)%Creset'"; 38 + law = "log --all --graph --pretty=format:'%C(auto)%h%d %w(100,0,8)%s %C(bold black)(%ar by <%aN>)%Creset'"; 39 + lad = "log --all --graph --pretty=format:'%Cgreen%ad%Creset %C(auto)%h%d %s %C(bold black)<%aN>%Creset' --date=format-local:'%Y-%m-%d %H:%M (%a)'"; 40 + }; 34 41 }; 35 42 }; 36 - } 43 + }
+62 -61
home/programs/ssh.nix
··· 1 - { isDarwin, isDesktop ? true }: 2 - { config, pkgs, lib, cfgLib, ... }: 3 - 1 + # SSH client configuration. 2 + # Platform detection via pkgs.stdenv.isDarwin. 3 + { 4 + config, 5 + pkgs, 6 + lib, 7 + osConfig, 8 + ... 9 + }: 4 10 let 5 - cfg = cfgLib.cfg; 11 + cfg = osConfig.myConfig; 12 + isDarwin = pkgs.stdenv.isDarwin; 6 13 userName = cfg.user.username; 7 - 14 + 8 15 # Tailscale binary path differs by platform. 9 - # macOS: use the absolute path bundled inside Tailscale.app — ProxyCommand 10 - # runs with a minimal environment so Homebrew PATH isn't available. 16 + # macOS: absolute path inside Tailscale.app — ProxyCommand runs with minimal 17 + # environment so Homebrew PATH isn't available. 11 18 # Linux: Nix package provides the binary. 12 - tailscaleBin = if isDarwin 13 - then "/Applications/Tailscale.app/Contents/MacOS/Tailscale" 14 - else "${pkgs.tailscale}/bin/tailscale"; 15 - 16 - # Define our internal Tailscale hosts 17 - # These will connect dynamically through Tailscale using ProxyCommand 18 - internalHosts = [ "laptop" "server" "macmini" ]; 19 - 20 - # Create SSH host blocks for Tailscale hosts with dynamic routing 21 - tailscaleHostBlocks = lib.listToAttrs (map (hostName: { 22 - name = hostName; 23 - value = { 24 - user = userName; 25 - proxyCommand = "${tailscaleBin} nc %h %p"; 26 - extraOptions = { 27 - # Connection multiplexing over Tailscale 28 - ControlMaster = "auto"; 29 - ControlPath = "~/.ssh/sockets/tailscale-%r@%h-%p"; 30 - ControlPersist = "600"; 19 + tailscaleBin = 20 + if isDarwin then 21 + "/Applications/Tailscale.app/Contents/MacOS/Tailscale" 22 + else 23 + "${pkgs.tailscale}/bin/tailscale"; 24 + 25 + internalHosts = [ 26 + "laptop" 27 + "server" 28 + "macmini" 29 + ]; 30 + 31 + tailscaleHostBlocks = lib.listToAttrs ( 32 + map (hostName: { 33 + name = hostName; 34 + value = { 35 + user = userName; 36 + proxyCommand = "${tailscaleBin} nc %h %p"; 37 + extraOptions = { 38 + ControlMaster = "auto"; 39 + ControlPath = "~/.ssh/sockets/tailscale-%r@%h-%p"; 40 + ControlPersist = "600"; 41 + }; 31 42 }; 32 - }; 33 - }) internalHosts); 34 - 35 - # Global SSH options 36 - globalExtraOptions = { 37 - # Reuse connections for speed 38 - ControlMaster = "auto"; 39 - ControlPath = "~/.ssh/sockets/%r@%h-%p"; 40 - ControlPersist = "600"; 41 - 42 - # Automatically add keys to agent 43 - AddKeysToAgent = "yes"; 44 - }; 43 + }) internalHosts 44 + ); 45 45 in 46 46 { 47 47 programs.ssh = { 48 48 enable = true; 49 49 enableDefaultConfig = false; 50 - 51 - # Tailscale host configurations with dynamic ProxyCommand routing 50 + 52 51 matchBlocks = tailscaleHostBlocks // { 53 - # Global SSH configuration for all other hosts (git forges, etc.) 54 52 "*" = { 55 - extraOptions = globalExtraOptions; 53 + extraOptions = { 54 + ControlMaster = "auto"; 55 + ControlPath = "~/.ssh/sockets/%r@%h-%p"; 56 + ControlPersist = "600"; 57 + AddKeysToAgent = "yes"; 58 + }; 56 59 }; 57 60 }; 58 61 }; 59 - 60 - # Linux desktop: enable SSH agent and load keys into it at login. 61 - # On macOS the system keychain handles this automatically. 62 - # The server doesn't need this — we SSH into it, not out from it. 63 - services.ssh-agent = lib.mkIf (!isDarwin && isDesktop) { 62 + 63 + # Linux desktop: enable SSH agent and load keys at login. 64 + services.ssh-agent = lib.mkIf (!isDarwin && cfg.isDesktop) { 64 65 enable = true; 65 66 }; 66 67 67 - # ksshaskpass pops a KWallet GUI prompt on first login after a reboot; 68 - # subsequent logins retrieve the passphrase from KWallet silently. 69 - # SSH_AUTH_SOCK must be set explicitly — systemd user services don't 70 - # inherit the shell environment, so reference the socket path directly. 71 - systemd.user.services.ssh-load-keys = lib.mkIf (!isDarwin && isDesktop) { 68 + systemd.user.services.ssh-load-keys = lib.mkIf (!isDarwin && cfg.isDesktop) { 72 69 Unit = { 73 70 Description = "Load SSH keys into agent via KWallet"; 74 - After = [ "ssh-agent.service" "graphical-session.target" ]; 71 + After = [ 72 + "ssh-agent.service" 73 + "graphical-session.target" 74 + ]; 75 75 PartOf = [ "graphical-session.target" ]; 76 76 }; 77 77 Service = { 78 - Type = "oneshot"; 78 + Type = "oneshot"; 79 79 ExecStart = "${pkgs.openssh}/bin/ssh-add"; 80 80 Environment = [ 81 81 "SSH_AUTH_SOCK=%t/ssh-agent" ··· 87 87 Install.WantedBy = [ "graphical-session.target" ]; 88 88 }; 89 89 90 - # macOS: Load SSH keys from Keychain into the agent at login. 91 - # Replaces the old `UseKeychain yes` ssh_config option (removed in Tahoe). 92 - # Equivalent to running `ssh-add --apple-load-keychain` manually after each reboot. 90 + # macOS: load SSH keys from Keychain into the agent at login. 93 91 launchd.agents.ssh-load-keychain = lib.mkIf isDarwin { 94 92 enable = true; 95 93 config = { 96 - ProgramArguments = [ "/usr/bin/ssh-add" "--apple-load-keychain" ]; 94 + ProgramArguments = [ 95 + "/usr/bin/ssh-add" 96 + "--apple-load-keychain" 97 + ]; 97 98 RunAtLoad = true; 98 99 StandardOutPath = "/tmp/ssh-add-keychain.log"; 99 100 StandardErrorPath = "/tmp/ssh-add-keychain.log"; 100 101 }; 101 102 }; 102 - 103 - # Ensure the socket directory exists 103 + 104 + # Ensure the socket directory exists. 104 105 home.file.".ssh/sockets/.keep".text = ""; 105 106 }
+12 -11
home/programs/terminal.nix
··· 1 - # Terminal emulator profile — shared by all non-Darwin hosts (laptop + server). 2 - # Controls the font and colour scheme used by Konsole / any KDE-aware terminal. 3 - # Font and size come from settings/config/desktop.nix — never hardcoded here. 4 - { lib, cfgLib, ... }: 5 - 1 + # Konsole terminal profile — all non-Darwin hosts. 2 + { 3 + osConfig, 4 + ... 5 + }: 6 6 let 7 - d = cfgLib.cfg.desktop; 7 + cfg = osConfig.myConfig; 8 + d = cfg.desktop; 8 9 in 9 10 { 10 11 programs.konsole = { 11 - enable = true; 12 + enable = true; 12 13 defaultProfile = "Catppuccin Mocha"; 13 14 profiles."Catppuccin Mocha" = { 14 - name = "Catppuccin Mocha"; 15 - colorScheme = "Catppuccin Mocha"; # installed by the catppuccin/konsole package 15 + name = "Catppuccin Mocha"; 16 + colorScheme = "Catppuccin Mocha"; 16 17 font = { 17 - name = d.monoFontConsole; # "FiraCode Nerd Font Mono" 18 - size = d.monoFontSize; # 11 18 + name = d.monoFontFamily; 19 + size = d.monoFontSize; 19 20 }; 20 21 }; 21 22 };
+39 -18
home/programs/vscode.nix
··· 1 + # VS Code configuration. 1 2 { 2 3 pkgs, 3 4 lib, 4 - cfgLib, 5 + osConfig, 5 6 ... 6 7 }: 8 + let 9 + cfg = osConfig.myConfig; 10 + d = cfg.desktop; 7 11 8 - let 9 - cfg = cfgLib.cfg; 12 + # Font strings derived from desktop options — single source of truth. 13 + editorFont = d.monoFontBase; # "FiraCode" 14 + terminalFont = "${d.monoFontBase} Nerd Font"; # "FiraCode Nerd Font" 10 15 11 - # Resolve "publisher.name" → pkgs.vscode-extensions.<publisher>.<n> 12 16 toNixpkgsExt = 13 17 extStr: 14 18 let ··· 16 20 in 17 21 pkgs.vscode-extensions.${builtins.elemAt parts 0}.${builtins.elemAt parts 1}; 18 22 19 - # Resolve "publisher.name" → pkgs.vscode-marketplace.<publisher>.<n> 20 - # Provided by the nix-vscode-extensions overlay added in flake.nix. 21 23 toMarketplaceExt = 22 24 extStr: 23 25 let 24 26 parts = lib.splitString "." extStr; 25 27 in 26 28 pkgs.vscode-marketplace.${builtins.elemAt parts 0}.${builtins.elemAt parts 1}; 29 + 30 + nixpkgsExtensions = [ 31 + "jnoortheen.nix-ide" 32 + "ms-python.python" 33 + "ms-python.debugpy" 34 + "rust-lang.rust-analyzer" 35 + "ms-dotnettools.csharp" 36 + "ms-dotnettools.csdevkit" 37 + "mads-hartmann.bash-ide-vscode" 38 + "timonwong.shellcheck" 39 + "foxundermoon.shell-format" 40 + "ms-azuretools.vscode-docker" 41 + "tamasfe.even-better-toml" 42 + "redhat.vscode-yaml" 43 + "bradlc.vscode-tailwindcss" 44 + "dbaeumer.vscode-eslint" 45 + "esbenp.prettier-vscode" 46 + "eamodio.gitlens" 47 + "editorconfig.editorconfig" 48 + "streetsidesoftware.code-spell-checker" 49 + "christian-kohler.path-intellisense" 50 + ]; 51 + 52 + marketplaceExtensions = [ 53 + "golang.go" 54 + "svelte.svelte-vscode" 55 + "ms-vscode.makefile-tools" 56 + ]; 27 57 in 28 58 { 29 59 programs.vscode = { ··· 31 61 32 62 profiles.default = { 33 63 extensions = 34 - map toNixpkgsExt cfg.development.vscode.extensions 35 - ++ map toMarketplaceExt cfg.development.vscode.marketplaceExtensions; 64 + map toNixpkgsExt nixpkgsExtensions ++ map toMarketplaceExt marketplaceExtensions; 36 65 37 66 userSettings = { 38 67 "workbench.colorTheme" = lib.mkDefault cfg.development.vscode.colorTheme; 39 68 "workbench.iconTheme" = lib.mkDefault cfg.development.vscode.iconTheme; 40 - "editor.fontFamily" = cfg.development.vscode.fontFamily; 69 + "editor.fontFamily" = "'${editorFont}', 'monospace'"; 41 70 "editor.fontSize" = cfg.development.vscode.fontSize; 42 71 "editor.lineHeight" = cfg.development.vscode.lineHeight; 43 72 "editor.fontLigatures" = cfg.development.vscode.fontLigatures; ··· 54 83 "files.autoSaveDelay" = 1000; 55 84 "git.autofetch" = true; 56 85 "git.confirmSync" = false; 57 - "terminal.integrated.fontFamily" = cfg.development.vscode.terminalFontFamily; 86 + "terminal.integrated.fontFamily" = "'${terminalFont}'"; 58 87 "terminal.integrated.fontSize" = cfg.development.vscode.terminalFontSize; 59 88 "workbench.startupEditor" = "none"; 60 89 "explorer.confirmDelete" = false; 61 90 "explorer.confirmDragAndDrop" = false; 62 91 63 - # ── Per-language default formatters ─────────────────────────────────── 64 92 "[javascript]"."editor.defaultFormatter" = "esbenp.prettier-vscode"; 65 93 "[typescript]"."editor.defaultFormatter" = "esbenp.prettier-vscode"; 66 94 "[svelte]"."editor.defaultFormatter" = "svelte.svelte-vscode"; ··· 75 103 "[makefile]"."editor.defaultFormatter" = "ms-vscode.makefile-tools"; 76 104 "[python]"."editor.defaultFormatter" = "charliermarsh.ruff"; 77 105 78 - # ── Nix IDE ─────────────────────────────────────────────────────────── 79 106 "nix.enableLanguageServer" = true; 80 107 "nix.serverPath" = "nil"; 81 108 "nix.serverSettings".nil.formatting.command = [ "nixfmt" ]; 82 109 83 - # ── Go ──────────────────────────────────────────────────────────────── 84 110 "go.useLanguageServer" = true; 85 111 86 - # ── Python ─────────────────────────────────────────────────────────── 87 112 "python.analysis.typeCheckingMode" = "basic"; 88 113 "ruff.lint.enable" = true; 89 114 90 - # ── Shell ───────────────────────────────────────────────────────────── 91 - # Point extensions at Nix-managed binaries so they work regardless of PATH. 92 115 "shellcheck.executablePath" = "shellcheck"; 93 116 "shellformat.path" = "shfmt"; 94 117 "bashIde.shellcheckPath" = "shellcheck"; 95 118 96 - # ── YAML ────────────────────────────────────────────────────────────── 97 119 "yaml.format.enable" = true; 98 120 "yaml.validate" = true; 99 121 100 - # ── TOML ────────────────────────────────────────────────────────────── 101 122 "evenBetterToml.formatter.alignEntries" = false; 102 123 "evenBetterToml.formatter.arrayTrailingComma" = true; 103 124 };
+98 -79
home/programs/zsh.nix
··· 1 - { hostName, isDarwin }: 1 + # Zsh configuration. 2 + # Platform detection via pkgs.stdenv.isDarwin — no args passed from outside. 2 3 { 3 4 config, 4 5 pkgs, 5 6 lib, 6 - cfgLib, 7 + osConfig, 7 8 ... 8 9 }: 9 - 10 10 let 11 - cfg = cfgLib.cfg; 12 - userName = config.home.username; 13 - 11 + cfg = osConfig.myConfig; 12 + isDarwin = pkgs.stdenv.isDarwin; 13 + hostName = config.home.username; # use as fallback; actual hostname from networking 14 14 in 15 15 { 16 16 programs.zsh = { ··· 20 20 syntaxHighlighting.enable = true; 21 21 22 22 shellAliases = lib.filterAttrs (n: v: v != null) ( 23 - # Common aliases 24 - cfg.shell.aliases 23 + # ── Modern CLI replacements ────────────────────────────────────────────── 24 + { 25 + ls = "eza --icons"; 26 + ll = "eza -l --icons --git"; 27 + la = "eza -la --icons --git"; 28 + lt = "eza --tree --level=2 --icons"; 29 + cat = "bat"; 30 + 31 + ".." = "cd .."; 32 + "..." = "cd ../.."; 33 + "...." = "cd ../../.."; 34 + 35 + rm = "rm -i"; 36 + cp = "cp -i"; 37 + mv = "mv -i"; 38 + 39 + h = "history"; 40 + c = "clear"; 41 + e = "$EDITOR"; 42 + 43 + du1 = "du -h -d 1"; 44 + df = "df -h"; 45 + 46 + lg = "lazygit"; 47 + 48 + # ── Git shortcuts ────────────────────────────────────────────────────── 49 + gs = "git status"; 50 + gss = "git status -s"; 51 + gl = "git log --oneline --graph --decorate"; 52 + ga = "git add"; 53 + gaa = "git add -A"; 54 + gc = "git commit"; 55 + gcm = "git commit -m"; 56 + gca = "git commit --amend"; 57 + gp = "git push"; 58 + gpf = "git push --force-with-lease"; 59 + gpl = "git pull"; 60 + gpr = "git pull --rebase"; 61 + gb = "git branch"; 62 + gco = "git checkout"; 63 + gcb = "git checkout -b"; 64 + gd = "git diff"; 65 + gds = "git diff --staged"; 25 66 26 - # Git shortcuts 27 - // cfg.shell.gitAliases 67 + # ── Nix tool aliases ────────────────────────────────────────────────── 68 + flake-bump = "nix run ~/.config/nix-config/tools#flake-bump"; 69 + gen-diff = "nix run ~/.config/nix-config/tools#gen-diff"; 70 + health-check = "nix run ~/.config/nix-config/tools#health-check"; 71 + update-all = "~/.config/nix-config/home/scripts/update-all"; 72 + update-everything = "~/.config/nix-config/home/scripts/update-everything"; 28 73 29 - # Dynamic rebuild aliases (hostname-based) 30 - // { 74 + # ── Platform-specific rebuild aliases ───────────────────────────────── 31 75 nrs = 32 76 if isDarwin then 33 - "sudo darwin-rebuild switch --flake .#${hostName}" 77 + "sudo darwin-rebuild switch --flake ~/.config/nix-config#macmini" 34 78 else 35 - "sudo nixos-rebuild switch --flake .#${hostName}"; 36 - nrb = 37 - if isDarwin then 38 - null # Not applicable on Darwin 39 - else 40 - "sudo nixos-rebuild boot --flake .#${hostName}"; 79 + "sudo nixos-rebuild switch --flake ~/.config/nix-config"; 80 + nrb = if isDarwin then null else "sudo nixos-rebuild boot --flake ~/.config/nix-config"; 41 81 nrt = 42 82 if isDarwin then 43 - "sudo darwin-rebuild test --flake .#${hostName}" 83 + "sudo darwin-rebuild test --flake ~/.config/nix-config#macmini" 44 84 else 45 - "sudo nixos-rebuild test --flake .#${hostName}"; 46 - hms = "home-manager switch --flake .#${userName}"; 47 - update = 85 + "sudo nixos-rebuild test --flake ~/.config/nix-config"; 86 + hms = "home-manager switch --flake ~/.config/nix-config"; 87 + 88 + # ── Platform-specific extras ────────────────────────────────────────── 89 + cleanup = 48 90 if isDarwin then 49 - "sudo darwin-rebuild switch --flake .#${hostName} && home-manager switch --flake .#${userName}" 91 + "sudo nix-collect-garbage -d" 50 92 else 51 - "sudo nixos-rebuild switch --flake .#${hostName} && home-manager switch --flake .#${userName}"; 93 + "sudo nix-collect-garbage -d && nix-collect-garbage -d"; 52 94 } 53 - 54 - # Shared Nix tool aliases (flake-bump, gen-diff, health-check) 55 - // cfg.shell.nixToolAliases 56 - 57 - # Linux-specific 58 - // (lib.optionalAttrs (!isDarwin) cfg.shell.linuxAliases) 59 - 60 - # macOS-specific 61 - // (lib.optionalAttrs isDarwin cfg.shell.darwinAliases) 62 95 ); 63 96 64 97 initContent = '' 65 98 # Display system info on new shell 66 99 fastfetch 67 - 100 + 68 101 # Initialize SSH agent (Linux only) 69 - ${lib.optionalString (!isDarwin) ''if [ -z "$SSH_AUTH_SOCK" ]; then 70 - export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket" 71 - fi''} 72 - 102 + ${lib.optionalString (!isDarwin) '' 103 + if [ -z "$SSH_AUTH_SOCK" ]; then 104 + export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket" 105 + fi 106 + ''} 107 + 73 108 # Initialize Starship prompt 74 109 eval "$(starship init zsh)" 75 - 76 - # Initialize fzf (fuzzy finder) 110 + 111 + # Initialize fzf 77 112 eval "$(fzf --zsh)" 78 113 79 - # Prompt settings 80 114 setopt PROMPT_SUBST 81 115 82 - # History settings 83 - HISTSIZE=${toString cfg.shell.history.size} 84 - SAVEHIST=${toString cfg.shell.history.saveSize} 85 - HISTFILE=${cfg.shell.history.file} 86 - ${lib.optionalString cfg.shell.history.ignoreDups "setopt HIST_IGNORE_ALL_DUPS"} 87 - ${lib.optionalString cfg.shell.history.ignoreDups "setopt HIST_FIND_NO_DUPS"} 88 - ${lib.optionalString cfg.shell.history.ignoreDups "setopt HIST_SAVE_NO_DUPS"} 89 - setopt SHARE_HISTORY # Share history between sessions 90 - setopt HIST_EXPIRE_DUPS_FIRST # Expire duplicates first 91 - setopt HIST_REDUCE_BLANKS # Remove superfluous blanks 116 + HISTSIZE=10000 117 + SAVEHIST=10000 118 + HISTFILE=~/.zsh_history 119 + setopt HIST_IGNORE_ALL_DUPS 120 + setopt HIST_FIND_NO_DUPS 121 + setopt HIST_SAVE_NO_DUPS 122 + setopt SHARE_HISTORY 123 + setopt HIST_EXPIRE_DUPS_FIRST 124 + setopt HIST_REDUCE_BLANKS 92 125 93 - # Key bindings 94 - bindkey '^[[A' history-beginning-search-backward # Up arrow 95 - bindkey '^[[B' history-beginning-search-forward # Down arrow 96 - bindkey '^[[H' beginning-of-line # Home key 97 - bindkey '^[[F' end-of-line # End key 98 - bindkey '^[[3~' delete-char # Delete key 126 + bindkey '^[[A' history-beginning-search-backward 127 + bindkey '^[[B' history-beginning-search-forward 128 + bindkey '^[[H' beginning-of-line 129 + bindkey '^[[F' end-of-line 130 + bindkey '^[[3~' delete-char 99 131 100 - # Completion settings 101 132 zstyle ':completion:*' menu select 102 - zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' # Case insensitive 103 - zstyle ':completion:*' list-colors "''${(s.:.)LS_COLORS}" # Colored completion 104 - zstyle ':completion:*' group-name ''' # Group completions 105 - 106 - # Better directory navigation 107 - setopt AUTO_CD # cd by typing directory name 108 - setopt AUTO_PUSHD # Push directories onto stack 109 - setopt PUSHD_IGNORE_DUPS # Don't push duplicates 110 - setopt PUSHD_MINUS # Swap meaning of +/- 133 + zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 134 + zstyle ':completion:*' list-colors "''${(s.:.)LS_COLORS}" 135 + zstyle ':completion:*' group-name ''' 136 + 137 + setopt AUTO_CD 138 + setopt AUTO_PUSHD 139 + setopt PUSHD_IGNORE_DUPS 140 + setopt PUSHD_MINUS 111 141 ''; 112 142 113 - # Migrated from .profile and .zprofile 114 143 profileExtra = 115 - # ── Cross-platform ────────────────────────────────────────────────────── 116 144 '' 117 - # Cargo (rustup-managed install, if present) 118 145 [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" 119 - 120 - # pipx / local user binaries 121 146 export PATH="$PATH:$HOME/.local/bin" 122 147 '' 123 - # ── macOS only ────────────────────────────────────────────────────────── 124 148 + lib.optionalString isDarwin '' 125 - # Deno 126 149 [ -f "$HOME/.deno/env" ] && . "$HOME/.deno/env" 127 - 128 - # Homebrew 129 150 [ -x "/opt/homebrew/bin/brew" ] && eval "$(/opt/homebrew/bin/brew shellenv)" 130 - 131 - # OrbStack 132 151 [ -f "$HOME/.orbstack/shell/init.zsh" ] && source "$HOME/.orbstack/shell/init.zsh" 133 152 ''; 134 153 };
+27 -17
hosts/laptop/default.nix
··· 1 - { config, pkgs, lib, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + pkgs, 4 + lib, 5 + ... 6 + }: 3 7 let 4 - cfg = cfgLib.cfg; 8 + cfg = config.myConfig; 5 9 in 6 10 { 7 11 imports = [ 8 12 ./hardware-configuration.nix 9 - ../../modules/common.nix 10 13 ../../modules/users.nix 11 14 ../../modules/desktop.nix 12 15 ../../modules/packages.nix ··· 14 17 ../../modules/gaming.nix 15 18 ]; 16 19 20 + myConfig.isDesktop = true; 21 + myConfig.gaming.enable = true; 22 + 17 23 networking.hostName = "laptop"; 18 24 19 25 # Firewall – trust Tailscale for inter-host SSH ··· 22 28 trustedInterfaces = [ "tailscale0" ]; 23 29 }; 24 30 25 - # Audio – backend driven from settings/config/audio.nix 31 + # Audio – backend driven from myConfig.audio 26 32 security.rtkit.enable = cfg.audio.enable; 27 33 services.pipewire = lib.mkIf (cfg.audio.enable && cfg.audio.backend == "pipewire") { 28 - enable = true; 29 - alsa.enable = true; 34 + enable = true; 35 + alsa.enable = true; 30 36 alsa.support32Bit = true; 31 - pulse.enable = true; 32 - jack.enable = true; 37 + pulse.enable = true; 38 + jack.enable = true; 33 39 }; 34 40 35 41 # Allow passwordless sudo for nixos-rebuild so remote one-liners work over SSH 36 42 # (no TTY is available in that context). Other sudo commands still require a password. 37 43 # The server keeps wheelNeedsPassword = true; this exception is laptop-only. 38 - security.sudo.extraRules = [{ 39 - users = [ cfg.user.username ]; 40 - commands = [{ 41 - command = "/run/current-system/sw/bin/nixos-rebuild"; 42 - options = [ "NOPASSWD" ]; 43 - }]; 44 - }]; 44 + security.sudo.extraRules = [ 45 + { 46 + users = [ cfg.user.username ]; 47 + commands = [ 48 + { 49 + command = "/run/current-system/sw/bin/nixos-rebuild"; 50 + options = [ "NOPASSWD" ]; 51 + } 52 + ]; 53 + } 54 + ]; 45 55 46 - system.stateVersion = cfg.system.stateVersion; 56 + system.stateVersion = cfg.stateVersion; 47 57 }
+9 -6
hosts/macmini/default.nix
··· 1 - { pkgs, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + pkgs, 4 + ... 5 + }: 3 6 let 4 - cfg = cfgLib.cfg; 7 + cfg = config.myConfig; 5 8 in 6 9 { 7 10 imports = [ ··· 24 27 # SMB/NetBIOS hostname (used by network discovery and file sharing) 25 28 system.defaults.smb.NetBIOSName = "macmini"; 26 29 27 - # Timezone — driven from settings/config/system.nix 28 - time.timeZone = cfg.system.timeZone; 30 + # Timezone — driven from myConfig.timeZone 31 + time.timeZone = cfg.timeZone; 29 32 30 33 users.users.${cfg.user.username} = { 31 34 home = "/Users/${cfg.user.username}"; 32 - shell = pkgs.${cfg.user.shell}; 35 + shell = pkgs.zsh; 33 36 }; 34 37 35 38 # nix-darwin uses an integer for stateVersion
+12 -5
hosts/server/default.nix
··· 1 - { cfgLib, ... }: 2 - 1 + { 2 + config, 3 + ... 4 + }: 3 5 let 4 - cfg = cfgLib.cfg; 6 + cfg = config.myConfig; 5 7 in 6 8 { 7 9 imports = [ 8 10 ./minimal-hardware.nix 9 - ../../modules/common.nix 10 11 ../../modules/users.nix 11 12 ../../modules/caddy.nix 12 13 ../../modules/cockpit.nix ··· 17 18 ../../profiles/server-hardened.nix 18 19 ]; 19 20 21 + # Service toggles — all services run on the server. 22 + myConfig.services.forgejo.enable = true; 23 + myConfig.services.pds.enable = true; 24 + myConfig.services.matrix.enable = true; 25 + myConfig.services.cloudflare.enable = true; 26 + 20 27 networking.hostName = "server"; 21 28 22 29 # Boot – clean /tmp on every boot ··· 28 35 wheelNeedsPassword = true; 29 36 }; 30 37 31 - system.stateVersion = cfg.system.stateVersion; 38 + system.stateVersion = cfg.stateVersion; 32 39 }
-38
lib/default.nix
··· 1 - { lib }: 2 - 3 - let 4 - # Import the central config once at the library level 5 - cfg = import ../settings/config.nix; 6 - 7 - in { 8 - # Expose the config so modules can just use `cfgLib.cfg` instead of importing 9 - inherit cfg; 10 - 11 - # Helper to create a module with auto-injected config 12 - # Usage: mkModule { config, pkgs, ... }: { ... } 13 - mkModule = moduleFunc: { config, pkgs, lib, ... }@args: 14 - moduleFunc (args // { inherit cfg; }); 15 - 16 - # Helper to create a home-manager program module with platform info 17 - # Usage: mkHomeProgram { isDarwin }: { config, pkgs, ... }: { ... } 18 - mkHomeProgram = { isDarwin ? false }: moduleFunc: { config, pkgs, lib, ... }@args: 19 - moduleFunc (args // { inherit cfg isDarwin; }); 20 - 21 - # Helper to resolve package names to actual packages, skipping missing ones 22 - # Usage: resolvePackages pkgs [ "firefox" "vscode" ] 23 - resolvePackages = pkgs: names: 24 - let 25 - toPkg = name: 26 - if pkgs ? ${name} then pkgs.${name} 27 - else builtins.trace "WARNING: package '${name}' not found in nixpkgs, skipping" null; 28 - in 29 - builtins.filter (x: x != null) (map toPkg names); 30 - 31 - # Helper to create SSH authorized keys excluding the current host 32 - # Usage: mkAuthorizedKeys hostName 33 - mkAuthorizedKeys = hostName: 34 - let 35 - allKeys = import ../modules/ssh-keys.nix; 36 - in 37 - lib.attrValues (lib.filterAttrs (name: _: name != hostName) allKeys); 38 - }
+33 -38
modules/cloudflare-tunnel.nix
··· 12 12 # Cloudflare tunnel setup (one-time, outside Nix): 13 13 # 1. cloudflared tunnel login 14 14 # 2. cloudflared tunnel create server 15 - # 3. Encrypt the resulting ~/.cloudflared/<UUID>.json with ragenix: 16 - # nix run github:yaxitech/ragenix -- -e secrets/age/cf-tunnel.json.age 17 - # 4. Set cfg.cloudflare.tunnelId to that UUID in settings/config/cloudflare.nix 15 + # 3. Encrypt the resulting ~/.cloudflared/<UUID>.json with sops: 16 + # sops --encrypt --age <age-pubkey> cf-tunnel.json > secrets/cf-tunnel.json 17 + # 4. Set myConfig.cloudflare.tunnelId to that UUID (modules/options.nix default 18 + # or a per-host override). 18 19 # 5. Add CNAME records in Cloudflare DNS for each service: 19 20 # pds.ewancroft.uk → <UUID>.cfargotunnel.com 20 21 # matrix.ewancroft.uk → <UUID>.cfargotunnel.com 21 22 # git.ewancroft.uk → <UUID>.cfargotunnel.com 22 23 ############################################################################## 23 - { config, lib, self, cfgLib, ... }: 24 - 24 + { 25 + config, 26 + lib, 27 + ... 28 + }: 25 29 let 26 - cfg = cfgLib.cfg.cloudflare; 27 - pdsCfg = cfgLib.cfg.pds; 28 - matrixCfg = cfgLib.cfg.matrix; 29 - forgejoCfg = cfgLib.cfg.forgejo; 30 - 31 - # Build ingress routes based on enabled services 32 - ingressRoutes = lib.mkMerge [ 33 - # PDS routes (if enabled) 34 - (lib.mkIf pdsCfg.enable { 35 - ${pdsCfg.hostname} = "http://127.0.0.1:${toString pdsCfg.caddyPort}"; 36 - "*.${pdsCfg.hostname}" = "http://127.0.0.1:${toString pdsCfg.caddyPort}"; 37 - }) 38 - 39 - # Matrix routes (if enabled) 40 - (lib.mkIf matrixCfg.enable { 41 - ${matrixCfg.hostname} = "http://127.0.0.1:${toString matrixCfg.caddyPort}"; 42 - }) 30 + cfg = config.myConfig; 43 31 44 - # Forgejo routes (if enabled) 45 - (lib.mkIf forgejoCfg.enable { 46 - ${forgejoCfg.hostname} = "http://127.0.0.1:${toString forgejoCfg.caddyPort}"; 47 - }) 48 - ]; 32 + # Build ingress routes based on enabled services. 33 + ingressRoutes = 34 + lib.optionalAttrs cfg.services.pds.enable { 35 + ${cfg.pds.hostname} = "http://127.0.0.1:${toString cfg.pds.caddyPort}"; 36 + "*.${cfg.pds.hostname}" = "http://127.0.0.1:${toString cfg.pds.caddyPort}"; 37 + } 38 + // lib.optionalAttrs cfg.services.matrix.enable { 39 + ${cfg.matrix.hostname} = "http://127.0.0.1:${toString cfg.matrix.caddyPort}"; 40 + } 41 + // lib.optionalAttrs cfg.services.forgejo.enable { 42 + ${cfg.forgejo.hostname} = "http://127.0.0.1:${toString cfg.forgejo.caddyPort}"; 43 + }; 49 44 in 50 - lib.mkIf cfg.enable { 45 + lib.mkIf cfg.services.cloudflare.enable { 51 46 52 - # ── Secrets ────────────────────────────────────────────────────────────────── 47 + # ── Secret ────────────────────────────────────────────────────────────────── 53 48 # JSON credentials file created by `cloudflared tunnel create server`. 54 - # Encrypted with: nix run github:yaxitech/ragenix -- -e secrets/age/cf-tunnel.json.age 55 - age.secrets."cf-tunnel.json" = { 56 - file = self + /secrets/age/cf-tunnel.json.age; 49 + # Encrypt with: sops --encrypt --age <age-pubkey> cf-tunnel.json > secrets/cf-tunnel.json 50 + sops.secrets."cf-tunnel.json" = { 51 + sopsFile = ../secrets/cf-tunnel.json; 52 + format = "binary"; 57 53 owner = "cloudflared"; 58 - mode = "0400"; 54 + mode = "0400"; 59 55 }; 60 56 61 - # ── Cloudflare tunnel ───────────────────────────────────────────────────────── 57 + # ── Cloudflare tunnel ────────────────────────────────────────────────────── 62 58 # cloudflared dials outbound to Cloudflare's edge — zero inbound ports needed. 63 59 # Single tunnel serves all configured services via hostname-based routing. 64 60 services.cloudflared = { 65 61 enable = true; 66 - tunnels.${cfg.tunnelId} = { 67 - credentialsFile = config.age.secrets."cf-tunnel.json".path; 62 + tunnels.${cfg.cloudflare.tunnelId} = { 63 + credentialsFile = config.sops.secrets."cf-tunnel.json".path; 68 64 default = "http_status:404"; 69 65 ingress = ingressRoutes; 70 66 }; 71 67 }; 72 68 73 - # ── Firewall ────────────────────────────────────────────────────────────────── 74 - # The Cloudflare tunnel is fully outbound — no ports need to be open. 69 + # The Cloudflare tunnel is fully outbound — no firewall ports need to be opened. 75 70 }
+8 -11
modules/cockpit.nix
··· 14 14 # https://server:9090 15 15 # 16 16 # Log in with any local system user that is a member of the wheel group. 17 - # 18 - # Port configured in settings/config/server.nix → cockpit.port 19 17 ############################################################################## 20 - { lib, cfgLib, ... }: 21 - 18 + { 19 + config, 20 + lib, 21 + ... 22 + }: 22 23 let 23 - cfg = cfgLib.cfg.server.cockpit; 24 + cfg = config.myConfig.server.cockpit; 24 25 in 25 26 lib.mkIf cfg.enable { 26 27 27 28 services.cockpit = { 28 29 enable = true; 29 - port = cfg.port; 30 - 31 - # Only bind to the Tailscale interface and loopback. 32 - # This ensures Cockpit is never reachable from the public internet, 33 - # even if the firewall is misconfigured. 30 + port = cfg.port; 34 31 settings.WebService.AllowUnencrypted = true; 35 32 }; 36 33 37 - # ── Firewall ────────────────────────────────────────────────────────────────── 34 + # ── Firewall ──────────────────────────────────────────────────────────────── 38 35 # Allow Cockpit only on the Tailscale interface (tailscale0). 39 36 # The trusted interface already bypasses the firewall (set in firewall.nix), 40 37 # so this rule is belt-and-braces — it blocks access from every other interface.
+37 -35
modules/common.nix
··· 1 - { config, pkgs, lib, cfgLib, ... }: 2 - 1 + # Common NixOS settings shared across all hosts. 2 + { 3 + config, 4 + lib, 5 + pkgs, 6 + ... 7 + }: 3 8 let 4 - cfg = cfgLib.cfg; 9 + cfg = config.myConfig; 5 10 in 6 11 { 7 - # Common NixOS settings shared across all hosts 8 - 9 - # Enable flakes and nix command 10 - nix.settings.experimental-features = cfg.nix.experimentalFeatures; 12 + nix.settings.experimental-features = [ 13 + "nix-command" 14 + "flakes" 15 + ]; 11 16 12 - # Nix store optimisation 13 - nix.settings.auto-optimise-store = cfg.nix.autoOptimise; 17 + nix.settings.auto-optimise-store = true; 14 18 15 - # Automatic garbage collection 16 19 nix.gc = { 17 - automatic = cfg.nix.gc.automatic; 18 - dates = cfg.nix.gc.dates; 19 - options = cfg.nix.gc.options; 20 + automatic = true; 21 + dates = "weekly"; 22 + options = "--delete-older-than 30d"; 20 23 }; 21 24 22 - # Enable zsh system-wide 23 25 programs.zsh.enable = true; 24 26 25 - # Symlink config repo into /etc/nixos for convenience 27 + # Symlink config repo into /etc/nixos for convenience. 26 28 system.activationScripts.linkConfigs = '' 27 29 mkdir -p /etc/nixos 28 30 if [ ! -L /etc/nixos ]; then ··· 31 33 fi 32 34 ''; 33 35 34 - # Automatic system upgrades 35 36 system.autoUpgrade = { 36 - enable = cfg.maintenance.autoUpgrade.enable; 37 - flake = "/home/${cfg.user.username}/.config/nix-config"; 38 - flags = map (i: "--update-input ${i}") cfg.maintenance.autoUpgrade.updateInputs 39 - ++ [ "--commit-lock-file" ]; 40 - dates = cfg.maintenance.autoUpgrade.dates; 41 - randomizedDelaySec = cfg.maintenance.autoUpgrade.randomizedDelaySec; 42 - allowReboot = cfg.maintenance.autoUpgrade.allowReboot; 37 + enable = true; 38 + flake = "/home/${cfg.user.username}/.config/nix-config"; 39 + flags = [ 40 + "--update-input" 41 + "nixpkgs" 42 + "--commit-lock-file" 43 + ]; 44 + dates = "daily"; 45 + randomizedDelaySec = "45min"; 46 + allowReboot = false; 43 47 }; 44 48 45 - # Default timezone and locale (can be overridden per host) 46 - time.timeZone = lib.mkDefault cfg.system.timeZone; 47 - i18n.defaultLocale = lib.mkDefault cfg.system.locale; 49 + time.timeZone = lib.mkDefault cfg.timeZone; 50 + i18n.defaultLocale = lib.mkDefault cfg.locale; 48 51 49 - # Console configuration defaults 50 52 console = { 51 - font = lib.mkDefault "Lat2-Terminus16"; 53 + font = lib.mkDefault "Lat2-Terminus16"; 52 54 keyMap = lib.mkDefault "uk"; 53 55 }; 54 56 55 - # Networking 56 - networking.networkmanager.enable = lib.mkDefault cfg.system.network.enableNetworkManager; 57 + networking.networkmanager.enable = lib.mkDefault true; 57 58 58 - # Boot configuration defaults 59 59 boot = { 60 60 loader = { 61 - systemd-boot.enable = lib.mkDefault (cfg.system.boot.loader == "systemd-boot"); 61 + systemd-boot.enable = lib.mkDefault true; 62 62 efi.canTouchEfiVariables = lib.mkDefault true; 63 63 }; 64 - kernelPackages = lib.mkDefault ( 65 - if cfg.system.kernel.useLatest then pkgs.linuxPackages_latest else pkgs.linuxPackages 66 - ); 64 + kernelPackages = lib.mkDefault pkgs.linuxPackages_latest; 67 65 }; 66 + 67 + # sops-nix: decrypt secrets using the host's SSH ed25519 key as an age key. 68 + # This key is generated on first boot and lives outside the Nix store. 69 + sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; 68 70 }
+24 -13
modules/darwin/common.nix
··· 1 - { config, pkgs, lib, cfgLib, ... }: 2 - 1 + # Common Darwin (macOS) settings shared across all hosts. 2 + { 3 + config, 4 + pkgs, 5 + ... 6 + }: 3 7 let 4 - cfg = cfgLib.cfg; 8 + cfg = config.myConfig; 5 9 in 6 10 { 7 - # Common Darwin (macOS) settings shared across all hosts 8 - 9 - # Enable zsh system-wide 10 11 programs.zsh.enable = true; 11 12 12 13 nix = { 13 14 # Explicit nix management via nix-darwin. 14 15 # NOTE: Set `nix.enable = false` if you use Determinate Nix, which manages 15 16 # the nix daemon itself and conflicts with nix-darwin's native management. 16 - enable = true; 17 + enable = true; 17 18 package = pkgs.nix; 18 19 19 20 settings = { 20 - experimental-features = cfg.nix.experimentalFeatures; 21 + experimental-features = [ 22 + "nix-command" 23 + "flakes" 24 + ]; 21 25 22 26 # IMPORTANT: Disable store optimisation on macOS. 23 27 # `auto-optimise-store = true` triggers a kernel bug on macOS that causes 24 - # build failures: https://github.com/NixOS/nix/issues/7273 28 + # build failures: https://github.com/NixOS/nix/issues/7273 25 29 # Use `nix store optimise` manually when needed instead. 26 30 auto-optimise-store = false; 27 31 28 32 # Allow the primary user to use trusted nix operations (e.g. adding 29 33 # substituters) without requiring root. 30 - trusted-users = [ "root" cfg.user.username ]; 34 + trusted-users = [ 35 + "root" 36 + cfg.user.username 37 + ]; 31 38 }; 32 39 33 40 # Automatic garbage collection (macOS launchd schedule) 34 41 gc = { 35 - automatic = cfg.nix.gc.automatic; 36 - interval = { Weekday = 0; Hour = 2; Minute = 0; }; # Every Sunday at 02:00 37 - options = cfg.nix.gc.options; 42 + automatic = true; 43 + interval = { 44 + Weekday = 0; 45 + Hour = 2; 46 + Minute = 0; 47 + }; # Every Sunday at 02:00 48 + options = "--delete-older-than 30d"; 38 49 }; 39 50 }; 40 51
+11 -10
modules/darwin/homebrew.nix
··· 1 - { config, pkgs, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + ... 4 + }: 3 5 let 4 - cfg = cfgLib.cfg; 6 + cfg = config.myConfig; 5 7 6 8 # Normalise every cask entry to an attrset and force greedy = true so 7 9 # that `brew upgrade` always overwrites out-of-date casks — including 8 10 # those that declare auto_updates or version :latest. 9 - makeGreedy = cask: 10 - if builtins.isString cask 11 - then { name = cask; greedy = true; } 12 - else cask // { greedy = true; }; 11 + makeGreedy = 12 + cask: 13 + if builtins.isString cask then { name = cask; greedy = true; } else cask // { greedy = true; }; 13 14 in 14 15 { 15 - # Homebrew configuration – all values driven from settings/config/darwin.nix 16 + # Homebrew configuration — all values driven from myConfig.darwin.homebrew 16 17 homebrew = { 17 18 inherit (cfg.darwin.homebrew) enable taps brews masApps; 18 19 casks = map makeGreedy cfg.darwin.homebrew.casks; 19 20 20 21 onActivation = { 21 22 autoUpdate = true; 22 - upgrade = true; 23 - cleanup = "uninstall"; 23 + upgrade = true; 24 + cleanup = "uninstall"; 24 25 }; 25 26 }; 26 27 }
+21 -7
modules/darwin/packages.nix
··· 1 - { config, pkgs, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + pkgs, 4 + ... 5 + }: 3 6 let 4 - cfg = cfgLib.cfg; 5 - resolve = cfgLib.resolvePackages pkgs; 7 + cfg = config.myConfig; 8 + 9 + resolvePackages = 10 + names: 11 + builtins.filter (x: x != null) ( 12 + map ( 13 + name: 14 + if pkgs ? ${name} then 15 + pkgs.${name} 16 + else 17 + builtins.trace "WARNING: package '${name}' not found in nixpkgs, skipping" null 18 + ) names 19 + ); 6 20 in 7 21 { 8 22 environment.systemPackages = 9 23 # Common CLI utilities (all systems) 10 - resolve cfg.packages.common 24 + resolvePackages cfg.packages.common 11 25 # Cross-platform development packages (shared with NixOS laptop) 12 - ++ resolve cfg.packages.development 26 + ++ resolvePackages cfg.packages.development 13 27 # macOS-specific packages (GNU replacements, macFUSE tools, build libs) 14 - ++ resolve cfg.darwin.packages; 28 + ++ resolvePackages cfg.packages.darwin; 15 29 16 30 programs = { 17 31 # zsh is already enabled in darwin/common.nix
+29 -25
modules/darwin/system.nix
··· 1 - { config, lib, pkgs, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + lib, 4 + ... 5 + }: 3 6 let 4 - cfg = cfgLib.cfg; 7 + cfg = config.myConfig; 5 8 in 6 9 { 7 - # Import Darwin system-defaults (auto-exported values from settings/darwin/domains/) 8 10 imports = [ 9 11 ../../settings/darwin 10 12 ]; 11 13 12 - # Keyboard – driven from settings/config/darwin.nix 14 + # Keyboard – driven from myConfig.darwin.keyboard 13 15 system.keyboard = { 14 - enableKeyMapping = cfg.darwin.keyboard.enableKeyMapping; 16 + enableKeyMapping = cfg.darwin.keyboard.enableKeyMapping; 15 17 remapCapsLockToControl = cfg.darwin.keyboard.remapCapsLockToControl; 16 18 }; 17 19 18 - # Startup chime – driven from settings/config/darwin.nix 20 + # Startup chime – driven from myConfig.darwin.startup.chime 19 21 system.startup.chime = cfg.darwin.startup.chime; 20 22 21 - # Touch ID for sudo – driven from settings/config/darwin.nix 23 + # Touch ID for sudo – driven from myConfig.darwin.security.touchIdForSudo 22 24 security.pam.services.sudo_local.touchIdAuth = cfg.darwin.security.touchIdForSudo; 23 25 24 - # ── Time Machine destination ────────────────────────────────────────────────── 26 + # ── Time Machine destination ────────────────────────────────────────────── 25 27 # No native nix-darwin option exists for tmutil setdestination, so we use an 26 - # activation script. It is idempotent: skipped if the URL is already registered. 28 + # activation script. It is idempotent: skipped if the URL is already registered. 27 29 # 28 30 # First-time setup (one-off, interactive — stores password in macOS keychain): 29 31 # sudo tmutil setdestination -p smb://<user>@server/TimeMachine 30 32 # After that, nrs registers it automatically on every rebuild without prompting. 31 33 system.activationScripts.timeMachineDestination = lib.mkIf cfg.server.timemachine.enable { 32 - text = let 33 - shareUrl = "smb://${cfg.user.username}@server/${cfg.server.timemachine.shareName}"; 34 - in '' 35 - shareUrl='${shareUrl}' 36 - echo "Checking Time Machine destination ($shareUrl)..." 37 - if /usr/bin/tmutil destinationinfo 2>/dev/null | /usr/bin/grep -qF "$shareUrl"; then 38 - echo " already registered, skipping" 39 - else 40 - echo " registering..." 41 - # -a adds alongside existing destinations rather than replacing them. 42 - # Credentials come from the macOS keychain — never stored in the Nix store. 43 - /usr/bin/tmutil setdestination -a "$shareUrl" 2>&1 || \ 44 - echo " WARNING: could not register (share may be unreachable right now)" 45 - fi 46 - ''; 34 + text = 35 + let 36 + shareUrl = "smb://${cfg.user.username}@server/${cfg.server.timemachine.shareName}"; 37 + in 38 + '' 39 + shareUrl='${shareUrl}' 40 + echo "Checking Time Machine destination ($shareUrl)..." 41 + if /usr/bin/tmutil destinationinfo 2>/dev/null | /usr/bin/grep -qF "$shareUrl"; then 42 + echo " already registered, skipping" 43 + else 44 + echo " registering..." 45 + # -a adds alongside existing destinations rather than replacing them. 46 + # Credentials come from the macOS keychain — never stored in the Nix store. 47 + /usr/bin/tmutil setdestination -a "$shareUrl" 2>&1 || \ 48 + echo " WARNING: could not register (share may be unreachable right now)" 49 + fi 50 + ''; 47 51 }; 48 52 }
+10 -21
modules/desktop.nix
··· 1 + # KDE Plasma 6 desktop environment. 1 2 { 2 3 pkgs, 3 4 lib, 4 - cfgLib, 5 + config, 5 6 ... 6 7 }: 7 - 8 8 let 9 - cfg = cfgLib.cfg; 9 + cfg = config.myConfig; 10 10 11 - # Safely resolve KDE package names under pkgs.kdePackages, 12 - # skipping any that do not exist rather than crashing the build. 13 11 resolveKde = 14 12 names: 15 13 builtins.filter (x: x != null) ( ··· 23 21 ); 24 22 in 25 23 { 26 - # X11/Wayland base – still required even in Wayland sessions 24 + # X11/Wayland base — required even in Wayland sessions. 27 25 services.xserver = { 28 26 enable = true; 29 - 30 27 videoDrivers = [ "modesetting" ]; 31 - 32 28 xkb.layout = "gb"; 33 29 }; 34 30 35 - # Display manager – driven from settings/config/desktop.nix 36 31 services.displayManager.sddm = lib.mkIf (cfg.desktop.displayManager == "sddm") { 37 32 enable = true; 38 - wayland.enable = true; # Prefer Wayland session for Plasma 6 33 + wayland.enable = true; 39 34 }; 40 35 41 - # Desktop environment – KDE Plasma 6 42 36 services.desktopManager.plasma6.enable = cfg.desktop.environment == "plasma6"; 43 37 44 - # Kvantum is the recommended theming engine for Qt/Plasma 45 38 environment.systemPackages = with pkgs; [ 46 - kdePackages.qtstyleplugin-kvantum # Kvantum Qt6 style engine (used by home-manager qt module) 47 - kdePackages.kcalc # Calculator (lightweight, commonly needed) 48 - libgtop # Required for resource monitors / KSysGuard sensors 39 + kdePackages.qtstyleplugin-kvantum 40 + kdePackages.kcalc 41 + libgtop 49 42 ]; 50 43 51 - # Mouse – mac: "com.apple.mouse.scaling" = 0.5 (slow/precise) 52 - # libinput accelSpeed: -1 (slowest) … 0 (default) … +1 (fastest) 53 44 services.libinput.mouse.accelSpeed = "-0.5"; 54 45 55 - # Touchpad – mac: trackpad.Clicking = false, swipescrolldirection = false 56 46 services.libinput.touchpad = { 57 - naturalScrolling = false; # mac: "com.apple.swipescrolldirection" = false 58 - tapping = false; # mac: trackpad.Clicking = false 47 + naturalScrolling = false; 48 + tapping = false; 59 49 scrollMethod = "twofinger"; 60 50 }; 61 51 62 - # Exclude unwanted default KDE packages – list driven from settings/config/desktop.nix 63 52 environment.plasma6.excludePackages = resolveKde cfg.desktop.plasma.excludePackages; 64 53 }
+34 -58
modules/forgejo.nix
··· 2 2 # Forgejo git forge — NixOS module. 3 3 # 4 4 # Architecture: 5 - # Forgejo (127.0.0.1:cfg.port) 5 + # Forgejo (127.0.0.1:cfg.forgejo.port) 6 6 # ↑ reverse proxy 7 - # Caddy (127.0.0.1:cfg.caddyPort — internal only, no TLS here) 8 - # ↑ Cloudflare tunnel (cloudflared — outbound only, no firewall ports needed) 7 + # Caddy (127.0.0.1:cfg.forgejo.caddyPort — internal only, no TLS here) 8 + # ↑ Cloudflare tunnel (outbound only, no firewall ports needed) 9 9 # 10 - # Non-secret settings live in settings/config/forgejo.nix. 11 - # Secrets decrypted by ragenix at activation time. 10 + # Secrets (sops-encrypted, age backend): 11 + # secrets/forgejo.env — KEY=value env file, must contain: 12 + # SECRET_KEY # openssl rand -hex 32 13 + # INTERNAL_TOKEN # openssl rand -hex 32 12 14 # 13 - # Required secrets (set in secrets/age/forgejo.env.age as KEY=value pairs): 14 - # SECRET_KEY # Generate with: openssl rand -hex 32 15 - # INTERNAL_TOKEN # Generate with: openssl rand -hex 32 16 - # 17 - # Cloudflare tunnel setup (one-time, outside Nix): 18 - # Handled by modules/cloudflare-tunnel.nix. 19 - # Add a CNAME in Cloudflare DNS: 20 - # git.ewancroft.uk → <UUID>.cfargotunnel.com 21 - # 22 - # First-run admin account: 23 - # The first user to register becomes admin, OR create one manually: 24 - # sudo -u forgejo forgejo admin user create \ 25 - # --username admin --password <pw> --email <email> --admin 15 + # Encrypt: sops --encrypt --age <host-age-pubkey> secrets/forgejo.env > secrets/forgejo.env 16 + # (Use .sops.yaml at the repo root to configure recipients automatically.) 26 17 ############################################################################## 27 - { config, lib, pkgs, self, cfgLib, ... }: 28 - 18 + { 19 + config, 20 + lib, 21 + ... 22 + }: 29 23 let 30 - cfg = cfgLib.cfg.forgejo; 31 - forgejoPort = toString cfg.port; 32 - caddyPort = toString cfg.caddyPort; 24 + cfg = config.myConfig; 25 + forgejo = cfg.forgejo; 26 + forgejoPort = toString forgejo.port; 27 + caddyPort = toString forgejo.caddyPort; 33 28 in 34 - lib.mkIf cfg.enable { 29 + lib.mkIf cfg.services.forgejo.enable { 35 30 36 - # ── Secrets ────────────────────────────────────────────────────────────────── 37 - age.secrets."forgejo.env" = { 38 - file = self + /secrets/age/forgejo.env.age; 31 + sops.secrets."forgejo.env" = { 32 + sopsFile = ../secrets/forgejo.env; 33 + format = "binary"; 39 34 owner = "forgejo"; 40 35 group = "forgejo"; 41 - mode = "0400"; 36 + mode = "0400"; 42 37 }; 43 38 44 - # ── Forgejo service ─────────────────────────────────────────────────────────── 45 39 services.forgejo = { 46 - enable = true; 40 + enable = true; 47 41 stateDir = "/srv/forgejo"; 48 42 49 43 settings = { 50 - DEFAULT.APP_NAME = cfg.appName; 44 + DEFAULT.APP_NAME = forgejo.appName; 51 45 52 46 server = { 53 - DOMAIN = cfg.hostname; 54 - ROOT_URL = "https://${cfg.hostname}"; 47 + DOMAIN = forgejo.hostname; 48 + ROOT_URL = "https://${forgejo.hostname}"; 55 49 HTTP_ADDR = "127.0.0.1"; 56 - HTTP_PORT = cfg.port; 50 + HTTP_PORT = forgejo.port; 57 51 }; 58 52 59 - service = { 60 - DISABLE_REGISTRATION = cfg.disableRegistration; 61 - }; 62 - 63 - # Use the environment file for secrets (SECRET_KEY, INTERNAL_TOKEN). 64 - # Forgejo reads these from the environment automatically. 53 + service.DISABLE_REGISTRATION = forgejo.disableRegistration; 65 54 }; 66 55 }; 67 56 68 57 systemd.services.forgejo = { 69 58 serviceConfig = { 70 - Restart = lib.mkForce "always"; 71 - RestartSec = cfgLib.cfg.server.servicePolicy.restartSec; 72 - # Inject age-decrypted secrets (SECRET_KEY, INTERNAL_TOKEN) into env. 73 - # services.forgejo.environmentFile was removed in nixos-25.11; use 74 - # systemd's EnvironmentFile directly instead. 75 - EnvironmentFile = config.age.secrets."forgejo.env".path; 59 + Restart = lib.mkForce "always"; 60 + RestartSec = cfg.server.servicePolicy.restartSec; 61 + EnvironmentFile = config.sops.secrets."forgejo.env".path; 76 62 }; 77 63 unitConfig = { 78 - StartLimitIntervalSec = cfgLib.cfg.server.servicePolicy.startLimitIntervalSec; 79 - StartLimitBurst = cfgLib.cfg.server.servicePolicy.startLimitBurst; 64 + StartLimitIntervalSec = cfg.server.servicePolicy.startLimitIntervalSec; 65 + StartLimitBurst = cfg.server.servicePolicy.startLimitBurst; 80 66 }; 81 67 }; 82 68 83 - # ── Caddy reverse proxy ─────────────────────────────────────────────────────── 84 - # Listens on localhost:caddyPort only — never exposed publicly. 85 - # Cloudflare handles TLS; Caddy receives plain HTTP from the tunnel daemon. 86 - # Using http:// prefix disables Caddy's automatic HTTPS / ACME entirely. 87 - # 88 - # Note: Caddy service itself is enabled by modules/caddy.nix. 89 69 services.caddy.virtualHosts."http://127.0.0.1:${caddyPort}" = { 90 70 extraConfig = '' 91 71 handle { ··· 93 73 } 94 74 ''; 95 75 }; 96 - 97 - # ── Firewall ────────────────────────────────────────────────────────────────── 98 - # Cloudflare tunnel is configured by modules/cloudflare-tunnel.nix. 99 - # SSH is handled by modules/server/firewall.nix and modules/server/ssh.nix. 100 76 }
+14 -11
modules/gaming.nix
··· 1 - { config, pkgs, lib, cfgLib, ... }: 2 - 1 + # Steam / gaming support. Active only when myConfig.gaming.enable = true. 2 + { 3 + config, 4 + pkgs, 5 + lib, 6 + ... 7 + }: 3 8 let 4 - cfg = cfgLib.cfg; 9 + cfg = config.myConfig; 5 10 in 6 11 { 7 - # Gaming – only active when settings/config/gaming.nix sets enable = true 8 - 9 12 programs.steam = lib.mkIf cfg.gaming.enable { 10 - enable = cfg.gaming.steam.enable; 11 - remotePlay.openFirewall = cfg.gaming.steam.openFirewall; 12 - dedicatedServer.openFirewall = cfg.gaming.steam.openFirewall; 13 - gamescopeSession.enable = true; 13 + enable = cfg.gaming.steam.enable; 14 + remotePlay.openFirewall = cfg.gaming.steam.openFirewall; 15 + dedicatedServer.openFirewall = cfg.gaming.steam.openFirewall; 16 + gamescopeSession.enable = true; 14 17 }; 15 18 16 19 programs.gamemode.enable = lib.mkIf cfg.gaming.enable cfg.gaming.steam.enable; ··· 18 21 hardware.graphics.enable32Bit = lib.mkIf cfg.gaming.enable true; 19 22 20 23 environment.systemPackages = lib.optionals cfg.gaming.enable (with pkgs; [ 21 - mangohud # Performance overlay 22 - gamescope # Gaming compositor 24 + mangohud 25 + gamescope 23 26 ]); 24 27 }
+29 -81
modules/matrix.nix
··· 1 1 ############################################################################## 2 - # Matrix Synapse homeserver module — NixOS module. 2 + # Matrix Synapse homeserver — NixOS module. 3 3 # 4 4 # Architecture: 5 - # Synapse (127.0.0.1:cfg.port) 5 + # Synapse (127.0.0.1:cfg.matrix.port) 6 6 # ↑ reverse proxy 7 - # Caddy (127.0.0.1:cfg.caddyPort — internal only, no TLS here) 8 - # ↑ Cloudflare tunnel (cloudflared — outbound only, no firewall ports needed) 9 - # 10 - # Non-secret settings live in settings/config/matrix.nix. 11 - # Secrets decrypted by ragenix at activation time. 7 + # Caddy (127.0.0.1:cfg.matrix.caddyPort — internal only, no TLS here) 8 + # ↑ Cloudflare tunnel (outbound only, no firewall ports needed) 12 9 # 13 - # Matrix delegation: 14 - # Since server_name (ewancroft.uk) differs from hostname (matrix.ewancroft.uk), 15 - # you need to set up .well-known delegation on your main website. 10 + # Secrets (sops-encrypted, age backend): 11 + # secrets/matrix.env — KEY=value env file, must contain: 12 + # REGISTRATION_SHARED_SECRET pwgen -s 64 1 13 + # MACAROON_SECRET_KEY pwgen -s 64 1 16 14 # 17 - # Add to your main website's nginx/caddy config at ewancroft.uk: 15 + # Matrix .well-known delegation: 16 + # Since server_name (ewancroft.uk) differs from the Matrix hostname, 17 + # serve at ewancroft.uk: 18 18 # /.well-known/matrix/server → {"m.server": "matrix.ewancroft.uk:443"} 19 - # /.well-known/matrix/client → see example below 20 - # 21 - # Example .well-known/matrix/client (serve as application/json): 22 - # { 23 - # "m.homeserver": { 24 - # "base_url": "https://matrix.ewancroft.uk" 25 - # } 26 - # } 27 - # 28 - # Example .well-known/matrix/server (serve as application/json): 29 - # { 30 - # "m.server": "matrix.ewancroft.uk:443" 31 - # } 32 - # 33 - # Required secrets (set in secrets/age/matrix.env.age as KEY=value pairs): 34 - # REGISTRATION_SHARED_SECRET # Generate with: pwgen -s 64 1 35 - # MACAROON_SECRET_KEY # Generate with: pwgen -s 64 1 36 - # 37 - # Cloudflare tunnel setup (one-time, outside Nix): 38 - # Handled by modules/cloudflare-tunnel.nix. 39 - # See that module for setup instructions. 19 + # /.well-known/matrix/client → {"m.homeserver": {"base_url": "https://matrix.ewancroft.uk"}} 40 20 ############################################################################## 41 21 { 42 22 config, 43 23 lib, 44 - self, 45 - cfgLib, 46 24 ... 47 25 }: 48 - 49 26 let 50 - cfg = cfgLib.cfg.matrix; 51 - synapsePort = toString cfg.port; 52 - caddyPort = toString cfg.caddyPort; 53 - matrixHost = cfg.hostname; 27 + cfg = config.myConfig; 28 + matrix = cfg.matrix; 29 + synapsePort = toString matrix.port; 30 + caddyPort = toString matrix.caddyPort; 54 31 in 55 - lib.mkIf cfg.enable { 32 + lib.mkIf cfg.services.matrix.enable { 56 33 57 - # ── Secrets ────────────────────────────────────────────────────────────────── 58 - age.secrets."matrix.env" = { 59 - file = self + /secrets/age/matrix.env.age; 34 + sops.secrets."matrix.env" = { 35 + sopsFile = ../secrets/matrix.env; 36 + format = "binary"; 60 37 owner = "matrix-synapse"; 61 38 group = "matrix-synapse"; 62 39 mode = "0400"; 63 40 }; 64 41 65 - # ── Matrix Synapse service ──────────────────────────────────────────────────── 66 42 services.matrix-synapse = { 67 43 enable = true; 68 44 dataDir = "/srv/matrix-synapse"; 69 45 70 46 settings = { 71 - server_name = cfg.serverName; # Domain used in Matrix IDs (@user:ewancroft.uk) 72 - 73 - # Public base URL for client-server API 74 - public_baseurl = "https://${matrixHost}"; 47 + server_name = matrix.serverName; 48 + public_baseurl = "https://${matrix.hostname}"; 75 49 76 - # Listener configuration 77 50 listeners = [ 78 51 { 79 - port = cfg.port; 52 + port = matrix.port; 80 53 bind_addresses = [ "127.0.0.1" ]; 81 54 type = "http"; 82 55 tls = false; 83 56 x_forwarded = true; 84 - 85 57 resources = [ 86 58 { 87 59 names = [ ··· 94 66 } 95 67 ]; 96 68 97 - # Database - using PostgreSQL for better performance 98 69 database = { 99 70 name = "psycopg2"; 100 - args = { 101 - database = "matrix-synapse"; 102 - }; 71 + args.database = "matrix-synapse"; 103 72 }; 104 73 105 - # Enable registration (you may want to disable this and use registration_shared_secret) 106 74 enable_registration = false; 107 - 108 - # Allow guests (optional) 109 75 allow_guest_access = false; 76 + url_preview_enabled = true; 110 77 111 - # URL previews 112 - url_preview_enabled = true; 113 78 url_preview_ip_range_blacklist = [ 114 79 "127.0.0.0/8" 115 80 "10.0.0.0/8" ··· 122 87 "fc00::/7" 123 88 ]; 124 89 125 - # Media 126 90 max_upload_size = "50M"; 127 - 128 - # Security 129 91 suppress_key_server_warning = true; 130 92 }; 131 93 132 - # Use environment file for secrets 133 - extraConfigFiles = [ config.age.secrets."matrix.env".path ]; 94 + extraConfigFiles = [ config.sops.secrets."matrix.env".path ]; 134 95 }; 135 96 136 - # Enable PostgreSQL for Synapse 137 97 services.postgresql = { 138 98 enable = true; 139 99 dataDir = "/srv/postgresql"; ··· 146 106 ]; 147 107 }; 148 108 149 - # Restart policy for Synapse 150 109 systemd.services.matrix-synapse = { 151 110 serviceConfig = { 152 111 Restart = lib.mkForce "always"; 153 - RestartSec = cfgLib.cfg.server.servicePolicy.restartSec; 112 + RestartSec = cfg.server.servicePolicy.restartSec; 154 113 }; 155 114 unitConfig = { 156 - StartLimitIntervalSec = cfgLib.cfg.server.servicePolicy.startLimitIntervalSec; 157 - StartLimitBurst = cfgLib.cfg.server.servicePolicy.startLimitBurst; 115 + StartLimitIntervalSec = cfg.server.servicePolicy.startLimitIntervalSec; 116 + StartLimitBurst = cfg.server.servicePolicy.startLimitBurst; 158 117 }; 159 118 }; 160 119 161 - # ── Caddy reverse proxy ─────────────────────────────────────────────────────── 162 - # Listens on localhost:caddyPort only — never exposed publicly. 163 - # Cloudflare handles TLS; Caddy receives plain HTTP from the tunnel daemon. 164 - # Using http:// prefix disables Caddy's automatic HTTPS / ACME entirely. 165 - # 166 - # Note: Caddy service itself is enabled by modules/caddy.nix. 167 120 services.caddy.virtualHosts."http://127.0.0.1:${caddyPort}" = { 168 121 extraConfig = '' 169 - # Handle Matrix client-server and server-server APIs 170 122 handle { 171 123 reverse_proxy http://127.0.0.1:${synapsePort} 172 124 } 173 125 ''; 174 126 }; 175 - 176 - # ── Firewall ────────────────────────────────────────────────────────────────── 177 - # Cloudflare tunnel is configured by modules/cloudflare-tunnel.nix. 178 - # SSH is handled by modules/server/firewall.nix and modules/server/ssh.nix. 179 127 }
+788
modules/options.nix
··· 1 + # Central NixOS module options — single source of truth for all configurable 2 + # values shared across hosts, modules, and home-manager. 3 + # 4 + # System modules access these via `config.myConfig.*`. 5 + # Home-manager modules access them via `osConfig.myConfig.*`. 6 + # 7 + # Per-host overrides live in hosts/<name>/default.nix. 8 + { 9 + lib, 10 + ... 11 + }: 12 + let 13 + inherit (lib) mkOption types; 14 + 15 + # -------------------------------------------------------------------------- 16 + # Type aliases 17 + # -------------------------------------------------------------------------- 18 + str = types.str; 19 + int = types.int; 20 + bool = types.bool; 21 + listStr = types.listOf types.str; 22 + nullStr = types.nullOr types.str; 23 + 24 + in 25 + { 26 + options.myConfig = { 27 + 28 + # ── User ────────────────────────────────────────────────────────────────── 29 + user = { 30 + username = mkOption { 31 + type = str; 32 + default = "ewan"; 33 + }; 34 + fullName = mkOption { 35 + type = str; 36 + default = "Ewan Croft"; 37 + }; 38 + email = mkOption { 39 + type = str; 40 + default = "git@ewancroft.uk"; 41 + }; 42 + }; 43 + 44 + # ── System ──────────────────────────────────────────────────────────────── 45 + stateVersion = mkOption { 46 + type = str; 47 + default = "25.11"; 48 + description = "NixOS / home-manager state version."; 49 + }; 50 + 51 + timeZone = mkOption { 52 + type = str; 53 + default = "Europe/London"; 54 + }; 55 + 56 + locale = mkOption { 57 + type = str; 58 + default = "en_GB.UTF-8"; 59 + }; 60 + 61 + # ── Host type ───────────────────────────────────────────────────────────── 62 + isDesktop = mkOption { 63 + type = bool; 64 + default = false; 65 + description = "Whether this host is an interactive desktop system."; 66 + }; 67 + 68 + # ── Audio ───────────────────────────────────────────────────────────────── 69 + audio = { 70 + enable = mkOption { 71 + type = bool; 72 + default = true; 73 + }; 74 + backend = mkOption { 75 + type = types.enum [ 76 + "pipewire" 77 + "pulseaudio" 78 + ]; 79 + default = "pipewire"; 80 + }; 81 + }; 82 + 83 + # ── Gaming ──────────────────────────────────────────────────────────────── 84 + gaming = { 85 + enable = mkOption { 86 + type = bool; 87 + default = false; 88 + }; 89 + steam = { 90 + enable = mkOption { 91 + type = bool; 92 + default = true; 93 + }; 94 + openFirewall = mkOption { 95 + type = bool; 96 + default = false; 97 + }; 98 + }; 99 + }; 100 + 101 + # ── Packages ────────────────────────────────────────────────────────────── 102 + packages = { 103 + common = mkOption { 104 + type = listStr; 105 + default = [ 106 + "fastfetch" 107 + "btop" 108 + "eza" 109 + "bat" 110 + "ripgrep" 111 + "fd" 112 + "fzf" 113 + "tree" 114 + "git" 115 + "lazygit" 116 + "unzip" 117 + "zip" 118 + "nano" 119 + "tmux" 120 + "openssh" 121 + "wget" 122 + "curl" 123 + "rsync" 124 + ]; 125 + }; 126 + 127 + development = mkOption { 128 + type = listStr; 129 + default = [ 130 + "nil" 131 + "nixfmt-rfc-style" 132 + "git-filter-repo" 133 + "gh" 134 + "go" 135 + "nodejs_22" 136 + "python313" 137 + "bun" 138 + "pnpm" 139 + "rustup" 140 + "dotnet-sdk" 141 + "gopls" 142 + "golangci-lint" 143 + "delve" 144 + "pipx" 145 + "uv" 146 + "ruff" 147 + "pyright" 148 + "shellcheck" 149 + "shfmt" 150 + "cmake" 151 + "autoconf" 152 + "libtool" 153 + "pkgconf" 154 + "m4" 155 + "ffmpeg" 156 + "exiftool" 157 + "atomicparsley" 158 + "get_iplayer" 159 + "tailscale" 160 + "websocat" 161 + "nmap" 162 + "jq" 163 + "zstd" 164 + "xz" 165 + "lz4" 166 + "brotli" 167 + "sqlite" 168 + "tesseract" 169 + "openjdk21" 170 + "php" 171 + "ollama" 172 + ]; 173 + }; 174 + 175 + fonts = mkOption { 176 + type = listStr; 177 + default = [ 178 + "fira-code" 179 + "jetbrains-mono" 180 + "meslo-lg" 181 + "roboto-mono" 182 + "sauce-code-pro" 183 + "ubuntu-mono" 184 + ]; 185 + }; 186 + 187 + linux = mkOption { 188 + type = listStr; 189 + default = [ "vlc" ]; 190 + }; 191 + 192 + desktop = mkOption { 193 + type = listStr; 194 + default = [ 195 + "papirus-icon-theme" 196 + "discord" 197 + "signal-desktop" 198 + "element-desktop" 199 + "spotify" 200 + "obsidian" 201 + "libreoffice-fresh" 202 + "gimp" 203 + "inkscape" 204 + "parsec-bin" 205 + "prismlauncher" 206 + ]; 207 + }; 208 + 209 + gaming = mkOption { 210 + type = listStr; 211 + default = [ 212 + "steam" 213 + "lutris" 214 + "wine" 215 + "winetricks" 216 + ]; 217 + }; 218 + 219 + server = mkOption { 220 + type = listStr; 221 + default = [ ]; 222 + description = "Server-only packages (added on top of common + development)."; 223 + }; 224 + 225 + darwin = mkOption { 226 + type = listStr; 227 + default = [ 228 + "coreutils" 229 + "parallel" 230 + "stow" 231 + "netcat" 232 + "openssl" 233 + "readline" 234 + "ncurses" 235 + "pcre" 236 + "pcre2" 237 + "libffi" 238 + "discord" 239 + "signal-desktop-bin" 240 + "obsidian" 241 + "vscode" 242 + "spotify" 243 + "transmission_4" 244 + ]; 245 + }; 246 + }; 247 + 248 + # ── Desktop theming ─────────────────────────────────────────────────────── 249 + desktop = { 250 + environment = mkOption { 251 + type = str; 252 + default = "plasma6"; 253 + description = "Desktop environment name (e.g. plasma6, gnome)."; 254 + }; 255 + 256 + displayManager = mkOption { 257 + type = str; 258 + default = "sddm"; 259 + description = "Display manager (e.g. sddm, gdm)."; 260 + }; 261 + 262 + uiFont = mkOption { 263 + type = str; 264 + default = "Noto Sans"; 265 + }; 266 + 267 + uiFontSize = mkOption { 268 + type = int; 269 + default = 10; 270 + }; 271 + 272 + monoFontBase = mkOption { 273 + type = str; 274 + default = "FiraCode"; 275 + description = "Base monospace font family (no Nerd Font suffix)."; 276 + }; 277 + 278 + monoFontFamily = mkOption { 279 + type = str; 280 + default = "FiraCode Nerd Font Mono"; 281 + description = "Full Nerd Font monospace family (for terminals and KDE)."; 282 + }; 283 + 284 + monoFontSize = mkOption { 285 + type = int; 286 + default = 11; 287 + }; 288 + 289 + theme = mkOption { 290 + type = str; 291 + default = "Catppuccin-Mocha-Standard-Green-Dark"; 292 + description = "GTK theme name."; 293 + }; 294 + 295 + iconTheme = mkOption { 296 + type = str; 297 + default = "Papirus-Dark"; 298 + }; 299 + 300 + plasma = { 301 + colorScheme = mkOption { 302 + type = str; 303 + default = "CatppuccinMochaGreen"; 304 + }; 305 + 306 + desktopTheme = mkOption { 307 + type = str; 308 + default = "breeze-dark"; 309 + }; 310 + 311 + excludePackages = mkOption { 312 + type = listStr; 313 + default = [ 314 + "oxygen" 315 + "elisa" 316 + ]; 317 + }; 318 + }; 319 + }; 320 + 321 + # ── SSH ─────────────────────────────────────────────────────────────────── 322 + ssh = { 323 + keyFile = mkOption { 324 + type = str; 325 + default = "~/.ssh/id_ed25519"; 326 + }; 327 + }; 328 + 329 + # ── Git ─────────────────────────────────────────────────────────────────── 330 + git = { 331 + defaultBranch = mkOption { 332 + type = str; 333 + default = "main"; 334 + }; 335 + editor = mkOption { 336 + type = str; 337 + default = "code --wait"; 338 + }; 339 + lfs.enable = mkOption { 340 + type = bool; 341 + default = true; 342 + }; 343 + signing = { 344 + enabled = mkOption { 345 + type = bool; 346 + default = true; 347 + }; 348 + format = mkOption { 349 + type = str; 350 + default = "ssh"; 351 + }; 352 + }; 353 + }; 354 + 355 + # ── Development / VS Code ───────────────────────────────────────────────── 356 + development.vscode = { 357 + enable = mkOption { 358 + type = bool; 359 + default = true; 360 + }; 361 + colorTheme = mkOption { 362 + type = str; 363 + default = "Catppuccin Mocha"; 364 + }; 365 + iconTheme = mkOption { 366 + type = str; 367 + default = "catppuccin-vsc-icons"; 368 + }; 369 + fontSize = mkOption { 370 + type = int; 371 + default = 14; 372 + }; 373 + terminalFontSize = mkOption { 374 + type = int; 375 + default = 13; 376 + }; 377 + lineHeight = mkOption { 378 + type = int; 379 + default = 22; 380 + }; 381 + fontLigatures = mkOption { 382 + type = bool; 383 + default = true; 384 + }; 385 + }; 386 + 387 + # ── Secrets ─────────────────────────────────────────────────────────────── 388 + secrets = { 389 + docker.enable = mkOption { 390 + type = bool; 391 + default = true; 392 + }; 393 + claude.enable = mkOption { 394 + type = bool; 395 + default = true; 396 + }; 397 + duckdns.enable = mkOption { 398 + type = bool; 399 + default = false; 400 + }; 401 + }; 402 + 403 + # ── Server service toggles ──────────────────────────────────────────────── 404 + services = { 405 + forgejo.enable = mkOption { 406 + type = bool; 407 + default = false; 408 + }; 409 + pds.enable = mkOption { 410 + type = bool; 411 + default = false; 412 + }; 413 + matrix.enable = mkOption { 414 + type = bool; 415 + default = false; 416 + }; 417 + cloudflare.enable = mkOption { 418 + type = bool; 419 + default = false; 420 + }; 421 + }; 422 + 423 + # ── Forgejo ─────────────────────────────────────────────────────────────── 424 + forgejo = { 425 + hostname = mkOption { 426 + type = str; 427 + default = "git.ewancroft.uk"; 428 + }; 429 + port = mkOption { 430 + type = int; 431 + default = 3001; 432 + }; 433 + caddyPort = mkOption { 434 + type = int; 435 + default = 3002; 436 + }; 437 + appName = mkOption { 438 + type = str; 439 + default = "Ewan's Git"; 440 + }; 441 + disableRegistration = mkOption { 442 + type = bool; 443 + default = true; 444 + }; 445 + }; 446 + 447 + # ── PDS ─────────────────────────────────────────────────────────────────── 448 + pds = { 449 + hostname = mkOption { 450 + type = str; 451 + default = "pds.ewancroft.uk"; 452 + }; 453 + port = mkOption { 454 + type = int; 455 + default = 3000; 456 + }; 457 + caddyPort = mkOption { 458 + type = int; 459 + default = 2020; 460 + }; 461 + adminEmail = mkOption { 462 + type = str; 463 + default = "pds@ewancroft.uk"; 464 + }; 465 + serviceHandleDomains = mkOption { 466 + type = listStr; 467 + default = [ ".ewancroft.uk" ]; 468 + }; 469 + crawlers = mkOption { 470 + type = listStr; 471 + default = [ 472 + "https://bsky.network" 473 + "https://relay.cerulea.blue" 474 + "https://relay.fire.hose.cam" 475 + "https://relay2.fire.hose.cam" 476 + "https://relay3.fr.hose.cam" 477 + "https://relay.hayescmd.net" 478 + "https://relay.xero.systems" 479 + "https://relay.upcloud.world" 480 + "https://relay.feeds.blue" 481 + "https://atproto.africa" 482 + ]; 483 + }; 484 + }; 485 + 486 + # ── Matrix ──────────────────────────────────────────────────────────────── 487 + matrix = { 488 + hostname = mkOption { 489 + type = str; 490 + default = "matrix.ewancroft.uk"; 491 + }; 492 + serverName = mkOption { 493 + type = str; 494 + default = "ewancroft.uk"; 495 + }; 496 + port = mkOption { 497 + type = int; 498 + default = 8008; 499 + }; 500 + caddyPort = mkOption { 501 + type = int; 502 + default = 8448; 503 + }; 504 + }; 505 + 506 + # ── Cloudflare ──────────────────────────────────────────────────────────── 507 + cloudflare = { 508 + tunnelId = mkOption { 509 + type = str; 510 + default = "63ec1b18-1358-4ee2-9093-713b4e7d9325"; 511 + description = "Cloudflare Tunnel UUID from `cloudflared tunnel create`."; 512 + }; 513 + }; 514 + 515 + # ── Server infrastructure ───────────────────────────────────────────────── 516 + server = { 517 + 518 + timemachine = { 519 + enable = mkOption { 520 + type = bool; 521 + default = false; 522 + }; 523 + shareName = mkOption { 524 + type = str; 525 + default = "TimeMachine"; 526 + }; 527 + path = mkOption { 528 + type = str; 529 + default = "/srv/timemachine"; 530 + }; 531 + maxSizeGB = mkOption { 532 + type = int; 533 + default = 0; 534 + description = "Soft cap in GB reported to macOS. 0 = unlimited."; 535 + }; 536 + validUsers = mkOption { 537 + type = listStr; 538 + default = [ ]; 539 + }; 540 + }; 541 + 542 + sshd = { 543 + enable = mkOption { 544 + type = bool; 545 + default = true; 546 + }; 547 + permitRootLogin = mkOption { 548 + type = str; 549 + default = "no"; 550 + }; 551 + passwordAuthentication = mkOption { 552 + type = bool; 553 + default = false; 554 + }; 555 + kbdInteractiveAuthentication = mkOption { 556 + type = bool; 557 + default = false; 558 + }; 559 + port = mkOption { 560 + type = int; 561 + default = 22; 562 + }; 563 + maxAuthTries = mkOption { 564 + type = int; 565 + default = 3; 566 + }; 567 + clientAliveInterval = mkOption { 568 + type = int; 569 + default = 300; 570 + }; 571 + clientAliveCountMax = mkOption { 572 + type = int; 573 + default = 2; 574 + }; 575 + x11Forwarding = mkOption { 576 + type = bool; 577 + default = false; 578 + }; 579 + }; 580 + 581 + fail2ban = { 582 + enable = mkOption { 583 + type = bool; 584 + default = true; 585 + }; 586 + maxRetry = mkOption { 587 + type = int; 588 + default = 5; 589 + }; 590 + banTime = mkOption { 591 + type = int; 592 + default = 600; 593 + }; 594 + findTime = mkOption { 595 + type = int; 596 + default = 600; 597 + }; 598 + }; 599 + 600 + firewall = { 601 + enable = mkOption { 602 + type = bool; 603 + default = true; 604 + }; 605 + allowPing = mkOption { 606 + type = bool; 607 + default = true; 608 + }; 609 + allowedTCPPorts = mkOption { 610 + type = types.listOf int; 611 + default = [ 22 ]; 612 + }; 613 + allowedUDPPorts = mkOption { 614 + type = types.listOf int; 615 + default = [ ]; 616 + }; 617 + }; 618 + 619 + servicePolicy = { 620 + restartSec = mkOption { 621 + type = int; 622 + default = 5; 623 + }; 624 + startLimitIntervalSec = mkOption { 625 + type = int; 626 + default = 300; 627 + }; 628 + startLimitBurst = mkOption { 629 + type = int; 630 + default = 5; 631 + }; 632 + }; 633 + 634 + storage.srv = { 635 + device = mkOption { 636 + type = str; 637 + default = "/dev/sdb"; 638 + }; 639 + fsType = mkOption { 640 + type = str; 641 + default = "ext4"; 642 + }; 643 + options = mkOption { 644 + type = listStr; 645 + default = [ 646 + "defaults" 647 + "noatime" 648 + ]; 649 + }; 650 + }; 651 + 652 + cockpit = { 653 + enable = mkOption { 654 + type = bool; 655 + default = true; 656 + }; 657 + port = mkOption { 658 + type = int; 659 + default = 9090; 660 + }; 661 + }; 662 + }; 663 + 664 + # ── Darwin ──────────────────────────────────────────────────────────────── 665 + darwin = { 666 + keyboard = { 667 + enableKeyMapping = mkOption { 668 + type = bool; 669 + default = true; 670 + }; 671 + remapCapsLockToControl = mkOption { 672 + type = bool; 673 + default = false; 674 + }; 675 + }; 676 + 677 + startup.chime = mkOption { 678 + type = bool; 679 + default = true; 680 + }; 681 + 682 + security.touchIdForSudo = mkOption { 683 + type = bool; 684 + default = true; 685 + }; 686 + 687 + homebrew = { 688 + enable = mkOption { 689 + type = bool; 690 + default = true; 691 + }; 692 + taps = mkOption { 693 + type = listStr; 694 + default = [ ]; 695 + }; 696 + brews = mkOption { 697 + type = listStr; 698 + default = [ 699 + "libmediainfo" 700 + "media-info" 701 + "libzen" 702 + "aribb24" 703 + "dav1d" 704 + "rav1e" 705 + "svt-av1" 706 + "x264" 707 + "x265" 708 + "xvid" 709 + "webp" 710 + "aom" 711 + "jpeg-xl" 712 + "highway" 713 + "flac" 714 + "lame" 715 + "opus" 716 + "vorbis-tools" 717 + "libsndfile" 718 + "libsamplerate" 719 + "rubberband" 720 + "speex" 721 + "theora" 722 + "mpg123" 723 + "little-cms2" 724 + "leptonica" 725 + "rtmpdump" 726 + "srt" 727 + "librist" 728 + "libmms" 729 + "lzo" 730 + "snappy" 731 + "xxhash" 732 + "yyjson" 733 + "freetds" 734 + "unixodbc" 735 + "summarize" 736 + "goat" 737 + "mas" 738 + ]; 739 + }; 740 + casks = mkOption { 741 + type = listStr; 742 + default = [ 743 + "element" 744 + "github" 745 + "claude" 746 + "firefox" 747 + "obs" 748 + "handbrake-app" 749 + "steam" 750 + "epic-games" 751 + "prismlauncher" 752 + "utm" 753 + "cloudflare-warp" 754 + "tailscale-app" 755 + "parsec" 756 + "onyx" 757 + "mos" 758 + "microsoft-excel" 759 + "microsoft-powerpoint" 760 + "microsoft-teams" 761 + "microsoft-word" 762 + "libreoffice" 763 + "logitune" 764 + "logi-options+" 765 + "roblox" 766 + "ea" 767 + "netnewswire" 768 + "altserver" 769 + ]; 770 + }; 771 + masApps = mkOption { 772 + type = types.attrsOf int; 773 + default = { 774 + "Amphetamine" = 937984704; 775 + "OneDrive" = 823766827; 776 + "OP Auto Clicker" = 6754914118; 777 + "Steam Link" = 1246969117; 778 + "TestFlight" = 899247664; 779 + "The Unarchiver" = 425424353; 780 + "WhatsApp" = 310633997; 781 + "Zone Bar" = 6755328989; 782 + }; 783 + }; 784 + }; 785 + }; 786 + 787 + }; 788 + }
+24 -12
modules/packages.nix
··· 1 - { config, pkgs, cfgLib, ... }: 2 - 1 + # System-wide packages for Linux desktop hosts. 2 + { 3 + config, 4 + pkgs, 5 + lib, 6 + ... 7 + }: 3 8 let 4 - cfg = cfgLib.cfg; 5 - resolve = cfgLib.resolvePackages pkgs; 9 + cfg = config.myConfig; 10 + 11 + resolvePackages = 12 + names: 13 + builtins.filter (x: x != null) ( 14 + map ( 15 + name: 16 + if pkgs ? ${name} then 17 + pkgs.${name} 18 + else 19 + builtins.trace "WARNING: package '${name}' not found in nixpkgs, skipping" null 20 + ) names 21 + ); 6 22 in 7 23 { 8 - # System-wide programs with dedicated NixOS module options 9 24 programs = { 10 25 firefox.enable = true; 11 - git.enable = true; 26 + git.enable = true; 12 27 }; 13 28 14 29 environment.systemPackages = 15 - # Common CLI utilities (all systems) 16 - resolve cfg.packages.common 17 - # Cross-platform development packages 18 - ++ resolve cfg.packages.development 19 - # NixOS desktop / GUI apps 20 - ++ resolve cfg.packages.desktop; 30 + resolvePackages cfg.packages.common 31 + ++ resolvePackages cfg.packages.development 32 + ++ lib.optionals cfg.isDesktop (resolvePackages cfg.packages.desktop); 21 33 }
+30 -51
modules/pds.nix
··· 2 2 # Bluesky ATProto Personal Data Server — NixOS module. 3 3 # 4 4 # Architecture: 5 - # PDS (127.0.0.1:cfg.port) 5 + # PDS (127.0.0.1:cfg.pds.port) 6 6 # ↑ reverse proxy 7 - # Caddy (127.0.0.1:cfg.caddyPort — internal only, no TLS here) 8 - # ↑ Cloudflare tunnel (cloudflared — outbound only, no firewall ports needed) 7 + # Caddy (127.0.0.1:cfg.pds.caddyPort — internal only, no TLS here) 8 + # ↑ Cloudflare tunnel (outbound only, no firewall ports needed) 9 9 # 10 - # Non-secret settings live in settings/config/pds.nix. 11 - # Secrets decrypted by ragenix at activation time. 12 - # 13 - # Required secrets (set in secrets/age/pds.env.age as KEY=value pairs): 14 - # PDS_JWT_SECRET openssl rand --hex 16 15 - # PDS_ADMIN_PASSWORD openssl rand --hex 16 16 - # PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX 17 - # openssl ecparam --name secp256k1 --genkey --noout --outform DER \ 18 - # | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32 19 - # PDS_EMAIL_SMTP_URL (optional — for email verification) 20 - # PDS_EMAIL_FROM_ADDRESS (optional — for email verification) 21 - # 22 - # Cloudflare tunnel setup (one-time, outside Nix): 23 - # Handled by modules/cloudflare-tunnel.nix. 24 - # See that module for setup instructions. 10 + # Secrets (sops-encrypted, age backend): 11 + # secrets/pds.env — KEY=value env file, must contain: 12 + # PDS_JWT_SECRET openssl rand --hex 16 13 + # PDS_ADMIN_PASSWORD openssl rand --hex 16 14 + # PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX see pds docs 15 + # PDS_EMAIL_SMTP_URL (optional) 16 + # PDS_EMAIL_FROM_ADDRESS (optional) 25 17 ############################################################################## 26 18 { 27 19 config, 28 20 lib, 29 21 pkgs, 30 - self, 31 - cfgLib, 32 22 ... 33 23 }: 34 - 35 24 let 36 - cfg = cfgLib.cfg.pds; 37 - pdsPort = toString cfg.port; 38 - caddyPort = toString cfg.caddyPort; 25 + cfg = config.myConfig; 26 + pds = cfg.pds; 27 + pdsPort = toString pds.port; 28 + caddyPort = toString pds.caddyPort; 39 29 40 30 # UK Online Safety Act age-assurance static responses. 41 - # Required for UK-based PDS instances (Online Safety Act 2023). 42 - # Source: https://gist.github.com/mary-ext/6e27b24a83838202908808ad528b3318 43 31 ageAssuranceBlocks = '' 44 32 handle /xrpc/app.bsky.unspecced.getAgeAssuranceState { 45 33 header Content-Type "application/json" ··· 61 49 } 62 50 ''; 63 51 in 64 - lib.mkIf cfg.enable { 52 + lib.mkIf cfg.services.pds.enable { 65 53 66 - # ── Secrets ────────────────────────────────────────────────────────────────── 67 - age.secrets."pds.env" = { 68 - file = self + /secrets/age/pds.env.age; 54 + sops.secrets."pds.env" = { 55 + sopsFile = ../secrets/pds.env; 56 + format = "binary"; 69 57 owner = "pds"; 70 58 group = "pds"; 71 59 mode = "0400"; 72 60 }; 73 61 74 - # ── PDS service ─────────────────────────────────────────────────────────────── 75 62 environment.systemPackages = [ pkgs.atproto-goat ]; 76 63 77 64 services.bluesky-pds = { 78 65 enable = true; 79 - environmentFiles = [ config.age.secrets."pds.env".path ]; 66 + environmentFiles = [ config.sops.secrets."pds.env".path ]; 80 67 settings = { 81 68 PDS_DATA_DIRECTORY = "/srv/bluesky-pds"; 82 - PDS_PORT = cfg.port; 83 - PDS_HOSTNAME = cfg.hostname; 84 - PDS_ADMIN_EMAIL = cfg.adminEmail; 85 - PDS_SERVICE_HANDLE_DOMAINS = lib.concatStringsSep "," cfg.serviceHandleDomains; 86 - PDS_CRAWLERS = lib.concatStringsSep "," cfg.crawlers; 69 + PDS_PORT = pds.port; 70 + PDS_HOSTNAME = pds.hostname; 71 + PDS_ADMIN_EMAIL = pds.adminEmail; 72 + PDS_SERVICE_HANDLE_DOMAINS = lib.concatStringsSep "," pds.serviceHandleDomains; 73 + PDS_CRAWLERS = lib.concatStringsSep "," pds.crawlers; 87 74 }; 88 75 }; 89 76 90 77 systemd.services.bluesky-pds = { 91 - serviceConfig.Restart = "always"; 92 - serviceConfig.RestartSec = cfgLib.cfg.server.servicePolicy.restartSec; 78 + serviceConfig = { 79 + Restart = "always"; 80 + RestartSec = cfg.server.servicePolicy.restartSec; 81 + }; 93 82 unitConfig = { 94 - StartLimitIntervalSec = cfgLib.cfg.server.servicePolicy.startLimitIntervalSec; 95 - StartLimitBurst = cfgLib.cfg.server.servicePolicy.startLimitBurst; 83 + StartLimitIntervalSec = cfg.server.servicePolicy.startLimitIntervalSec; 84 + StartLimitBurst = cfg.server.servicePolicy.startLimitBurst; 96 85 }; 97 86 }; 98 87 99 - # ── Caddy reverse proxy ─────────────────────────────────────────────────────── 100 - # Listens on localhost:caddyPort only — never exposed publicly. 101 - # Cloudflare handles TLS; Caddy receives plain HTTP from the tunnel daemon. 102 - # Using http:// prefix disables Caddy's automatic HTTPS / ACME entirely. 103 - # 104 - # Note: Caddy service itself is enabled by modules/caddy.nix. 105 88 services.caddy.virtualHosts."http://127.0.0.1:${caddyPort}" = { 106 89 extraConfig = '' 107 90 ${ageAssuranceBlocks} ··· 110 93 } 111 94 ''; 112 95 }; 113 - 114 - # ── Firewall ────────────────────────────────────────────────────────────────── 115 - # Cloudflare tunnel is configured by modules/cloudflare-tunnel.nix. 116 - # SSH is handled by modules/server/firewall.nix and modules/server/ssh.nix. 117 96 }
+3 -3
modules/server/disable-noise.nix
··· 1 - { lib, ... }: 1 + { ... }: 2 2 { 3 - services.avahi.enable = lib.mkDefault false; 4 - services.printing.enable = lib.mkDefault false; 3 + services.avahi.enable = false; 4 + services.printing.enable = false; 5 5 }
+11 -8
modules/server/firewall.nix
··· 1 - { lib, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + lib, 4 + ... 5 + }: 3 6 let 4 - cfg = cfgLib.cfg; 7 + cfg = config.myConfig; 5 8 in 6 9 { 7 10 networking.firewall = { 8 - enable = lib.mkDefault cfg.server.firewall.enable; 9 - allowedTCPPorts = cfg.server.firewall.allowedTCPPorts; 10 - allowedUDPPorts = cfg.server.firewall.allowedUDPPorts; 11 - 11 + enable = lib.mkDefault cfg.server.firewall.enable; 12 + allowedTCPPorts = cfg.server.firewall.allowedTCPPorts; 13 + allowedUDPPorts = cfg.server.firewall.allowedUDPPorts; 14 + 12 15 # Trust Tailscale interface for inter-host communication 13 16 trustedInterfaces = [ "tailscale0" ]; 14 - 17 + 15 18 # Allow ICMP (ping) if configured 16 19 allowPing = lib.mkDefault cfg.server.firewall.allowPing; 17 20 };
+12 -9
modules/server/intrusion.nix
··· 1 - { lib, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + lib, 4 + ... 5 + }: 3 6 let 4 - cfg = cfgLib.cfg; 7 + cfg = config.myConfig; 5 8 in 6 9 { 7 10 services.fail2ban = { 8 - enable = lib.mkDefault cfg.server.fail2ban.enable; 11 + enable = lib.mkDefault cfg.server.fail2ban.enable; 9 12 maxretry = cfg.server.fail2ban.maxRetry; 10 13 11 14 jails.sshd.settings = { 12 - enabled = true; 13 - port = toString cfg.server.sshd.port; 14 - filter = "sshd"; 15 + enabled = true; 16 + port = toString cfg.server.sshd.port; 17 + filter = "sshd"; 15 18 # NixOS uses the systemd journal — there is no /var/log/auth.log. 16 - backend = "systemd"; 19 + backend = "systemd"; 17 20 maxretry = cfg.server.fail2ban.maxRetry; 18 21 findtime = cfg.server.fail2ban.findTime; 19 - bantime = cfg.server.fail2ban.banTime; 22 + bantime = cfg.server.fail2ban.banTime; 20 23 }; 21 24 }; 22 25 }
+32 -15
modules/server/packages.nix
··· 1 - { config, pkgs, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + pkgs, 4 + ... 5 + }: 3 6 let 4 - cfg = cfgLib.cfg; 5 - resolve = cfgLib.resolvePackages pkgs; 7 + cfg = config.myConfig; 8 + 9 + resolvePackages = 10 + names: 11 + builtins.filter (x: x != null) ( 12 + map ( 13 + name: 14 + if pkgs ? ${name} then 15 + pkgs.${name} 16 + else 17 + builtins.trace "WARNING: package '${name}' not found in nixpkgs, skipping" null 18 + ) names 19 + ); 6 20 in 7 21 { 8 22 environment.systemPackages = 9 - # Common CLI utilities (shared with laptop via settings/config/packages.nix) 10 - resolve cfg.packages.common 23 + # Common CLI utilities (shared with laptop via myConfig.packages.common) 24 + resolvePackages cfg.packages.common 11 25 # Cross-platform development tools (shared with laptop) 12 - ++ resolve cfg.packages.development 13 - # Server-only extras (defined in settings/config/packages.nix → server) 14 - ++ resolve cfg.packages.server 26 + ++ resolvePackages cfg.packages.development 27 + # Server-only extras 28 + ++ resolvePackages cfg.packages.server 15 29 # Server-specific tools not suited to the shared package lists 16 30 ++ (with pkgs; [ 17 31 # System inspection ··· 22 36 usbutils 23 37 24 38 # Network diagnostics 25 - bind # dig / nslookup 26 - inetutils # telnet, ftp 39 + bind # dig / nslookup 40 + inetutils # telnet, ftp 27 41 traceroute 28 42 mtr 29 43 ··· 40 54 ]); 41 55 42 56 programs.command-not-found.enable = true; 43 - programs.bash.completion.enable = true; 57 + programs.bash.completion.enable = true; 44 58 45 - # ── Restrict imperative package installation ────────────────────────────────── 59 + # ── Restrict imperative package installation ────────────────────────────── 46 60 # Only root and members of the wheel group may connect to the Nix daemon 47 - # to build or install packages. This keeps the server fully declarative: 61 + # to build or install packages. This keeps the server fully declarative: 48 62 # non-privileged users cannot run `nix-env -i` or `nix profile install`. 49 - nix.settings.allowed-users = [ "root" "@wheel" ]; 63 + nix.settings.allowed-users = [ 64 + "root" 65 + "@wheel" 66 + ]; 50 67 }
+4 -6
modules/server/services.nix
··· 1 - { lib, cfgLib, ... }: 2 - 3 - let 4 - cfg = cfgLib.cfg; 5 - in 1 + { 2 + ... 3 + }: 6 4 { 7 5 # Tailscale VPN for inter-host communication 8 6 services.tailscale.enable = true; 9 7 10 8 # SSH daemon (server hardened configuration from modules/server/ssh.nix) 11 - # No additional SSH config needed here - it's handled by server-hardened profile 9 + # No additional SSH config needed here — it's handled by server-hardened profile. 12 10 }
+15 -12
modules/server/ssh.nix
··· 1 - { lib, cfgLib, ... }: 2 - 1 + { 2 + config, 3 + lib, 4 + ... 5 + }: 3 6 let 4 - cfg = cfgLib.cfg; 7 + cfg = config.myConfig; 5 8 in 6 9 { 7 10 services.openssh = { 8 11 enable = lib.mkDefault cfg.server.sshd.enable; 9 - ports = [ cfg.server.sshd.port ]; 12 + ports = [ cfg.server.sshd.port ]; 10 13 11 14 settings = { 12 - PermitRootLogin = cfg.server.sshd.permitRootLogin; 13 - PasswordAuthentication = cfg.server.sshd.passwordAuthentication; 14 - KbdInteractiveAuthentication = cfg.server.sshd.kbdInteractiveAuthentication; 15 - AllowUsers = [ cfg.user.username ]; 16 - MaxAuthTries = cfg.server.sshd.maxAuthTries; 17 - ClientAliveInterval = cfg.server.sshd.clientAliveInterval; 18 - ClientAliveCountMax = cfg.server.sshd.clientAliveCountMax; 19 - X11Forwarding = cfg.server.sshd.x11Forwarding; 15 + PermitRootLogin = cfg.server.sshd.permitRootLogin; 16 + PasswordAuthentication = cfg.server.sshd.passwordAuthentication; 17 + KbdInteractiveAuthentication = cfg.server.sshd.kbdInteractiveAuthentication; 18 + AllowUsers = [ cfg.user.username ]; 19 + MaxAuthTries = cfg.server.sshd.maxAuthTries; 20 + ClientAliveInterval = cfg.server.sshd.clientAliveInterval; 21 + ClientAliveCountMax = cfg.server.sshd.clientAliveCountMax; 22 + X11Forwarding = cfg.server.sshd.x11Forwarding; 20 23 }; 21 24 }; 22 25 }
+18 -12
modules/server/storage.nix
··· 4 4 # What this module does: 5 5 # 1. Runs a one-shot systemd service BEFORE the mount that formats the 6 6 # device with ext4 if it has no filesystem yet (safe: skipped if already 7 - # formatted). Set the device in settings/config/server.nix → storage.srv. 7 + # formatted). Set the device via myConfig.server.storage.srv.device. 8 8 # 2. Declares the /srv fileSystem entry (NixOS handles the actual mount). 9 9 # 3. Uses systemd-tmpfiles to create every required subdirectory with the 10 10 # correct ownership, after the mount is up. ··· 16 16 # /srv/bluesky-pds — Bluesky ATProto PDS data 17 17 # /srv/www — Static websites / reverse-proxied web roots 18 18 ############################################################################## 19 - { config, lib, pkgs, cfgLib, ... }: 20 - 19 + { 20 + config, 21 + pkgs, 22 + ... 23 + }: 21 24 let 22 - srv = cfgLib.cfg.server.storage.srv; 25 + srv = config.myConfig.server.storage.srv; 23 26 device = srv.device; 24 27 in 25 28 { 26 - # ── 1. Auto-format ──────────────────────────────────────────────────────────── 27 - # Runs before the filesystem is mounted. Formats with ext4 + label "srv" 29 + # ── 1. Auto-format ────────────────────────────────────────────────────────── 30 + # Runs before the filesystem is mounted. Formats with ext4 + label "srv" 28 31 # only if blkid reports no filesystem type on the device. 29 32 systemd.services."srv-autoformat" = { 30 33 description = "Auto-format ${device} as ext4 if unformatted"; 31 34 32 35 # Must complete before the mount unit tries to mount /srv 33 - before = [ "srv.mount" ]; 36 + before = [ "srv.mount" ]; 34 37 wantedBy = [ "srv.mount" ]; 35 38 36 39 # Only attempt if the device node actually exists (won't run in VM/CI ··· 38 41 unitConfig.ConditionPathExists = device; 39 42 40 43 serviceConfig = { 41 - Type = "oneshot"; 44 + Type = "oneshot"; 42 45 RemainAfterExit = true; 43 46 }; 44 47 45 - path = [ pkgs.util-linux pkgs.e2fsprogs ]; 48 + path = [ 49 + pkgs.util-linux 50 + pkgs.e2fsprogs 51 + ]; 46 52 47 53 script = '' 48 54 if blkid "${device}" | grep -q 'TYPE='; then ··· 54 60 ''; 55 61 }; 56 62 57 - # ── 2. /srv mount ───────────────────────────────────────────────────────────── 63 + # ── 2. /srv mount ────────────────────────────────────────────────────────── 58 64 fileSystems."/srv" = { 59 65 inherit (srv) device fsType options; 60 66 # Require the autoformat service to run first 61 - depends = [ "srv-autoformat.service" ]; 67 + depends = [ "srv-autoformat.service" ]; 62 68 neededForBoot = false; 63 69 }; 64 70 65 - # ── 3. Subdirectory creation ────────────────────────────────────────────────── 71 + # ── 3. Subdirectory creation ──────────────────────────────────────────────── 66 72 # systemd-tmpfiles creates these after /srv is mounted. 67 73 # 'd' = create directory if missing, set mode/owner, never remove on cleanup. 68 74 systemd.tmpfiles.rules = [
+62 -62
modules/server/timemachine.nix
··· 2 2 # Time Machine backup target 3 3 # 4 4 # Exposes a Samba share with the `fruit` VFS module so macOS clients can 5 - # use this server as a Time Machine destination. AFP was dropped in macOS 5 + # use this server as a Time Machine destination. AFP was dropped in macOS 6 6 # Ventura, so SMB + fruit is the correct modern approach. 7 7 # 8 - # What this module does: 9 - # 1. Configures Samba (smbd) with the fruit / streams_xattr VFS stack that 10 - # macOS requires for reliable extended-attribute and resource-fork handling. 11 - # 2. Advertises the share via Avahi (mDNS) so Macs discover it automatically 12 - # in System Settings -> General -> Time Machine. 13 - # 3. Creates /srv/timemachine with correct ownership via systemd-tmpfiles. 14 - # 15 8 # Setup (one-time, after deploying): 16 9 # - Add a Samba user for every Mac that will back up: 17 10 # sudo smbpasswd -a <username> 18 11 # - In macOS System Settings -> General -> Time Machine, click "Add Backup 19 12 # Disk...", choose the advertised share, and authenticate. 20 13 # 21 - # Settings knobs (settings/config/server.nix -> timemachine): 14 + # Settings knobs (myConfig.server.timemachine): 22 15 # enable - master toggle 23 16 # shareName - name visible to macOS (default "TimeMachine") 24 17 # path - filesystem path for backup data (default /srv/timemachine) 25 18 # maxSizeGB - soft storage cap reported to macOS (0 = unlimited) 26 19 # validUsers - list of Samba usernames allowed to write backups 27 20 ############################################################################## 28 - { config, lib, pkgs, cfgLib, ... }: 29 - 21 + { 22 + config, 23 + lib, 24 + ... 25 + }: 30 26 let 31 - tm = cfgLib.cfg.server.timemachine; 32 - cap = if tm.maxSizeGB > 0 then toString tm.maxSizeGB + " G" else ""; 27 + tm = config.myConfig.server.timemachine; 28 + cap = if tm.maxSizeGB > 0 then toString tm.maxSizeGB + " G" else ""; 33 29 users = lib.concatStringsSep " " tm.validUsers; 34 30 in 35 31 lib.mkIf tm.enable { 36 32 37 - # ── Samba ──────────────────────────────────────────────────────────────────── 33 + # ── Samba ────────────────────────────────────────────────────────────────── 38 34 services.samba = { 39 - enable = true; 40 - openFirewall = false; # we manage ports explicitly below 35 + enable = true; 36 + openFirewall = false; # ports managed explicitly below 41 37 42 38 settings = { 43 39 global = { 44 - # Server identity 45 - "workgroup" = "WORKGROUP"; 40 + "workgroup" = "WORKGROUP"; 46 41 "server string" = config.networking.hostName; 47 - "server role" = "standalone server"; 42 + "server role" = "standalone server"; 48 43 49 - # Disable printer sharing 50 44 "load printers" = "no"; 51 45 "printcap name" = "/dev/null"; 52 46 53 - # macOS interoperability -- fruit VFS stack 54 - "vfs objects" = "catia fruit streams_xattr"; 55 - "fruit:metadata" = "stream"; 56 - "fruit:model" = "MacSamba"; 57 - "fruit:posix_rename" = "yes"; 58 - "fruit:veto_appledouble" = "no"; 47 + # macOS interoperability — fruit VFS stack 48 + "vfs objects" = "catia fruit streams_xattr"; 49 + "fruit:metadata" = "stream"; 50 + "fruit:model" = "MacSamba"; 51 + "fruit:posix_rename" = "yes"; 52 + "fruit:veto_appledouble" = "no"; 59 53 "fruit:wipe_intentionally_left_blank_rfork" = "yes"; 60 - "fruit:delete_empty_adfiles" = "yes"; 54 + "fruit:delete_empty_adfiles" = "yes"; 61 55 62 - # Security 63 - "security" = "user"; 56 + "security" = "user"; 64 57 "map to guest" = "Never"; 65 - "ntlm auth" = "yes"; # required for older macOS clients 58 + "ntlm auth" = "yes"; # required for older macOS clients 66 59 "min protocol" = "SMB2"; 67 - "smb encrypt" = "desired"; 60 + "smb encrypt" = "desired"; 68 61 }; 69 62 70 - ${tm.shareName} = { 71 - "path" = tm.path; 72 - "valid users" = users; 73 - "public" = "no"; 74 - "writable" = "yes"; 75 - "browseable" = "yes"; 76 - "create mask" = "0600"; 77 - "directory mask" = "0700"; 78 - 79 - # Tell macOS this share is a Time Machine destination 80 - "fruit:time machine" = "yes"; 81 - } // lib.optionalAttrs (cap != "") { 82 - "fruit:time machine max size" = cap; 83 - }; 63 + ${tm.shareName} = 64 + { 65 + "path" = tm.path; 66 + "valid users" = users; 67 + "public" = "no"; 68 + "writable" = "yes"; 69 + "browseable" = "yes"; 70 + "create mask" = "0600"; 71 + "directory mask" = "0700"; 72 + "fruit:time machine" = "yes"; 73 + } 74 + // lib.optionalAttrs (cap != "") { 75 + "fruit:time machine max size" = cap; 76 + }; 84 77 }; 85 78 }; 86 79 87 80 # WS-Discovery so macOS/Windows can find the server by name on the LAN 88 81 services.samba-wsdd = { 89 - enable = true; 90 - interface = ""; # all interfaces 82 + enable = true; 83 + interface = ""; # all interfaces 91 84 }; 92 85 93 - # ── Avahi (mDNS) -- Macs discover the share automatically ─────────────────── 86 + # ── Avahi (mDNS) — Macs discover the share automatically ────────────────── 94 87 services.avahi = { 95 - enable = true; 96 - nssmdns4 = true; 88 + enable = true; 89 + nssmdns4 = true; 97 90 publish = { 98 - enable = true; 99 - addresses = true; 100 - domain = true; 101 - hinfo = true; 91 + enable = true; 92 + addresses = true; 93 + domain = true; 94 + hinfo = true; 102 95 userServices = true; 103 - workstation = true; 96 + workstation = true; 104 97 }; 105 98 106 99 # Three records macOS looks for when scanning for Time Machine targets: 107 - # _smb._tcp -- the actual file-sharing service 108 - # _device-info._tcp -- icon hint (shows as a NAS/Time Capsule in Finder) 109 - # _adisk._tcp -- Time Machine share advertisement 100 + # _smb._tcp — the actual file-sharing service 101 + # _device-info._tcp — icon hint (shows as a NAS/Time Capsule in Finder) 102 + # _adisk._tcp — Time Machine share advertisement 110 103 extraServiceFiles.timemachine = lib.mkAfter '' 111 104 <?xml version="1.0" standalone='no'?> 112 105 <!DOCTYPE service-group SYSTEM "avahi-service.dtd"> ··· 131 124 ''; 132 125 }; 133 126 134 - # ── Firewall ───────────────────────────────────────────────────────────────── 127 + # ── Firewall ──────────────────────────────────────────────────────────────── 135 128 # Samba needs TCP 445 (SMB) + 139 (NetBIOS session) and UDP 137-138 (NetBIOS). 136 129 # WS-Discovery uses UDP 3702. 137 130 networking.firewall = { 138 - allowedTCPPorts = [ 445 139 ]; 139 - allowedUDPPorts = [ 137 138 3702 ]; 131 + allowedTCPPorts = [ 132 + 445 133 + 139 134 + ]; 135 + allowedUDPPorts = [ 136 + 137 137 + 138 138 + 3702 139 + ]; 140 140 }; 141 141 142 - # ── Storage directory ───────────────────────────────────────────────────────── 142 + # ── Storage directory ────────────────────────────────────────────────────── 143 143 systemd.tmpfiles.rules = [ 144 144 "d ${tm.path} 0750 root sambashare -" 145 145 ];
+14 -23
modules/services.nix
··· 1 - { config, pkgs, lib, cfgLib, ... }: 2 - 1 + # Desktop system services (printing, avahi, SSH, locate, etc.). 2 + { 3 + config, 4 + pkgs, 5 + lib, 6 + ... 7 + }: 3 8 let 4 - cfg = cfgLib.cfg; 9 + cfg = config.myConfig; 5 10 in 6 11 { 7 - # Desktop system services 8 - 9 - # Printing (CUPS) 10 12 services.printing.enable = true; 11 13 12 - # Avahi – local network discovery 13 14 services.avahi = { 14 - enable = true; 15 - nssmdns4 = true; 15 + enable = true; 16 + nssmdns4 = true; 16 17 openFirewall = true; 17 18 }; 18 19 19 - # SSH daemon (desktop – password auth enabled for convenience) 20 20 services.openssh = { 21 - enable = true; 21 + enable = true; 22 22 settings = { 23 - PermitRootLogin = cfg.server.sshd.permitRootLogin; 24 - PasswordAuthentication = true; # Desktop: allow password login 23 + PermitRootLogin = cfg.server.sshd.permitRootLogin; 24 + PasswordAuthentication = true; # Desktop: allow password login. 25 25 }; 26 26 }; 27 27 28 - # Locate database 29 28 services.locate = { 30 - enable = true; 29 + enable = true; 31 30 package = pkgs.plocate; 32 31 }; 33 32 34 - # Virtual file systems (GVfs) 35 33 services.gvfs.enable = true; 36 - 37 - # GNOME Keyring 38 34 services.gnome.gnome-keyring.enable = true; 39 35 security.pam.services.login.enableGnomeKeyring = true; 40 36 41 - # D-Bus 42 37 services.dbus.enable = true; 43 - 44 - # Disk management 45 38 services.udisks2.enable = true; 46 - 47 - # Tailscale VPN 48 39 services.tailscale.enable = true; 49 40 }
+23 -11
modules/users.nix
··· 1 - { config, pkgs, lib, cfgLib, ... }: 1 + # Standard user configuration 2 + { 3 + config, 4 + pkgs, 5 + lib, 6 + ... 7 + }: 8 + let 9 + cfg = config.myConfig; 2 10 3 - let 4 - cfg = cfgLib.cfg; # Use config from cfgLib instead of importing 5 - authorizedKeys = cfgLib.mkAuthorizedKeys config.networking.hostName; 6 - shellPkg = if cfg.user.shell == "zsh" then pkgs.zsh else pkgs.bash; 11 + allKeys = import ./ssh-keys.nix; 12 + authorizedKeys = lib.attrValues ( 13 + lib.filterAttrs (name: _: name != config.networking.hostName) allKeys 14 + ); 7 15 in 8 16 { 9 - # Standard user configuration 10 - # Can be imported by any NixOS host 11 - 12 17 users.users.${cfg.user.username} = { 13 18 isNormalUser = true; 14 19 description = cfg.user.fullName; 15 - extraGroups = [ "networkmanager" "wheel" ] 16 - ++ lib.optionals (config.services.pipewire.enable or false) [ "audio" "video" ]; 17 - shell = shellPkg; 20 + extraGroups = 21 + [ 22 + "networkmanager" 23 + "wheel" 24 + ] 25 + ++ lib.optionals config.services.pipewire.enable [ 26 + "audio" 27 + "video" 28 + ]; 29 + shell = pkgs.zsh; 18 30 openssh.authorizedKeys.keys = authorizedKeys; 19 31 }; 20 32 }
-41
secrets/secrets.nix
··· 1 - let 2 - users = { 3 - ewan = "age1xl8ptkqm03skrdadqgprnez3trrc0k9t0ex052lweewqre2zc9qq7ljm3z"; 4 - }; 5 - 6 - systems = { 7 - macmini = "age10ysmz3603uupz0043mpznchtnh6jsnk5cu3eg05xalma4xjacppsgupgvj"; 8 - laptop = "age1s4exn5venvd2rkrvw9g6g9rua05quut62m6le8k79st0dryhcy3qq4n55k"; 9 - # Add the server key once the host exists: 10 - # nix-shell -p ssh-to-age --run 'ssh-keyscan <server-ip> | ssh-to-age' 11 - # Then uncomment and paste the result here, and run: 12 - # nix run github:yaxitech/ragenix -- --rules secrets/secrets.nix --rekey 13 - # server = "age1..."; 14 - }; 15 - 16 - all = (builtins.attrValues users) ++ (builtins.attrValues systems); 17 - 18 - # Until the server key is added above, secrets are encrypted for ewan 19 - # only (so rekeying works from the macmini/laptop today). 20 - # After adding the server key, change these to: [ users.ewan systems.server ] 21 - pdsKeys = [ users.ewan ]; 22 - matrixKeys = [ users.ewan ]; 23 - forgejoKeys = [ users.ewan ]; 24 - in 25 - { 26 - # Network credentials 27 - "age/wifi-home.age".publicKeys = all; 28 - 29 - # SSH key passphrases 30 - "age/ssh-passphrase.age".publicKeys = all; 31 - 32 - # PDS runtime secrets (KEY=value env file) 33 - "age/pds.env.age".publicKeys = pdsKeys; 34 - "age/duckdns.tar.gz.age".publicKeys = all; 35 - "age/docker-config.json.age".publicKeys = all; 36 - "age/claude.json.age".publicKeys = all; 37 - "age/matrix.env.age".publicKeys = matrixKeys; 38 - "age/cloudflare.token.age".publicKeys = matrixKeys; 39 - "age/cf-tunnel.json.age".publicKeys = matrixKeys; 40 - "age/forgejo.env.age".publicKeys = forgejoKeys; 41 - }
-3
settings/config.nix
··· 1 - # Central configuration - imports all settings from config/ 2 - # Edit files in settings/config/ to customize your system 3 - import ./config
-5
settings/config/audio.nix
··· 1 - { 2 - # Audio configuration 3 - enable = true; 4 - backend = "pipewire"; # "pipewire" or "pulseaudio" 5 - }
-12
settings/config/cloudflare.nix
··· 1 - { 2 - # Cloudflare Tunnel configuration. 3 - # Single tunnel for all services (PDS, Matrix, etc.) 4 - 5 - # Tunnel UUID from `cloudflared tunnel create server` 6 - # Replace this after running that command. 7 - tunnelId = "63ec1b18-1358-4ee2-9093-713b4e7d9325"; 8 - 9 - # Ingress routes - maps hostnames to internal services 10 - # These are configured automatically by service modules (pds.nix, matrix.nix, etc.) 11 - # but can be overridden here if needed. 12 - }
-190
settings/config/darwin.nix
··· 1 - { 2 - # macOS configuration (nix-darwin) 3 - 4 - # ─── Keyboard ──────────────────────────────────────────────────────────────── 5 - keyboard = { 6 - enableKeyMapping = true; 7 - remapCapsLockToControl = false; # Keep Caps Lock as Caps Lock 8 - }; 9 - 10 - # ─── Startup ───────────────────────────────────────────────────────────────── 11 - startup = { 12 - chime = true; # Let it bong 13 - }; 14 - 15 - # ─── Security ──────────────────────────────────────────────────────────────── 16 - security = { 17 - touchIdForSudo = true; # Allow Touch ID to authenticate sudo 18 - }; 19 - 20 - # ─── Homebrew ──────────────────────────────────────────────────────────────── 21 - homebrew = { 22 - enable = true; 23 - 24 - # Taps (repositories) 25 - taps = [ 26 - # Add custom taps here if needed 27 - ]; 28 - 29 - # CLI tools managed by Homebrew (complex media/codec dependencies) 30 - brews = [ 31 - # Media libraries 32 - "libmediainfo" 33 - "media-info" 34 - "libzen" 35 - 36 - # Video/audio codecs 37 - "aribb24" 38 - "dav1d" 39 - "rav1e" 40 - "svt-av1" 41 - "x264" 42 - "x265" 43 - "xvid" 44 - "webp" 45 - "aom" 46 - "jpeg-xl" 47 - "highway" 48 - 49 - # Audio 50 - "flac" 51 - "lame" 52 - "opus" 53 - "vorbis-tools" 54 - "libsndfile" 55 - "libsamplerate" 56 - "rubberband" 57 - "speex" 58 - "theora" 59 - "mpg123" 60 - 61 - # Image processing 62 - "little-cms2" 63 - "leptonica" 64 - 65 - # Network protocols 66 - "rtmpdump" 67 - "srt" 68 - "librist" 69 - "libmms" 70 - 71 - # Compression 72 - "lzo" 73 - "snappy" 74 - "xxhash" 75 - "yyjson" 76 - 77 - # Database drivers 78 - "freetds" 79 - "unixodbc" 80 - 81 - # Miscellaneous 82 - "summarize" 83 - "goat" 84 - "mas" 85 - ]; 86 - 87 - # GUI applications via Homebrew Cask 88 - # Note: apps available in nixpkgs are installed via darwin.packages below. 89 - casks = [ 90 - # Communication 91 - "element" # build fails in nixpkgs on darwin (requires Xcode 26 in Nix sandbox) 92 - 93 - # Productivity 94 - "github" # GitHub Desktop (not in nixpkgs) 95 - "claude" 96 - 97 - # Browsers 98 - "firefox" # Not available in nixpkgs-darwin 99 - 100 - # Media & Entertainment 101 - "obs" # OBS Studio (keep in Homebrew — complex macOS plugin deps) 102 - "handbrake-app" 103 - 104 - # Gaming 105 - "steam" 106 - "epic-games" 107 - "prismlauncher" # wayland dep build failure in nixpkgs on darwin (issue #455247) 108 - "utm" 109 - 110 - # Utilities 111 - "cloudflare-warp" 112 - "tailscale-app" # Renamed from tailscale 113 - # filezilla — not available on macOS in Homebrew or nixpkgs; use Cyberduck or ForkLift instead 114 - "parsec" # Remote desktop (Linux-only in nixpkgs) 115 - "onyx" 116 - "mos" # Mouse/trackpad customization (macOS-specific) 117 - 118 - # Office & Documents 119 - "microsoft-excel" 120 - "microsoft-powerpoint" 121 - "microsoft-teams" 122 - "microsoft-word" 123 - "libreoffice" 124 - 125 - # Hardware 126 - "logitune" # Logitech webcam 127 - "logi-options+" # Logitech devices (replaces deprecated logitech-options) 128 - 129 - # Gaming / social 130 - "roblox" 131 - "ea" # EA app (game launcher) 132 - 133 - # Other 134 - "netnewswire" # RSS reader 135 - "altserver" # AltStore sideloading server 136 - # 2fhey — not in Homebrew, install manually 137 - # letta-desktop — not in Homebrew, install manually 138 - # filezilla — removed from Homebrew, install manually 139 - ]; 140 - 141 - # Mac App Store apps (by ID) 142 - masApps = { 143 - "Amphetamine" = 937984704; 144 - # Mini Motorways — Apple Arcade, not available via MAS ID 145 - "OneDrive" = 823766827; # moved from casks 146 - "OP Auto Clicker" = 6754914118; 147 - "Steam Link" = 1246969117; # was incorrectly labelled "EA app" 148 - "TestFlight" = 899247664; 149 - "The Unarchiver" = 425424353; # moved from casks 150 - "WhatsApp" = 310633997; # moved from casks 151 - "Zone Bar" = 6755328989; 152 - }; 153 - }; 154 - 155 - # ─── Nixpkgs packages (macOS-only) ────────────────────────────────────────── 156 - # Cross-platform development packages live in settings/config/packages.nix 157 - # → development. Only add things here that are macOS-specific or provide 158 - # GNU replacements for the BSD tools macOS ships by default. 159 - packages = [ 160 - # GNU replacements for BSD tools macOS ships 161 - "coreutils" # GNU ls/cp/mv/etc (macOS has BSD variants) 162 - "parallel" # GNU parallel 163 - "stow" # GNU stow (symlink farm manager) 164 - "netcat" # GNU netcat (macOS has BSD nc) 165 - 166 - # Dev libraries needed on PATH for building on macOS 167 - # (on NixOS these are pulled in automatically as build deps) 168 - "openssl" 169 - "readline" 170 - "ncurses" 171 - "pcre" 172 - "pcre2" 173 - "libffi" 174 - 175 - # ── GUI apps (migrated from Homebrew Cask) ──────────────────────────────── 176 - # These are available in nixpkgs and managed declaratively. 177 - # mac-app-util (already in the flake) ensures they appear in Spotlight/Launchpad. 178 - "discord" # Communication 179 - "signal-desktop-bin" # Signal — officially the darwin path per nixpkgs 25.11 release notes 180 - # element-desktop — build requires Xcode 26 unavailable in Nix sandbox on darwin 181 - "obsidian" # Note-taking 182 - "vscode" # Editor (note: vscode-fhs fails on darwin, plain vscode is fine) 183 - "spotify" # Music 184 - "transmission_4" # BitTorrent client 185 - # filezilla — Linux-only in nixpkgs, installed via Homebrew cask instead 186 - # parsec-bin — Linux-only in nixpkgs, installed via Homebrew cask instead 187 - # prismlauncher — wayland dep build failure on darwin (nixpkgs issue #455247) 188 - ]; 189 - 190 - }
-58
settings/config/default.nix
··· 1 - let 2 - # Import server config once so we can read the service toggles below. 3 - serverCfg = import ./server.nix; 4 - svcToggles = serverCfg.services; 5 - in 6 - { 7 - # ============================================================================ 8 - # CENTRAL CONFIGURATION - SINGLE SOURCE OF TRUTH 9 - # ============================================================================ 10 - # All configurable values for the entire system are organized here. 11 - # Each category has its own file in settings/config/ for better organization. 12 - # 13 - # To customize your setup, edit the individual files: 14 - # - user.nix : User account settings 15 - # - system.nix : System-level configuration 16 - # - nix.nix : Nix package manager settings 17 - # - packages.nix : Package lists for different use cases 18 - # - git.nix : Git configuration and aliases 19 - # - shell.nix : Shell aliases and history settings 20 - # - desktop.nix : Desktop environment settings (Linux) 21 - # - ssh.nix : SSH configuration 22 - # - audio.nix : Audio backend configuration 23 - # - gaming.nix : Gaming-related settings 24 - # - server.nix : Server-specific configuration 25 - # ↳ services { } — master on/off switches for all services 26 - # - darwin.nix : macOS-specific settings 27 - # - secrets.nix : Secrets management configuration 28 - # - development.nix : Development tools and languages 29 - # - maintenance.nix : Backup and auto-update settings 30 - # - pds.nix : Bluesky Personal Data Server settings 31 - # - matrix.nix : Matrix Synapse homeserver settings 32 - # - forgejo.nix : Forgejo git forge settings 33 - # - cloudflare.nix : Cloudflare Tunnel configuration 34 - 35 - user = import ./user.nix; 36 - system = import ./system.nix; 37 - nix = import ./nix.nix; 38 - packages = import ./packages.nix; 39 - git = import ./git.nix; 40 - shell = import ./shell.nix; 41 - desktop = import ./desktop.nix; 42 - ssh = import ./ssh.nix; 43 - audio = import ./audio.nix; 44 - gaming = import ./gaming.nix; 45 - server = serverCfg; 46 - darwin = import ./darwin.nix; 47 - secrets = import ./secrets.nix; 48 - development = import ./development.nix; 49 - maintenance = import ./maintenance.nix; 50 - 51 - # Service configs — the `enable` flag is driven by server.nix `services.*` 52 - # so there is a single place to turn services on/off. All other settings 53 - # (ports, hostnames, restart policy …) remain in the individual files. 54 - forgejo = (import ./forgejo.nix) // { enable = svcToggles.forgejo; }; 55 - pds = (import ./pds.nix) // { enable = svcToggles.pds; }; 56 - matrix = (import ./matrix.nix) // { enable = svcToggles.matrix; }; 57 - cloudflare = (import ./cloudflare.nix) // { enable = svcToggles.cloudflare; }; 58 - }
-47
settings/config/desktop.nix
··· 1 - let 2 - # ── Single-source font primitives ───────────────────────────────────────── 3 - # Change a name here and Konsole, KDE, and VS Code all update at once. 4 - monoFontBase = "FiraCode"; # root font family name 5 - monoFontFamily = "${monoFontBase} Nerd Font Mono"; # full name for KDE / Konsole 6 - monoFontSize = 11; 7 - 8 - uiFont = "Noto Sans"; # closest open-source match to macOS San Francisco 9 - uiFontSize = 10; 10 - in 11 - { 12 - # Desktop environment configuration (Linux) 13 - enable = true; 14 - environment = "plasma6"; # "gnome" | "plasma6" | "xfce" 15 - displayManager = "sddm"; # "gdm" | "sddm" | "lightdm" 16 - 17 - # GTK/Qt theming 18 - theme = "Catppuccin-Mocha-Standard-Green-Dark"; 19 - iconTheme = "Papirus-Dark"; 20 - 21 - # ── Font primitives — single source of truth ────────────────────────────── 22 - # All consumers (KDE font roles, Konsole, VS Code) reference these; 23 - # nothing below is ever hardcoded elsewhere. 24 - inherit uiFont uiFontSize monoFontBase monoFontFamily monoFontSize; 25 - 26 - # Computed composites — derived, never typed twice 27 - monoFont = "${monoFontFamily} ${toString monoFontSize}"; # "FiraCode Nerd Font Mono 11" 28 - monoFontConsole = monoFontFamily; # Konsole font.name = family only (no trailing size) 29 - 30 - # ── KDE Plasma-specific settings ─────────────────────────────────────────── 31 - plasma = { 32 - # Color scheme applied by plasma-apply-colorscheme on login. 33 - # Mirrors macOS: NSGlobalDomain.AppleInterfaceStyle = "Dark" 34 - # NSGlobalDomain.AppleAccentColor = 3 (Green) 35 - colorScheme = "CatppuccinMochaGreen"; 36 - 37 - # Plasma desktop style (controls panel/widget chrome). 38 - desktopTheme = "breeze-dark"; 39 - 40 - # Packages to exclude from the default KDE Plasma install. 41 - # Must match attribute names under pkgs.kdePackages. 42 - excludePackages = [ 43 - "oxygen" # Legacy Oxygen theme — use Breeze/Catppuccin instead 44 - "elisa" # KDE music player — use Spotify instead 45 - ]; 46 - }; 47 - }
-86
settings/config/development.nix
··· 1 - let 2 - # Pull the font primitives from the single source of truth so VS Code 3 - # stays in sync with KDE/Konsole without hardcoding "FiraCode" twice. 4 - desktop = import ./desktop.nix; 5 - 6 - # VS Code uses the bare family name (no "Nerd Font Mono" suffix) for the 7 - # editor pane, and the Nerd Font variant (without "Mono") for the terminal. 8 - editorFont = desktop.monoFontBase; # "FiraCode" 9 - terminalFont = "${desktop.monoFontBase} Nerd Font"; # "FiraCode Nerd Font" 10 - in 11 - { 12 - # Development configuration 13 - 14 - # VS Code configuration 15 - vscode = { 16 - enable = true; 17 - 18 - # Theme 19 - colorTheme = "Catppuccin Mocha"; 20 - iconTheme = "catppuccin-vsc-icons"; 21 - 22 - # Editor appearance — font strings derived from desktop.nix primitives 23 - fontFamily = "'${editorFont}', 'monospace'"; # "'FiraCode', 'monospace'" 24 - terminalFontFamily = "'${terminalFont}'"; # "'FiraCode Nerd Font'" 25 - fontSize = 14; 26 - terminalFontSize = 13; 27 - lineHeight = 22; 28 - fontLigatures = true; 29 - 30 - # Extensions from nixpkgs (pkgs.vscode-extensions.<publisher>.<name>). 31 - # Must match attribute paths in the nixpkgs vscode-extensions set. 32 - extensions = [ 33 - # ── Nix ────────────────────────────────────────────────────────────────── 34 - "jnoortheen.nix-ide" # Nix LSP, formatting, error reporting 35 - 36 - # ── Python ─────────────────────────────────────────────────────────────── 37 - "ms-python.python" # Python IntelliSense + debugger 38 - "ms-python.debugpy" # Python debugger (required peer dep) 39 - 40 - # ── Rust ───────────────────────────────────────────────────────────────── 41 - "rust-lang.rust-analyzer" # Rust LSP 42 - 43 - # ── C# / VB.NET (.NET) ─────────────────────────────────────────────────── 44 - "ms-dotnettools.csharp" # C# and VB.NET language support 45 - "ms-dotnettools.csdevkit" # C# Dev Kit (solution explorer, test runner) 46 - 47 - # ── Shell / Bash ───────────────────────────────────────────────────────── 48 - "mads-hartmann.bash-ide-vscode" # Bash language server 49 - "timonwong.shellcheck" # ShellCheck linting for sh/bash/zsh 50 - "foxundermoon.shell-format" # shfmt formatter for shell scripts 51 - 52 - # ── Docker ─────────────────────────────────────────────────────────────── 53 - "ms-azuretools.vscode-docker" # Dockerfile syntax, linting, Docker integration 54 - 55 - # ── Data / config formats ───────────────────────────────────────────────── 56 - "tamasfe.even-better-toml" # TOML (Cargo.toml, starship.toml, pyproject.toml…) 57 - "redhat.vscode-yaml" # YAML with JSON schema validation 58 - 59 - # ── Web / Frontend ──────────────────────────────────────────────────────── 60 - "bradlc.vscode-tailwindcss" # Tailwind CSS IntelliSense 61 - "dbaeumer.vscode-eslint" # ESLint (JS/TS/Svelte) 62 - "esbenp.prettier-vscode" # Prettier formatter (JS/TS/CSS/HTML/JSON/YAML…) 63 - 64 - # ── Git ─────────────────────────────────────────────────────────────────── 65 - "eamodio.gitlens" # Git blame, history, diffing 66 - "editorconfig.editorconfig" # .editorconfig support 67 - 68 - # ── General quality-of-life ─────────────────────────────────────────────── 69 - "streetsidesoftware.code-spell-checker" # Spell checking in comments/strings 70 - "christian-kohler.path-intellisense" # Filename autocompletion 71 - 72 - # ── Theme / icons ───────────────────────────────────────────────────────── 73 - # catppuccin-vsc and catppuccin-vsc-icons are installed by the 74 - # catppuccin home-manager module automatically — do not declare here. 75 - ]; 76 - 77 - # Extensions from the VS Code Marketplace via the nix-vscode-extensions 78 - # overlay (pkgs.vscode-marketplace.<publisher>.<name>). 79 - # Use this for extensions not packaged in base nixpkgs 25.11. 80 - marketplaceExtensions = [ 81 - "golang.go" # Go language support (requires gopls on PATH) 82 - "svelte.svelte-vscode" # Svelte language server 83 - "ms-vscode.makefile-tools" # Makefile syntax, build targets, IntelliSense 84 - ]; 85 - }; 86 - }
-21
settings/config/forgejo.nix
··· 1 - { 2 - # Forgejo git forge configuration. 3 - # Non-secret settings only. Secrets (secret key, mailer password, etc.) 4 - # live in secrets/age/forgejo.env.age. 5 - 6 - # Public hostname. 7 - hostname = "git.ewancroft.uk"; 8 - 9 - # Internal port the Forgejo process listens on. Never exposed publicly. 10 - port = 3001; 11 - 12 - # Caddy internal listen port — Cloudflare tunnel routes here. 13 - caddyPort = 3002; 14 - 15 - # Display name shown in the UI. 16 - appName = "Ewan's Git"; 17 - 18 - # Disable public registration — invite-only or admin-created accounts only. 19 - disableRegistration = true; 20 - # Restart policy is shared: see settings/config/server.nix → servicePolicy. 21 - }
-8
settings/config/gaming.nix
··· 1 - { 2 - # Gaming configuration 3 - enable = true; 4 - steam = { 5 - enable = true; 6 - openFirewall = false; 7 - }; 8 - }
-49
settings/config/git.nix
··· 1 - { 2 - # Git configuration 3 - 4 - enable = true; 5 - defaultBranch = "main"; 6 - editor = "code --wait"; 7 - 8 - # Git LFS 9 - lfs = { 10 - enable = true; 11 - }; 12 - 13 - # Commit signing 14 - signing = { 15 - enabled = true; 16 - format = "ssh"; # "ssh" or "gpg" 17 - }; 18 - 19 - # Git aliases 20 - aliases = { 21 - la = "log --all --graph --pretty=format:'%C(auto)%h%d %s %C(bold black)(%ar by <%aN>)%Creset'"; 22 - law = "log --all --graph --pretty=format:'%C(auto)%h%d %w(100,0,8)%s %C(bold black)(%ar by <%aN>)%Creset'"; 23 - lad = "log --all --graph --pretty=format:'%Cgreen%ad%Creset %C(auto)%h%d %s %C(bold black)<%aN>%Creset' --date=format-local:'%Y-%m-%d %H:%M (%a)'"; 24 - }; 25 - 26 - # Global gitignore patterns 27 - globalIgnore = [ 28 - # OS generated 29 - ".DS_Store" 30 - ".DS_Store?" 31 - "._*" 32 - ".Spotlight-V100" 33 - ".Trashes" 34 - "ehthumbs.db" 35 - "Thumbs.db" 36 - 37 - # Editors 38 - ".vscode/" 39 - ".idea/" 40 - "*.swp" 41 - "*.swo" 42 - "*~" 43 - 44 - # Temporary 45 - "*.tmp" 46 - "*.bak" 47 - "*.log" 48 - ]; 49 - }
-21
settings/config/maintenance.nix
··· 1 - { 2 - # Backup & maintenance configuration 3 - 4 - # Automatic system updates (NixOS only – nix-darwin does not support system.autoUpgrade) 5 - autoUpgrade = { 6 - enable = true; 7 - allowReboot = false; 8 - dates = "daily"; 9 - randomizedDelaySec = "45min"; 10 - updateInputs = [ "nixpkgs" ]; # Inputs to update on each run 11 - }; 12 - 13 - # Backup configuration 14 - backup = { 15 - enable = false; 16 - paths = [ 17 - "/home" 18 - "/etc/nixos" 19 - ]; 20 - }; 21 - }
-20
settings/config/matrix.nix
··· 1 - { 2 - # Matrix Synapse homeserver configuration. 3 - # Non-secret settings only. Secrets (registration_shared_secret, macaroon_secret_key) 4 - # should be stored in secrets/age/matrix.env.age. 5 - 6 - # Public hostname — also used as the Caddy virtual host and the Cloudflare 7 - # tunnel public hostname. 8 - hostname = "matrix.ewancroft.uk"; 9 - 10 - # The base domain used for Matrix IDs (@user:domain). 11 - # Using your apex domain so users have clean Matrix IDs like @username:ewancroft.uk 12 - serverName = "ewancroft.uk"; 13 - 14 - # Internal port the Synapse process listens on. Never exposed publicly. 15 - port = 8008; 16 - 17 - # Caddy internal listen port — Cloudflare tunnel routes here. 18 - caddyPort = 8448; 19 - # Restart policy is shared: see settings/config/server.nix → servicePolicy. 20 - }
-27
settings/config/nix.nix
··· 1 - { 2 - # Nix configuration 3 - 4 - # Experimental features 5 - experimentalFeatures = [ "nix-command" "flakes" ]; 6 - 7 - # Store optimization 8 - autoOptimise = true; 9 - 10 - # Garbage collection 11 - gc = { 12 - automatic = true; 13 - dates = "weekly"; # "weekly", "daily", or specific time like "03:15" 14 - options = "--delete-older-than 30d"; 15 - }; 16 - 17 - # Channel/input versions 18 - # NOTE: These are for documentation only. Flake inputs cannot reference local files. 19 - # To update channels, edit the URLs directly in flake.nix inputs section. 20 - # These values are kept here for consistency and documentation. 21 - channels = { 22 - nixpkgs = "nixos-25.11"; 23 - nixpkgsDarwin = "nixpkgs-25.11-darwin"; 24 - homeManager = "release-25.11"; 25 - nixDarwin = "nix-darwin-25.11"; 26 - }; 27 - }
-174
settings/config/packages.nix
··· 1 - { 2 - # Package configuration 3 - 4 - allowUnfree = true; 5 - 6 - # ── Common CLI utilities (every system: laptop, macmini, server) ───────────── 7 - common = [ 8 - # System info & monitoring 9 - "fastfetch" 10 - "btop" # Modern htop alternative 11 - 12 - # Modern CLI tools 13 - "eza" # Modern ls 14 - "bat" # Modern cat with syntax highlighting 15 - "ripgrep" # Fast grep (rg) 16 - "fd" # Fast find 17 - "fzf" # Fuzzy finder 18 - "tree" 19 - 20 - # Version control 21 - "git" 22 - "lazygit" # Git TUI 23 - 24 - # Archives 25 - "unzip" 26 - "zip" 27 - 28 - # Editors & multiplexers 29 - "nano" 30 - "tmux" 31 - 32 - # Network tools 33 - "openssh" # SSH client 34 - "wget" 35 - "curl" 36 - 37 - # File sync 38 - "rsync" 39 - ]; 40 - 41 - # ── Development packages (laptop + macmini – NOT server) ───────────────────── 42 - # Cross-platform: installed via modules/packages.nix on NixOS and 43 - # modules/darwin/packages.nix on macOS. Keep macOS-only things in 44 - # settings/config/darwin.nix → packages. 45 - development = [ 46 - # Nix tooling 47 - "nil" # Nix language server (jnoortheen.nix-ide) 48 - "nixfmt-rfc-style" # Nix formatter 49 - 50 - # Version control (git is in common) 51 - "git-filter-repo" 52 - "gh" # GitHub CLI 53 - 54 - # Languages & runtimes 55 - "go" 56 - "nodejs_22" 57 - "python313" # Primary Python version 58 - "bun" # Fast TS/JS runtime & bundler 59 - "pnpm" # Fast package manager (SvelteKit) 60 - "rustup" # Rust toolchain manager 61 - "dotnet-sdk" # .NET SDK 62 - 63 - # Go tooling 64 - "gopls" # Go language server (golang.go extension) 65 - "golangci-lint" # Go linter 66 - "delve" # Go debugger 67 - 68 - # Python tooling 69 - "pipx" 70 - "uv" 71 - "ruff" # Fast Python linter + formatter 72 - "pyright" # Python type checker / language server 73 - 74 - # Shell tooling 75 - "shellcheck" # Shell script static analysis (timonwong.shellcheck extension) 76 - "shfmt" # Shell script formatter (foxundermoon.shell-format extension) 77 - 78 - # Build tools 79 - "cmake" 80 - "autoconf" 81 - "libtool" 82 - "pkgconf" 83 - "m4" 84 - 85 - # Media processing 86 - "ffmpeg" 87 - "exiftool" 88 - "atomicparsley" 89 - "get_iplayer" 90 - 91 - # Network / infra 92 - "tailscale" 93 - "websocat" 94 - "nmap" 95 - 96 - # Text processing 97 - "jq" 98 - 99 - # Compression 100 - "zstd" 101 - "xz" 102 - "lz4" 103 - "brotli" 104 - 105 - # Database 106 - "sqlite" 107 - 108 - # Image processing / OCR 109 - "tesseract" 110 - 111 - # Additional runtimes 112 - "openjdk21" # Java LTS 113 - "php" # PHP runtime 114 - "ollama" # Local LLM runtime 115 - ]; 116 - 117 - # ── Nerd Fonts to install ───────────────────────────────────────────────────── 118 - fonts = [ 119 - "fira-code" 120 - "jetbrains-mono" 121 - "meslo-lg" 122 - "roboto-mono" 123 - "sauce-code-pro" 124 - "ubuntu-mono" 125 - ]; 126 - 127 - # ── Linux-only packages ─────────────────────────────────────────────────────── 128 - linux = [ 129 - "vlc" 130 - # dconf2nix was only useful for exporting GNOME dconf settings; 131 - # KDE settings are managed directly by plasma-manager. 132 - ]; 133 - 134 - # ── Desktop/GUI packages (NixOS laptop) ────────────────────────────────────── 135 - desktop = [ 136 - # Theming 137 - "papirus-icon-theme" # Clean minimal icon theme 138 - 139 - # Communication 140 - "discord" 141 - "signal-desktop" 142 - "element-desktop" # Matrix client 143 - 144 - # Media 145 - "spotify" 146 - 147 - # Productivity 148 - "obsidian" # Note-taking (Markdown) 149 - "libreoffice-fresh" 150 - 151 - # Creative 152 - "gimp" # Image editing 153 - "inkscape" # Vector graphics 154 - 155 - # Gaming/Remote 156 - "parsec-bin" # Remote gaming/desktop 157 - "prismlauncher" # Minecraft launcher 158 - 159 - # System tools (KDE System Settings is built-in – no extra package needed) 160 - ]; 161 - 162 - # ── Gaming packages ─────────────────────────────────────────────────────────── 163 - gaming = [ 164 - "steam" 165 - "lutris" 166 - "wine" 167 - "winetricks" 168 - ]; 169 - 170 - # ── Server-only packages ────────────────────────────────────────────────────── 171 - server = [ 172 - # git + rsync come from common; only add server-specific extras here 173 - ]; 174 - }
-37
settings/config/pds.nix
··· 1 - { 2 - # Bluesky ATProto Personal Data Server configuration. 3 - # Non-secret settings only. Secrets (PDS_JWT_SECRET, PDS_ADMIN_PASSWORD, 4 - # PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, PDS_EMAIL_SMTP_URL, 5 - # PDS_EMAIL_FROM_ADDRESS) live in secrets/age/pds.env.age. 6 - 7 - # Public hostname — also used as the Caddy virtual host and the Cloudflare 8 - # tunnel public hostname. Subdomains are used for account handles. 9 - hostname = "pds.ewancroft.uk"; 10 - 11 - # Internal port the PDS process listens on. Never exposed publicly. 12 - port = 3000; 13 - 14 - # Email shown in the PDS admin panel. 15 - adminEmail = "pds@ewancroft.uk"; 16 - 17 - # Additional handle domains. ".ewancroft.uk" lets users have @user.ewancroft.uk handles. 18 - serviceHandleDomains = [ ".ewancroft.uk" ]; 19 - 20 - # ATProto relay crawlers — sourced from https://compare.hose.cam 21 - crawlers = [ 22 - "https://bsky.network" 23 - "https://relay.cerulea.blue" 24 - "https://relay.fire.hose.cam" 25 - "https://relay2.fire.hose.cam" 26 - "https://relay3.fr.hose.cam" 27 - "https://relay.hayescmd.net" 28 - "https://relay.xero.systems" 29 - "https://relay.upcloud.world" 30 - "https://relay.feeds.blue" 31 - "https://atproto.africa" 32 - ]; 33 - 34 - # Caddy internal listen port — Cloudflare tunnel routes here. 35 - caddyPort = 2020; 36 - # Restart policy is shared: see settings/config/server.nix → servicePolicy. 37 - }
-19
settings/config/secrets.nix
··· 1 - { 2 - # Secrets configuration 3 - enable = true; 4 - masterKeyPath = "~/.config/age/keys.txt"; 5 - 6 - # Core secrets (always present once set up) 7 - files = [ 8 - "ssh-passphrase" 9 - "wifi-home" 10 - ]; 11 - 12 - # Optional per-app secrets. 13 - # Set enable = true only after the corresponding .age file has been created 14 - # by the migration script (secrets/age/<name>.age must exist in the repo). 15 - docker = { enable = true; }; # ~/.docker/config.json 16 - claude = { enable = true; }; # ~/.claude.json 17 - duckdns = { enable = false; }; # ~/.duckdns/ — server/Linux only; enable per-host 18 - forgejo = { enable = false; }; # Forgejo SECRET_KEY + INTERNAL_TOKEN — enable after creating secrets/age/forgejo.env.age 19 - }
-98
settings/config/server.nix
··· 1 - { 2 - # Server configuration 3 - 4 - # ─── Service toggles ───────────────────────────────────────────────────────── 5 - # Master on/off switches for every server service. 6 - # The detailed settings for each service live in their own files (e.g. 7 - # settings/config/forgejo.nix) — only the enable flag lives here so you 8 - # have one place to see what's running. 9 - services = { 10 - forgejo = true; # Forgejo git forge (git.ewancroft.uk) 11 - pds = true; # Bluesky ATProto PDS (pds.ewancroft.uk) 12 - matrix = true; # Matrix Synapse homeserver (matrix.ewancroft.uk) 13 - cloudflare = true; # Cloudflare tunnel (outbound, all services) 14 - }; 15 - 16 - # ─── Time Machine ───────────────────────────────────────────────────────────── 17 - # Exposes an SMB share (with the fruit VFS module) that macOS backs up to. 18 - # Requires AFP/Samba — AFP was dropped in Ventura so SMB + fruit is used. 19 - # 20 - # After deploying, create a Samba user for each Mac: 21 - # sudo smbpasswd -a <username> 22 - # Then add the backup disk in macOS System Settings -> General -> Time Machine. 23 - timemachine = { 24 - enable = false; # set to true to activate 25 - shareName = "TimeMachine"; # name shown to macOS 26 - path = "/srv/timemachine"; # where backups are stored 27 - maxSizeGB = 0; # 0 = unlimited; set e.g. 500 to cap at 500 GB 28 - validUsers = [ ]; # e.g. [ "ewan" ] — must have a Samba password set 29 - }; 30 - 31 - # ─── Storage ────────────────────────────────────────────────────────────────── 32 - # /srv is mounted as a separate partition to isolate all service data 33 - # (forgejo, matrix-synapse, postgresql, bluesky-pds, www) from the root volume. 34 - # 35 - # Point `device` at the raw block device you want to use. 36 - # Run `lsblk` on the server to find the right path, e.g. /dev/sdb or /dev/sdb1. 37 - # 38 - # The system will automatically: 39 - # 1. Format the partition with ext4 (only if it has no filesystem yet) 40 - # 2. Mount it at /srv 41 - # 3. Create all required subdirectories with correct ownership 42 - # No manual setup is needed — just set the device and deploy. 43 - storage = { 44 - srv = { 45 - device = "/dev/sdb"; # ← set to your partition (use `lsblk` to find it) 46 - fsType = "ext4"; 47 - options = [ "defaults" "noatime" ]; 48 - # Subdirectories created automatically under /srv 49 - # Each service uses its own subdirectory 50 - }; 51 - }; 52 - 53 - # ─── Cockpit dashboard ────────────────────────────────────────────────────────── 54 - # Web-based server status dashboard (services, journals, metrics, terminal). 55 - # Accessible only over Tailscale — not exposed publicly. 56 - cockpit = { 57 - enable = true; 58 - port = 9090; # Cockpit default 59 - }; 60 - 61 - # ─── Shared systemd service restart policy ─────────────────────────────────── 62 - # Applied by default to forgejo, matrix-synapse, and bluesky-pds. 63 - # Override per-service by reading this value in the module and using lib.mkForce. 64 - servicePolicy = { 65 - restartSec = 5; # seconds before restarting after a crash 66 - startLimitIntervalSec = 300; # window for startLimitBurst 67 - startLimitBurst = 5; # max restarts within the window before giving up 68 - }; 69 - 70 - # SSH daemon 71 - sshd = { 72 - enable = true; 73 - permitRootLogin = "no"; 74 - passwordAuthentication = false; 75 - kbdInteractiveAuthentication = false; 76 - port = 22; 77 - maxAuthTries = 3; 78 - clientAliveInterval = 300; 79 - clientAliveCountMax = 2; 80 - x11Forwarding = false; 81 - }; 82 - 83 - # Fail2ban intrusion prevention 84 - fail2ban = { 85 - enable = true; 86 - maxRetry = 5; 87 - banTime = 600; # seconds – 10 minutes 88 - findTime = 600; # seconds – detection window 89 - }; 90 - 91 - # Firewall 92 - firewall = { 93 - enable = true; 94 - allowPing = true; 95 - allowedTCPPorts = [ 22 ]; # Add ports as needed 96 - allowedUDPPorts = [ ]; 97 - }; 98 - }
-96
settings/config/shell.nix
··· 1 - { 2 - # Shell configuration 3 - 4 - # Common aliases 5 - aliases = { 6 - # Modern CLI replacements 7 - ls = "eza --icons"; 8 - ll = "eza -l --icons --git"; 9 - la = "eza -la --icons --git"; 10 - lt = "eza --tree --level=2 --icons"; 11 - cat = "bat"; 12 - 13 - # Navigation 14 - ".." = "cd .."; 15 - "..." = "cd ../.."; 16 - "...." = "cd ../../.."; 17 - 18 - # Safety nets 19 - rm = "rm -i"; 20 - cp = "cp -i"; 21 - mv = "mv -i"; 22 - 23 - # Shortcuts 24 - h = "history"; 25 - c = "clear"; 26 - e = "$EDITOR"; 27 - 28 - # Disk usage 29 - du1 = "du -h -d 1"; 30 - df = "df -h"; 31 - 32 - # Git shortcuts (use lazygit for TUI) 33 - lg = "lazygit"; 34 - }; 35 - 36 - # Git aliases 37 - gitAliases = { 38 - # Status and info 39 - gs = "git status"; 40 - gss = "git status -s"; # Short status 41 - gl = "git log --oneline --graph --decorate"; 42 - 43 - # Adding and committing 44 - ga = "git add"; 45 - gaa = "git add -A"; # Add all 46 - gc = "git commit"; 47 - gcm = "git commit -m"; # Commit with message 48 - gca = "git commit --amend"; 49 - 50 - # Pushing and pulling 51 - gp = "git push"; 52 - gpf = "git push --force-with-lease"; # Safer force push 53 - gpl = "git pull"; 54 - gpr = "git pull --rebase"; # Pull with rebase 55 - 56 - # Branching 57 - gb = "git branch"; 58 - gco = "git checkout"; 59 - gcb = "git checkout -b"; # Create and checkout branch 60 - 61 - # Diffs 62 - gd = "git diff"; 63 - gds = "git diff --staged"; 64 - }; 65 - 66 - # ── Nix tool aliases — shared by both platforms ─────────────────────────── 67 - # Any alias present on both Linux and macOS belongs here exactly once. 68 - # Platform-specific aliases (cleanup, nrs/nrb/nrt) stay in their sections. 69 - nixToolAliases = { 70 - flake-bump = "nix run ~/.config/nix-config/tools#flake-bump"; 71 - gen-diff = "nix run ~/.config/nix-config/tools#gen-diff"; 72 - health-check = "nix run ~/.config/nix-config/tools#health-check"; 73 - update-all = "~/.config/nix-config/home/scripts/update-all"; 74 - update-everything = "~/.config/nix-config/home/scripts/update-everything"; 75 - }; 76 - 77 - # Linux-specific aliases 78 - linuxAliases = { 79 - # nixToolAliases are merged by zsh.nix — only put Linux-only entries here 80 - cleanup = "sudo nix-collect-garbage -d && nix-collect-garbage -d"; 81 - }; 82 - 83 - # macOS-specific aliases 84 - darwinAliases = { 85 - # nixToolAliases are merged by zsh.nix — only put macOS-only entries here 86 - cleanup = "sudo nix-collect-garbage -d"; 87 - }; 88 - 89 - # History configuration 90 - history = { 91 - size = 10000; 92 - saveSize = 10000; 93 - file = "~/.zsh_history"; 94 - ignoreDups = true; 95 - }; 96 - }
-12
settings/config/ssh.nix
··· 1 - { 2 - # SSH configuration 3 - 4 - keyFile = "~/.ssh/id_ed25519"; 5 - enable = true; 6 - 7 - # SSH agent configuration 8 - agent = { 9 - enable = true; # Enable SSH agent on Linux 10 - persistFile = "$HOME/.ssh/agent-env"; 11 - }; 12 - }
-23
settings/config/system.nix
··· 1 - { 2 - # System configuration 3 - stateVersion = "25.11"; 4 - timeZone = "Europe/London"; 5 - locale = "en_GB.UTF-8"; 6 - 7 - # Boot configuration 8 - boot = { 9 - loader = "systemd-boot"; # "systemd-boot" or "grub" 10 - enableConsole = true; 11 - }; 12 - 13 - # Kernel configuration 14 - kernel = { 15 - useLatest = true; # Use latest kernel instead of LTS 16 - }; 17 - 18 - # Network configuration 19 - network = { 20 - enableNetworkManager = true; 21 - hostName = null; # Set per-host in host config 22 - }; 23 - }
-7
settings/config/user.nix
··· 1 - { 2 - # User configuration 3 - username = "ewan"; 4 - fullName = "Ewan Croft"; 5 - email = "git@ewancroft.uk"; 6 - shell = "zsh"; # "zsh" or "bash" 7 - }
+109 -217
settings/plasma/default.nix
··· 8 8 # ✗ No internal migration flags or analytics timestamps 9 9 # 10 10 # All font families, sizes, color schemes, and icon themes come from 11 - # settings/config/desktop.nix — never hardcoded here. 11 + # myConfig.desktop — never hardcoded here. 12 12 # Wallpaper is applied by a systemd user service in home/programs/kde.nix. 13 13 # The Konsole profile lives in home/programs/terminal.nix. 14 - # 15 - # macOS-like layout: 16 - # ┌─────────────────────────────────────────────────────────────────┐ 17 - # │ [NixLogo] [File Edit View …] [Tray extras] [🕐] │ ← 28px menu bar 18 - # ├─────────────────────────────────────────────────────────────────┤ 19 - # │ desktop / windows │ 20 - # │ ╔═══════════════════════════════════════════════════════════╗ │ 21 - # │ ║ 🐬 📡 📝 🎵 🎮 💬 🦊 💻 ║ │ ← floating dock 22 - # │ ╚═══════════════════════════════════════════════════════════╝ │ 23 - # └─────────────────────────────────────────────────────────────────┘ 24 - # 25 - # After editing: nixos-rebuild switch (or home-manager switch) 26 14 ############################################################################## 27 - { lib, cfgLib, ... }: 28 - 15 + { 16 + osConfig, 17 + ... 18 + }: 29 19 let 30 - cfg = cfgLib.cfg; 31 - d = cfg.desktop; # shorthand 20 + d = osConfig.myConfig.desktop; 32 21 in 33 22 { 34 23 programs.plasma = { 35 24 36 - # ── Input – keyboard ─────────────────────────────────────────────────────── 37 - # mac: InitialKeyRepeat = 15 (15 × 15 ms = 225 ms before first repeat) 38 - # KeyRepeat = 2 (2 × 15 ms = 30 ms → ~33 repeats / second) 25 + # ── Input – keyboard ────────────────────────────────────────────────────── 39 26 input.keyboard = { 40 27 numlockOnStartup = "off"; 41 - repeatDelay = 225; 42 - repeatRate = 33; 28 + repeatDelay = 225; 29 + repeatRate = 33; 43 30 }; 44 31 45 - # ── KWin (window manager) ────────────────────────────────────────────────── 32 + # ── KWin (window manager) ───────────────────────────────────────────────── 46 33 kwin = { 47 34 48 - # Four named virtual desktops in one row — analogous to macOS Spaces. 49 - # mac: "com.apple.spaces"."spans-displays" = 0 (per-display Spaces) 50 35 virtualDesktops = { 51 - names = [ "Main" "Work" "Media" "Social" ]; 52 - rows = 1; 36 + names = [ 37 + "Main" 38 + "Work" 39 + "Media" 40 + "Social" 41 + ]; 42 + rows = 1; 53 43 }; 54 44 55 - # Borderless maximised windows — mirrors macOS hiding the titlebar in 56 - # full-screen mode. 57 45 borderlessMaximizedWindows = true; 58 46 59 - # Night light — constant warm tone at all times. 60 - # To switch to automatic sunset/sunrise for Birmingham UK, set: 61 - # mode = "location"; 62 - # location.latitude = "52.48"; 63 - # location.longitude = "-1.89"; 64 47 nightLight = { 65 48 enable = true; 66 - mode = "constant"; # valid: "constant" | "location" | "times" 49 + mode = "constant"; 67 50 temperature = { 68 - day = 6500; # neutral daylight white 69 - night = 4000; # warm amber (macOS default ≈ 3700 K) 51 + day = 6500; 52 + night = 4000; 70 53 }; 71 54 }; 72 55 73 - # Titlebar buttons on the LEFT — mac: close → minimise → maximise 74 - titlebarButtons.left = [ "close" "minimize" "maximize" ]; 75 - titlebarButtons.right = []; 56 + titlebarButtons.left = [ 57 + "close" 58 + "minimize" 59 + "maximize" 60 + ]; 61 + titlebarButtons.right = [ ]; 76 62 77 - # KWin visual effects ──────────────────────────────────────────────────── 78 63 effects = { 79 - 80 - # Frosted-glass background behind panels and menus. 81 - # Mirrors NSVisualEffectView — the ubiquitous blur layer in macOS UI. 82 64 blur = { 83 - enable = true; 84 - strength = 7; # 1–15; 7 ≈ macOS vibrancy intensity 85 - noiseStrength = 0; # no grain — macOS blur is clean 65 + enable = true; 66 + strength = 7; 67 + noiseStrength = 0; 86 68 }; 87 69 88 - # Magic Lamp minimise — window squishes into the dock icon. 89 - # mac: dock.minimize-to-application = true 90 70 minimization = { 91 71 animation = "magiclamp"; 92 - duration = 400; # ms; macOS default ≈ 300–400 ms 72 + duration = 400; 93 73 }; 94 74 95 - # Glide for window open / close — closest to macOS scale + fade. 96 75 windowOpenClose.animation = "glide"; 97 76 98 - # Slide between virtual desktops (macOS Spaces slide animation). 99 77 desktopSwitching = { 100 - animation = "slide"; 101 - navigationWrapping = false; # macOS does not wrap around Spaces 78 + animation = "slide"; 79 + navigationWrapping = false; 102 80 }; 103 81 104 - # Glass feel while moving / resizing windows. 105 82 translucency.enable = true; 106 - 107 - # macOS does not shake the cursor to locate it. 108 83 shakeCursor.enable = false; 109 - 110 - # macOS never dims inactive windows. 111 84 dimInactive.enable = false; 112 - 113 85 }; 114 86 115 - }; # end kwin 87 + }; 116 88 117 - # ── KWin raw configFile (no first-class plasma-manager option yet) ───────── 118 89 configFile."kwinrc" = { 119 - 120 - # Animation speed: 2 = Fast (KDE default 3 = Normal). 121 90 Compositing.AnimationSpeed = 2; 122 91 123 - # Hot corners — mirrors macOS dock.wvous-* settings exactly: 124 - # BL → Mission Control (wvous-bl-corner = 2) 125 - # BR → Show Desktop (wvous-br-corner = 4) 126 - # TL → None (wvous-tl-corner = 1) 127 - # TR → Lock Screen (wvous-tr-corner = 5 ≈ Screen Saver) 128 92 ElectricBorders = { 129 - BottomLeft = "Overview"; 93 + BottomLeft = "Overview"; 130 94 BottomRight = "ShowDesktop"; 131 - TopLeft = "None"; 132 - TopRight = "LockScreen"; 95 + TopLeft = "None"; 96 + TopRight = "LockScreen"; 133 97 }; 134 - 135 98 }; 136 99 137 100 # ── Workspace ───────────────────────────────────────────────────────────── 138 101 workspace = { 139 - 140 - # Color scheme — from settings/config/desktop.nix. 141 - # Explicit here so it always wins, even if the catppuccin module changes 142 - # its lib.mkDefault later. 143 - colorScheme = d.plasma.colorScheme; # "CatppuccinMochaGreen" 144 - 145 - # Plasma desktop style — from settings/config/desktop.nix. 146 - theme = d.plasma.desktopTheme; # "breeze-dark" 102 + colorScheme = d.plasma.colorScheme; 103 + theme = d.plasma.desktopTheme; 104 + iconTheme = d.iconTheme; 147 105 148 - # Icon theme — from settings/config/desktop.nix. 149 - # Uses the proper API (runs plasma-changeicons on login) instead of the 150 - # raw configFile."kdeglobals".Icons.Theme approach. 151 - iconTheme = d.iconTheme; # "Papirus-Dark" 152 - 153 - # Window decorations — Breeze (left-side buttons configured in kwin above). 154 106 windowDecorations = { 155 107 library = "org.kde.breeze"; 156 - theme = "Breeze"; 108 + theme = "Breeze"; 157 109 }; 158 110 159 - # No splash screen — macOS shows nothing on login. 160 111 splashScreen.theme = "None"; 161 - 162 - # Single-click selects, double-click opens — standard macOS Finder behaviour. 163 112 clickItemTo = "select"; 164 - 165 - # Disable middle-click paste — macOS has no X11 primary selection. 166 113 enableMiddleClickPaste = false; 167 - 168 - # Tooltip delay — 500 ms ≈ macOS hover timing (KDE default 700 ms). 169 114 tooltipDelay = 500; 170 - 171 - # Breeze widget style — cleanest, most macOS-like Qt widget rendering. 172 115 widgetStyle = "breeze"; 173 - 174 - # Cursor theme — uncomment to use a macOS-inspired cursor. 175 - # Requires pkgs.capitaine-cursors (or pkgs.apple-cursor) in home.packages. 176 - # cursor = { 177 - # theme = "capitaine-cursors"; 178 - # size = 24; 179 - # cursorFeedback = "Bouncing"; # macOS: app icon bounces in dock on launch 180 - # }; 181 - 182 116 }; 183 117 184 - # ── Fonts — all roles, all from settings/config/desktop.nix ─────────────── 185 - # d.uiFont / d.uiFontSize / d.monoFontFamily / d.monoFontSize are the 186 - # single source of truth; change them once, everything updates. 118 + # ── Fonts ───────────────────────────────────────────────────────────────── 187 119 fonts = { 188 120 general = { 189 - family = d.uiFont; # "Noto Sans" 190 - pointSize = d.uiFontSize; # 10 121 + family = d.uiFont; 122 + pointSize = d.uiFontSize; 191 123 }; 192 124 fixedWidth = { 193 - family = d.monoFontFamily; # "FiraCode Nerd Font Mono" 194 - pointSize = d.monoFontSize; # 11 125 + family = d.monoFontFamily; 126 + pointSize = d.monoFontSize; 195 127 }; 196 128 small = { 197 - family = d.uiFont; 198 - pointSize = d.uiFontSize - 2; # 8 — status bars, breadcrumbs, subtitles 129 + family = d.uiFont; 130 + pointSize = d.uiFontSize - 2; 199 131 }; 200 132 toolbar = { 201 - family = d.uiFont; 133 + family = d.uiFont; 202 134 pointSize = d.uiFontSize; 203 135 }; 204 136 menu = { 205 - family = d.uiFont; 137 + family = d.uiFont; 206 138 pointSize = d.uiFontSize; 207 139 }; 208 140 windowTitle = { 209 - family = d.uiFont; 141 + family = d.uiFont; 210 142 pointSize = d.uiFontSize; 211 - weight = "medium"; # macOS window titles are slightly heavier than body 143 + weight = "medium"; 212 144 }; 213 145 }; 214 146 215 147 # ── Windows ─────────────────────────────────────────────────────────────── 216 - # Allow apps to remember their own window positions. 217 - # mac: WindowServer stores geometry per app. 218 148 windows.allowWindowsToRememberPositions = true; 219 149 220 - # ── KRunner — Spotlight equivalent (Meta+Space) ─────────────────────────── 150 + # ── KRunner ─────────────────────────────────────────────────────────────── 221 151 krunner = { 222 - position = "center"; # macOS Spotlight is a centred overlay 223 - activateWhenTypingOnDesktop = false; # Spotlight requires explicit shortcut 152 + position = "center"; 153 + activateWhenTypingOnDesktop = false; 224 154 shortcuts = { 225 - launch = [ "Meta+Space" "Alt+F2" ]; 226 - runCommandOnClipboard = [ "Meta+Shift+Space" "Alt+Shift+F2" ]; 155 + launch = [ 156 + "Meta+Space" 157 + "Alt+F2" 158 + ]; 159 + runCommandOnClipboard = [ 160 + "Meta+Shift+Space" 161 + "Alt+Shift+F2" 162 + ]; 227 163 }; 228 164 }; 229 165 230 - # ── Keyboard shortcuts ───────────────────────────────────────────────────── 166 + # ── Keyboard shortcuts ──────────────────────────────────────────────────── 231 167 shortcuts = { 232 168 kwin = { 233 - # Virtual desktop / Spaces navigation 234 - # mac: Ctrl+1–4 and Ctrl+← / → switch Spaces 235 - "Switch to Desktop 1" = [ "Meta+1" ]; 236 - "Switch to Desktop 2" = [ "Meta+2" ]; 237 - "Switch to Desktop 3" = [ "Meta+3" ]; 238 - "Switch to Desktop 4" = [ "Meta+4" ]; 239 - "Switch One Desktop to the Left" = [ "Meta+Left" ]; 169 + "Switch to Desktop 1" = [ "Meta+1" ]; 170 + "Switch to Desktop 2" = [ "Meta+2" ]; 171 + "Switch to Desktop 3" = [ "Meta+3" ]; 172 + "Switch to Desktop 4" = [ "Meta+4" ]; 173 + "Switch One Desktop to the Left" = [ "Meta+Left" ]; 240 174 "Switch One Desktop to the Right" = [ "Meta+Right" ]; 241 - 242 - # Move window to a named Space 243 175 "Window to Desktop 1" = [ "Meta+Shift+1" ]; 244 176 "Window to Desktop 2" = [ "Meta+Shift+2" ]; 245 177 "Window to Desktop 3" = [ "Meta+Shift+3" ]; 246 178 "Window to Desktop 4" = [ "Meta+Shift+4" ]; 247 - 248 - # Window management — macOS Cmd+* equivalents 249 - "Window Close" = [ "Meta+Q" ]; # Cmd+Q (quit) 250 - "Window Minimize" = [ "Meta+H" ]; # Cmd+H (hide / minimise) 251 - "Toggle Window Maximized" = [ "Meta+Ctrl+F" ]; # Ctrl+Cmd+F (full-screen toggle) 252 - 253 - # Mission Control equivalents 254 - "Overview" = [ "Meta+W" ]; # all desktops + all windows 255 - "Expose" = [ "Meta+D" ]; # windows on current desktop only 256 - "Show Desktop" = [ "Meta+Shift+D" ]; # reveal desktop (≡ Fn+F11 on mac) 179 + "Window Close" = [ "Meta+Q" ]; 180 + "Window Minimize" = [ "Meta+H" ]; 181 + "Toggle Window Maximized" = [ "Meta+Ctrl+F" ]; 182 + "Overview" = [ "Meta+W" ]; 183 + "Expose" = [ "Meta+D" ]; 184 + "Show Desktop" = [ "Meta+Shift+D" ]; 257 185 }; 258 186 }; 259 187 260 - # ── Spectacle — macOS screenshot shortcut mapping ───────────────────────── 261 - # mac: Cmd+Shift+3 = full screen → Meta+Shift+3 262 - # Cmd+Shift+4 = region → Meta+Shift+4 263 - # Cmd+Shift+5 = UI → Meta+Shift+5 264 188 spectacle.shortcuts = { 265 - captureEntireDesktop = [ "Meta+Shift+3" ]; 189 + captureEntireDesktop = [ "Meta+Shift+3" ]; 266 190 captureRectangularRegion = [ "Meta+Shift+4" ]; 267 - launch = [ "Meta+Shift+5" ]; 268 - captureActiveWindow = [ "Meta+Shift+Alt+4" ]; 191 + launch = [ "Meta+Shift+5" ]; 192 + captureActiveWindow = [ "Meta+Shift+Alt+4" ]; 269 193 }; 270 194 271 - # Spectacle save location and format 272 - # mac: system.defaults.screencapture.location = "~/Desktop", type = "png" 273 195 configFile."spectaclerc" = { 274 - General.launchAction = "DoNotTakeScreenshot"; 275 - ImageSave.defaultFolder = "%DESKTOP%"; 196 + General.launchAction = "DoNotTakeScreenshot"; 197 + ImageSave.defaultFolder = "%DESKTOP%"; 276 198 ImageSave.saveImageFormat = "PNG"; 277 199 }; 278 200 279 - # ── Screen locker ────────────────────────────────────────────────────────── 280 - # mac: "Require password … after sleep or screen saver begins" (immediately) 281 - # + 5-minute idle auto-lock. 201 + # ── Screen locker ───────────────────────────────────────────────────────── 282 202 kscreenlocker = { 283 - autoLock = true; 284 - timeout = 5; # lock after 5 min idle 285 - lockOnResume = true; # lock on wake from sleep 286 - passwordRequired = true; 287 - passwordRequiredDelay = 0; # no grace period 203 + autoLock = true; 204 + timeout = 5; 205 + lockOnResume = true; 206 + passwordRequired = true; 207 + passwordRequiredDelay = 0; 288 208 }; 289 209 290 - # ── Notifications — top-right (macOS notification banner position) ───────── 291 210 configFile."plasmanotifyrc".Notifications.PopupPosition = "TopRight"; 292 211 293 - # ── Dolphin — mirrors macOS Finder preferences ──────────────────────────── 294 - # mac: finder.AppleShowAllExtensions = true 295 - # finder._FXSortFoldersFirst = true 296 - # finder.ShowPathbar = true 297 - # finder.ShowStatusBar = false 298 - # finder._FXShowPosixPathInTitle = true 299 - # finder.FXDefaultSearchScope = "SCcf" 212 + # ── Dolphin ─────────────────────────────────────────────────────────────── 300 213 configFile."dolphinrc" = { 301 214 General = { 302 - ShowHiddenFiles = true; 303 - SortFoldersFirst = true; 304 - ShowStatusBar = false; 215 + ShowHiddenFiles = true; 216 + SortFoldersFirst = true; 217 + ShowStatusBar = false; 305 218 BreadcrumbNavigation = true; 306 219 }; 307 - DetailsMode."ExpandableFolders" = false; 220 + DetailsMode."ExpandableFolders" = false; 308 221 "KFileDialog Settings"."Show Full Path" = true; 309 222 }; 310 223 311 - # ── Panels ───────────────────────────────────────────────────────────────── 312 - 224 + # ── Panels ──────────────────────────────────────────────────────────────── 313 225 panels = [ 314 226 315 - # ── Top menu bar ────────────────────────────────────────────────────────── 316 - # Full-width, always visible, 28 px tall. 317 - # Left: [Launcher] [Global App Menu — File Edit View …] 318 - # Right: [System Tray] [Clock 12h + seconds] 227 + # Top menu bar 319 228 { 320 229 location = "top"; 321 230 floating = false; 322 - height = 28; 231 + height = 28; 323 232 324 233 widgets = [ 325 - 326 - # Application launcher — the "Apple menu" / NixOS logo equivalent. 327 234 { 328 235 name = "org.kde.plasma.kickoff"; 329 236 config.General = { 330 - icon = "start-here-kde-symbolic"; 237 + icon = "start-here-kde-symbolic"; 331 238 showButtonOk = "false"; 332 239 }; 333 240 } 334 - 335 - # Global app menu — active window's menu bar appears inline here, 336 - # just as on macOS every app's menu bar lives at the top. 337 241 "org.kde.plasma.appmenu" 338 - 339 - # Flexible spacer — pushes tray + clock to the far right. 340 242 "org.kde.plasma.panelspacer" 341 - 342 - # System tray — equivalent to macOS menu-bar extras. 343 243 { 344 244 name = "org.kde.plasma.systemtray"; 345 245 config.General.shownItems = 3; 346 246 } 347 - 348 - # Digital clock — mirrors macOS menuExtraClock: 349 - # ShowAMPM = true, ShowSeconds = true 350 - # ShowDate = 0 (never), ShowDayOfWeek = false 351 247 { 352 248 name = "org.kde.plasma.digitalclock"; 353 249 config.Appearance = { 354 - use24hFormat = "0"; # 12-hour with AM/PM 355 - showSeconds = "true"; 356 - showDate = "false"; 357 - showDayOfWeek = "false"; 250 + use24hFormat = "0"; 251 + showSeconds = "true"; 252 + showDate = "false"; 253 + showDayOfWeek = "false"; 358 254 displayTimezoneAsCode = "false"; 359 255 }; 360 256 } 361 - 362 257 ]; 363 258 } 364 259 365 - # ── Bottom floating dock ────────────────────────────────────────────────── 366 - # Centred, floating, always visible. 367 - # mac: dock.autohide = false dock.tilesize = 46 dock.orientation = "bottom" 368 - # Height 56 px ≈ icon size 46 + padding. 260 + # Bottom floating dock 369 261 { 370 - location = "bottom"; 371 - floating = true; 372 - height = 56; 373 - alignment = "center"; 374 - lengthMode = "fit"; # shrinks to fit its launchers 375 - hiding = "none"; # always visible 262 + location = "bottom"; 263 + floating = true; 264 + height = 56; 265 + alignment = "center"; 266 + lengthMode = "fit"; 267 + hiding = "none"; 376 268 377 269 widgets = [ 378 270 { 379 271 name = "org.kde.plasma.icontasks"; 380 272 config.General = { 381 273 showOnlyCurrentDesktop = "false"; 382 - showOnlyCurrentScreen = "false"; 274 + showOnlyCurrentScreen = "false"; 383 275 launchers = [ 384 - "applications:org.kde.dolphin.desktop" # Dolphin ≡ Finder 276 + "applications:org.kde.dolphin.desktop" 385 277 "applications:signal-desktop.desktop" 386 278 "applications:obsidian.desktop" 387 279 "applications:spotify.desktop" ··· 396 288 ]; 397 289 } 398 290 399 - ]; # end panels 291 + ]; 400 292 401 - }; # end programs.plasma 293 + }; 402 294 }