22 Commits
Author SHA1 Message Date
Noah Masur 2ac07e2874 remove home-manager module from nixos 2026-09-12 22:29:53 +00:00
Noah Masur 985ab2b433 feat(services): integrate api service for flame 2026-09-12 21:56:19 +00:00
github-actions[bot] d6209c039b flake.lock: Update
Flake lock file updates:

• Updated input 'home-manager':
    'github:nix-community/home-manager/693e8ce0fb240a73c116a03cfd7b19269c87af88' (2026-09-04)
  → 'github:nix-community/home-manager/cd1c9e552f41894aeb5cc5cb353d5a1d61550357' (2026-09-12)
• Updated input 'nix-index-database':
    'github:nix-community/nix-index-database/dbb978fa87faf13398e90ccbc52c343d21c7dd6d' (2026-08-30)
  → 'github:nix-community/nix-index-database/116ad1c2adb642405ef8916f6a94c8626f971344' (2026-09-06)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/801bef6abd86b91e51083066b83fb354a11fc640' (2026-09-04)
  → 'github:nixos/nixpkgs/8ce4ef6cb6f871616146b9fe26d2a5ae594e94fe' (2026-09-10)
• Updated input 'nixpkgs-stable':
    'github:nixos/nixpkgs/a5cc6f2c37bf518436dc8d1c288ccd0c43c2f4c4' (2026-09-03)
  → 'github:nixos/nixpkgs/21a67dc470149f337cecafbe965d8d252a390518' (2026-09-11)
• Updated input 'nur':
    'github:nix-community/nur/82ca52a282cd5f90afabcf7c9acd27571b253523' (2026-09-05)
  → 'github:nix-community/nur/2c789298fc525713063e3b88359d02101651144f' (2026-09-12)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/c361047d3a538f547f1617bb6b410411929ac9cc' (2026-09-05)
  → 'github:oxalica/rust-overlay/228ecefb6329d5a531b77b46b581a2f0c26ee056' (2026-09-12)
• Updated input 'wsl':
    'github:nix-community/NixOS-WSL/eaeb18da90024448a60eb1ec7132eafa4003404e' (2026-07-21)
  → 'github:nix-community/NixOS-WSL/72c92b11bb8289e6651c7fef29cc0a885fd6a255' (2026-09-11)
2026-09-12 08:10:45 +00:00
Noah Masur 3e246645a2 paperless: configure OIDC authentication 2026-09-07 18:16:45 +00:00
Noah Masur ed8be9dee6 jellyfin: forward client IP headers from caddy and cloudflare 2026-09-07 17:00:31 +00:00
Noah Masur ab98a202da nextcloud: implement OIDC authentication 2026-09-07 16:34:45 +00:00
Noah Masur e00560c42c post-TUI lag: two-phase autosuggestion toggle; Ctrl-b was never activated
Root cause of 'Ctrl-b does nothing': home-manager was not switched, so the
binding built into the flake was never deployed (~/.config/fish lacked
heal-autosuggest and \cb; the built config had both). Activation needs the
home rebuild, not a system rebuild.

heal-autosuggest is now two-phase (set 0; repaint; set 1; repaint) to give
the reader a disabled-state repaint, which is what clears the wedged
in_flight_autosuggest_request per reader.rs update_autosuggestion. Matches
the manual cure (which had prompt cycles between off and on); the old
back-to-back toggle and the postexec hook did not. User chose the
session-preserving toggle over exec fish.
2026-09-07 11:17:44 -04:00
Noah Masur d4e56dd190 post-TUI lag: autosuggestion culprit confirmed; add self-heal hook
A/B in a live lagging shell: disabling fish_autosuggestion_enabled cures
the lag instantly, and re-enabling does NOT bring it back — the toggle
resets the wedged reader state. __autosuggestion_unwedge (fish_postexec)
now applies that reset after every command, at the moment TUIs exit.
Builtins only, invisible, respects a deliberate manual disable. Flight
recorder stays armed until the hook is proven in real use.

Also from this investigation: wedged-thread evidence (sampler attach
cures), lag-sample tool, flight recorder in the zellij fish wrapper.
2026-09-07 11:17:44 -04:00
Noah Masur db49e746b8 remove zellij-switch 2026-09-07 11:17:39 -04:00
Noah Masur 7f8400ff58 feat(immich): configure oidc authentication 2026-09-07 15:03:27 +00:00
Noah Masur d3914768c8 allow keybase gui 2026-09-06 18:00:20 -04:00
Noah Masur fd9cae9c40 fix(ssh): use openssh.authorizedPrincipals for cloudflare certificates 2026-09-06 19:34:37 +00:00
Noah Masur f593fdb81f feat(mealie): configure oidc authentication 2026-09-06 18:56:48 +00:00
Noah Masur 01ee98155a feat(actualbudget): configure oidc authentication 2026-09-06 18:18:57 +00:00
Noah Masur 2a3cbfb5f5 fix(grafana): allow oauth email lookup to match existing user 2026-09-06 18:04:46 +00:00
Noah Masur ca1343f593 configure grafana for oidc 2026-09-06 17:55:33 +00:00
Noah Masur a053a9ab0f define setup for oidc services 2026-09-06 15:02:21 +00:00
Noah Masur 90dc80f7b4 add pocket-id for auth 2026-09-06 12:18:17 +00:00
Noah Masur aff596aa64 add hister 2026-09-05 21:15:02 +00:00
github-actions[bot] 6e0f4e2000 flake.lock: Update
Flake lock file updates:

• Updated input 'home-manager':
    'github:nix-community/home-manager/99c9ec63390f1d8c14d95d9e8b17cc29cfbd4e11' (2026-08-27)
  → 'github:nix-community/home-manager/693e8ce0fb240a73c116a03cfd7b19269c87af88' (2026-09-04)
• Updated input 'nix-index-database':
    'github:nix-community/nix-index-database/c51d5c2ba69c907a34e90c9b6b80cd2b93811745' (2026-08-23)
  → 'github:nix-community/nix-index-database/dbb978fa87faf13398e90ccbc52c343d21c7dd6d' (2026-08-30)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/9fbb54b33e91ee4ca368e35a78e0613c720600b3' (2026-08-26)
  → 'github:nixos/nixpkgs/801bef6abd86b91e51083066b83fb354a11fc640' (2026-09-04)
• Updated input 'nixpkgs-stable':
    'github:nixos/nixpkgs/d57af924f160a5084293c71c2043f058bd1cdb60' (2026-08-27)
  → 'github:nixos/nixpkgs/a5cc6f2c37bf518436dc8d1c288ccd0c43c2f4c4' (2026-09-03)
• Updated input 'nur':
    'github:nix-community/nur/5b6adc26a9ef1bc45c09fcc845e7d08258c7e0ca' (2026-08-29)
  → 'github:nix-community/nur/82ca52a282cd5f90afabcf7c9acd27571b253523' (2026-09-05)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/996e9b0b019a4a9eb9e9a5641aefa06d801b5895' (2026-08-29)
  → 'github:oxalica/rust-overlay/c361047d3a538f547f1617bb6b410411929ac9cc' (2026-09-05)
2026-09-05 07:54:54 +00:00
Noah Masur 72c4f4823b fix nix-autoupgrade email notification to use system msmtp config 2026-08-30 14:46:53 -06:00
Noah Masur a93fa75779 fix invalid url in autoupgrade 2026-08-30 14:46:51 -06:00
34 changed files with 1266 additions and 86 deletions
+122 -1
View File
@@ -1,6 +1,122 @@
# Changelog # Changelog
## 2026-08-29 (root cause found and fixed) ## 2026-09-12
- **Integrated `api` service for `flame`**:
- Added `github:nmasur/api` as a flake input with `inputs.nixpkgs.follows = "nixpkgs"`.
- Added hostname `api = "api.masu.rs"`.
- Added `inputs.api.overlays.default` to system overlays and `inputs.api.nixosModules.default` to `buildNixos` and `generateImage` modules in `lib/default.nix`.
- Created preset `platforms/nixos/modules/nmasur/presets/services/api/api.nix` with options for `nmasur.presets.services.api.enable`:
- Configures `services.api.enable = true` and `services.api.hostname = hostnames.api`.
- Configures `services.api.backends.actual` with `actualServerUrl`, `apiKeysFile`, `serverPasswordFile`, and budget sync ID files for `budget1` and `budget2`.
- Added secret definitions for `api-actual-keys`, `api-actual-budget1-sync-id`, and `api-actual-budget2-sync-id` owned by `api_actual` with prefixes `API_KEYS=`, `ACTUAL_SYNC_ID_BUDGET1=`, and `ACTUAL_SYNC_ID_BUDGET2=`.
- Reused `config.secrets.actualbudget-password.dest` for upstream Actual server password.
- Ordered `systemd.services.api-actual` after and required by `postgresql-setup.service`, secret services, and `actual.service`.
- Added PostgreSQL peer authentication for `api_actual` (`local api_actual api_actual peer`) and database backup via `services.postgresqlBackup.databases = [ "api_actual" ]`.
- Added `systemd.services.postgresql-setup.preStart` hook in `postgresql.nix` to refresh collation version on `template1` and `postgres` (`ALTER DATABASE ... REFRESH COLLATION VERSION`), preventing database creation failure after glibc upgrades.
- Mounted generated `caddyRoutes` onto `nmasur.presets.services.caddy.routes`.
- Added `hostnames.api` to `services.cloudflare-dyndns.domains`.
- Added `/var/lib/api-actual` to restic backups.
- Enabled `nmasur.presets.services.api` and disabled `actualtap` in `platforms/nixos/modules/nmasur/profiles/communications.nix`.
## 2026-09-07
- **Configured OpenID Connect (OIDC) authentication for Paperless-ngx**:
- Added secret management for `paperless-oidc-secret.age` via `secrets.paperless-oidc-secret` owned by `paperless:paperless` (0440).
- Configured `systemd.services.paperless-oidc-secret-secret` to run before and be required by all Paperless units, dynamically generating `/var/private/paperless-env` with `PAPERLESS_SOCIALACCOUNT_PROVIDERS` configured for Pocket ID (`auth.masu.rs`) using `client_id = "e6ff7fce-8e67-4c66-8f32-5ec3db4740a9"`, `oauth_pkce_enabled = true`, and email authentication linking (`email_authentication = true`, `verified_email = true`) in the native `APPS` schema.
- Added `services.paperless.environmentFile = "${config.secretsDirectory}/paperless-env"` to securely pass the OIDC configuration to Paperless services without exposing secrets in the Nix store.
- Configured Paperless settings to enable `allauth.socialaccount.providers.openid_connect` in `PAPERLESS_APPS` and enabled `PAPERLESS_REDIRECT_LOGIN_TO_SSO`.
- Configured reverse proxy trust settings (`PAPERLESS_USE_X_FORWARD_HOST`, `PAPERLESS_PROXY_SSL_HEADER`, and `PAPERLESS_TRUSTED_PROXIES`) to ensure callback URIs preserve the `https://` scheme behind Caddy.
- Fixed `systemd.services.paperless-secret` to order before and be required by `paperless-scheduler.service` instead of non-existent `paperless.service`.
- **Configured real client IP forwarding for Jellyfin reverse proxy**:
- Added a `map` handler to Jellyfin's Caddy route in `platforms/nixos/modules/nmasur/presets/services/jellyfin.nix` to resolve `{client_ip}` using Cloudflare's `CF-Connecting-IP` header when available, falling back to `{http.request.remote.host}` for direct local LAN connections.
- Configured `reverse_proxy.headers.request.set` to forward `X-Real-IP`, `X-Forwarded-For`, and `X-Forwarded-Proto` with the resolved client IP and request scheme to Jellyfin.
- **Configured OpenID Connect (OIDC) authentication for Nextcloud**:
- Added `user_oidc` to `services.nextcloud.extraApps`.
- Added secret management for `nextcloud-oidc-secret.age` via `secrets.nextcloud-oidc-secret` with owner `nextcloud` and group `nextcloud` (0440).
- Configured `systemd.services.nextcloud-oidc-secret-secret` to be required by and order before `nextcloud-setup.service`.
- Set `services.nextcloud.settings.overwriteprotocol = "https"` to ensure correct scheme handling for redirect URIs behind reverse proxy.
- Configured automated idempotent upsert of the Pocket ID provider via `systemd.services.nextcloud-setup.postStart` running `nextcloud-occ user_oidc:provider pocket-id` with client ID `c8a32c58-a781-4f14-9070-f498fdfda438`, `--clientsecret-file`, discovery URI `https://${hostnames.auth}/.well-known/openid-configuration`, `--mapping-uid="preferred_username"`, and `--unique-uid=0` to connect OIDC logins directly to existing local Nextcloud accounts.
- Updated `docs/oidc-services.md` with the verified configuration and callback URI (`https://cloud.masu.rs/apps/user_oidc/code`).
## 2026-09-06
- **Configured OpenID Connect (OIDC) authentication for Immich**:
- Configured `services.immich.settings.oauth` with OIDC settings pointing to Pocket ID (`auth.masu.rs`), using client ID `1f4e0f8d-6cee-4d67-8d53-74bf6c18ae09`.
- Added secret management for `immich-oidc-secret.age` via `secrets.immich-oidc-secret` with owner `immich` and group `shared` (0440).
- Wired client secret substitution using NixOS's native `clientSecret._secret = config.secrets.immich-oidc-secret.dest`, leveraging `utils.genJqSecretsReplacement` with systemd `LoadCredential`.
- Configured `systemd.services.immich-server` to order after `immich-oidc-secret-secret.service`.
- Updated `docs/oidc-services.md` with the verified configuration and redirect URIs.
- **Fixed OpenSSH authorized principals certificate authentication for Cloudflare Tunnel**:
- Replaced manual `environment.etc."ssh/authorized_principals/${username}"` symlink and `Match User` config with NixOS native `users.users.<name>.openssh.authorizedPrincipals`.
- Root cause: `environment.etc` without an explicit `mode` creates symlinks pointing into `/nix/store`, which has group-writable mode `0775` (`nixbld` group). Under `StrictModes yes`, sshd refused authentication with `bad ownership or modes for directory /nix/store`, causing certificate principal matching to fail with `Certificate does not contain an authorized principal`.
- Setting `users.users.<name>.openssh.authorizedPrincipals` causes NixOS to generate `/etc/ssh/authorized_principals.d/<name>` with `mode = "0444"`, copying the file instead of symlinking into the store, and automatically configuring `services.openssh.settings.AuthorizedPrincipalsFile = "/etc/ssh/authorized_principals.d/%u"`.
- Also added `mode = "0444"` to `/etc/ssh/ca.pub` and moved `TrustedUserCAKeys` into `services.openssh.settings`.
- **Configured OpenID Connect (OIDC) authentication for Mealie**:
- Configured `services.mealie.settings` with OIDC settings pointing to Pocket ID (`auth.masu.rs`), using client ID `040925ed-b39e-4442-b8e8-369c948c0cd2`.
- Added secret management for `mealie-oidc-secret.age` via `secrets.mealie-oidc-secret`, using `prefix = "OIDC_CLIENT_SECRET="` to generate an environment file.
- Configured `services.mealie.credentialsFile` to load the client secret via systemd's `EnvironmentFile` without exposing it in the world-readable Nix store or systemd unit file.
- Configured `systemd.services.mealie` to order after `mealie-oidc-secret-secret.service`.
- Updated `docs/oidc-services.md` with the verified configuration and callback URI (`https://cooking.masu.rs/login`).
- **Configured OpenID Connect (OIDC) authentication for Actual Budget**:
- Configured `services.actual.settings` with `loginMethod = "openid"` and `openId` settings pointing to Pocket ID (`auth.masu.rs`), using client ID `92afe9f8-7ef6-42ab-8a06-701df3c7179d`.
- Added secret management for `actualbudget-oidc-secret.age` via `secrets.actualbudget-oidc-secret`, set with owner `actualbudget` and group `shared` (0440).
- Configured `systemd.services.actual` to order after the decrypted secret service and granted the dynamic unit access via `SupplementaryGroups = [ "shared" ]` and `PrivateUsers = false`.
- Updated `docs/oidc-services.md` with the verified callback URI (`https://money.masu.rs/openid/callback`) and NixOS configuration snippet.
## 2026-09-03: Ctrl-b did nothing because home-manager was never switched; two-phase toggle
- "Ctrl-b does nothing" root cause: the binding was built into the flake but never activated. The deployed `~/.config/fish/functions/` had no `heal-autosuggest.fish` and no `\cb` binding (0 matches), while the freshly-built config had both. On this setup a system rebuild does not switch home-manager — activation needs the home rebuild (`rebuild-home` / the Alt-Shift-H binding, i.e. `home-manager switch --flake`) followed by a fresh pane so `config.fish` re-runs `fish_user_key_bindings`. No fix works until it is actually activated; this should be the FIRST check next time a "did nothing" is reported.
- Made `heal-autosuggest` a two-phase toggle (chosen by the user over `exec fish`, to preserve the session): disable autosuggestions, `commandline -f repaint`, re-enable, `commandline -f repaint`. Grounded in `reader.rs` `update_autosuggestion`, which clears the wedged `in_flight_autosuggest_request` only on a repaint taken while autosuggestions are disabled. The old one-liner and the `fish_postexec` hook did a single back-to-back `set 0; set 1` with no disabled-state repaint, which likely never flushed the stuck request — matching the manual cure, which had prompt cycles between the off and the on. Still unverified against the real bug (not reproducible in a harness); confirm by activating, then pressing Ctrl-b in a live lagging shell.
- If the two-phase toggle still does not cure once activated: fall back to `exec fish` on the key (guaranteed per the user's day-one report that a fresh shell always fixes it), and capture a flight-recorder log for the upstream fish report.
## 2026-09-01: postexec hook fires but does not cure — moved heal to a keybinding
- Honest status: the `fish_postexec` self-heal hook IS registered and DOES fire (verified in the real config), yet the lag persists. So toggling `fish_autosuggestion_enabled` off/on from `fish_postexec` does not cure it, even though the user typing the same `set … 0; set … 1` at the prompt does.
- Why (code-level): variable dispatch is synchronous (`env_dispatch.rs``reader_set_autosuggestion_enabled` on every `set`), so the two sets net to no change and schedule a repaint. That repaint only has effect from inside the reader's active input loop. `fish_postexec` runs BETWEEN commands, outside that loop, so its effect is superseded before the next prompt. A key binding runs inside the loop; `fish_postexec` cannot.
- Change: bound **Ctrl-b** to a new `heal-autosuggest` function (toggle + `commandline -f repaint`) via `fish_user_key_bindings` (Ctrl-g was already taken). Verified that when `fish_user_key_bindings` runs to completion — as it does in the real config, since the existing `\cn` etc. work (`__fish_config_interactive.fish:105`) — `\cb` binds to heal-autosuggest and the toggle resets the variable. NOT verified to cure the real lag: the bug still cannot be reproduced in a harness, so only a test in a live lagging shell can confirm. The `fish_postexec`/`fish_cancel` hook is kept (harmless) but is no longer considered the fix.
- Guaranteed fallback if Ctrl-b does not cure: a fresh shell (`exec fish`), which the user has confirmed from the very start always fixes it — Ctrl-b can be rebound to that. The only path to a truly automatic fix is a flight-recorder capture (`~/.local/state/lag-triage/RECORD`) of the reader during an actual episode, then an upstream fish report.
## 2026-08-31 (evening): self-heal hook was never firing — fish cannot autoload event handlers
- The `__autosuggestion_unwedge` hook did not work because it was installed via `programs.fish.functions`, which writes to fish's **autoload** directory — and fish only registers `--on-event` handlers when a function is actually loaded, which never happens for a hook nothing calls by name. Verified in a PTY test: the autoloaded handler never fires; the identical definition `source`d eagerly fires immediately. (The zellij module's `__fish_update_cwd_osc` works as an autoloaded event function only because it overrides a function fish itself loads.)
- Meanwhile the user confirmed the instant back-to-back toggle (`set -g fish_autosuggestion_enabled 0 && set -g fish_autosuggestion_enabled 1`) cures a lagging shell — so the handler body is right; only its registration was broken.
- Fix: the handler is now defined eagerly in `config.fish` via `programs.fish.interactiveShellInit` (lag-triage module), registered on **fish_postexec** (fires after every command — the moment TUIs exit) and **fish_cancel** (fires on Ctrl-C at the prompt), so a bare Ctrl-C is an instant no-command cure. Verified in an interactive PTY against the actual nix-generated snippet: registers at startup, fires on both events, still respects a deliberate manual disable.
- Coverage note: if a wedge forms with no command running (and no Ctrl-C), it heals at the next command; worst-case lag window is "until you run anything or press Ctrl-C".
## 2026-08-31 (later): automatic self-heal hook
- Confirmed by A/B in the live shell: after curing the lag with `set -g fish_autosuggestion_enabled 0`, re-enabling with `1` does **not** bring the lag back — the toggle resets the wedged autosuggestion state rather than merely masking it.
- Added `__autosuggestion_unwedge` (lag-triage module): a `fish_postexec` event handler that toggles `fish_autosuggestion_enabled` off/on after every command — i.e. at the exact moment a TUI has just exited, when the wedge forms. Builtins only, no visible output (verified in an interactive PTY test), and it skips the reset when the user has deliberately disabled autosuggestions.
- Honest caveat: the manual cure had keystrokes between the off and the on; whether the instant off/on inside an event handler resets the same reader-internal state is unproven. The flight recorder therefore STAYS ARMED (`~/.local/state/lag-triage/RECORD`) until the hook has survived normal use for a while. If lag recurs despite the hook: cure manually (`set … 0`, type a few chars, `set … 1`), and keep the flight log for that pid — then the hook needs the stronger form (disable at postexec, re-enable one prompt-cycle later, scoped to TUI commands).
- Limitations by design: the hook fires only in shells that run commands, so a wedge formed without any command executing in that shell (if that is possible — e.g. floating-pane TUIs never touch the pane shell) would not be healed until the next command runs there.
## 2026-08-31: culprit confirmed — fish's autosuggestion pipeline
- A/B test in a live lagging shell (pid 56089): `set -g fish_autosuggestion_enabled 0` (builtin only, nothing else) **instantly cured the lag**. The post-TUI typing lag is in fish 4.8.1's autosuggestion pipeline.
- Sampling that shell afterwards showed it had **only one thread** (the main thread): the poisoned state is main-thread-side bookkeeping, not a hung worker still sitting in the process. Source review (`src/threads/threads.rs`, `src/threads/debounce.rs`): `ThreadPool::perform` silently queues work with no spawn and no wake when it believes `total_threads == max_threads` — a leaked `total_threads` count (workers that died without decrementing, e.g. across a TUI's lifetime) would strand all future autosuggestion work forever; the Debounce then abandons its token every 500ms and re-enqueues per keystroke. The exact step that delays keystroke *echo* is still unproven — the flight recorder (armed via `~/.local/state/lag-triage/RECORD`) logs the reader's per-keystroke behavior and will capture it on the next occurrence in a recorded shell.
- Precedent: fish had a closely-related bug class before (#11841 — unread terminal query responses "causing noticeable lags"). No fish release newer than 4.8.1 exists, so no upstream fix to adopt; an upstream report with the flight-recorder capture is the path to a real fix.
- Practical interim cure (harmless, instant, in the lagging shell): `set -g fish_autosuggestion_enabled 0`, and re-enable with `1` — whether lag returns on re-enable is the next discriminating datum.
## 2026-08-30 (later): sampler attach CURES the lag — wedged-thread evidence + flight recorder
- Major new datum: in a lagging shell, running `mkdir` + `/usr/bin/sample $fish_pid … &` + `disown` **cured the lag instantly**, before any planned reset/toggle test could run. Plain external commands do NOT cure it (the 2026-08-29 triage ran many and the lag survived), so the distinguishing action is the sampler **attaching and suspending/resuming fish's threads**. Conclusion: a fish-internal thread/wait is wedged (missed wakeup or stuck blocking wait), and per-keystroke work at the main commandline stalls against it; suspension/resume kicks it loose. Consistent with: `read` prompts unaffected (no autosuggestion/highlight pipeline), subshells immune (fresh threads), raw input clean. The captured sample (`~/.local/state/lag-triage/fish-sample.txt`) shows only the post-cure state — sampling is a cure, not a capture.
- Therefore the observer must be running BEFORE the lag starts: the `fish-no-query-term` wrapper is now a **flight recorder**`touch ~/.local/state/lag-triage/RECORD`, then every newly spawned pane shell logs `FISH_DEBUG=reader,term-support,proc-termowner,iothread,fd-monitor,topic-monitor` to `~/.local/state/lag-triage/flight/fish-<ts>-<pid>.log` (3-day auto-cleanup; remove RECORD to disable, zero overhead when off). When lag next occurs, the log already contains what each keystroke did during the lag.
- `lag-sample` now takes a PID and should be run from a DIFFERENT pane (`echo $fish_pid` — a builtin — in the lagging shell to get it), since attaching from inside cures the lag.
- **Next-occurrence checklist (in order, least perturbing first):** (1) in the lagging shell, builtins only: `set -g fish_autosuggestion_enabled 0` → type at the real commandline; if cured, the autosuggestion/debounce path is implicated (a worker thread was seen in `HistorySearch::go_to_next_match`); (2) still laggy: `fish_default_key_bindings` → test (vi-mode path); (3) from another pane: `kill -WINCH <pid>` → test, then `kill -CONT <pid>` → test (discriminates reader-wakeup vs generic unwedge; if WINCH cures, a window resize would too); (4) from another pane: `lag-sample <pid>` while typing in the lagging pane; (5) immediately save the flight log for that pid.
## 2026-08-30
- **The post-TUI typing lag is NOT resolved** by the `fish-no-query-term` wrapper: lag recurred in a fresh zellij session after exiting Claude Code, in a shell verified (via `ps eww`) to have `fish_features=no-query-term` in its environment. The query-term reader-degradation bug proven on 2026-08-29 is real (and the wrapper stays as hardening against it), but it is not the mechanism behind this lag. Downgraded the entry below from "root cause" to "a root cause".
- Known constraints on the real mechanism: per-keystroke lag at the main fish commandline; fish `read` prompts unaffected; raw input reaches the pane practical as plain bytes; a subshell/`exec fish` cures it (process-local state). Note the 2026-08-29 triage's reset ladder short-circuited on a false "y" at stage A, so stages BG (mouse/keypad/altscreen/stty/DECSTR resets) were never actually tested against real lag.
- Added `lag-sample` (fish function): stack-samples the lagging fish process plus the zellij server/client via `/usr/bin/sample` for 8s while the user types at the commandline. This directly names where the time goes (fish reader? highlighting/autosuggestion threads? zellij render loop?) instead of inferring it. Next occurrence: run `lag-sample` in the lagging shell, type junk at the prompt until done, then inspect `~/.local/state/lag-triage/sample-*.txt`. Follow with `unlag` (full reset ladder, never yet truly tested), then A/B toggles: `set -g fish_autosuggestion_enabled 0`, `fish_default_key_bindings`.
## 2026-08-29 (a root cause found and fixed — but not THE lag)
- **Root-caused and fixed the recurring post-TUI typing lag** (fish + Zellij + Ghostty) using a `lag-triage` capture from a live lagging shell plus a deterministic PTY reproduction (`presets/programs/lag-triage/upstream_repro.py`): - **Root-caused and fixed the recurring post-TUI typing lag** (fish + Zellij + Ghostty) using a `lag-triage` capture from a live lagging shell plus a deterministic PTY reproduction (`presets/programs/lag-triage/upstream_repro.py`):
- **Root cause chain**: (1) fish latches feature flags from its **startup environment**, before `config.fish` runs — so the existing `set -gx fish_features no-query-term` in `shellInit` never applied to the shell that set it, only to its children. (2) Zellij spawns pane shells via `default_shell` with no `fish_features` in the environment, so every pane's fish latched `query-term` **on** (the fish 4.8.1 default; the triage log from the lagging shell confirmed `query-term on` while `$fish_features` was correctly set to `no-query-term`). (3) With query-term on, fish sends OSC 11 + CPR (`\e[6n`) + DA1 (`\e[0c`) after **every** command and waits for replies relayed by Zellij. (4) Reproduced on fish 4.8.1: if the terminal fails to reply during just **one** such cycle — answering everything before and after — that fish process's interactive reader is **permanently degraded** (keystroke echo >3s, never recovers; ~35ms before). In production Zellij drops/mangles a relay during TUI teardown or heavy output (cf. zellij-org/zellij#5158), e.g. after `nh home switch`, nvim, jjui, yazi. - **Root cause chain**: (1) fish latches feature flags from its **startup environment**, before `config.fish` runs — so the existing `set -gx fish_features no-query-term` in `shellInit` never applied to the shell that set it, only to its children. (2) Zellij spawns pane shells via `default_shell` with no `fish_features` in the environment, so every pane's fish latched `query-term` **on** (the fish 4.8.1 default; the triage log from the lagging shell confirmed `query-term on` while `$fish_features` was correctly set to `no-query-term`). (3) With query-term on, fish sends OSC 11 + CPR (`\e[6n`) + DA1 (`\e[0c`) after **every** command and waits for replies relayed by Zellij. (4) Reproduced on fish 4.8.1: if the terminal fails to reply during just **one** such cycle — answering everything before and after — that fish process's interactive reader is **permanently degraded** (keystroke echo >3s, never recovers; ~35ms before). In production Zellij drops/mangles a relay during TUI teardown or heavy output (cf. zellij-org/zellij#5158), e.g. after `nh home switch`, nvim, jjui, yazi.
@@ -102,6 +218,11 @@
to bind, the failure took down the entire terminal instead of erroring to bind, the failure took down the entire terminal instead of erroring
gracefully. gracefully.
## 2026-09-06
- Configured Grafana OIDC authentication via Pocket ID in `platforms/nixos/modules/nmasur/presets/services/grafana/grafana.nix`.
- Enabled `auth.oauth_allow_insecure_email_lookup = true` in Grafana settings to allow linking an incoming OAuth login to an existing Grafana user account with the same email.
## 2026-07-25 ## 2026-07-25
- Added jjui config generation to `jujutsu.nix` in Home Manager to include custom GitHub ruleset bypass commands (`ctrl+b` and `ctrl+shift+b`). - Added jjui config generation to `jujutsu.nix` in Home Manager to include custom GitHub ruleset bypass commands (`ctrl+b` and `ctrl+shift+b`).
+275
View File
@@ -0,0 +1,275 @@
# OIDC Service Analysis & Setup Requirements
This document analyzes the services defined in this repository (specifically within `platforms/nixos/modules/nmasur/presets/services/` and related server profiles like `flame` and `swan`), evaluates their OpenID Connect (OIDC) compatibility, and details the requirements for configuring OIDC logins.
---
## Identity Provider Context: Pocket ID
The configuration already includes a central self-hosted identity provider: **Pocket ID** (`platforms/nixos/modules/nmasur/presets/services/pocket-id/pocket-id.nix`), hosted on the communications server (`flame`) behind Caddy at **`https://auth.masu.rs`**.
Pocket ID is an OpenID Connect (OIDC) provider with WebAuthn/Passkey support. It exposes the following standard OIDC endpoints:
- **Issuer URL:** `https://auth.masu.rs`
- **Discovery Endpoint:** `https://auth.masu.rs/.well-known/openid-configuration`
- **Authorization Endpoint:** `https://auth.masu.rs/authorize`
- **Token Endpoint:** `https://auth.masu.rs/api/oidc/token`
- **Userinfo Endpoint:** `https://auth.masu.rs/api/oidc/userinfo`
- **JWKS Endpoint:** `https://auth.masu.rs/.well-known/jwks.json`
---
## 1. Service Compatibility Breakdown
### Tier 1: First-Class / Native OIDC Support
These services support OpenID Connect natively without requiring external authentication proxies or custom code:
| Service | Hostname | OIDC Support Level | Configuration Method |
| :--- | :--- | :--- | :--- |
| **Immich** | `photos.masu.rs` | Native core feature | NixOS config (`services.immich.settings.oauth`) |
| **Gitea** | `git.masu.rs` | Native core feature | NixOS config / CLI or Web UI |
| **Nextcloud** | `cloud.masu.rs` | Native (official `user_oidc` app) | Nextcloud app + `nextcloud-occ user_oidc:provider` |
| **Grafana** | `metrics.masu.rs` | Native (Generic OAuth) | NixOS config (`services.grafana.settings."auth.generic_oauth"`) |
| **Paperless-ngx** | `paper.masu.rs` | Native (`django-allauth`) | NixOS env vars (`PAPERLESS_SOCIALACCOUNT_PROVIDERS`) |
| **Mealie** | `cooking.masu.rs` | Native core feature | NixOS config (`services.mealie.settings` + `credentialsFile`) |
| **Karakeep / Hoarder** | `keep.masu.rs` | Native (NextAuth OIDC) | NixOS env vars (`OAUTH_WELLKNOWN_URL`, etc.) |
| **Audiobookshelf** | `read.masu.rs` | Native core feature (v2.3+) | Web UI (Settings → Authentication) |
| **Actual Budget** | `money.masu.rs` | Native core feature (v24.3+) | NixOS config (`services.actual.settings.openId`) |
---
### Tier 2: OIDC via Plugins or Reverse Proxy Header Auth
These services do not have generic OIDC in their core web UI, but can support single sign-on through plugins or reverse proxy headers (`Remote-User` / `X-Forwarded-User`):
| Service | Hostname | Strategy | Notes |
| :--- | :--- | :--- | :--- |
| **Jellyfin** | `stream.masu.rs` | `jellyfin-plugin-sso` | Web clients work well; TV and native apps typically rely on Quick Connect. |
| **Calibre-Web** | `books.masu.rs` | Reverse proxy header auth | Set `services.calibre-web.options.reverseProxyAuth.enable = true` behind an authenticating reverse proxy. |
| **File Browser** | `files.masu.rs` | Reverse proxy header auth | Set `auth.method = "proxy"` and `auth.header = "X-Forwarded-User"` behind an authenticating reverse proxy. |
| **Navidrome** | `music.masu.rs` | Reverse proxy header auth | Supports `ReverseProxyUserHeader` for web UI; Subsonic API clients (Feishin, etc.) still require native user passwords. |
| **Stalwart** | `contacts.masu.rs` | OIDC Directory / SASL OAuth | Stalwart supports OIDC directories, but CardDAV/CalDAV clients usually require HTTP Basic Auth or application passwords. |
---
### Tier 3: Incompatible or No Native OIDC Support
- **Vaultwarden (`vault.masu.rs`):** Incompatible for vault decryption. Vaultwarden uses client-side zero-knowledge encryption where vault keys are derived from the user's master password. Bitwarden Enterprise SSO relies on a proprietary Key Connector that Vaultwarden does not implement.
- **n8n (`n8n.masu.rs`):** SAML/OIDC SSO is an **Enterprise / commercial-only feature**; it is disabled in the free self-hosted Community edition.
- **The Arr Stack (`download.masu.rs`):** Radarr, Sonarr, Lidarr, Readarr, Prowlarr, Bazarr, and Sabnzbd only support API keys, Basic Auth, or Forms.
- **Transmission (`transmission.masu.rs`):** BitTorrent daemon; supports only HTTP Basic Auth / RPC whitelist.
- **Uptime Kuma (`status.masu.rs`), ntfy (`ntfy.masu.rs`), The Lounge (`irc.masu.rs`), Pgweb (`pg.masu.rs`), Mathesar (`mathesar.masu.rs`), Hister (`hister.masu.rs`):** No native generic OIDC login mechanism.
- **Infrastructure / Daemon Services:** PostgreSQL, InfluxDB, bind, avahi, cloudflared, wireguard, litestream (no user web UI).
---
## 2. General Setup Requirements
Configuring OIDC requires setup across four layers:
### A. Pocket ID Setup
For each service, an OIDC client application must be registered in the Pocket ID admin interface:
1. **Client ID:** A unique identifier slug (e.g. `gitea`, `immich`, `paperless`).
2. **Client Secret:** A cryptographically secure random token.
3. **Redirect URIs / Callback URLs:** The exact target URLs the service exposes for authorization code callbacks.
4. **Scopes:** Usually `openid`, `profile`, and `email`.
### B. Secrets Management (agenix)
All client secrets should be encrypted with `agenix` under the respective service directory:
- Example: `platforms/nixos/modules/nmasur/presets/services/<service>/<service>-oidc.age`
- Defined in NixOS under `config.secrets.<service>-oidc` with appropriate owner/group permissions.
### C. Network & Reverse Proxy (Caddy)
1. **Back-Channel Connectivity:** When a user logs in, the service backend makes a server-to-server HTTPS call to `https://auth.masu.rs/api/oidc/token` to exchange the authorization code for tokens. Services hosted on `swan` (NAS) must be able to resolve and reach `auth.masu.rs` over HTTPS.
2. **Proxy Headers:** Caddy's `reverse_proxy` handles `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-For` by default. Services should have reverse proxy trust enabled (e.g., `trusted_proxies = ["127.0.0.1"]`) so generated redirect URIs preserve the `https://` scheme.
---
## 3. Detailed Setup Requirements for Tier 1 Services
### 1. Immich (`photos.masu.rs`)
- **Pocket ID Redirect URIs:**
- Web: `https://photos.masu.rs/auth/login`
- Mobile: `app.immich:///oauth-callback`
- **Setup in `immich/immich.nix`:**
```nix
services.immich.settings.oauth = {
enabled = true;
issuerUrl = "https://${hostnames.auth}";
clientId = "1f4e0f8d-6cee-4d67-8d53-74bf6c18ae09";
clientSecret._secret = config.secrets.immich-oidc-secret.dest;
scope = "openid profile email";
autoRegister = true;
buttonText = "Login with Pocket ID";
};
```
### 2. Gitea (`git.masu.rs`)
- **Pocket ID Redirect URI:** `https://git.masu.rs/user/oauth2/pocket-id/callback`
- **Setup in Gitea:**
Can be configured in the Web UI under **Site Administration → Authentication Sources** or via CLI:
```bash
gitea admin auth add-oauth \
--name "Pocket ID" \
--provider openidConnect \
--key "<client_id>" \
--secret "<client_secret>" \
--auto-discover-url "https://auth.masu.rs/.well-known/openid-configuration"
```
- Optional: Enable automatic account linking by matching email.
### 3. Nextcloud (`cloud.masu.rs`)
- **Pocket ID Redirect URI:** `https://cloud.masu.rs/apps/user_oidc/code`
- **Setup in `nextcloud/nextcloud.nix`:**
```nix
secrets.nextcloud-oidc-secret = {
source = ./nextcloud-oidc-secret.age;
dest = "${config.secretsDirectory}/nextcloud-oidc-secret";
owner = "nextcloud";
group = "nextcloud";
permissions = "0440";
};
systemd.services.nextcloud-oidc-secret-secret = {
requiredBy = [ "nextcloud-setup.service" ];
before = [ "nextcloud-setup.service" ];
};
services.nextcloud = {
settings.overwriteprotocol = "https";
extraApps = {
user_oidc = config.services.nextcloud.package.packages.apps.user_oidc;
};
};
systemd.services.nextcloud-setup = {
after = [ "nextcloud-oidc-secret-secret.service" ];
postStart = ''
${config.services.nextcloud.occ}/bin/nextcloud-occ user_oidc:provider pocket-id \
--clientid="c8a32c58-a781-4f14-9070-f498fdfda438" \
--clientsecret-file="${config.secrets.nextcloud-oidc-secret.dest}" \
--discoveryuri="https://${hostnames.auth}/.well-known/openid-configuration" \
--scope="openid profile email" \
--mapping-uid="preferred_username" \
--unique-uid=0
'';
};
```
### 4. Grafana (`metrics.masu.rs`)
- **Pocket ID Redirect URI:** `https://metrics.masu.rs/login/generic_oauth`
- **Setup in `grafana/grafana.nix`:**
```nix
services.grafana.settings = {
auth.oauth_allow_insecure_email_lookup = true;
"auth.generic_oauth" = {
enabled = true;
name = "Pocket ID";
allow_sign_up = true;
client_id = "85d879ed-1a86-4984-b33d-43806500ef98";
client_secret = "$__file{${config.secrets.grafana-oidc-secret.dest}}";
scopes = "openid profile email";
auth_url = "https://${hostnames.auth}/authorize";
token_url = "https://${hostnames.auth}/api/oidc/token";
api_url = "https://${hostnames.auth}/api/oidc/userinfo";
login_attribute_path = "preferred_username";
skip_org_role_sync = true;
};
};
```
### 5. Paperless-ngx (`paper.masu.rs`)
- **Pocket ID Redirect URI:** `https://paper.masu.rs/accounts/oidc/pocket-id/login/callback/`
- **Setup in `paperless/paperless.nix`:**
```nix
services.paperless.settings = {
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
PAPERLESS_REDIRECT_LOGIN_TO_SSO = true;
};
# Configured via environmentFile to pass the client secret, enable PKCE, and link by email:
# PAPERLESS_SOCIALACCOUNT_PROVIDERS = builtins.toJSON {
# openid_connect = {
# APPS = [
# {
# provider_id = "pocket-id";
# name = "Pocket ID";
# client_id = "e6ff7fce-8e67-4c66-8f32-5ec3db4740a9";
# secret = "...";
# settings = {
# server_url = "https://auth.masu.rs";
# token_auth_method = "client_secret_basic";
# oauth_pkce_enabled = true;
# email_authentication = true;
# verified_email = true;
# };
# }
# ];
# };
# };
```
### 6. Mealie (`cooking.masu.rs`)
- **Pocket ID Redirect URI:** `https://cooking.masu.rs/login`
- **Setup in `mealie/mealie.nix`:**
```nix
services.mealie = {
credentialsFile = config.secrets.mealie-oidc-secret.dest;
settings = {
OIDC_AUTH_ENABLED = "true";
OIDC_SIGNUP_ENABLED = "true";
OIDC_CONFIGURATION_URL = "https://${hostnames.auth}/.well-known/openid-configuration";
OIDC_CLIENT_ID = "040925ed-b39e-4442-b8e8-369c948c0cd2";
OIDC_PROVIDER_NAME = "Pocket ID";
OIDC_USER_CLAIM = "email";
};
};
```
### 7. Karakeep / Hoarder (`keep.masu.rs`)
- **Pocket ID Redirect URI:** `https://keep.masu.rs/api/auth/callback/custom`
- **Setup in `karakeep.nix`:**
```nix
services.karakeep.extraEnvironment = {
OAUTH_WELLKNOWN_URL = "https://auth.masu.rs/.well-known/openid-configuration";
OAUTH_CLIENT_ID = "hoarder";
OAUTH_CLIENT_SECRET = "...";
OAUTH_PROVIDER_NAME = "Pocket ID";
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING = "true";
};
```
### 8. Audiobookshelf (`read.masu.rs`)
- **Pocket ID Redirect URIs:**
- Web: `https://read.masu.rs/auth/openid/callback`
- Mobile: `audiobookshelf://oauth`
- **Setup in Audiobookshelf:**
Configured in the Web UI (**Settings → Authentication → OpenID Connect**):
- Issuer URL: `https://auth.masu.rs`
- Client ID & Client Secret
- Match user by email or username
### 9. Actual Budget (`money.masu.rs`)
- **Pocket ID Redirect URI:** `https://money.masu.rs/openid/callback`
- **Setup in `actualbudget/actualbudget.nix`:**
```nix
services.actual.settings = {
loginMethod = "openid";
openId = {
discoveryURL = "https://${hostnames.auth}/.well-known/openid-configuration";
client_id = "92afe9f8-7ef6-42ab-8a06-701df3c7179d";
client_secret._secret = config.secrets.actualbudget-oidc-secret.dest;
server_hostname = "https://${hostnames.budget}";
authMethod = "openid";
};
};
```
*Note:* The optional end-to-end budget encryption password remains separate from the server authentication.
---
## 4. Forward Auth Architecture for Remaining Services
For services without native OIDC support (such as Calibre-Web, File Browser, Uptime Kuma, and the Arr stack), you can implement **Forward Auth via Caddy**:
1. Deploy an authenticating proxy (such as **OAuth2-Proxy** or **Authelia**) configured with Pocket ID as its OIDC provider.
2. Configure Caddy routes using the `forward_auth` directive to verify user sessions with the proxy before forwarding requests to the target service.
3. For services supporting reverse proxy authentication (Calibre-Web and File Browser), Caddy injects identity headers (e.g. `Remote-User: noah` or `X-Forwarded-User: noah`), enabling seamless single sign-on without requiring separate logins.
Generated
+85 -30
View File
@@ -1,10 +1,31 @@
{ {
"nodes": { "nodes": {
"api": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1789229011,
"narHash": "sha256-sbC3E9CAQwWto+ynyA0zXvEMMK5jboVnYPYoqTcxm0Y=",
"owner": "nmasur",
"repo": "api",
"rev": "28a522a274fe86da6d3390adcc026a6618911cb0",
"type": "github"
},
"original": {
"owner": "nmasur",
"repo": "api",
"type": "github"
}
},
"cl-nix-lite": { "cl-nix-lite": {
"inputs": { "inputs": {
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"systems": "systems", "systems": "systems_2",
"treefmt-nix": "treefmt-nix" "treefmt-nix": "treefmt-nix"
}, },
"locked": { "locked": {
@@ -151,6 +172,24 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": { "inputs": {
"systems": [ "systems": [
"mac-app-util", "mac-app-util",
@@ -170,9 +209,9 @@
"type": "indirect" "type": "indirect"
} }
}, },
"flake-utils_2": { "flake-utils_3": {
"inputs": { "inputs": {
"systems": "systems_3" "systems": "systems_4"
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1731533236,
@@ -195,11 +234,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1787797243, "lastModified": 1789183968,
"narHash": "sha256-8+Q7NOB7RPajRjA4pbCDLzqH+MTjnG9x6LUPLfL2joA=", "narHash": "sha256-pEWnYdIF1pBMZwarp/rEPzl+Lknhm5hhjSfeLxH8nAA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "99c9ec63390f1d8c14d95d9e8b17cc29cfbd4e11", "rev": "cd1c9e552f41894aeb5cc5cb353d5a1d61550357",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -213,9 +252,9 @@
"inputs": { "inputs": {
"cl-nix-lite": "cl-nix-lite", "cl-nix-lite": "cl-nix-lite",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": "flake-utils", "flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3", "nixpkgs": "nixpkgs_3",
"systems": "systems_2", "systems": "systems_3",
"treefmt-nix": "treefmt-nix_2" "treefmt-nix": "treefmt-nix_2"
}, },
"locked": { "locked": {
@@ -259,11 +298,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1787457452, "lastModified": 1788680125,
"narHash": "sha256-FJh4esFS3zqNNuKwvN3t6wrJGewqp1AUF9DAEvoKPD8=", "narHash": "sha256-amGSoDobwmp4CFvCn841ws2iuitus+HUJdD/gKgsrJA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nix-index-database", "repo": "nix-index-database",
"rev": "c51d5c2ba69c907a34e90c9b6b80cd2b93811745", "rev": "116ad1c2adb642405ef8916f6a94c8626f971344",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -274,7 +313,7 @@
}, },
"nix2vim": { "nix2vim": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_2", "flake-utils": "flake-utils_3",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
@@ -362,11 +401,11 @@
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1787848966, "lastModified": 1789114715,
"narHash": "sha256-7gDpu5hpq0rOYnYMcOWqSzquK4HA/xU8Xf3CjUBOOJA=", "narHash": "sha256-ugpsyk3NM2s87vXfUiIIiibbJ4Pp0JPS5p/3mfs+q+c=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d57af924f160a5084293c71c2043f058bd1cdb60", "rev": "21a67dc470149f337cecafbe965d8d252a390518",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -442,11 +481,11 @@
}, },
"nixpkgs_6": { "nixpkgs_6": {
"locked": { "locked": {
"lastModified": 1787736819, "lastModified": 1789006805,
"narHash": "sha256-cV5xEJJK3BvhU8rEd4mC9UsmDi5qscv/kzGPhBRC5WA=", "narHash": "sha256-xB8mKMOx1IA9vTDNLmJZ6n4wCMq/cuWBBOzGCRnqxrU=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9fbb54b33e91ee4ca368e35a78e0613c720600b3", "rev": "8ce4ef6cb6f871616146b9fe26d2a5ae594e94fe",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -480,11 +519,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1787998758, "lastModified": 1789200175,
"narHash": "sha256-yJdP/9J2IIISclYPVaATjifKm5ldMtBnLwiJhdBt7SE=", "narHash": "sha256-JriLm/izGMXB4uAHWXKj13kXa3nKZj2r/sR5xHdisgs=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nur", "repo": "nur",
"rev": "5b6adc26a9ef1bc45c09fcc845e7d08258c7e0ca", "rev": "2c789298fc525713063e3b88359d02101651144f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -495,6 +534,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"api": "api",
"darwin": "darwin", "darwin": "darwin",
"disko": "disko", "disko": "disko",
"home-manager": "home-manager", "home-manager": "home-manager",
@@ -517,11 +557,11 @@
"nixpkgs": "nixpkgs_7" "nixpkgs": "nixpkgs_7"
}, },
"locked": { "locked": {
"lastModified": 1787993548, "lastModified": 1789196581,
"narHash": "sha256-+IAEnmmx5YIhUWo0lp15jLLHchnXo5yKgWsi6C6Cf+0=", "narHash": "sha256-yJr1Bt4fKkKIpPYbsKGqJ0VFdoDnURgRwuffmBQ2WzY=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "996e9b0b019a4a9eb9e9a5641aefa06d801b5895", "rev": "228ecefb6329d5a531b77b46b581a2f0c26ee056",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -546,6 +586,21 @@
} }
}, },
"systems_2": { "systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": { "locked": {
"lastModified": 1689347925, "lastModified": 1689347925,
"narHash": "sha256-ozenz5bFe1UUqOn7f60HRmgc01BgTGIKZ4Xl+HbocGQ=", "narHash": "sha256-ozenz5bFe1UUqOn7f60HRmgc01BgTGIKZ4Xl+HbocGQ=",
@@ -560,7 +615,7 @@
"type": "github" "type": "github"
} }
}, },
"systems_3": { "systems_4": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@@ -575,7 +630,7 @@
"type": "github" "type": "github"
} }
}, },
"systems_4": { "systems_5": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@@ -634,11 +689,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1784642409, "lastModified": 1789164534,
"narHash": "sha256-hcbDqFuySAJawljt5r0sKBCJKYnbtGD0T/ZIozH1Dq0=", "narHash": "sha256-DoYGPM6QpnYBLWj9gGw6ZwAzIX+HrAVov1BoT+8Jixo=",
"owner": "nix-community", "owner": "nix-community",
"repo": "NixOS-WSL", "repo": "NixOS-WSL",
"rev": "eaeb18da90024448a60eb1ec7132eafa4003404e", "rev": "72c92b11bb8289e6651c7fef29cc0a885fd6a255",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -655,7 +710,7 @@
"rust-overlay": [ "rust-overlay": [
"rust-overlay" "rust-overlay"
], ],
"systems": "systems_4" "systems": "systems_5"
}, },
"locked": { "locked": {
"lastModified": 1781873766, "lastModified": 1781873766,
+9
View File
@@ -86,6 +86,12 @@
inputs.rust-overlay.follows = "rust-overlay"; inputs.rust-overlay.follows = "rust-overlay";
}; };
# Generic self-hosted REST API service
api = {
url = "github:nmasur/api";
inputs.nixpkgs.follows = "nixpkgs";
};
# # Text editor # # Text editor
# helix = { # helix = {
# url = "github:helix-editor/helix"; # url = "github:helix-editor/helix";
@@ -127,7 +133,9 @@
baseName = "masu.rs"; baseName = "masu.rs";
in in
{ {
api = "api.${baseName}";
audiobooks = "read.${baseName}"; audiobooks = "read.${baseName}";
auth = "auth.${baseName}";
bookmarks = "keep.${baseName}"; bookmarks = "keep.${baseName}";
books = "books.${baseName}"; books = "books.${baseName}";
budget = "money.${baseName}"; budget = "money.${baseName}";
@@ -136,6 +144,7 @@
download = "download.${baseName}"; download = "download.${baseName}";
files = "files.${baseName}"; files = "files.${baseName}";
git = "git.${baseName}"; git = "git.${baseName}";
hister = "hister.${baseName}";
imap = "imap.purelymail.com"; imap = "imap.purelymail.com";
influxdb = "influxdb.${baseName}"; influxdb = "influxdb.${baseName}";
irc = "irc.${baseName}"; irc = "irc.${baseName}";
+5 -11
View File
@@ -66,6 +66,7 @@ lib
overlays = [ overlays = [
inputs.nur.overlays.default inputs.nur.overlays.default
inputs.nix2vim.overlay inputs.nix2vim.overlay
inputs.api.overlays.default
(final: prev: { (final: prev: {
zellij-switch = inputs.zellij-switch.packages.${prev.stdenv.hostPlatform.system}.default; zellij-switch = inputs.zellij-switch.packages.${prev.stdenv.hostPlatform.system}.default;
}) })
@@ -105,6 +106,7 @@ lib
# Build-time-only dep of karakeep's frontend; CVEs don't reach # Build-time-only dep of karakeep's frontend; CVEs don't reach
# the runtime closure. Remove once nixpkgs bumps it. # the runtime closure. Remove once nixpkgs bumps it.
"pnpm-9.15.9" "pnpm-9.15.9"
"keybase-gui-6.5.1"
]; ];
config.allowUnfree = true; config.allowUnfree = true;
} }
@@ -155,19 +157,10 @@ lib
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
inputs.nix-index-database.nixosModules.default inputs.nix-index-database.nixosModules.default
inputs.disko.nixosModules.disko inputs.disko.nixosModules.disko
inputs.api.nixosModules.default
inputs.wsl.nixosModules.wsl inputs.wsl.nixosModules.wsl
{ imports = (nixFiles ../platforms/nixos); } { imports = (nixFiles ../platforms/nixos); }
module (builtins.removeAttrs module [ "home-manager" ])
# (builtins.removeAttrs module [ "home-manager" ])
{
home-manager = {
extraSpecialArgs = {
inherit colorscheme;
}
// specialArgs;
}
// homeModule.home-manager;
}
]; ];
}; };
@@ -228,6 +221,7 @@ lib
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
inputs.nix-index-database.nixosModules.default inputs.nix-index-database.nixosModules.default
inputs.disko.nixosModules.disko inputs.disko.nixosModules.disko
inputs.api.nixosModules.default
inputs.wsl.nixosModules.wsl inputs.wsl.nixosModules.wsl
{ {
imports = (nixFiles ../platforms/nixos) ++ (nixFiles ../platforms/generators); imports = (nixFiles ../platforms/nixos) ++ (nixFiles ../platforms/generators);
@@ -23,6 +23,24 @@ in
home.packages = [ term-probe ]; home.packages = [ term-probe ];
# Ctrl-b: EXECUTE the proven manual cure as a real commandline. The cure
# is not the variable's end value (it starts and ends at 1) — it is the
# reader fully exiting readline and re-entering, which only command
# EXECUTION does. Setting the variable inline (a plain binding body, or the
# old fish_postexec/fish_cancel hook) never makes the reader exit/re-enter,
# so it never cured and, with extra repaints on a wedged reader, made it
# worse. `commandline -f execute` reproduces exactly what typing the cure
# and pressing Enter does — indistinguishable to fish from the manual cure.
# Note: this submits the current commandline, so it runs the cure in place
# of whatever is typed (fine for a rescue key hit at an empty prompt).
# Ctrl-b chosen because it is otherwise unbound (Ctrl-g is taken).
nmasur.presets.programs.fish.fish_user_key_bindings = # fish
''
for mode in insert default visual
bind -M $mode \cb heal-autosuggest
end
'';
programs.fish.functions = { programs.fish.functions = {
lag-triage = { lag-triage = {
description = "Diagnose post-TUI typing lag in the current shell"; description = "Diagnose post-TUI typing lag in the current shell";
@@ -32,6 +50,22 @@ in
description = "Reset terminal state left behind by a TUI"; description = "Reset terminal state left behind by a TUI";
body = builtins.readFile ./unlag.fish; body = builtins.readFile ./unlag.fish;
}; };
lag-sample = {
description = "Stack-sample fish and zellij while typing lag is happening";
body = builtins.readFile ./lag-sample.fish;
};
heal-autosuggest = {
description = "Heal post-TUI typing lag by executing the autosuggestion-toggle cure (bind to a key)";
# Replace the commandline with the exact cure the user runs by hand and
# execute it. Executing (not inline-setting) is what cures: it forces
# the reader to leave and re-enter readline. Runs in place of whatever
# is currently typed.
body = # fish
''
commandline -r 'set -g fish_autosuggestion_enabled 0; and set -g fish_autosuggestion_enabled 1'
commandline -f execute
'';
};
}; };
}; };
@@ -0,0 +1,53 @@
# Capture stack samples of this fish process, the zellij server, and the
# zellij client WHILE the typing lag is happening. This names the guilty
# component directly: if fish's main thread is busy/blocked per keystroke the
# stacks show exactly where; if fish is idle while typing feels laggy, the
# delay is in zellij's render path instead.
#
# CAUTION (learned 2026-08-30): attaching the sampler to a lagging fish CURES
# the lag (thread suspend/resume unwedges it), so run this from a DIFFERENT
# pane with the lagging shell's pid: `lag-sample <pid>` (get it in the lagging
# shell with the builtin-only `echo $fish_pid`). Have someone type in the
# lagging pane while sampling runs — the first samples may catch the wedge.
# With no argument it samples the current shell.
set -l target $fish_pid
if test (count $argv) -ge 1; and test -n "$argv[1]"
set target $argv[1]
end
set -l outdir ~/.local/state/lag-triage
mkdir -p $outdir
set -l ts (date +%Y%m%d-%H%M%S)
set -l dur 8
set -l fishfile $outdir/sample-$ts-fish-$target.txt
/usr/bin/sample $target $dur 1 -file $fishfile &>/dev/null &
disown
# this session's zellij server (socket path ends in the session name)
set -l serverpid (pgrep -f "zellij --server.*/$ZELLIJ_SESSION_NAME\$")
test -z "$serverpid"; and set serverpid (pgrep -f "zellij --server" | head -3)
for pid in $serverpid
/usr/bin/sample $pid $dur 1 -file $outdir/sample-$ts-zellij-server-$pid.txt &>/dev/null &
disown
end
# zellij clients (attached to ghostty): named zellij but without --server args
set -l allserver (pgrep -f "zellij --server")
set -l clientpid
for pid in (pgrep -x zellij)
contains $pid $allserver; or set -a clientpid $pid
end
for pid in $clientpid[1..3]
/usr/bin/sample $pid $dur 1 -file $outdir/sample-$ts-zellij-client-$pid.txt &>/dev/null &
disown
end
# notify when done, without occupying the commandline
fish -c "sleep (math $dur + 2); echo; echo '== lag-sample done: '$outdir'/sample-$ts-*.txt =='" &
disown
echo "Sampling fish (pid $target), zellij server(s) [$serverpid], client(s) [$clientpid] for $dur s."
echo ">>> TYPE CONTINUOUSLY IN THE LAGGING PANE NOW (junk text is fine) <<<"
echo "Files: $outdir/sample-$ts-*.txt"
@@ -20,8 +20,21 @@ let
# harness on fish 4.8.1 (see docs/CHANGELOG.md 2026-08-29 and # harness on fish 4.8.1 (see docs/CHANGELOG.md 2026-08-29 and
# presets/programs/lag-triage/upstream_repro.py). Spawning fish with the # presets/programs/lag-triage/upstream_repro.py). Spawning fish with the
# variable already exported makes every pane shell immune. # variable already exported makes every pane shell immune.
# Flight recorder for the still-unsolved post-TUI typing lag: sampling the
# process CURES the lag (a wedged thread gets kicked loose), so the only way
# to observe it is a recorder that is already running before the lag starts.
# Armed by `touch ~/.local/state/lag-triage/RECORD`; new panes then log
# fish's reader/thread internals to ~/.local/state/lag-triage/flight/.
# Remove the RECORD file to disable (zero overhead when off).
fish-no-query-term = pkgs.writeShellScriptBin "fish-no-query-term" '' fish-no-query-term = pkgs.writeShellScriptBin "fish-no-query-term" ''
export fish_features=no-query-term export fish_features=no-query-term
dir="$HOME/.local/state/lag-triage"
if [ -e "$dir/RECORD" ]; then
mkdir -p "$dir/flight"
find "$dir/flight" -type f -mtime +3 -delete 2>/dev/null
export FISH_DEBUG='reader,term-support,proc-termowner,iothread,fd-monitor,topic-monitor'
export FISH_DEBUG_OUTPUT="$dir/flight/fish-$(date +%Y%m%d-%H%M%S)-$$.log"
fi
exec ${lib.getExe pkgs.fish} "$@" exec ${lib.getExe pkgs.fish} "$@"
''; '';
@@ -41,8 +54,6 @@ in
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
home.packages = [ pkgs.zellij-switch ];
programs.fish = { programs.fish = {
shellAbbrs.z = "zellij"; shellAbbrs.z = "zellij";
functions = { functions = {
@@ -170,19 +181,23 @@ in
keybinds = { keybinds = {
locked = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) { locked = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) {
# For servers that I SSH into, use c-a-G to unlock # For servers that I SSH into, use c-a-G to unlock
"bind \"Ctrl Alt G\"" = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) { "bind \"Ctrl Alt G\"" =
SwitchToMode = { lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable)
_args = [ "normal" ]; {
}; SwitchToMode = {
}; _args = [ "normal" ];
};
};
}; };
normal = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) { normal = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) {
# For servers that I SSH into, use c-a-G to lock # For servers that I SSH into, use c-a-G to lock
"bind \"Ctrl Alt G\"" = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) { "bind \"Ctrl Alt G\"" =
SwitchToMode = { lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable)
_args = [ "locked" ]; {
}; SwitchToMode = {
}; _args = [ "locked" ];
};
};
}; };
session = { session = {
"bind \"w\"" = { "bind \"w\"" = {
@@ -47,6 +47,7 @@ in
host = cfg.host; host = cfg.host;
passwordeval = "${pkgs.age}/bin/age --decrypt --identity ${config.identityFile} ${pkgs.writeText "mailpass-system.age" (builtins.readFile cfg.passwordFile)}"; passwordeval = "${pkgs.age}/bin/age --decrypt --identity ${config.identityFile} ${pkgs.writeText "mailpass-system.age" (builtins.readFile cfg.passwordFile)}";
user = cfg.user; user = cfg.user;
from = cfg.user;
from_full_name = "${config.networking.hostName} System"; from_full_name = "${config.networking.hostName} System";
port = 465; port = 465;
tls = true; tls = true;
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBycCtF
S1ErMkNoME1ZNzYramZ6cFJGL1VLbU1sSnVBaTA2Y3B2M1FUYTM0Cnl1a0dmdEpt
U3puRS9RUEUyVVBDelRRRGlGQXdWQzc4cHprZGNRb2RuYmcKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIDJ1M1dmTnB4Vzg3WDZ3QzZDZ1phYTU4YUk0U1VmV2M5cHZ2bENE
eEE5aDQKODFrOWJBWjE0WFJKRTVrb2dlTmVHdEVHeVc4MU9GRHhTY1l5Mzh1YnZo
dwotPiBzc2gtZWQyNTUxOSBuanZYNUEgM2JNTmVuTCtiSnNOS2UybE5qQXptcW92
QngyaGh5Y0VqOGZEaWpkazYzawpjZmExWmF5MjV0NE05ZXVzenhYVXpRaGg4R25x
QkpFa080S3haTXpTUi80Ci0+IHNzaC1lZDI1NTE5IENxSU9VQSBQcjgrV0c3S21I
RXY5WnlSdWRML1g4ZmtvNWVmTGNwb3owZUFWN0s0ckhZCm1PbGtKY1hOanBGU0V6
MzRrd3lPSUIrUVY3ZW5VTzFrWnJjblNvZmVIYk0KLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IGdoRHVhbG52OE5wUEFwWjlvb0xZSUpOS0loNm1QOWZ0N2RGZ2xMcW5RQXcK
UUpNdDh1aUN0cHM2N0M3bGMvcjIwT2YrV01sdjdBQThkM3BxQ1hXY1A3bwotLS0g
K21sZndObVJLdkNBR3IvWHZ3c0RjSEJQajFUV2tFUmNXWDQ3RU1DNzJKcwon7wF2
i2V9/fXS9QBfTvUJGP9MQ1gCe+UxstI3IPv+ODudHLeCGT0JoxkN6DZtayxKYYJn
iKghS8tUNdzshOBqpA==
-----END AGE ENCRYPTED FILE-----
@@ -32,6 +32,22 @@ in
enable = true; enable = true;
settings = { settings = {
port = cfg.port; port = cfg.port;
loginMethod = "openid";
openId = {
discoveryURL = "https://${hostnames.auth}/.well-known/openid-configuration";
client_id = "92afe9f8-7ef6-42ab-8a06-701df3c7179d";
client_secret._secret = config.secrets.actualbudget-oidc-secret.dest;
server_hostname = "https://${hostnames.budget}";
authMethod = "openid";
};
};
};
systemd.services.actual = {
after = [ "actualbudget-oidc-secret-secret.service" ];
serviceConfig = {
PrivateUsers = lib.mkForce false;
SupplementaryGroups = [ "shared" ];
}; };
}; };
@@ -107,6 +123,17 @@ in
owner = builtins.toString config.users.users.actualbudget.uid; owner = builtins.toString config.users.users.actualbudget.uid;
group = builtins.toString config.users.users.actualbudget.uid; group = builtins.toString config.users.users.actualbudget.uid;
}; };
secrets.actualbudget-oidc-secret = {
source = ./actualbudget-oidc-secret.age;
dest = "${config.secretsDirectory}/actualbudget-oidc-secret";
owner = config.users.users.actualbudget.name;
group = config.users.groups.shared.name;
permissions = "0440";
};
systemd.services.actualbudget-oidc-secret-secret = {
requiredBy = [ "actual.service" ];
before = [ "actual.service" ];
};
# Allow web traffic to Caddy # Allow web traffic to Caddy
nmasur.presets.services.caddy.routes = lib.mkAfter [ nmasur.presets.services.caddy.routes = lib.mkAfter [
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBaUWNy
UGZPWkFhOS9nZ0N4bWY1S1FUc2pIVS95R2NLK2xLMWFmamNFNDBNCm5yQXhCNXM4
cUtqbHdjcVRmTWUzd2JoeUtMWDQ3ZEtwcFdsSU9MSE1pcDQKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIEtDMUo5cERoSHd0dm9jNCtMNDNWTkNXbTc2bjlPbUR4K0phSWdT
QVR2bUEKY3Y0b29MbzVCSVN0UWhkcEtzN2pBT3A4OWhpeitGQjAvLzg1OUVRdENQ
QQotPiBzc2gtZWQyNTUxOSBuanZYNUEgOWc0aXJtMWNKcXpUMEw4blJCSTZnVS9H
YmVyVE13TG1qaTcrdThMRk94WQovNHl5ZHJrYlhRNDVQUkc5TXRKd1Y2TXRUb0p0
NElrT1BzbWwwd2pFb1VvCi0+IHNzaC1lZDI1NTE5IENxSU9VQSB0RlpZVnAybFZk
R0E1TTVSR3lxVmpqVnNPOHo2VG5xckdONllVcUZWRzFJCk1RUnhoUzBPc1h5cmdn
MFdtSHRvU0xBRWllKy90dW9aV2NhVUVqaVpoVDgKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IFY2RmJMOHo1V0U1K3FNeVBhdDVYUE5lYnFHV2ZqZm5Uek1waDhNeGIwaDQK
UFJBcFdBb2hlOWxoTHIwdUtibjk2Q1E1emFJbVlxNDdLSzlqbWI1dm5XbwotLS0g
RDV3bWVQUG95c2h4S0lJQlJ4dlNBYkVNb2sxTlUxc0tlQ2pzb04yVmhPcwpv4CRQ
kRFkaF8/q1wY4JC7oD6VbJzZWpMsAUbQQWEs8QI9nI2HyqHB4yZ/dT8wSfoh7JPY
fHIVqN25xXGTu7uR2S/8Iic=
-----END AGE ENCRYPTED FILE-----
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyA5Y0JT
SjBqU1dEUVIwSXFBY3h3M3FDMFFpUTdSVmp6S3Y5QXYxK2Rjc2hrCll4UzN0RXVl
a2s0ZkxOdDIxd05kVkdJZWJHQmQ3ZDNjeGRpWnVFQ1Jzd3cKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIDJHS2JTM2R1NzNLSG12bC9yRURBYWx5K1N4d2VWUS8vU2NHamta
amsvUlUKRy8wOWc0cStGN3M1RnpGc2Z0aCt3Y3JiU3dZd3I0V0sxOWdSSE5OUEVy
QQotPiBzc2gtZWQyNTUxOSBuanZYNUEgSmJBbngwMVN3UHpvcHkxUjkvQ1FJaVdX
MzZrVnZtS1VRQVRGWVJocnB6WQpvb01VZ2tleGh6VW9FRTJjN0EwMncyVWZ4WHpi
WXRGZklZcHRkSDYyYmNZCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBKTE0xWlMzRTh0
cEJjUU9NY0NiRWNGU1hvNzQ4clRQRTRJaU1VVzgrZDJZClluVXZiTHp6YUZxOWF3
U0UzWkRIM0xicmw3Qzc5QnhjeGNKOWFGMnptc28KLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IFNkS01KbzhFcW9iS05OcVI2Szl3dXI5QTAvejdhRU1EbEhnQTcwRGo2MWcK
V01MTmhoYlE5YnBvL21qT0tubXRTOGtDYno2Wjd6M2hlY3Z4UHBjQU9WbwotLS0g
VWpndk84OHJSRVMwUFg4aUYwQmNVamIwN3BVL1dzcEV2LzZBODE5bmdoYwpg3LTS
kiMJpjLJ12CGFqmMCjwWwiJHjsC8FM47Ptf2uRxjD1XsOq6MWKHYSI9KxzrgJdyY
uxhU1m3Jfi5+7+oWX6BhfQA=
-----END AGE ENCRYPTED FILE-----
@@ -0,0 +1,18 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBlZEhP
b1ZUdTlROVAydktUNG9lRDVEejMxVmRtNjlhQ2V1NWMyblZFZHlJClczMHM4RXda
TzYzOUFhWmxxUTF0bjJXWVJoSS9HaWZ6TFJ1dndWQXF6WWcKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIGZuR28vNVFqLzUxUHJ4Nlc1c2h5ODNQQnh2bFZRU3hDS3VidERF
T3htUmsKRjd6ZVZuTE1lN2hrOTJaaWQxdE01djl0QlJBVUNnRHlEQ3JMWmMxNkEw
cwotPiBzc2gtZWQyNTUxOSBuanZYNUEgM3ZnOFd6REZNNkxXdW9NT2gzYkJ3ZUhm
UlRjb3kxRnppN2M2UERLNUFrSQp0YWRBL3JXZWs5Zyt0RjRBS2JjUkJockE3Ti9m
TzlNR1c4dFJYUzdRSzBNCi0+IHNzaC1lZDI1NTE5IENxSU9VQSA0SFRseGsxMnlI
TzhueGZGS2VzMzVxcmNDZVdPZDYwQ0c4SjNhbWp5OUVJCkpnSXJiVVZiTHY5SDg2
WnpmRDZQcHlOWnpySTZmUG9hTFdremp2MXEvRFUKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IHhHWFk4ckt5UCtGM3VGaVljOGl3aFV1UDhCVm5PV0lqRGJRcGg0RHJGejgK
NVR2cmFWTW9IYTRsT0V0VGs4VHBYY1BPM3A3bUY1enA2cVZhU0NTWXI2awotLS0g
a1Q4SnBSWXJxREpuaEJtS2VJaTZiNmVWdFp1QXhUOFJRSVVUUEUrVFA2SQqmH3al
3mHIyywuvc6U0lf0FheRGCp//BFORrAqo9bgJmUj/SsduvT1DNyJBGfi62k/8llH
/oQmcYx8rTIbZzCkDHu18DCxJxiZahaIVV/pilQAtSZVfdq/pPnzCBaB1QMMOnWY
cB/H5eQh
-----END AGE ENCRYPTED FILE-----
@@ -0,0 +1,104 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (config.nmasur.settings) hostnames;
cfg = config.nmasur.presets.services.api;
in
{
options.nmasur.presets.services.api = {
enable = lib.mkEnableOption "API gateway and backend services";
};
config = lib.mkIf cfg.enable {
services.api = {
enable = true;
hostname = hostnames.api;
backends.actual = {
enable = true;
actualServerUrl = "http://127.0.0.1:${builtins.toString config.nmasur.presets.services.actualbudget.port}";
apiKeysFile = config.secrets.api-actual-keys.dest;
serverPasswordFile = config.secrets.actualbudget-password.dest;
budgets = {
budget1 = {
syncIdFile = config.secrets.api-actual-budget1-sync-id.dest;
};
budget2 = {
syncIdFile = config.secrets.api-actual-budget2-sync-id.dest;
};
};
};
};
secrets = {
api-actual-keys = {
source = ./api-actual-keys.age;
dest = "${config.secretsDirectory}/api-actual-keys";
owner = "api_actual";
group = "api_actual";
prefix = "API_KEYS=";
};
api-actual-budget1-sync-id = {
source = ./api-actual-budget1-sync-id.age;
dest = "${config.secretsDirectory}/api-actual-budget1-sync-id";
owner = "api_actual";
group = "api_actual";
prefix = "ACTUAL_SYNC_ID_BUDGET1=";
};
api-actual-budget2-sync-id = {
source = ./api-actual-budget2-sync-id.age;
dest = "${config.secretsDirectory}/api-actual-budget2-sync-id";
owner = "api_actual";
group = "api_actual";
prefix = "ACTUAL_SYNC_ID_BUDGET2=";
};
};
systemd.services.api-actual = {
after = [
"postgresql-setup.service"
"actual.service"
"api-actual-keys-secret.service"
"api-actual-budget1-sync-id-secret.service"
"api-actual-budget2-sync-id-secret.service"
"actualbudget-password-secret.service"
];
requires = [
"postgresql-setup.service"
"api-actual-keys-secret.service"
"api-actual-budget1-sync-id-secret.service"
"api-actual-budget2-sync-id-secret.service"
"actualbudget-password-secret.service"
];
};
# Postgres peer authentication for api_actual
services.postgresql.authentication = lib.mkAfter ''
local api_actual api_actual peer
'';
# Backup PostgreSQL database for api_actual
services.postgresqlBackup = {
enable = true;
databases = [ "api_actual" ];
};
# Allow web traffic to Caddy
nmasur.presets.services.caddy.routes = config.services.api.caddyRoutes;
# Configure Cloudflare DNS to point to this machine
services.cloudflare-dyndns.domains = [ hostnames.api ];
# Backups
services.restic.backups.default.paths = [ "/var/lib/api-actual" ];
};
}
@@ -25,7 +25,7 @@
# Set ca = "<public key>" # Set ca = "<public key>"
let let
inherit (config.nmasur.settings) username; inherit (config.nmasur.settings) username hostnames;
cfg = config.nmasur.presets.services.cloudflared; cfg = config.nmasur.presets.services.cloudflared;
in in
@@ -68,27 +68,24 @@ in
# Grant Cloudflare access to SSH into this server # Grant Cloudflare access to SSH into this server
environment.etc = { environment.etc = {
"ssh/ca.pub".text = '' "ssh/ca.pub" = {
${cfg.tunnel.ca} text = ''
''; ${cfg.tunnel.ca}
'';
# Must match the username portion of the email address in Cloudflare mode = "0444";
# Access };
"ssh/authorized_principals".text = ''
${username}
'';
}; };
users.users.${username}.openssh.authorizedPrincipals = [
username
"${username}@${hostnames.mail}"
];
# Adjust SSH config to allow access from Cloudflare's certificate # Adjust SSH config to allow access from Cloudflare's certificate
services.openssh.extraConfig = '' services.openssh.settings = {
PubkeyAuthentication yes TrustedUserCAKeys = "/etc/ssh/ca.pub";
TrustedUserCAKeys /etc/ssh/ca.pub Macs = [ "hmac-sha2-512" ]; # Fix for failure to find matching mac
Match User '${username}' };
AuthorizedPrincipalsFile /etc/ssh/authorized_principals
# if there is no existing AuthenticationMethods
AuthenticationMethods publickey
'';
services.openssh.settings.Macs = [ "hmac-sha2-512" ]; # Fix for failure to find matching mac
# Create credentials file for Cloudflare # Create credentials file for Cloudflare
secrets.cloudflared = { secrets.cloudflared = {
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBmRTZ2
Q0pxZ2ZOZ21HcG9EWHJOR0FZbU5UeWFTQXZwRCtKK2hXWDZsQVJzCjh1MURnU0dr
MERUWGVETHFZZjhiZnJsMVlqTDdSNnZwbDNQNFRCRDZuQ3MKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIFl0V3Jac3IydVJDOVI3c3lscWZiQzU4TG1tYlNZSWZHbmtSakRl
bGhVbk0KNzNFZDFXM3FhWkNWbUliRG00dGN3WjNxZktCT2RPd2FUbi9nS1NobEtQ
cwotPiBzc2gtZWQyNTUxOSBuanZYNUEga29CZkplcC9yN25JU0lNakQxSW92UFBz
bCtHQUNPd0FBdzVMaEhRVjJoWQpmV2xsWFcwU0o0SlljbjEzY2dPT0VlazVhZ3h3
bDEwNzRGUWdJM0Y3UDhvCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBzVWhPT01tSmZh
UlpDb0ltZGdiRXRkVTFEQW45Tk5PMDVqdFczRTBlQ0hVCm1OOEJDNEN4K1lFZU1r
cU1ENVRqM3BLV3lVTzRpOUhjbVFCU0t4Q0owR28KLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IFRTU1FBa2d2ZGRCc0JuanRpbEE2OU53M1htOGhJT1hoM0s5R2Rqbk1FMXcK
SE1qbjFid1ZnUmljUDBlMU4yTlU0bWlOenhFN3Bkd3BSS3NIYStQNHFHYwotLS0g
c2ZwZjNGaTQxVVBKUC9keTZhOGtxRHA0MkpmRkNwUGFjMlFXVnM0Y0VTWQpi8R/5
BRFSxYiEYeB54XjyVXRcyct94jH4wentMOYUFcZP+kd3dwKnXdtrqZDlCk3eM9sy
xzOoHpd8BhmmRK9hgw==
-----END AGE ENCRYPTED FILE-----
@@ -29,13 +29,25 @@ in
}; };
secrets.grafana-secret-key = { secrets.grafana-secret-key = {
source = ./grafana-secret-key.age; source = ./grafana-secret-key.age;
dest = "${config.secretsDirectory}/grafana-secret-key"; dest = "${config.secretsDirectory}/grafana-secret-key";
owner = "grafana"; owner = "grafana";
group = "grafana"; group = "grafana";
permissions = "0440"; permissions = "0440";
}; };
systemd.services.grafana-secret-key-secret = { systemd.services.grafana-secret-key-secret = {
requiredBy = [ "grafana.service" ];
before = [ "grafana.service" ];
};
secrets.grafana-oidc-secret = {
source = ./grafana-oidc-secret.age;
dest = "${config.secretsDirectory}/grafana-oidc-secret";
owner = "grafana";
group = "grafana";
permissions = "0440";
};
systemd.services.grafana-oidc-secret-secret = {
requiredBy = [ "grafana.service" ]; requiredBy = [ "grafana.service" ];
before = [ "grafana.service" ]; before = [ "grafana.service" ];
}; };
@@ -44,11 +56,15 @@ in
enable = true; enable = true;
settings = { settings = {
security.secret_key = "$__file{${config.secrets.grafana-secret-key.dest}}"; security.secret_key = "$__file{${config.secrets.grafana-secret-key.dest}}";
auth = {
oauth_allow_insecure_email_lookup = true;
};
server = { server = {
domain = hostnames.metrics; domain = hostnames.metrics;
http_addr = "127.0.0.1"; http_addr = "127.0.0.1";
http_port = 3000; http_port = 3000;
protocol = "http"; protocol = "http";
root_url = "https://${hostnames.metrics}/";
}; };
smtp = rec { smtp = rec {
enabled = true; enabled = true;
@@ -58,6 +74,19 @@ in
from_name = "Grafana"; from_name = "Grafana";
from_address = user; from_address = user;
}; };
"auth.generic_oauth" = {
enabled = true;
name = "Pocket ID";
allow_sign_up = true;
client_id = "85d879ed-1a86-4984-b33d-43806500ef98";
client_secret = "$__file{${config.secrets.grafana-oidc-secret.dest}}";
scopes = "openid profile email";
auth_url = "https://${hostnames.auth}/authorize";
token_url = "https://${hostnames.auth}/api/oidc/token";
api_url = "https://${hostnames.auth}/api/oidc/userinfo";
login_attribute_path = "preferred_username";
skip_org_role_sync = true;
};
}; };
provision = { provision = {
enable = true; enable = true;
@@ -0,0 +1,59 @@
{
config,
lib,
...
}:
let
inherit (config.nmasur.settings) hostnames;
cfg = config.nmasur.presets.services.hister;
in
{
options.nmasur.presets.services.hister = {
enable = lib.mkEnableOption "Hister web history service";
port = lib.mkOption {
type = lib.types.port;
description = "Port to use for the localhost";
default = 4433;
};
};
config = lib.mkIf cfg.enable {
services.hister = {
enable = true;
port = cfg.port;
settings = {
app = {
user_handling = true;
};
server = {
base_url = "https://${hostnames.hister}";
};
};
};
# Adding new users:
# sudo -u hister hister --config /run/hister/config.yml create-user <username> --admin
# Allow web traffic to Caddy
nmasur.presets.services.caddy.routes = [
{
match = [ { host = [ hostnames.hister ]; } ];
handle = [
{
handler = "reverse_proxy";
upstreams = [ { dial = "localhost:${builtins.toString cfg.port}"; } ];
}
];
}
];
# Configure Cloudflare DNS to point to this machine
services.cloudflare-dyndns.domains = [ hostnames.hister ];
};
}
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBOUXFn
MmFqRWIxOUltQ3lPd1czZ3prbElXaURRV01rSUJGczhka0FOZkNrCkVFaXp2cnd0
eTFhQUVlY001WnhRUVowSEdORS9MQWdTajhiUWtBVnpaZjAKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIHg2ZGVSVDRSRVBUenBSTWhVbTFTRzFESXRyTEl6OEdoUDJmV2FZ
T0tPMXMKZ281MmhJd1EyV3VsNTVlNHYxcWE5cGdmTW5mVkNiM1J5ejNWQlBVQ3hM
UQotPiBzc2gtZWQyNTUxOSBuanZYNUEgUDhObFRyMCsxWkhGK1lKU25WenllZmRj
NnB5RHJyQjlscGE4NW1uVEJuQQpUQTFXNklzdEcrZjlOSUM3ZzV5OGlvREdJRjlu
MERGUkdjcXR6REtNUU1ZCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBob2ZLQnNYVFpx
aVBEb2ZVOWVpUGZtU3FTUmNoS214cUxGeVZhM1NFZkNZCk5IV2VDMnc3L2Y4OEEx
b1ZSVys0dHpDUlo5UFA2akNKSHFNTi9JTUlnNEEKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IDZBd3k2ZE85Nm53MGdhNnJkUWFWVXEzTFRWU1lMSWp2L1FaYkdZRy9zQ2cK
TENkNFhsUi9VY2xPSFMwVmRNeXBxS1dFR1VtRDZsQ0xRSTdJQkJxODhiSQotLS0g
bnpxUGRtVVYvc1R5VUk5VDE4Mmx0WEVNMDZNc0tSSktCTzJlcjJmL3Y0bwqr2hdI
U3VNIk0qy4sWUdRCPqdyGK+F4qmcbD5MrPBQYNrc2aQ/zr5E/OkZn7k6vCvGMC9k
zrjpVlAxLB2cRsBmVw==
-----END AGE ENCRYPTED FILE-----
@@ -10,6 +10,18 @@ in
options.nmasur.presets.services.immich.enable = lib.mkEnableOption "Immich photo manager"; options.nmasur.presets.services.immich.enable = lib.mkEnableOption "Immich photo manager";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
secrets.immich-oidc-secret = {
source = ./immich-oidc-secret.age;
dest = "${config.secretsDirectory}/immich-oidc-secret";
owner = config.services.immich.user;
group = config.services.immich.group;
permissions = "0440";
};
systemd.services.immich-oidc-secret-secret = {
requiredBy = [ "immich-server.service" ];
before = [ "immich-server.service" ];
};
services.immich = { services.immich = {
enable = true; enable = true;
port = 2283; port = 2283;
@@ -19,7 +31,18 @@ in
machine-learning.environment = { }; machine-learning.environment = { };
mediaLocation = "/data/images"; mediaLocation = "/data/images";
secretsFile = null; secretsFile = null;
settings.server.externalDomain = "https://${hostnames.photos}"; settings = {
server.externalDomain = "https://${hostnames.photos}";
oauth = {
enabled = true;
issuerUrl = "https://${hostnames.auth}";
clientId = "1f4e0f8d-6cee-4d67-8d53-74bf6c18ae09";
clientSecret._secret = config.secrets.immich-oidc-secret.dest;
scope = "openid profile email";
autoRegister = true;
buttonText = "Login with Pocket ID";
};
};
environment = { environment = {
IMMICH_ENV = "production"; IMMICH_ENV = "production";
IMMICH_LOG_LEVEL = "log"; IMMICH_LOG_LEVEL = "log";
@@ -28,6 +51,10 @@ in
}; };
}; };
systemd.services.immich-server = {
after = [ "immich-oidc-secret-secret.service" ];
};
nmasur.presets.services.caddy.routes = [ nmasur.presets.services.caddy.routes = [
{ {
match = [ { host = [ hostnames.photos ]; } ]; match = [ { host = [ hostnames.photos ]; } ];
@@ -44,9 +44,27 @@ in
{ {
match = [ { host = [ hostnames.stream ]; } ]; match = [ { host = [ hostnames.stream ]; } ];
handle = [ handle = [
# Resolve client IP: use Cloudflare's CF-Connecting-IP if present, otherwise remote host
{
handler = "map";
source = "{http.request.header.CF-Connecting-IP}";
destinations = [ "{client_ip}" ];
defaults = [ "{http.request.remote.host}" ];
mappings = [
{
input_regexp = "^(.+)$";
outputs = [ "\${1}" ];
}
];
}
{ {
handler = "reverse_proxy"; handler = "reverse_proxy";
upstreams = [ { dial = "localhost:8096"; } ]; upstreams = [ { dial = "localhost:8096"; } ];
headers.request.set = {
"X-Real-IP" = [ "{client_ip}" ];
"X-Forwarded-For" = [ "{client_ip}" ];
"X-Forwarded-Proto" = [ "{http.request.scheme}" ];
};
} }
]; ];
} }
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBhZWw0
UTJSYitUUTY0b3A3RDZZTjhnWjh3QWdTaFBzZVRpRWs2UW43R3k0Cmg1ODRWWnI5
TFdSbmtlTU9zdzhyTHpzMW1hNmk3Tk1sRjZlbFYwZURML1kKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIHVvaGZjeVh3Zm8yWUcyMEZnb1VITHpveGY4aWxTOVhUTzVpbnI0
c2lYQ0EKNHI4ZEVpUWFYRjVxTTUrTElwdTliWVc2RjdJT21wTTFtSi9ydUo1MVF3
UQotPiBzc2gtZWQyNTUxOSBuanZYNUEgODhzWTIwVjJvZzZKWGJKNkNHRXFSOWI2
bDZab1hLUFBOUmNBZXhEQUtoNAozRXBXNUF6THN4Ry9OcG9mb0pTNGZrL01zTzhr
QWZpRHg1dUNtdnFJTnhNCi0+IHNzaC1lZDI1NTE5IENxSU9VQSA2NWYza2ZDWmda
N3Eycll2cXNPM0Q3UVk5OHlta1p5OTU3QlQwY3dxNm1NCktLaUxVTkYwMFpZV1px
SUFUU1JYZ0pyUU9oRURKWVNVck5VSXdVT3N3b0UKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IGZNV3NRWVg5N3FQeUdKYjhwQ2xydzBsaUZDVzl5Y1BrTlhZNCtOOWduZ28K
dnozT0lFa2NsYW9hbGpTL2pNK1YwcjM2QkVoWHJUUVdjT09XMlJGNUx0RQotLS0g
My9aVWJuVlBQbTNYZWd2bXUzUGp0NzZXcmFicXNob2ROeWY1cnViWXNQRQpYZ/zy
L37ulzvRl/Wt2+tIz9fgfOtK2KHuagnPD27T9e8bC9oVinJngwrzfFsV4t7/GCCs
iI+gfp86+RPSbIwrCA==
-----END AGE ENCRYPTED FILE-----
@@ -10,16 +10,37 @@ in
options.nmasur.presets.services.mealie.enable = lib.mkEnableOption "mealie recipe manager"; options.nmasur.presets.services.mealie.enable = lib.mkEnableOption "mealie recipe manager";
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
secrets.mealie-oidc-secret = {
source = ./mealie-oidc-secret.age;
dest = "${config.secretsDirectory}/mealie-oidc-secret";
prefix = "OIDC_CLIENT_SECRET=";
};
systemd.services.mealie-oidc-secret-secret = {
requiredBy = [ "mealie.service" ];
before = [ "mealie.service" ];
};
services.mealie = { services.mealie = {
enable = true; enable = true;
port = 9099; port = 9099;
database.createLocally = true; database.createLocally = true;
listenAddress = "127.0.0.1"; listenAddress = "127.0.0.1";
credentialsFile = config.secrets.mealie-oidc-secret.dest;
settings = { settings = {
TOKEN_TIME = 7200; # Hours for login to last (300 days) TOKEN_TIME = 7200; # Hours for login to last (300 days)
OIDC_AUTH_ENABLED = "true";
OIDC_SIGNUP_ENABLED = "true";
OIDC_CONFIGURATION_URL = "https://${hostnames.auth}/.well-known/openid-configuration";
OIDC_CLIENT_ID = "040925ed-b39e-4442-b8e8-369c948c0cd2";
OIDC_PROVIDER_NAME = "Pocket ID";
OIDC_USER_CLAIM = "email";
}; };
}; };
systemd.services.mealie = {
after = [ "mealie-oidc-secret-secret.service" ];
};
# Fix BASE_URL for downloading backups # Fix BASE_URL for downloading backups
systemd.services.mealie.environment.BASE_URL = lib.mkForce "https://${hostnames.recipes}"; systemd.services.mealie.environment.BASE_URL = lib.mkForce "https://${hostnames.recipes}";
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyAzMEJs
WW9mb1E5MW5vT0tYeDhKSnFSRWZFVFdrL0d6Vk45UUVBd3JubFNvClpIRDYzM1Zh
dEtMaFNSeEF4aHY2cWpZbUNNMUw0UW93SFV0NVBMZ2wvKzgKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIDlGUUQwdlFwd1JiOE9Sb0svWTM4cDVHbGNQeFVDcUJsa0xqZGp6
SGxkUlUKMHpPSFFSQXZSYjdwNUhnRnUxZzJVQWxLblB5MjBTZjRpM0d4NDJFSk1Z
RQotPiBzc2gtZWQyNTUxOSBuanZYNUEgK055eTE2NG9yN3FEZ1BSaTZKTGpWUmt0
YlFWU0hjSnZ3MVZSdE12Y0VVTQovNWpGM2Y0TUlNSHRVQmRJK3VCNTBHTFAzdlYx
MXNJOVNOdTJiNlZZdWkwCi0+IHNzaC1lZDI1NTE5IENxSU9VQSArblhobTFoN1BQ
ZUhuL04yVUtTRnF1V0tPR1BVK3lzWUVrMlhJQWxmZXhZClJkUnU0QUxuRnZ3Vjg2
TVFzdGs5dDJSbFZMQnFRamxObnFpZVBaTExFajQKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IFE1clkxRFFZbmVEV01LVWNXaVR6ZkdrcHJYUFNaSXI1STUrSmN5ejJnU2MK
STRhOWlXTFU2emVvVUkxMmxKc1FmbmJrQ1RFU3RLa2RBSUJlak5CRG1obwotLS0g
WFhjSmJxSmVMQlR0bjRKWVR1cllmQ1JjUkZZYWpTbEFzTjNQVzRvbHNJbwrrqVI1
yl6w97t0CDhjOml46f3wlob0kztDAdOFmUSiEVfiQU0eCrfNinQg7aohu5Y8MJpD
k2Du2m/AV6aIPR47LA==
-----END AGE ENCRYPTED FILE-----
@@ -33,6 +33,7 @@ in
# Allow access when hitting either of these hosts or IPs # Allow access when hitting either of these hosts or IPs
trusted_domains = [ hostnames.content ]; trusted_domains = [ hostnames.content ];
trusted_proxies = [ "127.0.0.1" ]; trusted_proxies = [ "127.0.0.1" ];
overwriteprotocol = "https";
maintenance_window_start = 4; # Run jobs at 4am UTC maintenance_window_start = 4; # Run jobs at 4am UTC
log_type = "file"; log_type = "file";
loglevel = 1; # Include all actions in the log loglevel = 1; # Include all actions in the log
@@ -41,6 +42,7 @@ in
extraApps = { extraApps = {
calendar = config.services.nextcloud.package.packages.apps.calendar; calendar = config.services.nextcloud.package.packages.apps.calendar;
contacts = config.services.nextcloud.package.packages.apps.contacts; contacts = config.services.nextcloud.package.packages.apps.contacts;
user_oidc = config.services.nextcloud.package.packages.apps.user_oidc;
# These apps are defined and pinned by overlay in flake. # These apps are defined and pinned by overlay in flake.
# news = pkgs.nextcloudApps.news; # news = pkgs.nextcloudApps.news;
# external = pkgs.nextcloudApps.external; # external = pkgs.nextcloudApps.external;
@@ -211,6 +213,32 @@ in
before = [ "nextcloud-setup.service" ]; before = [ "nextcloud-setup.service" ];
}; };
secrets.nextcloud-oidc-secret = {
source = ./nextcloud-oidc-secret.age;
dest = "${config.secretsDirectory}/nextcloud-oidc-secret";
owner = "nextcloud";
group = "nextcloud";
permissions = "0440";
};
systemd.services.nextcloud-oidc-secret-secret = {
requiredBy = [ "nextcloud-setup.service" ];
before = [ "nextcloud-setup.service" ];
};
# Configure Pocket ID OIDC provider after Nextcloud setup enables apps
systemd.services.nextcloud-setup = {
after = [ "nextcloud-oidc-secret-secret.service" ];
postStart = ''
${config.services.nextcloud.occ}/bin/nextcloud-occ user_oidc:provider pocket-id \
--clientid="c8a32c58-a781-4f14-9070-f498fdfda438" \
--clientsecret-file="${config.secrets.nextcloud-oidc-secret.dest}" \
--discoveryuri="https://${hostnames.auth}/.well-known/openid-configuration" \
--scope="openid profile email" \
--mapping-uid="preferred_username" \
--unique-uid=0
'';
};
# Grant user access to Nextcloud directories # Grant user access to Nextcloud directories
users.users.${username}.extraGroups = [ "nextcloud" ]; users.users.${username}.extraGroups = [ "nextcloud" ];
@@ -6,7 +6,6 @@
}: }:
let let
inherit (config.nmasur.settings) username;
cfg = config.nmasur.presets.services.nix-autoupgrade; cfg = config.nmasur.presets.services.nix-autoupgrade;
in in
@@ -27,7 +26,7 @@ in
system.autoUpgrade = { system.autoUpgrade = {
enable = true; enable = true;
dates = "09:33"; dates = "09:33";
flake = "git+${cfg.repo}"; flake = "git+ssh://${cfg.repo}";
randomizedDelaySec = "25min"; randomizedDelaySec = "25min";
operation = "switch"; operation = "switch";
allowReboot = true; allowReboot = true;
@@ -55,8 +54,7 @@ in
systemctl status $SERVICE_ID >> $TEMPFILE systemctl status $SERVICE_ID >> $TEMPFILE
set -e set -e
${lib.getExe pkgs.msmtp} \ ${lib.getExe pkgs.msmtp} \
--file=${config.home-manager.users.${username}.xdg.configHome}/msmtp/config \ --account=default \
--account=system \
${address} < $TEMPFILE ${address} < $TEMPFILE
''; '';
}; };
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBCa2J1
QjFGRjdZa0RHcjd0V0JXL3lUNzRGczNHanIzb3BtaE93NlNvcGhnCnNOM3JBc0t3
VENnWVp1b09Sd1JwcDk1Q0pQM3BBMlJmWHh5TTJHakIvcGsKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIDljQUIrenlGZHMxSHlReG80VVhaSXhsbFZ0L2dtcCtWbTRvNE1i
WEdOeVkKUk9HK25Fci9TYWRSS1htSU9BNHlqbHpGT3c5bkI4b3RUL09QK1BYTE1Y
VQotPiBzc2gtZWQyNTUxOSBuanZYNUEgNXk5bzRhR1BoTjlMeHVRR2UyUEZJN0Y1
ZXAyU3BjeFJvRHhER0ptaVlsVQo0K3p6bXBwcTVIdGlTajBucWV5dzZKM2JyWWNB
R2s2UnhTMFVPRGwrVWhRCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBaL3NTdHBjVFRX
ODNrcDVmZGFxNVRwbGkrcC9heFN5bmVCZ0xId1NESUhFCm5aQS9xZmhkSWRwMFl1
b3FPdUhPZU5YdVQvTEQ3eVdRODBhVzJSWjdyek0KLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IHdDR3YwYTFyWlZQZWFDZ1cvWk00bjdRdW16c1M4eDlDaHl4Y3lkbVFGVFEK
dndhaG5pUTM3aU04WHZUSHJDdUdkZ0VWRXRaL2t6cUJoZTR1bTh6NlBFawotLS0g
NXRDeDFZL0RvaURVK2k3R0NVZGdKR0diZ2RwZjQxTFhuNk82SW9GMGVaRQqYDG6J
+/POhVn84NGh8ganDIzNUqQF7uBIxdUMiRoFa0kU4eJd9V4vmjsjtr/lbQ9O+Xfq
WJJ8Hjwyzwopx1Phrw==
-----END AGE ENCRYPTED FILE-----
@@ -1,6 +1,11 @@
# Paperless-ngx is a document scanning and management solution. # Paperless-ngx is a document scanning and management solution.
{ config, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
inherit (config.nmasur.settings) hostnames username; inherit (config.nmasur.settings) hostnames username;
@@ -17,12 +22,26 @@ in
enable = true; enable = true;
mediaDir = "/data/generic/paperless"; mediaDir = "/data/generic/paperless";
passwordFile = config.secrets.paperless.dest; passwordFile = config.secrets.paperless.dest;
environmentFile = "${config.secretsDirectory}/paperless-env";
configureTika = true; # Enable processing of emails configureTika = true; # Enable processing of emails
settings = { settings = {
PAPERLESS_OCR_USER_ARGS = builtins.toJSON { invalidate_digital_signatures = true; }; PAPERLESS_OCR_USER_ARGS = builtins.toJSON { invalidate_digital_signatures = true; };
PAPERLESS_URL = "https://${hostnames.paperless}"; PAPERLESS_URL = "https://${hostnames.paperless}";
PAPERLESS_DATE_ORDER = "MDY"; # Check document for US-formatted dates PAPERLESS_DATE_ORDER = "MDY"; # Check document for US-formatted dates
# OIDC Authentication with Pocket ID
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
PAPERLESS_REDIRECT_LOGIN_TO_SSO = true;
PAPERLESS_USE_X_FORWARD_HOST = true;
PAPERLESS_PROXY_SSL_HEADER = [
"HTTP_X_FORWARDED_PROTO"
"https"
];
PAPERLESS_TRUSTED_PROXIES = [
"127.0.0.1"
"::1"
];
# Enable if changing the path name in Caddy # Enable if changing the path name in Caddy
# PAPERLESS_FORCE_SCRIPT_NAME = "/paperless"; # PAPERLESS_FORCE_SCRIPT_NAME = "/paperless";
# PAPERLESS_STATIC_URL = "/paperless/static/"; # PAPERLESS_STATIC_URL = "/paperless/static/";
@@ -61,8 +80,60 @@ in
permissions = "0440"; permissions = "0440";
}; };
systemd.services.paperless-secret = { systemd.services.paperless-secret = {
requiredBy = [ "paperless.service" ]; requiredBy = [ "paperless-scheduler.service" ];
before = [ "paperless.service" ]; before = [ "paperless-scheduler.service" ];
};
secrets.paperless-oidc-secret = {
source = ./paperless-oidc-secret.age;
dest = "${config.secretsDirectory}/paperless-oidc-secret";
owner = "paperless";
group = "paperless";
permissions = "0440";
};
systemd.services.paperless-oidc-secret-secret = {
requiredBy = [
"paperless-secret-key.service"
"paperless-scheduler.service"
"paperless-task-queue.service"
"paperless-consumer.service"
"paperless-web.service"
];
before = [
"paperless-secret-key.service"
"paperless-scheduler.service"
"paperless-task-queue.service"
"paperless-consumer.service"
"paperless-web.service"
];
postStart = ''
SECRET="$(tr -d '\r\n' < '${config.secrets.paperless-oidc-secret.dest}')"
JSON=$(${pkgs.jq}/bin/jq -nc \
--arg secret "$SECRET" \
--arg auth "https://${hostnames.auth}" \
'{
openid_connect: {
APPS: [
{
provider_id: "pocket-id",
name: "Pocket ID",
client_id: "e6ff7fce-8e67-4c66-8f32-5ec3db4740a9",
secret: $secret,
settings: {
server_url: $auth,
token_auth_method: "client_secret_basic",
oauth_pkce_enabled: true,
email_authentication: true,
verified_email: true
}
}
]
}
}')
echo "PAPERLESS_SOCIALACCOUNT_PROVIDERS='$JSON'" > '${config.secretsDirectory}/paperless-env'
chown paperless:paperless '${config.secretsDirectory}/paperless-env'
chmod 0440 '${config.secretsDirectory}/paperless-env'
'';
}; };
# Fix paperless shared permissions # Fix paperless shared permissions
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyB3ajlu
V0RkeHB5WE8zWGhrYnFpaU9LQ2l0dHd6bWNydll6Y3F4ampaT0JnCi9QTS81aldn
NVhhSm00RWJDaHl0V0ludEJleW9HaitKNlZwQlFRcWdVZUUKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIG8waldTNzdJNmJKQjdPS2ZpV1ZpcTN3RGVjSHR5UmlyQzBsM3Va
SFdLM2sKYVhhczRxS1BNdElSN3ppa2ZPNjZhajFhVlVkNngydFlibHhTYVZFWS9C
cwotPiBzc2gtZWQyNTUxOSBuanZYNUEgeVBqak4yelZnUXBZbFMrR3AzU05hNldy
NDl4SkpTY1BBS2xHQ3J5TkZ3TQpYRGJKSlFzMFB5c3lWMGZKOTl1TWRURnk3U3hi
dkMrK0FDNVVKL3RzTXJBCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBEQXlSRFpNYVRN
bTdFejNXMEcvaGZkbDF1NnFjZFdXdHpFV2ZjMGhMWWtNCkpvTFJVUUF0eEl1U2N0
Z1hScXRXQndxU3kzL0pPYjNQL01XUzUwbTFwRUEKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IHpNVnUwWURYSHFHMUxVMmhIZkMrdHBEYVdEQTZ0R3plcU1Na21IQkZoSHMK
THBiRlg2NWhFcSs0SkUveTRMdXdxajl1emFoV0NvTmRSZ3JTZTltZFovQQotLS0g
aUg4aEIwWHhkZFFNVnQ5ZVJpWjlFQis3Tjk5NHBNMWVzTTYxdEdhSlJ6VQrk/zq+
MS3OPv0vbS4yDmLFFrJUdUPj0WuJ/yPp32Elz3+FW7QFDqNKChZnC3rTTVjc/MKL
IVlGYXpoYuwve4ABi40QAcN0Qs8XqeIt2A==
-----END AGE ENCRYPTED FILE-----
@@ -0,0 +1,58 @@
{ config, lib, ... }:
let
inherit (config.nmasur.settings) hostnames;
cfg = config.nmasur.presets.services.pocket-id;
in
{
options.nmasur.presets.services.pocket-id.enable = lib.mkEnableOption "Pocket ID OIDC provider";
config = lib.mkIf cfg.enable {
secrets.pocket-id = {
source = ./pocket-id.age;
dest = "${config.secretsDirectory}/pocket-id";
owner = "pocket-id";
group = "pocket-id";
permissions = "0440";
};
systemd.services.pocket-id-secret = {
requiredBy = [ "pocket-id.service" ];
before = [ "pocket-id.service" ];
};
services.pocket-id = {
enable = true;
settings = {
APP_URL = "https://${hostnames.auth}";
TRUST_PROXY = true;
PORT = 3034;
};
credentials = {
ENCRYPTION_KEY = config.secrets.pocket-id.dest;
};
};
# Allow web traffic to Caddy
nmasur.presets.services.caddy.routes = [
{
match = [ { host = [ hostnames.auth ]; } ];
handle = [
{
handler = "reverse_proxy";
upstreams = [
{ dial = "localhost:${builtins.toString config.services.pocket-id.settings.PORT}"; }
];
}
];
}
];
# Configure Cloudflare DNS to point to this machine
services.cloudflare-dyndns.domains = [ hostnames.auth ];
};
}
@@ -37,6 +37,18 @@ in
} }
]; ];
}; };
# Refresh collation version if glibc was upgraded across system releases
systemd.services.postgresql-setup = {
preStart = ''
while ! ${config.services.postgresql.package}/bin/psql -d postgres -tAc 'SELECT 1' >/dev/null 2>&1; do
if ! systemctl is-active --quiet postgresql.service; then break; fi
sleep 0.1
done
${config.services.postgresql.package}/bin/psql -d postgres -tAc 'ALTER DATABASE template1 REFRESH COLLATION VERSION;' || true
${config.services.postgresql.package}/bin/psql -d postgres -tAc 'ALTER DATABASE postgres REFRESH COLLATION VERSION;' || true
'';
};
}; };
} }
@@ -29,7 +29,7 @@ in
}; };
}; };
programs.fish.enable = lib.mkDefault config.home-manager.users.${username}.programs.fish.enable; programs.fish.enable = lib.mkDefault (config.home-manager.users.${username}.programs.fish.enable or true);
# Allows us to declaritively set password # Allows us to declaritively set password
users.mutableUsers = lib.mkDefault false; users.mutableUsers = lib.mkDefault false;
@@ -62,7 +62,7 @@ in
# serviceConfig.TimeoutStartSec = lib.mkForce "45m"; # serviceConfig.TimeoutStartSec = lib.mkForce "45m";
# }; # };
allowUnfreePackages = config.home-manager.users.${username}.allowUnfreePackages; allowUnfreePackages = config.home-manager.users.${username}.allowUnfreePackages or [];
wsl.enable = lib.mkDefault false; wsl.enable = lib.mkDefault false;
@@ -22,7 +22,7 @@ in
services = { services = {
actualbudget.enable = lib.mkDefault true; actualbudget.enable = lib.mkDefault true;
actualtap = { actualtap = {
enable = lib.mkDefault true; enable = lib.mkDefault false;
instances = { instances = {
budget1 = { budget1 = {
port = 3031; port = 3031;
@@ -38,11 +38,13 @@ in
}; };
}; };
}; };
api.enable = lib.mkDefault true;
caddy.enable = lib.mkDefault true; caddy.enable = lib.mkDefault true;
cloudflare.enable = lib.mkDefault true; cloudflare.enable = lib.mkDefault true;
cloudflared.enable = lib.mkDefault true; cloudflared.enable = lib.mkDefault true;
gitea.enable = lib.mkDefault true; gitea.enable = lib.mkDefault true;
grafana.enable = lib.mkDefault true; grafana.enable = lib.mkDefault true;
hister.enable = lib.mkDefault true;
influxdb2.enable = lib.mkDefault true; influxdb2.enable = lib.mkDefault true;
karakeep.enable = lib.mkDefault true; karakeep.enable = lib.mkDefault true;
litestream.enable = lib.mkDefault true; litestream.enable = lib.mkDefault true;
@@ -54,6 +56,7 @@ in
nix-autoupgrade.enable = lib.mkDefault true; # On by default for communications nix-autoupgrade.enable = lib.mkDefault true; # On by default for communications
ntfy-sh.enable = lib.mkDefault true; ntfy-sh.enable = lib.mkDefault true;
pgweb.enable = lib.mkDefault true; pgweb.enable = lib.mkDefault true;
pocket-id.enable = lib.mkDefault true;
postgresql.enable = lib.mkDefault true; postgresql.enable = lib.mkDefault true;
stalwart.enable = lib.mkDefault true; stalwart.enable = lib.mkDefault true;
thelounge.enable = lib.mkDefault true; thelounge.enable = lib.mkDefault true;