···1616# 1. Ignore everything in the secrets directory by default
1717/secrets/*
18181919-# 2. Whitelist specific Ragenix/Config files that MUST be in Git
2020-!/secrets/age/
2121-!/secrets/age/*.age
2222-!/secrets/secrets.nix
1919+# 2. Whitelist sops-encrypted secret files and the sops config itself
2020+!.sops.yaml
2121+!/secrets/*.env
2222+!/secrets/*.json
2323+!/secrets/*.yaml
2424+!/secrets/*.tar.gz
2325!/secrets/setup.sh
24262527# 3. Explicitly block sensitive patterns everywhere just in case
+51
.sops.yaml
···11+# sops configuration — defines which age keys can decrypt each secret file.
22+#
33+# Key derivation:
44+# User key : age-keygen (stored at ~/.config/age/keys.txt)
55+# Host keys : derived from each host's SSH ed25519 host key at deploy time:
66+# ssh-keyscan <host> | ssh-to-age
77+#
88+# To add a new host:
99+# 1. nix-shell -p ssh-to-age --run 'ssh-keyscan <host-ip> | ssh-to-age'
1010+# 2. Add the result below and uncomment the server reference in creation_rules.
1111+# 3. Re-encrypt all secrets: sops updatekeys secrets/<file>
1212+#
1313+# To encrypt a new secret:
1414+# sops --encrypt secrets/mysecret.env > secrets/mysecret.env (in-place)
1515+# or: sops secrets/mysecret.env (opens $EDITOR)
1616+1717+keys:
1818+ # User key — always included so secrets can be edited from any machine.
1919+ - &ewan age1xl8ptkqm03skrdadqgprnez3trrc0k9t0ex052lweewqre2zc9qq7ljm3z
2020+2121+ # Host keys — derived from /etc/ssh/ssh_host_ed25519_key on each machine.
2222+ # sops-nix decrypts at activation time using the host's own key.
2323+ - &macmini age10ysmz3603uupz0043mpznchtnh6jsnk5cu3eg05xalma4xjacppsgupgvj
2424+ - &laptop age1s4exn5venvd2rkrvw9g6g9rua05quut62m6le8k79st0dryhcy3qq4n55k
2525+ # - &server age1... # uncomment after: ssh-keyscan <server> | ssh-to-age
2626+2727+creation_rules:
2828+ # ── Secrets available on all machines ──────────────────────────────────────
2929+ - path_regex: secrets/(wifi-home|ssh-passphrase|docker-config\.json|claude\.json|duckdns\.tar\.gz)$
3030+ key_groups:
3131+ - age:
3232+ - *ewan
3333+ - *macmini
3434+ - *laptop
3535+ # - *server
3636+3737+ # ── Server-only secrets ─────────────────────────────────────────────────────
3838+ - path_regex: secrets/(pds\.env|matrix\.env|cloudflare\.token|cf-tunnel\.json|forgejo\.env)$
3939+ key_groups:
4040+ - age:
4141+ - *ewan
4242+ # - *server # uncomment once the server age key is known
4343+4444+ # ── Fallback: any other file under secrets/ ─────────────────────────────────
4545+ - path_regex: secrets/.*
4646+ key_groups:
4747+ - age:
4848+ - *ewan
4949+ - *macmini
5050+ - *laptop
5151+ # - *server
···193193194194## Customization
195195196196-All laptop-specific customization should be done through `settings/config/` files, not by editing the host file directly.
196196+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`.
197197198198### Common Customizations
199199200200| What to change | Where to edit |
201201|---|---|
202202-| Username / email | `settings/config/user.nix` |
203203-| Desktop theme | `settings/config/desktop.nix` → `theme` / `iconTheme` |
204204-| Fonts | `settings/config/desktop.nix` → `monoFont` |
205205-| Add applications | `settings/config/packages.nix` → `desktop` list |
206206-| Gaming enable/disable | `settings/config/gaming.nix` → `enable` |
207207-| Audio backend | `settings/config/audio.nix` → `backend` |
208208-| VSCode extensions | `settings/config/development.nix` → `vscode.extensions` |
209209-| Shell aliases | `settings/config/shell.nix` → `aliases` |
210210-| Git settings | `settings/config/git.nix` |
202202+| Username / email | `modules/options.nix` → `user.*` defaults |
203203+| Desktop theme | `modules/options.nix` → `desktop.theme` / `desktop.iconTheme` |
204204+| Fonts | `modules/options.nix` → `desktop.monoFontBase` |
205205+| Add applications | `modules/options.nix` → `packages.desktop` list |
206206+| Gaming enable/disable | `hosts/laptop/default.nix` → `myConfig.gaming.enable` |
207207+| Audio backend | `modules/options.nix` → `audio.backend` |
208208+| VSCode extensions | `home/programs/vscode.nix` |
209209+| Shell aliases | `home/programs/zsh.nix` → `shellAliases` |
210210+| Git settings | `modules/options.nix` → `git.*` or `home/programs/git.nix` |
211211| Plasma layout | `settings/plasma/default.nix` |
212212| Konsole theme | `home/programs/kde.nix` |
213213···215215216216**System-wide packages** (available to all users):
217217```nix
218218-# settings/config/packages.nix
219219-desktop = [
220220- "firefox"
221221- "vlc"
222222- "gimp"
223223- # Add your package here
224224-];
218218+# modules/options.nix
219219+packages.desktop = mkOption {
220220+ default = [
221221+ # ... existing list ...
222222+ "my-new-package" # add here
223223+ ];
224224+};
225225```
226226227227-**User packages** (just for your home):
227227+**Linux-only user packages**:
228228```nix
229229-# settings/config/packages.nix
230230-linux = [
231231- "htop"
232232- "neofetch"
233233- # Add your package here
234234-];
229229+# modules/options.nix
230230+packages.linux = mkOption {
231231+ default = [
232232+ "vlc"
233233+ "my-linux-tool" # add here
234234+ ];
235235+};
235236```
236237237238### Disabling Gaming
238239239239-If you don't game, disable the gaming module:
240240+Remove or comment out the override in the laptop host file:
240241```nix
241241-# settings/config/gaming.nix
242242-{
243243- enable = false; # Change to false
244244- # ... rest stays the same
245245-}
242242+# hosts/laptop/default.nix
243243+# myConfig.gaming.enable = true; ← remove this line
246244```
247245246246+The default in `modules/options.nix` is `false`, so removing the override disables it.
247247+248248### Changing Audio Backend
249249250250To switch from PipeWire to PulseAudio:
251251```nix
252252-# settings/config/audio.nix
253253-{
254254- enable = true;
255255- backend = "pulseaudio"; # Change from "pipewire"
256256-}
252252+# modules/options.nix (changes all hosts) — or override in hosts/laptop/default.nix
253253+myConfig.audio.backend = "pulseaudio";
257254```
258255259256### KDE Plasma Customization
+19-18
docs/hosts-macmini.md
···70707171```
7272hosts/macmini/
7373-└── default.nix
7373+└── default.nix # host-specific imports and myConfig.* overrides
74747575modules/darwin/
7676-├── common.nix # Shared macOS nix settings (gc, flakes, zsh)
7777-├── packages.nix # Nix-managed CLI tools
7878-├── homebrew.nix # Homebrew formulae and casks
7979-└── system.nix # macOS system settings
7676+├── common.nix # Shared macOS nix settings (gc, flakes, zsh)
7777+├── packages.nix # Nix-managed CLI tools
7878+├── homebrew.nix # Homebrew formulae and casks
7979+└── system.nix # macOS system settings + Time Machine activation
8080+8181+modules/options.nix # ⭐ All darwin.* option values live here
80828183settings/darwin/
8282-└── default.nix # macOS system.defaults (Dock, Finder, login window, etc.)
8383-8484-settings/config/darwin.nix # All darwin values — edit here
8484+└── default.nix # macOS system.defaults (Dock, Finder, login window, etc.)
8585```
86868787## Package Management
88888989-### What goes in Nix (`settings/config/darwin.nix` → `packages`)
8989+### What goes in Nix (`modules/options.nix` → `packages.darwin`)
9090CLI tools, development tools, languages — anything with good Nix packaging.
91919292-### What goes in Homebrew (`settings/config/darwin.nix` → `homebrew`)
9393-- **Casks** — GUI apps (VLC, OrbStack, etc.)
9494-- **Brews** — Complex media codecs and libraries that work better via brew
9292+### What goes in Homebrew (`modules/options.nix` → `darwin.homebrew`)
9393+- **`casks`** — GUI apps
9494+- **`brews`** — Complex media codecs and libraries that work better via brew
9595+- **`masApps`** — Mac App Store apps
95969696-Edit `settings/config/darwin.nix` to add packages to either list.
9797+Edit `modules/options.nix` (the `darwin.homebrew` defaults) to add packages to either list.
97989899## System Settings
99100100100-Controlled via `settings/config/darwin.nix`:
101101-- `keyboard` — key mapping, Caps Lock
102102-- `startup.chime` — boot chime
103103-- `security.touchIdForSudo` — Touch ID for sudo
101101+High-level toggles are options in `modules/options.nix`:
102102+- `darwin.keyboard.*` — key mapping, Caps Lock
103103+- `darwin.startup.chime` — boot chime
104104+- `darwin.security.touchIdForSudo` — Touch ID for sudo
104105105105-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.
106106+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.
106107107108## Architecture
108109
+60-57
docs/hosts-overview.md
···120120121121```
122122┌─────────────────────────────────────────┐
123123-│ settings/config/*.nix │ ← Global values (DRY)
124124-│ (user, packages, theme, git, etc.) │
123123+│ modules/options.nix │ ← All option declarations + defaults
125124└─────────────────────────────────────────┘
126125 ↓
127126┌─────────────────────────────────────────┐
128127│ modules/*.nix │ ← Reusable components
129129-│ (common, desktop, gaming, services) │
128128+│ (common, desktop, gaming, services) │ read via config.myConfig.*
130129└─────────────────────────────────────────┘
131130 ↓
132131┌──────────────┬──────────────┬───────────┐
133133-│ laptop/ │ server/ │ macmini/ │ ← Host-specific
134134-│ default.nix │ default.nix │default.nix│ (imports + overrides)
132132+│ laptop/ │ server/ │ macmini/ │ ← Per-host overrides
133133+│ default.nix │ default.nix │default.nix│ myConfig.isDesktop = true; etc.
135134└──────────────┴──────────────┴───────────┘
136135```
137136···156155| `darwin/homebrew.nix` | ❌ | ❌ | ✅ | Homebrew management |
157156| `darwin/system.nix` | ❌ | ❌ | ✅ | macOS system defaults |
158157159159-## Settings Scope
158158+## Option Scope
160159161161-How `settings/config/` values are used across hosts:
160160+Which `myConfig.*` option categories are active on each host:
162161163163-| Setting File | laptop | server | macmini | Notes |
162162+| Option category | laptop | server | macmini | Notes |
164163|---|:---:|:---:|:---:|---|
165165-| `user.nix` | ✅ | ✅ | ✅ | Used everywhere |
166166-| `system.nix` | ✅ | ✅ | Partial | NixOS-specific |
167167-| `nix.nix` | ✅ | ✅ | ✅ | Nix itself |
168168-| `packages.nix` | ✅ | Minimal | ❌ | Linux packages |
169169-| `git.nix` | ✅ | ✅ | ✅ | Via home-manager |
170170-| `shell.nix` | ✅ | ✅ | ✅ | Via home-manager |
171171-| `desktop.nix` | ✅ | ❌ | ❌ | Desktop-only |
172172-| `ssh.nix` | ✅ | ✅ | ✅ | Via home-manager |
173173-| `audio.nix` | ✅ | ❌ | ❌ | Desktop-only |
174174-| `gaming.nix` | ✅ | ❌ | ❌ | Desktop-only |
175175-| `server.nix` | ❌ | ✅ | ❌ | Server-only |
176176-| `darwin.nix` | ❌ | ❌ | ✅ | macOS-only |
177177-| `secrets.nix` | ✅ | ✅ | ✅ | All (via age) |
178178-| `development.nix` | ✅ | ❌ | ✅ | Development hosts |
164164+| `user.*` | ✅ | ✅ | ✅ | Used everywhere |
165165+| `stateVersion`, `timeZone`, `locale` | ✅ | ✅ | Partial | NixOS-specific |
166166+| `packages.common` / `.development` | ✅ | ✅ | ✅ | All hosts |
167167+| `packages.desktop` / `.linux` | ✅ | ❌ | ❌ | `isDesktop = true` hosts |
168168+| `packages.darwin` | ❌ | ❌ | ✅ | macOS only |
169169+| `desktop.*` | ✅ | ❌ | ❌ | Desktop-only |
170170+| `audio.*` | ✅ | ❌ | ❌ | Desktop-only |
171171+| `gaming.*` | ✅ | ❌ | ❌ | `gaming.enable = true` on laptop |
172172+| `server.*` | ❌ | ✅ | ❌ | Server-only |
173173+| `services.*` | ❌ | ✅ | ❌ | Toggled in `hosts/server/default.nix` |
174174+| `darwin.*` | ❌ | ❌ | ✅ | macOS-only |
175175+| `secrets.*` | ✅ | ✅ | ✅ | All hosts (via sops-nix) |
176176+| `development.vscode` | ✅ | ❌ | ✅ | Development hosts |
179177180178## Network Architecture
181179···221219222220Platform-specific modules are conditionally imported:
223221```nix
224224-# home/home.nix
222222+# home/default.nix
225223imports = [
226224 ./programs/git.nix # All platforms
227225 ./programs/zsh.nix # All platforms
···229227 ./programs/starship.nix # All platforms
230228 ./programs/vscode.nix # All platforms
231229] ++ lib.optionals (!isDarwin) [
232232- ./programs/kde.nix # Linux only
230230+ ./programs/terminal.nix # Konsole — Linux only
231231+] ++ lib.optionals (cfg.isDesktop && !isDarwin) [
232232+ ./programs/kde.nix # KDE Plasma — Linux desktop only
233233];
234234```
235235···238238### Scenario 1: Change Username Everywhere
239239240240```bash
241241-# Edit once (on macmini, your primary computer)
242242-vim settings/config/user.nix
243243-# Change: username = "newname";
241241+# Edit the default in modules/options.nix
242242+vim modules/options.nix
243243+# Change: username = mkOption { ... default = "newname"; };
244244245245# Apply to macmini (local)
246246-darwin-rebuild switch --flake .#macmini
246246+nrs # alias for: sudo darwin-rebuild switch --flake .#macmini
247247248248# Apply to laptop (when you use it)
249249-ssh laptop sudo nixos-rebuild switch --flake .#laptop
249249+ssh laptop 'cd ~/.config/nix-config && nrs'
250250251251# Apply to server (when deployed)
252252-ssh server sudo nixos-rebuild switch --flake .#server
252252+ssh server 'cd ~/.config/nix-config && nrs'
253253```
254254255255### Scenario 2: Add Package to macOS (Primary)
256256257257```bash
258258-# Edit on macmini
259259-vim settings/config/darwin.nix
260260-# Add to "packages" or "homebrew.casks"
258258+# Edit modules/options.nix
259259+vim modules/options.nix
260260+# Add to packages.darwin or darwin.homebrew.casks list
261261262262# Apply immediately
263263-darwin-rebuild switch --flake .#macmini
263263+nrs
264264265265# Applies to: macmini ✅, laptop ❌, server ❌
266266```
···268268### Scenario 3: Add Package to Linux Hosts Only
269269270270```bash
271271-# Edit on macmini
272272-vim settings/config/packages.nix
273273-# Add to "linux" or "desktop" list
271271+# Edit modules/options.nix
272272+vim modules/options.nix
273273+# Add to packages.linux or packages.desktop list
274274275275# Apply to laptop (when you use it)
276276-ssh laptop sudo nixos-rebuild switch --flake .#laptop
276276+ssh laptop 'cd ~/.config/nix-config && nrs'
277277278278# Applies to: laptop ✅, server ✅ (when deployed), macmini ❌
279279```
···281281### Scenario 4: Test Config on Secondary Before Primary
282282283283```bash
284284-# Make a risky change on macmini
285285-vim settings/config/packages.nix
284284+# Make a risky change
285285+vim modules/options.nix
286286287287# Test on laptop first (secondary, less critical)
288288ssh laptop sudo nixos-rebuild test --flake .#laptop
289289290290# If it works, apply to macmini (primary)
291291-darwin-rebuild switch --flake .#macmini
291291+nrs
292292```
293293294294-### Scenario 5: Change Shell Alias Everywhere
294294+### Scenario 5: Add a Shell Alias Everywhere
295295296296```bash
297297-# Edit once on macmini (primary)
298298-vim settings/config/shell.nix
299299-# Add alias to "aliases"
297297+# Shell aliases live in home/programs/zsh.nix
298298+vim home/programs/zsh.nix
299299+# Add to shellAliases
300300301301-# Apply to macmini immediately (you're using it now)
302302-darwin-rebuild switch --flake .#macmini
301301+# Apply to macmini immediately
302302+nrs
303303304304# Apply to laptop next time you use it
305305-ssh laptop sudo nixos-rebuild switch --flake .#laptop
305305+ssh laptop 'cd ~/.config/nix-config && nrs'
306306307307# Propagates via home-manager to all hosts
308308```
···367367368368**NixOS hosts** (laptop, server):
369369```bash
370370-# Auto-runs weekly (configured in settings/config/nix.nix)
370370+# Auto-runs weekly (configured in modules/common.nix)
371371sudo nix-collect-garbage -d
372372373373# Manual cleanup
···384384### Updates
385385386386**Automated** (laptop, server):
387387-- Configured in `settings/config/maintenance.nix`
387387+- Configured in `modules/common.nix` via `system.autoUpgrade`
388388- Daily auto-upgrades (if enabled)
389389- Weekly garbage collection
390390391391**Manual** (macmini):
392392-- Update when needed
393393-- No auto-upgrade configured (macOS best practice)
392392+- `nix flake update && nrs`
393393+- No auto-upgrade in nix-darwin (macOS best practice)
394394395395### Health Checks
396396···509509510510### Secrets Not Available on Host
511511512512-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.
512512+Secrets are managed via sops-nix. Check that:
513513+- The host's age key is listed in `.sops.yaml` as a recipient for that secret
514514+- The secret has been re-encrypted with `sops updatekeys secrets/<file>` after adding the key
515515+- Check activation logs: `journalctl -b | grep sops`
513516514517## Best Practices
515518516516-1. **Keep hosts/*/default.nix minimal** — just imports and overrides
517517-2. **Use settings/config/ for shared values** — edit once, apply everywhere
519519+1. **Keep hosts/*/default.nix minimal** — just imports and `myConfig.*` overrides
520520+2. **Change defaults in modules/options.nix** — shared values live there, not in host files
5185213. **Test on one host before deploying to all** — laptop → test → others
5195224. **Document host-specific quirks** — in host file comments
5205235. **Use version control** — commit after working changes
521521-6. **Keep secrets separate** — never commit unencrypted secrets
522522-7. **Regular backups** — especially of ~/.config/nix-config
524524+6. **Never commit unencrypted secrets** — always encrypt with `sops` first
525525+7. **Re-encrypt after adding a new host** — `sops updatekeys secrets/<file>` for every affected secret
5235268. **Monitor all hosts** — check logs after rebuild
524527525528## Resources
+40-37
docs/hosts-server.md
···28282929### 1. Generate PDS secrets
30303131-If you haven't already done this (check whether `secrets/age/pds.env.age` is
3232-populated with real secrets — not a placeholder):
3131+If `secrets/pds.env` doesn't exist yet or contains placeholder values:
33323433```bash
3534# Generate each secret separately — do NOT reuse values
···3837PDS_PLC_ROTATION_KEY=$(openssl ecparam --name secp256k1 --genkey --noout \
3938 --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32)
40394141-# Edit the secret file (ragenix opens $EDITOR):
4242-nix run github:yaxitech/ragenix -- \
4343- --rules secrets/secrets.nix \
4444- --editor "code --wait" \
4545- -e secrets/age/pds.env.age
4040+# Write plaintext to a temp file, then encrypt with sops
4141+cat > /tmp/pds.env << EOF
4242+PDS_JWT_SECRET=${PDS_JWT_SECRET}
4343+PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD}
4444+PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${PDS_PLC_ROTATION_KEY}
4545+PDS_EMAIL_SMTP_URL=smtps://resend:<api-key>@smtp.resend.com:465/
4646+PDS_EMAIL_FROM_ADDRESS=pds@ewancroft.uk
4747+EOF
4848+4949+sops --encrypt /tmp/pds.env > secrets/pds.env
5050+rm /tmp/pds.env
5151+git add secrets/pds.env
4652```
47534848-The file should contain (one per line):
4949-5050-```
5151-PDS_JWT_SECRET=<value>
5252-PDS_ADMIN_PASSWORD=<value>
5353-PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=<value>
5454-PDS_EMAIL_SMTP_URL=smtps://resend:<api-key>@smtp.resend.com:465/
5555-PDS_EMAIL_FROM_ADDRESS=pds@ewancroft.uk
5454+To edit an existing secret:
5555+```bash
5656+sops secrets/pds.env
5657```
57585859### 2. Create the Cloudflare tunnel
···8384The JSON credentials file is at `~/.cloudflared/<UUID>.json` after step 2:
84858586```bash
8686-cp ~/.cloudflared/<UUID>.json /tmp/cf-tunnel-pds.json
8787-8888-nix run github:yaxitech/ragenix -- \
8989- --rules secrets/secrets.nix \
9090- --editor "code --wait" \
9191- -e secrets/age/cf-tunnel-pds.json.age
9292-9393-# Paste the JSON file contents into the editor, save and close.
9494-# Delete the plaintext copy:
9595-rm /tmp/cf-tunnel-pds.json
8787+# Encrypt directly with sops (reads .sops.yaml for recipients)
8888+sops --encrypt ~/.cloudflared/<UUID>.json > secrets/cf-tunnel.json
8989+git add secrets/cf-tunnel.json
9690```
97919892### 5. Add the DNS CNAME in Cloudflare
···129123nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
130124```
131125132132-Paste the result into `secrets/secrets.nix`:
126126+Paste the result into `.sops.yaml` under `keys:` and add it to the relevant `creation_rules`:
133127134134-```nix
135135-systems = {
128128+```yaml
129129+keys:
136130 # ...
137137- server = "age1..."; # ← paste here
138138-};
131131+ - &server age1... # ← paste here
132132+133133+creation_rules:
134134+ - path_regex: secrets/(pds\.env|matrix\.env|...)
135135+ key_groups:
136136+ - age:
137137+ - *ewan
138138+ - *server # ← uncomment / add
139139```
140140141141-Also change `pdsKeys` from `[ users.ewan ]` to `[ users.ewan systems.server ]`.
141141+### 3. Re-encrypt secrets for the server
142142143143-### 3. Rekey secrets for the server
144144-145145-From your macmini or laptop (you need your private age key):
143143+From your macmini or laptop (you need your personal age key `~/.config/age/keys.txt`):
146144147145```bash
148146cd ~/.config/nix-config
149149-nix run github:yaxitech/ragenix -- --rules secrets/secrets.nix --rekey
150150-git add secrets/age/ secrets/secrets.nix
151151-git commit -m "secrets: add server key and rekey PDS secrets"
147147+# Re-encrypt each server secret with the new key added
148148+sops updatekeys secrets/pds.env
149149+sops updatekeys secrets/matrix.env
150150+sops updatekeys secrets/cf-tunnel.json
151151+sops updatekeys secrets/cloudflare.token
152152+sops updatekeys secrets/forgejo.env
153153+git add .sops.yaml secrets/
154154+git commit -m "secrets: add server age key, re-encrypt server secrets"
152155git push
153156```
154157
+106-63
docs/secrets.md
···11# Secrets Management
2233-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`.
33+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.
4455-## Quick Start
55+## How it works
6677-```bash
88-bash ./secrets/setup.sh
99-```
77+- Each secret file is committed to the repo **already encrypted** — it is useless without the private key.
88+- `sops` uses the rules in `.sops.yaml` at the repo root to know which age keys can decrypt each file.
99+- 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.
1010+- On macOS, your personal age key (`~/.config/age/keys.txt`) is used.
10111111-**What the script does:**
1212-1. Manages `~/.config/age/keys.txt` (your master identity — copy this to every machine)
1313-2. Converts the machine's SSH host key to an age key and adds it to the `systems` block in `secrets/secrets.nix`
1414-3. Additively updates `secrets.nix` without removing existing entries
1515-4. Validates Nix syntax and re-encrypts (rekeys) secrets for the new hardware
1212+## Key inventory
16131717-## Adding a New Secret
1414+Keys are declared in `.sops.yaml`:
18151919-### 1. Register it in `settings/config/secrets.nix`
1616+| Name | Type | Location |
1717+|---|---|---|
1818+| `ewan` | User (personal) | `~/.config/age/keys.txt` |
1919+| `macmini` | Host | `/etc/ssh/ssh_host_ed25519_key` on macmini |
2020+| `laptop` | Host | `/etc/ssh/ssh_host_ed25519_key` on laptop |
2121+| `server` | Host | `/etc/ssh/ssh_host_ed25519_key` on server *(add after first boot)* |
20222121-```nix
2222-{
2323- files = [
2424- "ssh-passphrase"
2525- "wifi-home"
2626- "my-new-secret" # add here
2727- ];
2828-}
2929-```
2323+## Quick reference
30243131-`modules/secrets.nix` automatically generates `age.secrets` entries for every file in this list.
2525+```bash
2626+# Edit an existing secret (opens $EDITOR with decrypted content)
2727+sops secrets/pds.env
32283333-### 2. Encrypt the secret
2929+# Encrypt a new file in-place
3030+sops --encrypt secrets/new-secret.env > secrets/new-secret.env
34313535-```nix
3636-# Using ragenix (recommended)
3737-nix run github:yaxitech/ragenix -- \
3838- --rules secrets/secrets.nix \
3939- --editor "code --wait" \
4040- -e secrets/age/my-new-secret.age
3232+# Re-encrypt all secrets after adding a new key to .sops.yaml
3333+sops updatekeys secrets/pds.env
4134```
42354343-Or encrypt directly with rage:
3636+## Adding a new secret
3737+3838+### 1. Create and encrypt the file
44394540```bash
4646-rage -e -r "$(cat ~/.ssh/id_ed25519.pub)" my-secret.txt > secrets/age/my-new-secret.age
4141+# Create plaintext in /tmp, never in the repo
4242+cat > /tmp/my-secret.env << 'EOF'
4343+MY_KEY=some-value
4444+EOF
4545+4646+# Encrypt it into the secrets directory
4747+sops --encrypt /tmp/my-secret.env > secrets/my-secret.env
4848+rm /tmp/my-secret.env
4749```
48504949-### 3. Register the public key in `secrets/secrets.nix`
5151+`sops` reads `.sops.yaml` automatically and encrypts for the correct recipients based on the filename.
5252+5353+### 2. Declare it in the NixOS module that uses it
50545155```nix
5252-"age/my-new-secret.age".publicKeys = all; # or a subset
5656+# e.g. modules/my-service.nix
5757+sops.secrets."my-secret.env" = {
5858+ sopsFile = ../secrets/my-secret.env;
5959+ format = "binary"; # for env files / raw content
6060+ owner = "my-service";
6161+ mode = "0400";
6262+};
5363```
54645555-### 4. Rebuild
6565+For structured files (YAML/JSON/dotenv), you can also extract individual keys:
56665757-The secret is now available at `config.age.secrets.my-new-secret.path`.
6767+```nix
6868+sops.secrets."my-service/api-key" = {
6969+ sopsFile = ../secrets/my-service.yaml;
7070+ # sops-nix extracts the "my-service/api-key" key automatically
7171+};
7272+```
58735959-## Using Secrets in Config
7474+### 3. Reference the decrypted path
60756176```nix
6262-# Service password file
6363-services.someService.passwordFile = config.age.secrets.my-secret.path;
6464-6565-# Environment variable
6666-systemd.services.myservice.environment.TOKEN_FILE =
6767- config.age.secrets.api-token.path;
7777+# In a systemd service
7878+systemd.services.my-service.serviceConfig.EnvironmentFile =
7979+ config.sops.secrets."my-secret.env".path;
68806981# In a script
7082script = ''
7171- TOKEN=$(cat ${config.age.secrets.api-token.path})
8383+ TOKEN=$(cat ${config.sops.secrets."my-service/api-key".path})
7284'';
7385```
74867575-## Rekeying (after adding a new machine)
8787+### 4. Home-manager secrets
76887777-```bash
7878-nix run github:yaxitech/ragenix -- --rules secrets/secrets.nix --rekey
8989+Home-manager secrets use the same `sops-nix` module (via `sops-nix.homeManagerModules.sops`):
9090+9191+```nix
9292+# home/default.nix
9393+sops.secrets."claude-config" = {
9494+ sopsFile = ../secrets/claude.json;
9595+ path = "${config.home.homeDirectory}/.claude.json";
9696+ mode = "0600";
9797+};
7998```
80998181-## File Structure
100100+The `path` field places the decrypted file at a specific location rather than `/run/user/<uid>/secrets/`.
101101+102102+## Adding a new host
821038383-```
8484-secrets/
8585-├── secrets.nix # Public key mappings — safe to commit
8686-├── setup.sh # Key management automation
8787-└── age/
8888- ├── *.age # Encrypted secrets — safe to commit
8989- └── ...
104104+When a new machine is provisioned, its SSH host key must be added to `.sops.yaml` so it can decrypt the secrets it needs.
901059191-~/.config/age/keys.txt # ⚠️ Private master key — NEVER commit
106106+```bash
107107+# 1. Get the host's age public key from its SSH host key
108108+ssh-keyscan <host-ip> | ssh-to-age
109109+110110+# 2. Add the result to .sops.yaml under `keys:`
111111+# - &server age1...
112112+113113+# 3. Reference it in the relevant creation_rules
114114+115115+# 4. Re-encrypt every secret the host needs
116116+sops updatekeys secrets/pds.env
117117+sops updatekeys secrets/cf-tunnel.json
118118+# ... etc
92119```
931209494-## Security Rules
121121+## Existing secrets
951229696-1. `~/.config/age/keys.txt` is your master private key — treat it like your SSH private key
9797-2. Sync `keys.txt` to other machines via `scp` over Tailscale (never via git)
9898-3. `.age` files are safe to commit — they are useless without the private key
9999-4. **UI preferences are NOT secrets** — they live in `settings/gnome/` and `settings/darwin/`
123123+| File | Purpose | Accessible by |
124124+|---|---|---|
125125+| `secrets/wifi-home` | Home WiFi passphrase | all hosts |
126126+| `secrets/ssh-passphrase` | SSH private key passphrase | all hosts |
127127+| `secrets/docker-config.json` | Docker Hub credentials | all hosts |
128128+| `secrets/claude.json` | Claude API / config | all hosts |
129129+| `secrets/duckdns.tar.gz` | DuckDNS config bundle | all hosts |
130130+| `secrets/pds.env` | Bluesky PDS runtime secrets | ewan + server |
131131+| `secrets/matrix.env` | Matrix Synapse secrets | ewan + server |
132132+| `secrets/forgejo.env` | Forgejo `SECRET_KEY` etc. | ewan + server |
133133+| `secrets/cloudflare.token` | Cloudflare API token | ewan + server |
134134+| `secrets/cf-tunnel.json` | Cloudflare tunnel credentials | ewan + server |
135135+136136+## Security rules
137137+138138+1. `~/.config/age/keys.txt` is your personal private key — treat it like an SSH private key. Never commit it.
139139+2. Sync it to other machines via `scp` over Tailscale: `scp ~/.config/age/keys.txt ewan@laptop:~/.config/age/keys.txt`
140140+3. Encrypted secret files (in `secrets/`) **are** committed to git — they are useless without a matching private key.
141141+4. Host keys are derived from the host's SSH `ed25519` host key and are never stored anywhere beyond the key itself.
100142101143## Troubleshooting
102144103145| Error | Cause | Fix |
104146|---|---|---|
105105-| "No rule for file" | `.age` file not in `secrets.nix` | Add it to `secrets/secrets.nix` |
106106-| "Decryption failed" | New system key added but not rekeyed | Run `--rekey` from a machine that has access |
107107-| Path errors | Running from wrong directory | Pass `--rules secrets/secrets.nix` explicitly |
147147+| `no matching keys` | Secret not encrypted for this key | Add key to `.sops.yaml`, run `sops updatekeys <file>` |
148148+| `key not found` | Missing `~/.config/age/keys.txt` or host SSH key | Restore key or re-derive host key |
149149+| `failed to decrypt` | Wrong key or corrupted file | Verify key with `age-keygen --to-public-key` |
150150+| Secret path is empty | sops-nix activation failed | Check `journalctl -b | grep sops` |
+127-80
docs/settings-config.md
···11-# Configuration Directory — Single Source of Truth
11+# Configuration Options Reference
22+33+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`.
44+55+> 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.
2633-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.**
77+## Option map
4855-## File Map
99+### User (`myConfig.user`)
61077-| File | What it controls |
1111+| Option | Default | Description |
1212+|---|---|---|
1313+| `username` | `"ewan"` | Unix username |
1414+| `fullName` | `"Ewan Croft"` | Display name for Git, etc. |
1515+| `email` | `"git@ewancroft.uk"` | Email for Git commits |
1616+1717+### System
1818+1919+| Option | Default | Description |
2020+|---|---|---|
2121+| `stateVersion` | `"25.11"` | NixOS / home-manager state version |
2222+| `timeZone` | `"Europe/London"` | System timezone |
2323+| `locale` | `"en_GB.UTF-8"` | Default locale |
2424+| `isDesktop` | `false` | Whether this is an interactive desktop — set `true` in the host file |
2525+2626+### Audio (`myConfig.audio`)
2727+2828+| Option | Default |
829|---|---|
99-| `user.nix` | Username, full name, email, shell |
1010-| `system.nix` | State version, timezone, locale, boot, kernel, network |
1111-| `nix.nix` | Experimental features, store optimisation, garbage collection |
1212-| `packages.nix` | Package lists — common, fonts, linux, desktop, gaming, server |
1313-| `git.nix` | Branch, editor, LFS, commit signing, aliases, global gitignore |
1414-| `shell.nix` | Aliases, git shortcuts, platform aliases, history |
1515-| `desktop.nix` | Theme, icon theme, mono fonts, display manager, KDE Plasma settings |
1616-| `ssh.nix` | Key file path, SSH agent |
1717-| `audio.nix` | Backend (pipewire / pulseaudio) |
1818-| `gaming.nix` | Enable flag, Steam, Gamemode |
1919-| `server.nix` | sshd, fail2ban, firewall |
2020-| `darwin.nix` | Homebrew brews/casks, nixpkgs packages, keyboard, startup, security |
2121-| `secrets.nix` | Age key path, secret file list |
2222-| `development.nix` | Languages, VS Code theme/fonts/extensions |
2323-| `maintenance.nix` | Auto-upgrade, backup |
2424-| `paths.nix` | Config repo path, home-manager path |
3030+| `enable` | `true` |
3131+| `backend` | `"pipewire"` |
25322626-## Usage
3333+### Gaming (`myConfig.gaming`)
27342828-```nix
2929-let
3030- cfg = import ../../settings/config.nix;
3131-in {
3232- home.username = cfg.user.username;
3333- programs.git.userEmail = cfg.user.email;
3434- home.stateVersion = cfg.system.stateVersion;
3535- environment.systemPackages = map (p: pkgs.${p}) cfg.packages.common;
3636- programs.vscode.profiles.default.extensions =
3737- map toExt cfg.development.vscode.extensions;
3838-}
3939-```
3535+| Option | Default | Notes |
3636+|---|---|---|
3737+| `enable` | `false` | Set `true` in `hosts/laptop/default.nix` |
3838+| `steam.enable` | `true` | |
3939+| `steam.openFirewall` | `false` | |
4040+4141+### Packages (`myConfig.packages`)
4242+4343+| Option | Description |
4444+|---|---|
4545+| `common` | CLI tools on every host |
4646+| `development` | Languages and tooling (laptop + macmini) |
4747+| `fonts` | Nerd Font names to install via home-manager |
4848+| `linux` | Linux-only GUI extras (e.g. `vlc`) |
4949+| `desktop` | Linux desktop GUI apps |
5050+| `gaming` | Gaming packages |
5151+| `server` | Server-only extras |
5252+| `darwin` | macOS-specific Nix packages |
5353+5454+### Desktop (`myConfig.desktop`)
5555+5656+| Option | Default |
5757+|---|---|
5858+| `environment` | `"plasma6"` |
5959+| `displayManager` | `"sddm"` |
6060+| `uiFont` / `uiFontSize` | `"Noto Sans"` / `10` |
6161+| `monoFontBase` | `"FiraCode"` |
6262+| `monoFontFamily` | `"FiraCode Nerd Font Mono"` |
6363+| `monoFontSize` | `11` |
6464+| `theme` | `"Catppuccin-Mocha-Standard-Green-Dark"` |
6565+| `iconTheme` | `"Papirus-Dark"` |
6666+| `plasma.colorScheme` | `"CatppuccinMochaGreen"` |
6767+| `plasma.desktopTheme` | `"breeze-dark"` |
6868+| `plasma.excludePackages` | `["oxygen" "elisa"]` |
6969+7070+### Git (`myConfig.git`)
7171+7272+| Option | Default |
7373+|---|---|
7474+| `defaultBranch` | `"main"` |
7575+| `editor` | `"code --wait"` |
7676+| `lfs.enable` | `true` |
7777+| `signing.enabled` | `true` |
7878+| `signing.format` | `"ssh"` |
40794141-## Quick-edit cheatsheet
8080+### Development / VS Code (`myConfig.development.vscode`)
42814343-| I want to change… | Edit |
8282+| Option | Default |
4483|---|---|
4545-| Username / email | `user.nix` |
4646-| Timezone / locale | `system.nix` |
4747-| Add a package (Linux) | `packages.nix` → `common` or `desktop` |
4848-| Add a package (macOS) | `darwin.nix` → `packages` |
4949-| Add a Homebrew cask | `darwin.nix` → `homebrew.casks` |
5050-| Git alias | `git.nix` → `aliases` |
5151-| Shell alias | `shell.nix` → `aliases` |
5252-| Theme / icon theme | `desktop.nix` → `theme` / `iconTheme` |
5353-| Monospace font | `desktop.nix` → `monoFont` / `monoFontConsole` |
5454-| KDE Plasma packages | `desktop.nix` → `plasma.excludePackages` |
5555-| VS Code extensions | `development.nix` → `vscode.extensions` |
5656-| VS Code font | `development.nix` → `vscode.fontFamily` |
5757-| Enable gaming | `gaming.nix` → `enable = true` |
5858-| SSH port | `server.nix` → `sshd.port` |
5959-| Firewall ports | `server.nix` → `firewall.allowedTCPPorts` |
6060-| Auto-upgrade | `maintenance.nix` → `autoUpgrade.enable` |
6161-| Add a secret | `secrets.nix` → `files` list, then create the `.age` file |
6262-| macOS Touch ID sudo | `darwin.nix` → `security.touchIdForSudo` |
6363-| macOS startup chime | `darwin.nix` → `startup.chime` |
8484+| `enable` | `true` |
8585+| `colorTheme` | `"Catppuccin Mocha"` |
8686+| `iconTheme` | `"catppuccin-vsc-icons"` |
8787+| `fontSize` | `14` |
8888+| `terminalFontSize` | `13` |
8989+| `lineHeight` | `22` |
9090+| `fontLigatures` | `true` |
64916565-## Adding a new secret
9292+### Secrets (`myConfig.secrets`)
66936767-```bash
6868-# 1. Encrypt it
6969-rage -e -r "$(cat ~/.ssh/id_ed25519.pub)" my-secret.txt > secrets/age/my-secret.age
9494+| Option | Default | What it enables |
9595+|---|---|---|
9696+| `docker.enable` | `true` | `~/.docker/config.json` |
9797+| `claude.enable` | `true` | `~/.claude.json` |
9898+| `duckdns.enable` | `false` | `~/.duckdns/` bundle |
70997171-# 2. Register in settings/config/secrets.nix
7272-# files = [ "ssh-passphrase" "wifi-home" "my-secret" ];
100100+### Server services (`myConfig.services`)
101101+102102+| Option | Default | Set in |
103103+|---|---|---|
104104+| `forgejo.enable` | `false` | `hosts/server/default.nix` |
105105+| `pds.enable` | `false` | `hosts/server/default.nix` |
106106+| `matrix.enable` | `false` | `hosts/server/default.nix` |
107107+| `cloudflare.enable` | `false` | `hosts/server/default.nix` |
108108+109109+### Server SSH (`myConfig.server.sshd`)
731107474-# 3. Rebuild — available at config.age.secrets.my-secret.path
7575-```
111111+| Option | Default |
112112+|---|---|
113113+| `enable` | `true` |
114114+| `permitRootLogin` | `"no"` |
115115+| `passwordAuthentication` | `false` |
116116+| `port` | `22` |
117117+| `maxAuthTries` | `3` |
118118+| `x11Forwarding` | `false` |
761197777-## Adding a new settings category
120120+### Firewall (`myConfig.server.firewall`)
781217979-```bash
8080-# 1. Create the file
8181-cat > settings/config/monitoring.nix << 'EOF'
8282-{
8383- prometheus = { enable = false; port = 9090; };
8484- grafana = { enable = false; port = 3000; };
8585-}
8686-EOF
122122+| Option | Default |
123123+|---|---|
124124+| `enable` | `true` |
125125+| `allowPing` | `true` |
126126+| `allowedTCPPorts` | `[22]` |
127127+| `allowedUDPPorts` | `[]` |
871288888-# 2. Register in default.nix
8989-# monitoring = import ./monitoring.nix;
129129+### Darwin (`myConfig.darwin`)
901309191-# 3. Use anywhere
9292-# cfg.monitoring.prometheus.enable
9393-```
131131+| Option | Default |
132132+|---|---|
133133+| `keyboard.enableKeyMapping` | `true` |
134134+| `keyboard.remapCapsLockToControl` | `false` |
135135+| `startup.chime` | `true` |
136136+| `security.touchIdForSudo` | `true` |
137137+| `homebrew.enable` | `true` |
138138+| `homebrew.taps` | `[]` |
139139+| `homebrew.brews` | *(media codec list — see options.nix)* |
140140+| `homebrew.casks` | *(GUI app list — see options.nix)* |
141141+| `homebrew.masApps` | *(Mac App Store apps — see options.nix)* |
941429595-## Further Reading
143143+## Further reading
961449797-- [settings.md](settings.md) — overview and export workflow
9898-- [settings-structure.md](settings-structure.md) — why the config is modular
9999-- [REFERENCE.md](REFERENCE.md) — quick-reference command card
145145+- [settings.md](settings.md) — how to make changes
146146+- [REFERENCE.md](REFERENCE.md) — command reference
+61-65
docs/settings-structure.md
···11-# Settings Structure
11+# Configuration Structure
2233-## Why split into modules?
33+> **Note**: The `settings/config/` directory and `settings/config.nix` have been removed. This document describes the current structure.
4455-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.
55+## How configuration is organised
66+77+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.
6879```
88-settings/
99-├── config.nix # 3 lines — just imports config/
1010-└── config/
1111- ├── default.nix # Combines all modules into one attrset
1212- ├── user.nix # Username, email, shell
1313- ├── system.nix # Timezone, locale, boot, kernel
1414- ├── nix.nix # Flakes, store optimisation, GC
1515- ├── packages.nix # Package lists per context
1616- ├── git.nix # Branch, editor, signing, aliases
1717- ├── shell.nix # Aliases, history
1818- ├── desktop.nix # Theme, fonts, KDE Plasma settings
1919- ├── ssh.nix # Key file, agent
2020- ├── audio.nix # Backend (pipewire / pulseaudio)
2121- ├── gaming.nix # Enable flag, Steam
2222- ├── server.nix # sshd, fail2ban, firewall
2323- ├── darwin.nix # Homebrew, nixpkgs packages, keyboard, security
2424- ├── secrets.nix # Age key path, secret file list
2525- ├── development.nix # Languages, VS Code
2626- ├── maintenance.nix # Auto-upgrade, backup
2727- └── paths.nix # Config repo and home-manager paths
1010+modules/options.nix # Single source of truth for all option declarations + defaults
1111+hosts/<n>/default.nix # Per-host overrides using the module system
2812```
29133030-## Usage
1414+## Why this approach?
1515+1616+The previous approach imported a plain Nix attrset (`settings/config.nix`) and threaded it through a custom `cfgLib` helper. This had several downsides:
31173232-The API is unchanged from a flat file — everything is accessed via `cfg.<domain>.<key>`:
1818+- No type checking — typos and wrong types silently produced bad configs
1919+- No documentation — `nix-option` couldn't introspect the values
2020+- Manual wiring — every module had to receive the config as an argument
2121+- Duplication — defaults lived in `settings/config/` *and* had to be mirrored in `modules/options.nix` definitions
33223434-```nix
3535-let
3636- cfg = import ../settings/config.nix;
3737-in {
3838- home.username = cfg.user.username;
3939- programs.git.userEmail = cfg.user.email;
4040- home.packages = map (p: pkgs.${p}) cfg.packages.common;
4141- time.timeZone = cfg.system.timeZone;
4242-}
2323+Using the NixOS module system directly gives type checking, proper `mkDefault`/`mkForce` priority, and means every module gets `config.myConfig` automatically — no wiring needed.
2424+2525+## Directory layout
2626+2727+```
2828+settings/
2929+├── darwin/ # macOS system.defaults — a plain NixOS module
3030+│ └── default.nix # Dock, Finder, NSGlobalDomain, trackpad, etc.
3131+└── plasma/ # KDE Plasma declarative settings (plasma-manager)
3232+ └── default.nix
4333```
3434+3535+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.
44364537## Edit frequency guide
46384747-| File | Edit frequency |
4848-|---|---|
4949-| `user.nix` | 🔴 Rare |
5050-| `system.nix` | 🔴 Rare |
5151-| `nix.nix` | 🔴 Rare |
5252-| `ssh.nix` | 🔴 Rare |
5353-| `audio.nix` | 🔴 Rare |
5454-| `paths.nix` | 🔴 Rare |
5555-| `secrets.nix` | 🔴 Per-secret |
5656-| `server.nix` | 🔴 Per-host |
5757-| `gaming.nix` | 🔴 Per-host |
5858-| `packages.nix` | 🟡 Occasional |
5959-| `git.nix` | 🟡 Occasional |
6060-| `desktop.nix` | 🟡 Occasional |
6161-| `darwin.nix` | 🟡 Occasional |
6262-| `development.nix` | 🟡 Occasional |
6363-| `maintenance.nix` | 🟡 Occasional |
6464-| `shell.nix` | 🟢 Frequent |
3939+| File | Edit frequency | When |
4040+|---|---|---|
4141+| `modules/options.nix` | 🟡 Occasional | Adding/changing global defaults |
4242+| `hosts/laptop/default.nix` | 🔴 Rare | Laptop-specific overrides |
4343+| `hosts/server/default.nix` | 🔴 Rare | Service toggles, server-specific config |
4444+| `hosts/macmini/default.nix` | 🔴 Rare | macOS-specific overrides |
4545+| `settings/darwin/default.nix` | 🟡 Occasional | macOS UI defaults (Dock, Finder, etc.) |
4646+| `settings/plasma/default.nix` | 🟡 Occasional | KDE Plasma layout and behaviour |
4747+| `home/programs/kde.nix` | 🟡 Occasional | KDE fonts, theme, Konsole |
65486666-## Adding a new category
4949+## Adding a new option
5050+5151+```nix
5252+# 1. Declare it in modules/options.nix
5353+myNewThing = {
5454+ enable = mkOption {
5555+ type = bool;
5656+ default = false;
5757+ description = "Enable the new thing.";
5858+ };
5959+ port = mkOption {
6060+ type = int;
6161+ default = 9000;
6262+ };
6363+};
67646868-```bash
6969-# 1. Create the file
7070-cat > settings/config/monitoring.nix << 'EOF'
7171-{
7272- prometheus = { enable = false; port = 9090; };
7373- grafana = { enable = false; port = 3000; };
6565+# 2. Use it in a module
6666+lib.mkIf config.myConfig.myNewThing.enable {
6767+ # ...
7468}
7575-EOF
76697777-# 2. Register in default.nix
7878-# monitoring = import ./monitoring.nix;
7070+# 3. Override per-host if needed
7171+# hosts/server/default.nix
7272+myConfig.myNewThing.enable = true;
7373+```
7474+7575+## Further reading
79768080-# 3. Use anywhere
8181-# cfg.monitoring.prometheus.enable
8282-```
7777+- [settings.md](settings.md) — practical how-to guide
7878+- [settings-config.md](settings-config.md) — full option reference
+93-39
docs/settings.md
···11-# Settings — Central Configuration
11+# Configuration — How It Works
2233-`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.
33+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.
4455-## Structure
55+## Where settings live
6677```
88-settings/
99-├── config.nix # Entry point — imports config/
1010-├── config/ # All configurable values (one file per domain)
1111-│ ├── default.nix # Imports all sub-modules
1212-│ ├── user.nix
1313-│ ├── system.nix
1414-│ ├── packages.nix
1515-│ ├── desktop.nix
1616-│ ├── darwin.nix
1717-│ └── ...
1818-├── plasma/ # KDE Plasma declarative settings
1919-│ └── default.nix
2020-└── darwin/ # macOS system defaults (Dock, Finder, trackpad, etc.)
2121- └── default.nix
88+modules/options.nix # Declares all options with defaults
99+hosts/<name>/default.nix # Per-host overrides (myConfig.* = ...)
2210```
23112424-## Usage
1212+## Accessing settings in modules
25131414+System-level modules (`modules/*.nix`):
2615```nix
2727-let
2828- cfg = import ../settings/config.nix;
2929-in {
3030- home.username = cfg.user.username;
1616+{ config, ... }:
1717+let cfg = config.myConfig; in
1818+{
1919+ time.timeZone = cfg.timeZone;
2020+ users.users.${cfg.user.username} = { ... };
2121+}
2222+```
2323+2424+Home-manager modules (`home/**/*.nix`):
2525+```nix
2626+{ osConfig, ... }:
2727+let cfg = osConfig.myConfig; in
2828+{
3129 programs.git.userEmail = cfg.user.email;
3232- home.stateVersion = cfg.system.stateVersion;
3330}
3431```
35323636-## Benefits
3333+## Option categories
37343838-- **Single source of truth** — change one value, updates everywhere
3939-- **DRY** — no duplication across modules or hosts
4040-- **Discoverable** — clear file names, each file focused on one domain
4141-- **Safe** — impossible to have inconsistent settings across hosts
3535+| Category | Key options |
3636+|---|---|
3737+| `myConfig.user` | `username`, `fullName`, `email` |
3838+| `myConfig` | `stateVersion`, `timeZone`, `locale`, `isDesktop` |
3939+| `myConfig.audio` | `enable`, `backend` |
4040+| `myConfig.gaming` | `enable`, `steam.*` |
4141+| `myConfig.packages` | `common`, `development`, `fonts`, `linux`, `desktop`, `darwin` |
4242+| `myConfig.desktop` | `environment`, `displayManager`, fonts, theme, KDE Plasma settings |
4343+| `myConfig.ssh` | `keyFile` |
4444+| `myConfig.git` | `defaultBranch`, `editor`, `lfs`, `signing` |
4545+| `myConfig.development.vscode` | `enable`, theme, font, size settings |
4646+| `myConfig.secrets` | `docker.enable`, `claude.enable`, `duckdns.enable` |
4747+| `myConfig.services` | `forgejo.enable`, `pds.enable`, `matrix.enable`, `cloudflare.enable` |
4848+| `myConfig.server` | `sshd.*`, `fail2ban.*`, `firewall.*`, `timemachine.*`, … |
4949+| `myConfig.darwin` | `keyboard.*`, `startup.*`, `security.*`, `homebrew.*` |
5050+| `myConfig.forgejo` | `hostname`, `port`, `appName`, … |
5151+| `myConfig.pds` | `hostname`, `port`, `adminEmail`, `crawlers`, … |
5252+| `myConfig.matrix` | `hostname`, `serverName`, `port`, … |
5353+| `myConfig.cloudflare` | `tunnelId` |
42544343-## Exporting GUI Settings
5555+## Making a change
44564545-### KDE Plasma (Linux)
5757+### Change a value that has a suitable default
46584747-KDE Plasma settings are managed declaratively via `plasma-manager`. Instead of exporting settings from the GUI, you should:
5959+Most values default to something sensible in `modules/options.nix`. You rarely need to touch anything — just rebuild.
48604949-1. Edit `settings/plasma/default.nix` for desktop layout and behavior
5050-2. Edit `home/programs/kde.nix` for user-level Plasma configuration
6161+### Override a value for one host
51625252-Changes are applied automatically on next Home Manager rebuild. This ensures your configuration is reproducible and version-controlled.
6363+```nix
6464+# hosts/laptop/default.nix
6565+{
6666+ myConfig.isDesktop = true;
6767+ myConfig.gaming.enable = true;
6868+}
6969+```
53705454-### macOS
7171+### Change a default that applies to all hosts
55725656-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.
7373+Edit the `default = ...` in `modules/options.nix`:
57745858-## Further Reading
7575+```nix
7676+# modules/options.nix
7777+timeZone = mkOption {
7878+ type = str;
7979+ default = "Europe/London"; # ← change here
8080+};
8181+```
59826060-- [settings-config.md](settings-config.md) — full per-file reference and quick-edit map
6161-- [settings-structure.md](settings-structure.md) — why the config is split into modules
8383+## Quick-edit cheatsheet
8484+8585+| I want to change… | Where |
8686+|---|---|
8787+| Username / email | `modules/options.nix` → `user.*` defaults |
8888+| Timezone / locale | `modules/options.nix` → `timeZone` / `locale` |
8989+| Add a package (Linux) | `modules/options.nix` → `packages.common` or `packages.desktop` |
9090+| Add a package (macOS) | `modules/options.nix` → `packages.darwin` |
9191+| Add a Homebrew cask | `modules/options.nix` → `darwin.homebrew.casks` |
9292+| Toggle desktop mode | `hosts/<n>/default.nix` → `myConfig.isDesktop = true` |
9393+| Enable gaming | `hosts/<n>/default.nix` → `myConfig.gaming.enable = true` |
9494+| Theme / icon theme | `modules/options.nix` → `desktop.theme` / `desktop.iconTheme` |
9595+| Monospace font | `modules/options.nix` → `desktop.monoFontBase` |
9696+| VS Code font / theme | `modules/options.nix` → `development.vscode.*` |
9797+| SSH port (server) | `modules/options.nix` → `server.sshd.port` |
9898+| Firewall ports | `modules/options.nix` → `server.firewall.allowedTCPPorts` |
9999+| Enable a server service | `hosts/server/default.nix` → `myConfig.services.<n>.enable = true` |
100100+| macOS Touch ID sudo | `modules/options.nix` → `darwin.security.touchIdForSudo` |
101101+| macOS startup chime | `modules/options.nix` → `darwin.startup.chime` |
102102+103103+## macOS system.defaults
104104+105105+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.
106106+107107+## KDE Plasma settings
108108+109109+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`.
110110+111111+## Further reading
112112+113113+- [REFERENCE.md](REFERENCE.md) — quick-reference command card
114114+- [hosts-overview.md](hosts-overview.md) — how the three hosts relate to each other
115115+- [secrets.md](secrets.md) — secrets management with sops-nix
···11-{ lib }:
22-33-let
44- # Import the central config once at the library level
55- cfg = import ../settings/config.nix;
66-77-in {
88- # Expose the config so modules can just use `cfgLib.cfg` instead of importing
99- inherit cfg;
1010-1111- # Helper to create a module with auto-injected config
1212- # Usage: mkModule { config, pkgs, ... }: { ... }
1313- mkModule = moduleFunc: { config, pkgs, lib, ... }@args:
1414- moduleFunc (args // { inherit cfg; });
1515-1616- # Helper to create a home-manager program module with platform info
1717- # Usage: mkHomeProgram { isDarwin }: { config, pkgs, ... }: { ... }
1818- mkHomeProgram = { isDarwin ? false }: moduleFunc: { config, pkgs, lib, ... }@args:
1919- moduleFunc (args // { inherit cfg isDarwin; });
2020-2121- # Helper to resolve package names to actual packages, skipping missing ones
2222- # Usage: resolvePackages pkgs [ "firefox" "vscode" ]
2323- resolvePackages = pkgs: names:
2424- let
2525- toPkg = name:
2626- if pkgs ? ${name} then pkgs.${name}
2727- else builtins.trace "WARNING: package '${name}' not found in nixpkgs, skipping" null;
2828- in
2929- builtins.filter (x: x != null) (map toPkg names);
3030-3131- # Helper to create SSH authorized keys excluding the current host
3232- # Usage: mkAuthorizedKeys hostName
3333- mkAuthorizedKeys = hostName:
3434- let
3535- allKeys = import ../modules/ssh-keys.nix;
3636- in
3737- lib.attrValues (lib.filterAttrs (name: _: name != hostName) allKeys);
3838-}
+33-38
modules/cloudflare-tunnel.nix
···1212# Cloudflare tunnel setup (one-time, outside Nix):
1313# 1. cloudflared tunnel login
1414# 2. cloudflared tunnel create server
1515-# 3. Encrypt the resulting ~/.cloudflared/<UUID>.json with ragenix:
1616-# nix run github:yaxitech/ragenix -- -e secrets/age/cf-tunnel.json.age
1717-# 4. Set cfg.cloudflare.tunnelId to that UUID in settings/config/cloudflare.nix
1515+# 3. Encrypt the resulting ~/.cloudflared/<UUID>.json with sops:
1616+# sops --encrypt --age <age-pubkey> cf-tunnel.json > secrets/cf-tunnel.json
1717+# 4. Set myConfig.cloudflare.tunnelId to that UUID (modules/options.nix default
1818+# or a per-host override).
1819# 5. Add CNAME records in Cloudflare DNS for each service:
1920# pds.ewancroft.uk → <UUID>.cfargotunnel.com
2021# matrix.ewancroft.uk → <UUID>.cfargotunnel.com
2122# git.ewancroft.uk → <UUID>.cfargotunnel.com
2223##############################################################################
2323-{ config, lib, self, cfgLib, ... }:
2424-2424+{
2525+ config,
2626+ lib,
2727+ ...
2828+}:
2529let
2626- cfg = cfgLib.cfg.cloudflare;
2727- pdsCfg = cfgLib.cfg.pds;
2828- matrixCfg = cfgLib.cfg.matrix;
2929- forgejoCfg = cfgLib.cfg.forgejo;
3030-3131- # Build ingress routes based on enabled services
3232- ingressRoutes = lib.mkMerge [
3333- # PDS routes (if enabled)
3434- (lib.mkIf pdsCfg.enable {
3535- ${pdsCfg.hostname} = "http://127.0.0.1:${toString pdsCfg.caddyPort}";
3636- "*.${pdsCfg.hostname}" = "http://127.0.0.1:${toString pdsCfg.caddyPort}";
3737- })
3838-3939- # Matrix routes (if enabled)
4040- (lib.mkIf matrixCfg.enable {
4141- ${matrixCfg.hostname} = "http://127.0.0.1:${toString matrixCfg.caddyPort}";
4242- })
3030+ cfg = config.myConfig;
43314444- # Forgejo routes (if enabled)
4545- (lib.mkIf forgejoCfg.enable {
4646- ${forgejoCfg.hostname} = "http://127.0.0.1:${toString forgejoCfg.caddyPort}";
4747- })
4848- ];
3232+ # Build ingress routes based on enabled services.
3333+ ingressRoutes =
3434+ lib.optionalAttrs cfg.services.pds.enable {
3535+ ${cfg.pds.hostname} = "http://127.0.0.1:${toString cfg.pds.caddyPort}";
3636+ "*.${cfg.pds.hostname}" = "http://127.0.0.1:${toString cfg.pds.caddyPort}";
3737+ }
3838+ // lib.optionalAttrs cfg.services.matrix.enable {
3939+ ${cfg.matrix.hostname} = "http://127.0.0.1:${toString cfg.matrix.caddyPort}";
4040+ }
4141+ // lib.optionalAttrs cfg.services.forgejo.enable {
4242+ ${cfg.forgejo.hostname} = "http://127.0.0.1:${toString cfg.forgejo.caddyPort}";
4343+ };
4944in
5050-lib.mkIf cfg.enable {
4545+lib.mkIf cfg.services.cloudflare.enable {
51465252- # ── Secrets ──────────────────────────────────────────────────────────────────
4747+ # ── Secret ──────────────────────────────────────────────────────────────────
5348 # JSON credentials file created by `cloudflared tunnel create server`.
5454- # Encrypted with: nix run github:yaxitech/ragenix -- -e secrets/age/cf-tunnel.json.age
5555- age.secrets."cf-tunnel.json" = {
5656- file = self + /secrets/age/cf-tunnel.json.age;
4949+ # Encrypt with: sops --encrypt --age <age-pubkey> cf-tunnel.json > secrets/cf-tunnel.json
5050+ sops.secrets."cf-tunnel.json" = {
5151+ sopsFile = ../secrets/cf-tunnel.json;
5252+ format = "binary";
5753 owner = "cloudflared";
5858- mode = "0400";
5454+ mode = "0400";
5955 };
60566161- # ── Cloudflare tunnel ─────────────────────────────────────────────────────────
5757+ # ── Cloudflare tunnel ──────────────────────────────────────────────────────
6258 # cloudflared dials outbound to Cloudflare's edge — zero inbound ports needed.
6359 # Single tunnel serves all configured services via hostname-based routing.
6460 services.cloudflared = {
6561 enable = true;
6666- tunnels.${cfg.tunnelId} = {
6767- credentialsFile = config.age.secrets."cf-tunnel.json".path;
6262+ tunnels.${cfg.cloudflare.tunnelId} = {
6363+ credentialsFile = config.sops.secrets."cf-tunnel.json".path;
6864 default = "http_status:404";
6965 ingress = ingressRoutes;
7066 };
7167 };
72687373- # ── Firewall ──────────────────────────────────────────────────────────────────
7474- # The Cloudflare tunnel is fully outbound — no ports need to be open.
6969+ # The Cloudflare tunnel is fully outbound — no firewall ports need to be opened.
7570}
+8-11
modules/cockpit.nix
···1414# https://server:9090
1515#
1616# Log in with any local system user that is a member of the wheel group.
1717-#
1818-# Port configured in settings/config/server.nix → cockpit.port
1917##############################################################################
2020-{ lib, cfgLib, ... }:
2121-1818+{
1919+ config,
2020+ lib,
2121+ ...
2222+}:
2223let
2323- cfg = cfgLib.cfg.server.cockpit;
2424+ cfg = config.myConfig.server.cockpit;
2425in
2526lib.mkIf cfg.enable {
26272728 services.cockpit = {
2829 enable = true;
2929- port = cfg.port;
3030-3131- # Only bind to the Tailscale interface and loopback.
3232- # This ensures Cockpit is never reachable from the public internet,
3333- # even if the firewall is misconfigured.
3030+ port = cfg.port;
3431 settings.WebService.AllowUnencrypted = true;
3532 };
36333737- # ── Firewall ──────────────────────────────────────────────────────────────────
3434+ # ── Firewall ────────────────────────────────────────────────────────────────
3835 # Allow Cockpit only on the Tailscale interface (tailscale0).
3936 # The trusted interface already bypasses the firewall (set in firewall.nix),
4037 # so this rule is belt-and-braces — it blocks access from every other interface.
···44# What this module does:
55# 1. Runs a one-shot systemd service BEFORE the mount that formats the
66# device with ext4 if it has no filesystem yet (safe: skipped if already
77-# formatted). Set the device in settings/config/server.nix → storage.srv.
77+# formatted). Set the device via myConfig.server.storage.srv.device.
88# 2. Declares the /srv fileSystem entry (NixOS handles the actual mount).
99# 3. Uses systemd-tmpfiles to create every required subdirectory with the
1010# correct ownership, after the mount is up.
···1616# /srv/bluesky-pds — Bluesky ATProto PDS data
1717# /srv/www — Static websites / reverse-proxied web roots
1818##############################################################################
1919-{ config, lib, pkgs, cfgLib, ... }:
2020-1919+{
2020+ config,
2121+ pkgs,
2222+ ...
2323+}:
2124let
2222- srv = cfgLib.cfg.server.storage.srv;
2525+ srv = config.myConfig.server.storage.srv;
2326 device = srv.device;
2427in
2528{
2626- # ── 1. Auto-format ────────────────────────────────────────────────────────────
2727- # Runs before the filesystem is mounted. Formats with ext4 + label "srv"
2929+ # ── 1. Auto-format ──────────────────────────────────────────────────────────
3030+ # Runs before the filesystem is mounted. Formats with ext4 + label "srv"
2831 # only if blkid reports no filesystem type on the device.
2932 systemd.services."srv-autoformat" = {
3033 description = "Auto-format ${device} as ext4 if unformatted";
31343235 # Must complete before the mount unit tries to mount /srv
3333- before = [ "srv.mount" ];
3636+ before = [ "srv.mount" ];
3437 wantedBy = [ "srv.mount" ];
35383639 # Only attempt if the device node actually exists (won't run in VM/CI
···3841 unitConfig.ConditionPathExists = device;
39424043 serviceConfig = {
4141- Type = "oneshot";
4444+ Type = "oneshot";
4245 RemainAfterExit = true;
4346 };
44474545- path = [ pkgs.util-linux pkgs.e2fsprogs ];
4848+ path = [
4949+ pkgs.util-linux
5050+ pkgs.e2fsprogs
5151+ ];
46524753 script = ''
4854 if blkid "${device}" | grep -q 'TYPE='; then
···5460 '';
5561 };
56625757- # ── 2. /srv mount ─────────────────────────────────────────────────────────────
6363+ # ── 2. /srv mount ──────────────────────────────────────────────────────────
5864 fileSystems."/srv" = {
5965 inherit (srv) device fsType options;
6066 # Require the autoformat service to run first
6161- depends = [ "srv-autoformat.service" ];
6767+ depends = [ "srv-autoformat.service" ];
6268 neededForBoot = false;
6369 };
64706565- # ── 3. Subdirectory creation ──────────────────────────────────────────────────
7171+ # ── 3. Subdirectory creation ────────────────────────────────────────────────
6672 # systemd-tmpfiles creates these after /srv is mounted.
6773 # 'd' = create directory if missing, set mode/owner, never remove on cleanup.
6874 systemd.tmpfiles.rules = [
+62-62
modules/server/timemachine.nix
···22# Time Machine backup target
33#
44# Exposes a Samba share with the `fruit` VFS module so macOS clients can
55-# use this server as a Time Machine destination. AFP was dropped in macOS
55+# use this server as a Time Machine destination. AFP was dropped in macOS
66# Ventura, so SMB + fruit is the correct modern approach.
77#
88-# What this module does:
99-# 1. Configures Samba (smbd) with the fruit / streams_xattr VFS stack that
1010-# macOS requires for reliable extended-attribute and resource-fork handling.
1111-# 2. Advertises the share via Avahi (mDNS) so Macs discover it automatically
1212-# in System Settings -> General -> Time Machine.
1313-# 3. Creates /srv/timemachine with correct ownership via systemd-tmpfiles.
1414-#
158# Setup (one-time, after deploying):
169# - Add a Samba user for every Mac that will back up:
1710# sudo smbpasswd -a <username>
1811# - In macOS System Settings -> General -> Time Machine, click "Add Backup
1912# Disk...", choose the advertised share, and authenticate.
2013#
2121-# Settings knobs (settings/config/server.nix -> timemachine):
1414+# Settings knobs (myConfig.server.timemachine):
2215# enable - master toggle
2316# shareName - name visible to macOS (default "TimeMachine")
2417# path - filesystem path for backup data (default /srv/timemachine)
2518# maxSizeGB - soft storage cap reported to macOS (0 = unlimited)
2619# validUsers - list of Samba usernames allowed to write backups
2720##############################################################################
2828-{ config, lib, pkgs, cfgLib, ... }:
2929-2121+{
2222+ config,
2323+ lib,
2424+ ...
2525+}:
3026let
3131- tm = cfgLib.cfg.server.timemachine;
3232- cap = if tm.maxSizeGB > 0 then toString tm.maxSizeGB + " G" else "";
2727+ tm = config.myConfig.server.timemachine;
2828+ cap = if tm.maxSizeGB > 0 then toString tm.maxSizeGB + " G" else "";
3329 users = lib.concatStringsSep " " tm.validUsers;
3430in
3531lib.mkIf tm.enable {
36323737- # ── Samba ────────────────────────────────────────────────────────────────────
3333+ # ── Samba ──────────────────────────────────────────────────────────────────
3834 services.samba = {
3939- enable = true;
4040- openFirewall = false; # we manage ports explicitly below
3535+ enable = true;
3636+ openFirewall = false; # ports managed explicitly below
41374238 settings = {
4339 global = {
4444- # Server identity
4545- "workgroup" = "WORKGROUP";
4040+ "workgroup" = "WORKGROUP";
4641 "server string" = config.networking.hostName;
4747- "server role" = "standalone server";
4242+ "server role" = "standalone server";
48434949- # Disable printer sharing
5044 "load printers" = "no";
5145 "printcap name" = "/dev/null";
52465353- # macOS interoperability -- fruit VFS stack
5454- "vfs objects" = "catia fruit streams_xattr";
5555- "fruit:metadata" = "stream";
5656- "fruit:model" = "MacSamba";
5757- "fruit:posix_rename" = "yes";
5858- "fruit:veto_appledouble" = "no";
4747+ # macOS interoperability — fruit VFS stack
4848+ "vfs objects" = "catia fruit streams_xattr";
4949+ "fruit:metadata" = "stream";
5050+ "fruit:model" = "MacSamba";
5151+ "fruit:posix_rename" = "yes";
5252+ "fruit:veto_appledouble" = "no";
5953 "fruit:wipe_intentionally_left_blank_rfork" = "yes";
6060- "fruit:delete_empty_adfiles" = "yes";
5454+ "fruit:delete_empty_adfiles" = "yes";
61556262- # Security
6363- "security" = "user";
5656+ "security" = "user";
6457 "map to guest" = "Never";
6565- "ntlm auth" = "yes"; # required for older macOS clients
5858+ "ntlm auth" = "yes"; # required for older macOS clients
6659 "min protocol" = "SMB2";
6767- "smb encrypt" = "desired";
6060+ "smb encrypt" = "desired";
6861 };
69627070- ${tm.shareName} = {
7171- "path" = tm.path;
7272- "valid users" = users;
7373- "public" = "no";
7474- "writable" = "yes";
7575- "browseable" = "yes";
7676- "create mask" = "0600";
7777- "directory mask" = "0700";
7878-7979- # Tell macOS this share is a Time Machine destination
8080- "fruit:time machine" = "yes";
8181- } // lib.optionalAttrs (cap != "") {
8282- "fruit:time machine max size" = cap;
8383- };
6363+ ${tm.shareName} =
6464+ {
6565+ "path" = tm.path;
6666+ "valid users" = users;
6767+ "public" = "no";
6868+ "writable" = "yes";
6969+ "browseable" = "yes";
7070+ "create mask" = "0600";
7171+ "directory mask" = "0700";
7272+ "fruit:time machine" = "yes";
7373+ }
7474+ // lib.optionalAttrs (cap != "") {
7575+ "fruit:time machine max size" = cap;
7676+ };
8477 };
8578 };
86798780 # WS-Discovery so macOS/Windows can find the server by name on the LAN
8881 services.samba-wsdd = {
8989- enable = true;
9090- interface = ""; # all interfaces
8282+ enable = true;
8383+ interface = ""; # all interfaces
9184 };
92859393- # ── Avahi (mDNS) -- Macs discover the share automatically ───────────────────
8686+ # ── Avahi (mDNS) — Macs discover the share automatically ──────────────────
9487 services.avahi = {
9595- enable = true;
9696- nssmdns4 = true;
8888+ enable = true;
8989+ nssmdns4 = true;
9790 publish = {
9898- enable = true;
9999- addresses = true;
100100- domain = true;
101101- hinfo = true;
9191+ enable = true;
9292+ addresses = true;
9393+ domain = true;
9494+ hinfo = true;
10295 userServices = true;
103103- workstation = true;
9696+ workstation = true;
10497 };
1059810699 # Three records macOS looks for when scanning for Time Machine targets:
107107- # _smb._tcp -- the actual file-sharing service
108108- # _device-info._tcp -- icon hint (shows as a NAS/Time Capsule in Finder)
109109- # _adisk._tcp -- Time Machine share advertisement
100100+ # _smb._tcp — the actual file-sharing service
101101+ # _device-info._tcp — icon hint (shows as a NAS/Time Capsule in Finder)
102102+ # _adisk._tcp — Time Machine share advertisement
110103 extraServiceFiles.timemachine = lib.mkAfter ''
111104 <?xml version="1.0" standalone='no'?>
112105 <!DOCTYPE service-group SYSTEM "avahi-service.dtd">
···131124 '';
132125 };
133126134134- # ── Firewall ─────────────────────────────────────────────────────────────────
127127+ # ── Firewall ────────────────────────────────────────────────────────────────
135128 # Samba needs TCP 445 (SMB) + 139 (NetBIOS session) and UDP 137-138 (NetBIOS).
136129 # WS-Discovery uses UDP 3702.
137130 networking.firewall = {
138138- allowedTCPPorts = [ 445 139 ];
139139- allowedUDPPorts = [ 137 138 3702 ];
131131+ allowedTCPPorts = [
132132+ 445
133133+ 139
134134+ ];
135135+ allowedUDPPorts = [
136136+ 137
137137+ 138
138138+ 3702
139139+ ];
140140 };
141141142142- # ── Storage directory ─────────────────────────────────────────────────────────
142142+ # ── Storage directory ──────────────────────────────────────────────────────
143143 systemd.tmpfiles.rules = [
144144 "d ${tm.path} 0750 root sambashare -"
145145 ];
···11-{
22- # Cloudflare Tunnel configuration.
33- # Single tunnel for all services (PDS, Matrix, etc.)
44-55- # Tunnel UUID from `cloudflared tunnel create server`
66- # Replace this after running that command.
77- tunnelId = "63ec1b18-1358-4ee2-9093-713b4e7d9325";
88-99- # Ingress routes - maps hostnames to internal services
1010- # These are configured automatically by service modules (pds.nix, matrix.nix, etc.)
1111- # but can be overridden here if needed.
1212-}
-190
settings/config/darwin.nix
···11-{
22- # macOS configuration (nix-darwin)
33-44- # ─── Keyboard ────────────────────────────────────────────────────────────────
55- keyboard = {
66- enableKeyMapping = true;
77- remapCapsLockToControl = false; # Keep Caps Lock as Caps Lock
88- };
99-1010- # ─── Startup ─────────────────────────────────────────────────────────────────
1111- startup = {
1212- chime = true; # Let it bong
1313- };
1414-1515- # ─── Security ────────────────────────────────────────────────────────────────
1616- security = {
1717- touchIdForSudo = true; # Allow Touch ID to authenticate sudo
1818- };
1919-2020- # ─── Homebrew ────────────────────────────────────────────────────────────────
2121- homebrew = {
2222- enable = true;
2323-2424- # Taps (repositories)
2525- taps = [
2626- # Add custom taps here if needed
2727- ];
2828-2929- # CLI tools managed by Homebrew (complex media/codec dependencies)
3030- brews = [
3131- # Media libraries
3232- "libmediainfo"
3333- "media-info"
3434- "libzen"
3535-3636- # Video/audio codecs
3737- "aribb24"
3838- "dav1d"
3939- "rav1e"
4040- "svt-av1"
4141- "x264"
4242- "x265"
4343- "xvid"
4444- "webp"
4545- "aom"
4646- "jpeg-xl"
4747- "highway"
4848-4949- # Audio
5050- "flac"
5151- "lame"
5252- "opus"
5353- "vorbis-tools"
5454- "libsndfile"
5555- "libsamplerate"
5656- "rubberband"
5757- "speex"
5858- "theora"
5959- "mpg123"
6060-6161- # Image processing
6262- "little-cms2"
6363- "leptonica"
6464-6565- # Network protocols
6666- "rtmpdump"
6767- "srt"
6868- "librist"
6969- "libmms"
7070-7171- # Compression
7272- "lzo"
7373- "snappy"
7474- "xxhash"
7575- "yyjson"
7676-7777- # Database drivers
7878- "freetds"
7979- "unixodbc"
8080-8181- # Miscellaneous
8282- "summarize"
8383- "goat"
8484- "mas"
8585- ];
8686-8787- # GUI applications via Homebrew Cask
8888- # Note: apps available in nixpkgs are installed via darwin.packages below.
8989- casks = [
9090- # Communication
9191- "element" # build fails in nixpkgs on darwin (requires Xcode 26 in Nix sandbox)
9292-9393- # Productivity
9494- "github" # GitHub Desktop (not in nixpkgs)
9595- "claude"
9696-9797- # Browsers
9898- "firefox" # Not available in nixpkgs-darwin
9999-100100- # Media & Entertainment
101101- "obs" # OBS Studio (keep in Homebrew — complex macOS plugin deps)
102102- "handbrake-app"
103103-104104- # Gaming
105105- "steam"
106106- "epic-games"
107107- "prismlauncher" # wayland dep build failure in nixpkgs on darwin (issue #455247)
108108- "utm"
109109-110110- # Utilities
111111- "cloudflare-warp"
112112- "tailscale-app" # Renamed from tailscale
113113- # filezilla — not available on macOS in Homebrew or nixpkgs; use Cyberduck or ForkLift instead
114114- "parsec" # Remote desktop (Linux-only in nixpkgs)
115115- "onyx"
116116- "mos" # Mouse/trackpad customization (macOS-specific)
117117-118118- # Office & Documents
119119- "microsoft-excel"
120120- "microsoft-powerpoint"
121121- "microsoft-teams"
122122- "microsoft-word"
123123- "libreoffice"
124124-125125- # Hardware
126126- "logitune" # Logitech webcam
127127- "logi-options+" # Logitech devices (replaces deprecated logitech-options)
128128-129129- # Gaming / social
130130- "roblox"
131131- "ea" # EA app (game launcher)
132132-133133- # Other
134134- "netnewswire" # RSS reader
135135- "altserver" # AltStore sideloading server
136136- # 2fhey — not in Homebrew, install manually
137137- # letta-desktop — not in Homebrew, install manually
138138- # filezilla — removed from Homebrew, install manually
139139- ];
140140-141141- # Mac App Store apps (by ID)
142142- masApps = {
143143- "Amphetamine" = 937984704;
144144- # Mini Motorways — Apple Arcade, not available via MAS ID
145145- "OneDrive" = 823766827; # moved from casks
146146- "OP Auto Clicker" = 6754914118;
147147- "Steam Link" = 1246969117; # was incorrectly labelled "EA app"
148148- "TestFlight" = 899247664;
149149- "The Unarchiver" = 425424353; # moved from casks
150150- "WhatsApp" = 310633997; # moved from casks
151151- "Zone Bar" = 6755328989;
152152- };
153153- };
154154-155155- # ─── Nixpkgs packages (macOS-only) ──────────────────────────────────────────
156156- # Cross-platform development packages live in settings/config/packages.nix
157157- # → development. Only add things here that are macOS-specific or provide
158158- # GNU replacements for the BSD tools macOS ships by default.
159159- packages = [
160160- # GNU replacements for BSD tools macOS ships
161161- "coreutils" # GNU ls/cp/mv/etc (macOS has BSD variants)
162162- "parallel" # GNU parallel
163163- "stow" # GNU stow (symlink farm manager)
164164- "netcat" # GNU netcat (macOS has BSD nc)
165165-166166- # Dev libraries needed on PATH for building on macOS
167167- # (on NixOS these are pulled in automatically as build deps)
168168- "openssl"
169169- "readline"
170170- "ncurses"
171171- "pcre"
172172- "pcre2"
173173- "libffi"
174174-175175- # ── GUI apps (migrated from Homebrew Cask) ────────────────────────────────
176176- # These are available in nixpkgs and managed declaratively.
177177- # mac-app-util (already in the flake) ensures they appear in Spotlight/Launchpad.
178178- "discord" # Communication
179179- "signal-desktop-bin" # Signal — officially the darwin path per nixpkgs 25.11 release notes
180180- # element-desktop — build requires Xcode 26 unavailable in Nix sandbox on darwin
181181- "obsidian" # Note-taking
182182- "vscode" # Editor (note: vscode-fhs fails on darwin, plain vscode is fine)
183183- "spotify" # Music
184184- "transmission_4" # BitTorrent client
185185- # filezilla — Linux-only in nixpkgs, installed via Homebrew cask instead
186186- # parsec-bin — Linux-only in nixpkgs, installed via Homebrew cask instead
187187- # prismlauncher — wayland dep build failure on darwin (nixpkgs issue #455247)
188188- ];
189189-190190-}
-58
settings/config/default.nix
···11-let
22- # Import server config once so we can read the service toggles below.
33- serverCfg = import ./server.nix;
44- svcToggles = serverCfg.services;
55-in
66-{
77- # ============================================================================
88- # CENTRAL CONFIGURATION - SINGLE SOURCE OF TRUTH
99- # ============================================================================
1010- # All configurable values for the entire system are organized here.
1111- # Each category has its own file in settings/config/ for better organization.
1212- #
1313- # To customize your setup, edit the individual files:
1414- # - user.nix : User account settings
1515- # - system.nix : System-level configuration
1616- # - nix.nix : Nix package manager settings
1717- # - packages.nix : Package lists for different use cases
1818- # - git.nix : Git configuration and aliases
1919- # - shell.nix : Shell aliases and history settings
2020- # - desktop.nix : Desktop environment settings (Linux)
2121- # - ssh.nix : SSH configuration
2222- # - audio.nix : Audio backend configuration
2323- # - gaming.nix : Gaming-related settings
2424- # - server.nix : Server-specific configuration
2525- # ↳ services { } — master on/off switches for all services
2626- # - darwin.nix : macOS-specific settings
2727- # - secrets.nix : Secrets management configuration
2828- # - development.nix : Development tools and languages
2929- # - maintenance.nix : Backup and auto-update settings
3030- # - pds.nix : Bluesky Personal Data Server settings
3131- # - matrix.nix : Matrix Synapse homeserver settings
3232- # - forgejo.nix : Forgejo git forge settings
3333- # - cloudflare.nix : Cloudflare Tunnel configuration
3434-3535- user = import ./user.nix;
3636- system = import ./system.nix;
3737- nix = import ./nix.nix;
3838- packages = import ./packages.nix;
3939- git = import ./git.nix;
4040- shell = import ./shell.nix;
4141- desktop = import ./desktop.nix;
4242- ssh = import ./ssh.nix;
4343- audio = import ./audio.nix;
4444- gaming = import ./gaming.nix;
4545- server = serverCfg;
4646- darwin = import ./darwin.nix;
4747- secrets = import ./secrets.nix;
4848- development = import ./development.nix;
4949- maintenance = import ./maintenance.nix;
5050-5151- # Service configs — the `enable` flag is driven by server.nix `services.*`
5252- # so there is a single place to turn services on/off. All other settings
5353- # (ports, hostnames, restart policy …) remain in the individual files.
5454- forgejo = (import ./forgejo.nix) // { enable = svcToggles.forgejo; };
5555- pds = (import ./pds.nix) // { enable = svcToggles.pds; };
5656- matrix = (import ./matrix.nix) // { enable = svcToggles.matrix; };
5757- cloudflare = (import ./cloudflare.nix) // { enable = svcToggles.cloudflare; };
5858-}
-47
settings/config/desktop.nix
···11-let
22- # ── Single-source font primitives ─────────────────────────────────────────
33- # Change a name here and Konsole, KDE, and VS Code all update at once.
44- monoFontBase = "FiraCode"; # root font family name
55- monoFontFamily = "${monoFontBase} Nerd Font Mono"; # full name for KDE / Konsole
66- monoFontSize = 11;
77-88- uiFont = "Noto Sans"; # closest open-source match to macOS San Francisco
99- uiFontSize = 10;
1010-in
1111-{
1212- # Desktop environment configuration (Linux)
1313- enable = true;
1414- environment = "plasma6"; # "gnome" | "plasma6" | "xfce"
1515- displayManager = "sddm"; # "gdm" | "sddm" | "lightdm"
1616-1717- # GTK/Qt theming
1818- theme = "Catppuccin-Mocha-Standard-Green-Dark";
1919- iconTheme = "Papirus-Dark";
2020-2121- # ── Font primitives — single source of truth ──────────────────────────────
2222- # All consumers (KDE font roles, Konsole, VS Code) reference these;
2323- # nothing below is ever hardcoded elsewhere.
2424- inherit uiFont uiFontSize monoFontBase monoFontFamily monoFontSize;
2525-2626- # Computed composites — derived, never typed twice
2727- monoFont = "${monoFontFamily} ${toString monoFontSize}"; # "FiraCode Nerd Font Mono 11"
2828- monoFontConsole = monoFontFamily; # Konsole font.name = family only (no trailing size)
2929-3030- # ── KDE Plasma-specific settings ───────────────────────────────────────────
3131- plasma = {
3232- # Color scheme applied by plasma-apply-colorscheme on login.
3333- # Mirrors macOS: NSGlobalDomain.AppleInterfaceStyle = "Dark"
3434- # NSGlobalDomain.AppleAccentColor = 3 (Green)
3535- colorScheme = "CatppuccinMochaGreen";
3636-3737- # Plasma desktop style (controls panel/widget chrome).
3838- desktopTheme = "breeze-dark";
3939-4040- # Packages to exclude from the default KDE Plasma install.
4141- # Must match attribute names under pkgs.kdePackages.
4242- excludePackages = [
4343- "oxygen" # Legacy Oxygen theme — use Breeze/Catppuccin instead
4444- "elisa" # KDE music player — use Spotify instead
4545- ];
4646- };
4747-}
-86
settings/config/development.nix
···11-let
22- # Pull the font primitives from the single source of truth so VS Code
33- # stays in sync with KDE/Konsole without hardcoding "FiraCode" twice.
44- desktop = import ./desktop.nix;
55-66- # VS Code uses the bare family name (no "Nerd Font Mono" suffix) for the
77- # editor pane, and the Nerd Font variant (without "Mono") for the terminal.
88- editorFont = desktop.monoFontBase; # "FiraCode"
99- terminalFont = "${desktop.monoFontBase} Nerd Font"; # "FiraCode Nerd Font"
1010-in
1111-{
1212- # Development configuration
1313-1414- # VS Code configuration
1515- vscode = {
1616- enable = true;
1717-1818- # Theme
1919- colorTheme = "Catppuccin Mocha";
2020- iconTheme = "catppuccin-vsc-icons";
2121-2222- # Editor appearance — font strings derived from desktop.nix primitives
2323- fontFamily = "'${editorFont}', 'monospace'"; # "'FiraCode', 'monospace'"
2424- terminalFontFamily = "'${terminalFont}'"; # "'FiraCode Nerd Font'"
2525- fontSize = 14;
2626- terminalFontSize = 13;
2727- lineHeight = 22;
2828- fontLigatures = true;
2929-3030- # Extensions from nixpkgs (pkgs.vscode-extensions.<publisher>.<name>).
3131- # Must match attribute paths in the nixpkgs vscode-extensions set.
3232- extensions = [
3333- # ── Nix ──────────────────────────────────────────────────────────────────
3434- "jnoortheen.nix-ide" # Nix LSP, formatting, error reporting
3535-3636- # ── Python ───────────────────────────────────────────────────────────────
3737- "ms-python.python" # Python IntelliSense + debugger
3838- "ms-python.debugpy" # Python debugger (required peer dep)
3939-4040- # ── Rust ─────────────────────────────────────────────────────────────────
4141- "rust-lang.rust-analyzer" # Rust LSP
4242-4343- # ── C# / VB.NET (.NET) ───────────────────────────────────────────────────
4444- "ms-dotnettools.csharp" # C# and VB.NET language support
4545- "ms-dotnettools.csdevkit" # C# Dev Kit (solution explorer, test runner)
4646-4747- # ── Shell / Bash ─────────────────────────────────────────────────────────
4848- "mads-hartmann.bash-ide-vscode" # Bash language server
4949- "timonwong.shellcheck" # ShellCheck linting for sh/bash/zsh
5050- "foxundermoon.shell-format" # shfmt formatter for shell scripts
5151-5252- # ── Docker ───────────────────────────────────────────────────────────────
5353- "ms-azuretools.vscode-docker" # Dockerfile syntax, linting, Docker integration
5454-5555- # ── Data / config formats ─────────────────────────────────────────────────
5656- "tamasfe.even-better-toml" # TOML (Cargo.toml, starship.toml, pyproject.toml…)
5757- "redhat.vscode-yaml" # YAML with JSON schema validation
5858-5959- # ── Web / Frontend ────────────────────────────────────────────────────────
6060- "bradlc.vscode-tailwindcss" # Tailwind CSS IntelliSense
6161- "dbaeumer.vscode-eslint" # ESLint (JS/TS/Svelte)
6262- "esbenp.prettier-vscode" # Prettier formatter (JS/TS/CSS/HTML/JSON/YAML…)
6363-6464- # ── Git ───────────────────────────────────────────────────────────────────
6565- "eamodio.gitlens" # Git blame, history, diffing
6666- "editorconfig.editorconfig" # .editorconfig support
6767-6868- # ── General quality-of-life ───────────────────────────────────────────────
6969- "streetsidesoftware.code-spell-checker" # Spell checking in comments/strings
7070- "christian-kohler.path-intellisense" # Filename autocompletion
7171-7272- # ── Theme / icons ─────────────────────────────────────────────────────────
7373- # catppuccin-vsc and catppuccin-vsc-icons are installed by the
7474- # catppuccin home-manager module automatically — do not declare here.
7575- ];
7676-7777- # Extensions from the VS Code Marketplace via the nix-vscode-extensions
7878- # overlay (pkgs.vscode-marketplace.<publisher>.<name>).
7979- # Use this for extensions not packaged in base nixpkgs 25.11.
8080- marketplaceExtensions = [
8181- "golang.go" # Go language support (requires gopls on PATH)
8282- "svelte.svelte-vscode" # Svelte language server
8383- "ms-vscode.makefile-tools" # Makefile syntax, build targets, IntelliSense
8484- ];
8585- };
8686-}
-21
settings/config/forgejo.nix
···11-{
22- # Forgejo git forge configuration.
33- # Non-secret settings only. Secrets (secret key, mailer password, etc.)
44- # live in secrets/age/forgejo.env.age.
55-66- # Public hostname.
77- hostname = "git.ewancroft.uk";
88-99- # Internal port the Forgejo process listens on. Never exposed publicly.
1010- port = 3001;
1111-1212- # Caddy internal listen port — Cloudflare tunnel routes here.
1313- caddyPort = 3002;
1414-1515- # Display name shown in the UI.
1616- appName = "Ewan's Git";
1717-1818- # Disable public registration — invite-only or admin-created accounts only.
1919- disableRegistration = true;
2020- # Restart policy is shared: see settings/config/server.nix → servicePolicy.
2121-}
···11-{
22- # Backup & maintenance configuration
33-44- # Automatic system updates (NixOS only – nix-darwin does not support system.autoUpgrade)
55- autoUpgrade = {
66- enable = true;
77- allowReboot = false;
88- dates = "daily";
99- randomizedDelaySec = "45min";
1010- updateInputs = [ "nixpkgs" ]; # Inputs to update on each run
1111- };
1212-1313- # Backup configuration
1414- backup = {
1515- enable = false;
1616- paths = [
1717- "/home"
1818- "/etc/nixos"
1919- ];
2020- };
2121-}
-20
settings/config/matrix.nix
···11-{
22- # Matrix Synapse homeserver configuration.
33- # Non-secret settings only. Secrets (registration_shared_secret, macaroon_secret_key)
44- # should be stored in secrets/age/matrix.env.age.
55-66- # Public hostname — also used as the Caddy virtual host and the Cloudflare
77- # tunnel public hostname.
88- hostname = "matrix.ewancroft.uk";
99-1010- # The base domain used for Matrix IDs (@user:domain).
1111- # Using your apex domain so users have clean Matrix IDs like @username:ewancroft.uk
1212- serverName = "ewancroft.uk";
1313-1414- # Internal port the Synapse process listens on. Never exposed publicly.
1515- port = 8008;
1616-1717- # Caddy internal listen port — Cloudflare tunnel routes here.
1818- caddyPort = 8448;
1919- # Restart policy is shared: see settings/config/server.nix → servicePolicy.
2020-}
-27
settings/config/nix.nix
···11-{
22- # Nix configuration
33-44- # Experimental features
55- experimentalFeatures = [ "nix-command" "flakes" ];
66-77- # Store optimization
88- autoOptimise = true;
99-1010- # Garbage collection
1111- gc = {
1212- automatic = true;
1313- dates = "weekly"; # "weekly", "daily", or specific time like "03:15"
1414- options = "--delete-older-than 30d";
1515- };
1616-1717- # Channel/input versions
1818- # NOTE: These are for documentation only. Flake inputs cannot reference local files.
1919- # To update channels, edit the URLs directly in flake.nix inputs section.
2020- # These values are kept here for consistency and documentation.
2121- channels = {
2222- nixpkgs = "nixos-25.11";
2323- nixpkgsDarwin = "nixpkgs-25.11-darwin";
2424- homeManager = "release-25.11";
2525- nixDarwin = "nix-darwin-25.11";
2626- };
2727-}
-174
settings/config/packages.nix
···11-{
22- # Package configuration
33-44- allowUnfree = true;
55-66- # ── Common CLI utilities (every system: laptop, macmini, server) ─────────────
77- common = [
88- # System info & monitoring
99- "fastfetch"
1010- "btop" # Modern htop alternative
1111-1212- # Modern CLI tools
1313- "eza" # Modern ls
1414- "bat" # Modern cat with syntax highlighting
1515- "ripgrep" # Fast grep (rg)
1616- "fd" # Fast find
1717- "fzf" # Fuzzy finder
1818- "tree"
1919-2020- # Version control
2121- "git"
2222- "lazygit" # Git TUI
2323-2424- # Archives
2525- "unzip"
2626- "zip"
2727-2828- # Editors & multiplexers
2929- "nano"
3030- "tmux"
3131-3232- # Network tools
3333- "openssh" # SSH client
3434- "wget"
3535- "curl"
3636-3737- # File sync
3838- "rsync"
3939- ];
4040-4141- # ── Development packages (laptop + macmini – NOT server) ─────────────────────
4242- # Cross-platform: installed via modules/packages.nix on NixOS and
4343- # modules/darwin/packages.nix on macOS. Keep macOS-only things in
4444- # settings/config/darwin.nix → packages.
4545- development = [
4646- # Nix tooling
4747- "nil" # Nix language server (jnoortheen.nix-ide)
4848- "nixfmt-rfc-style" # Nix formatter
4949-5050- # Version control (git is in common)
5151- "git-filter-repo"
5252- "gh" # GitHub CLI
5353-5454- # Languages & runtimes
5555- "go"
5656- "nodejs_22"
5757- "python313" # Primary Python version
5858- "bun" # Fast TS/JS runtime & bundler
5959- "pnpm" # Fast package manager (SvelteKit)
6060- "rustup" # Rust toolchain manager
6161- "dotnet-sdk" # .NET SDK
6262-6363- # Go tooling
6464- "gopls" # Go language server (golang.go extension)
6565- "golangci-lint" # Go linter
6666- "delve" # Go debugger
6767-6868- # Python tooling
6969- "pipx"
7070- "uv"
7171- "ruff" # Fast Python linter + formatter
7272- "pyright" # Python type checker / language server
7373-7474- # Shell tooling
7575- "shellcheck" # Shell script static analysis (timonwong.shellcheck extension)
7676- "shfmt" # Shell script formatter (foxundermoon.shell-format extension)
7777-7878- # Build tools
7979- "cmake"
8080- "autoconf"
8181- "libtool"
8282- "pkgconf"
8383- "m4"
8484-8585- # Media processing
8686- "ffmpeg"
8787- "exiftool"
8888- "atomicparsley"
8989- "get_iplayer"
9090-9191- # Network / infra
9292- "tailscale"
9393- "websocat"
9494- "nmap"
9595-9696- # Text processing
9797- "jq"
9898-9999- # Compression
100100- "zstd"
101101- "xz"
102102- "lz4"
103103- "brotli"
104104-105105- # Database
106106- "sqlite"
107107-108108- # Image processing / OCR
109109- "tesseract"
110110-111111- # Additional runtimes
112112- "openjdk21" # Java LTS
113113- "php" # PHP runtime
114114- "ollama" # Local LLM runtime
115115- ];
116116-117117- # ── Nerd Fonts to install ─────────────────────────────────────────────────────
118118- fonts = [
119119- "fira-code"
120120- "jetbrains-mono"
121121- "meslo-lg"
122122- "roboto-mono"
123123- "sauce-code-pro"
124124- "ubuntu-mono"
125125- ];
126126-127127- # ── Linux-only packages ───────────────────────────────────────────────────────
128128- linux = [
129129- "vlc"
130130- # dconf2nix was only useful for exporting GNOME dconf settings;
131131- # KDE settings are managed directly by plasma-manager.
132132- ];
133133-134134- # ── Desktop/GUI packages (NixOS laptop) ──────────────────────────────────────
135135- desktop = [
136136- # Theming
137137- "papirus-icon-theme" # Clean minimal icon theme
138138-139139- # Communication
140140- "discord"
141141- "signal-desktop"
142142- "element-desktop" # Matrix client
143143-144144- # Media
145145- "spotify"
146146-147147- # Productivity
148148- "obsidian" # Note-taking (Markdown)
149149- "libreoffice-fresh"
150150-151151- # Creative
152152- "gimp" # Image editing
153153- "inkscape" # Vector graphics
154154-155155- # Gaming/Remote
156156- "parsec-bin" # Remote gaming/desktop
157157- "prismlauncher" # Minecraft launcher
158158-159159- # System tools (KDE System Settings is built-in – no extra package needed)
160160- ];
161161-162162- # ── Gaming packages ───────────────────────────────────────────────────────────
163163- gaming = [
164164- "steam"
165165- "lutris"
166166- "wine"
167167- "winetricks"
168168- ];
169169-170170- # ── Server-only packages ──────────────────────────────────────────────────────
171171- server = [
172172- # git + rsync come from common; only add server-specific extras here
173173- ];
174174-}
-37
settings/config/pds.nix
···11-{
22- # Bluesky ATProto Personal Data Server configuration.
33- # Non-secret settings only. Secrets (PDS_JWT_SECRET, PDS_ADMIN_PASSWORD,
44- # PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, PDS_EMAIL_SMTP_URL,
55- # PDS_EMAIL_FROM_ADDRESS) live in secrets/age/pds.env.age.
66-77- # Public hostname — also used as the Caddy virtual host and the Cloudflare
88- # tunnel public hostname. Subdomains are used for account handles.
99- hostname = "pds.ewancroft.uk";
1010-1111- # Internal port the PDS process listens on. Never exposed publicly.
1212- port = 3000;
1313-1414- # Email shown in the PDS admin panel.
1515- adminEmail = "pds@ewancroft.uk";
1616-1717- # Additional handle domains. ".ewancroft.uk" lets users have @user.ewancroft.uk handles.
1818- serviceHandleDomains = [ ".ewancroft.uk" ];
1919-2020- # ATProto relay crawlers — sourced from https://compare.hose.cam
2121- crawlers = [
2222- "https://bsky.network"
2323- "https://relay.cerulea.blue"
2424- "https://relay.fire.hose.cam"
2525- "https://relay2.fire.hose.cam"
2626- "https://relay3.fr.hose.cam"
2727- "https://relay.hayescmd.net"
2828- "https://relay.xero.systems"
2929- "https://relay.upcloud.world"
3030- "https://relay.feeds.blue"
3131- "https://atproto.africa"
3232- ];
3333-3434- # Caddy internal listen port — Cloudflare tunnel routes here.
3535- caddyPort = 2020;
3636- # Restart policy is shared: see settings/config/server.nix → servicePolicy.
3737-}
-19
settings/config/secrets.nix
···11-{
22- # Secrets configuration
33- enable = true;
44- masterKeyPath = "~/.config/age/keys.txt";
55-66- # Core secrets (always present once set up)
77- files = [
88- "ssh-passphrase"
99- "wifi-home"
1010- ];
1111-1212- # Optional per-app secrets.
1313- # Set enable = true only after the corresponding .age file has been created
1414- # by the migration script (secrets/age/<name>.age must exist in the repo).
1515- docker = { enable = true; }; # ~/.docker/config.json
1616- claude = { enable = true; }; # ~/.claude.json
1717- duckdns = { enable = false; }; # ~/.duckdns/ — server/Linux only; enable per-host
1818- forgejo = { enable = false; }; # Forgejo SECRET_KEY + INTERNAL_TOKEN — enable after creating secrets/age/forgejo.env.age
1919-}
-98
settings/config/server.nix
···11-{
22- # Server configuration
33-44- # ─── Service toggles ─────────────────────────────────────────────────────────
55- # Master on/off switches for every server service.
66- # The detailed settings for each service live in their own files (e.g.
77- # settings/config/forgejo.nix) — only the enable flag lives here so you
88- # have one place to see what's running.
99- services = {
1010- forgejo = true; # Forgejo git forge (git.ewancroft.uk)
1111- pds = true; # Bluesky ATProto PDS (pds.ewancroft.uk)
1212- matrix = true; # Matrix Synapse homeserver (matrix.ewancroft.uk)
1313- cloudflare = true; # Cloudflare tunnel (outbound, all services)
1414- };
1515-1616- # ─── Time Machine ─────────────────────────────────────────────────────────────
1717- # Exposes an SMB share (with the fruit VFS module) that macOS backs up to.
1818- # Requires AFP/Samba — AFP was dropped in Ventura so SMB + fruit is used.
1919- #
2020- # After deploying, create a Samba user for each Mac:
2121- # sudo smbpasswd -a <username>
2222- # Then add the backup disk in macOS System Settings -> General -> Time Machine.
2323- timemachine = {
2424- enable = false; # set to true to activate
2525- shareName = "TimeMachine"; # name shown to macOS
2626- path = "/srv/timemachine"; # where backups are stored
2727- maxSizeGB = 0; # 0 = unlimited; set e.g. 500 to cap at 500 GB
2828- validUsers = [ ]; # e.g. [ "ewan" ] — must have a Samba password set
2929- };
3030-3131- # ─── Storage ──────────────────────────────────────────────────────────────────
3232- # /srv is mounted as a separate partition to isolate all service data
3333- # (forgejo, matrix-synapse, postgresql, bluesky-pds, www) from the root volume.
3434- #
3535- # Point `device` at the raw block device you want to use.
3636- # Run `lsblk` on the server to find the right path, e.g. /dev/sdb or /dev/sdb1.
3737- #
3838- # The system will automatically:
3939- # 1. Format the partition with ext4 (only if it has no filesystem yet)
4040- # 2. Mount it at /srv
4141- # 3. Create all required subdirectories with correct ownership
4242- # No manual setup is needed — just set the device and deploy.
4343- storage = {
4444- srv = {
4545- device = "/dev/sdb"; # ← set to your partition (use `lsblk` to find it)
4646- fsType = "ext4";
4747- options = [ "defaults" "noatime" ];
4848- # Subdirectories created automatically under /srv
4949- # Each service uses its own subdirectory
5050- };
5151- };
5252-5353- # ─── Cockpit dashboard ──────────────────────────────────────────────────────────
5454- # Web-based server status dashboard (services, journals, metrics, terminal).
5555- # Accessible only over Tailscale — not exposed publicly.
5656- cockpit = {
5757- enable = true;
5858- port = 9090; # Cockpit default
5959- };
6060-6161- # ─── Shared systemd service restart policy ───────────────────────────────────
6262- # Applied by default to forgejo, matrix-synapse, and bluesky-pds.
6363- # Override per-service by reading this value in the module and using lib.mkForce.
6464- servicePolicy = {
6565- restartSec = 5; # seconds before restarting after a crash
6666- startLimitIntervalSec = 300; # window for startLimitBurst
6767- startLimitBurst = 5; # max restarts within the window before giving up
6868- };
6969-7070- # SSH daemon
7171- sshd = {
7272- enable = true;
7373- permitRootLogin = "no";
7474- passwordAuthentication = false;
7575- kbdInteractiveAuthentication = false;
7676- port = 22;
7777- maxAuthTries = 3;
7878- clientAliveInterval = 300;
7979- clientAliveCountMax = 2;
8080- x11Forwarding = false;
8181- };
8282-8383- # Fail2ban intrusion prevention
8484- fail2ban = {
8585- enable = true;
8686- maxRetry = 5;
8787- banTime = 600; # seconds – 10 minutes
8888- findTime = 600; # seconds – detection window
8989- };
9090-9191- # Firewall
9292- firewall = {
9393- enable = true;
9494- allowPing = true;
9595- allowedTCPPorts = [ 22 ]; # Add ports as needed
9696- allowedUDPPorts = [ ];
9797- };
9898-}
-96
settings/config/shell.nix
···11-{
22- # Shell configuration
33-44- # Common aliases
55- aliases = {
66- # Modern CLI replacements
77- ls = "eza --icons";
88- ll = "eza -l --icons --git";
99- la = "eza -la --icons --git";
1010- lt = "eza --tree --level=2 --icons";
1111- cat = "bat";
1212-1313- # Navigation
1414- ".." = "cd ..";
1515- "..." = "cd ../..";
1616- "...." = "cd ../../..";
1717-1818- # Safety nets
1919- rm = "rm -i";
2020- cp = "cp -i";
2121- mv = "mv -i";
2222-2323- # Shortcuts
2424- h = "history";
2525- c = "clear";
2626- e = "$EDITOR";
2727-2828- # Disk usage
2929- du1 = "du -h -d 1";
3030- df = "df -h";
3131-3232- # Git shortcuts (use lazygit for TUI)
3333- lg = "lazygit";
3434- };
3535-3636- # Git aliases
3737- gitAliases = {
3838- # Status and info
3939- gs = "git status";
4040- gss = "git status -s"; # Short status
4141- gl = "git log --oneline --graph --decorate";
4242-4343- # Adding and committing
4444- ga = "git add";
4545- gaa = "git add -A"; # Add all
4646- gc = "git commit";
4747- gcm = "git commit -m"; # Commit with message
4848- gca = "git commit --amend";
4949-5050- # Pushing and pulling
5151- gp = "git push";
5252- gpf = "git push --force-with-lease"; # Safer force push
5353- gpl = "git pull";
5454- gpr = "git pull --rebase"; # Pull with rebase
5555-5656- # Branching
5757- gb = "git branch";
5858- gco = "git checkout";
5959- gcb = "git checkout -b"; # Create and checkout branch
6060-6161- # Diffs
6262- gd = "git diff";
6363- gds = "git diff --staged";
6464- };
6565-6666- # ── Nix tool aliases — shared by both platforms ───────────────────────────
6767- # Any alias present on both Linux and macOS belongs here exactly once.
6868- # Platform-specific aliases (cleanup, nrs/nrb/nrt) stay in their sections.
6969- nixToolAliases = {
7070- flake-bump = "nix run ~/.config/nix-config/tools#flake-bump";
7171- gen-diff = "nix run ~/.config/nix-config/tools#gen-diff";
7272- health-check = "nix run ~/.config/nix-config/tools#health-check";
7373- update-all = "~/.config/nix-config/home/scripts/update-all";
7474- update-everything = "~/.config/nix-config/home/scripts/update-everything";
7575- };
7676-7777- # Linux-specific aliases
7878- linuxAliases = {
7979- # nixToolAliases are merged by zsh.nix — only put Linux-only entries here
8080- cleanup = "sudo nix-collect-garbage -d && nix-collect-garbage -d";
8181- };
8282-8383- # macOS-specific aliases
8484- darwinAliases = {
8585- # nixToolAliases are merged by zsh.nix — only put macOS-only entries here
8686- cleanup = "sudo nix-collect-garbage -d";
8787- };
8888-8989- # History configuration
9090- history = {
9191- size = 10000;
9292- saveSize = 10000;
9393- file = "~/.zsh_history";
9494- ignoreDups = true;
9595- };
9696-}