feat(vikunja): deploy Vikunja with Keycloak SSO and PostgreSQL #31

Open
martin wants to merge 3 commits from feat/vikunja-keycloak-sso into main
Owner

Summary

  • Deploy the complete Vikunja service on nidaros, including its service module, public URL, and reverse proxy on pi4.
  • Use a dedicated PostgreSQL database and role owned by that role.
  • Configure Keycloak OIDC authentication and disable local login.
  • Include the user-managed, SOPS-encrypted OIDC client-secret file.

Validation

  • nix fmt on the changed Nix files
  • nh os build . -H nidaros succeeded with PostgreSQL enabled for Vikunja
## Summary - Deploy the complete Vikunja service on `nidaros`, including its service module, public URL, and reverse proxy on `pi4`. - Use a dedicated PostgreSQL database and role owned by that role. - Configure Keycloak OIDC authentication and disable local login. - Include the user-managed, SOPS-encrypted OIDC client-secret file. ## Validation - `nix fmt` on the changed Nix files - `nh os build . -H nidaros` succeeded with PostgreSQL enabled for Vikunja
feat(vikunja): add Keycloak SSO service
Some checks failed
ci/woodpecker/pr/pr-review Pipeline was canceled
dfb69ef6d8
Co-authored-by: OpenCode <opencode@opencode.ai>
martin self-assigned this 2026-10-11 19:51:03 +00:00
feat(vikunja): use PostgreSQL database
Some checks failed
ci/woodpecker/pr/pr-review Pipeline was canceled
3730ed3a1e
Co-authored-by: OpenCode <opencode@opencode.ai>
martin changed title from feat(vikunja): deploy Vikunja with Keycloak SSO to feat(vikunja): deploy Vikunja with Keycloak SSO and PostgreSQL 2026-10-11 19:57:33 +00:00
chore(secrets): add Vikunja OIDC secret
All checks were successful
ci/woodpecker/pr/pr-review Pipeline was successful
d074ab12c5
Co-authored-by: OpenCode <opencode@openai.com>
Collaborator

{
"summary": "# Review Summary\n\nWell-integrated addition that follows the repo's conventions: vikunja.nix matches the AGENTS.md module template (cfg alias, mkEnableOption, lib.mkIf, camelCase, sentence descriptions), and the "Adding a New Self-Hosted Service" checklist is fully covered — registry entry, module import, host enable, SOPS secret, and the pi4 reverse proxy. Reachability checks out: Vikunja binds systemConfig.address.private, pi4's localAddress proxies to that same IP, port 8094 is unused anywhere else, and nidaros' firewall already accepts the whole 192.168.10.0/24 range, so no port opening is needed. Registering the domain in web-services.nix also auto-enrolls a blackbox health probe. The root-owned SOPS secret used as a systemd EnvironmentFile is correct for a DynamicUser service (systemd reads it as root; setting a service owner would break activation).\n\nBlocking: shared/secrets/secrets.yaml is re-indented from 2 to 4 spaces across the entire file. The repo has no yamlfmt config, so treefmt's yamlfmt defaults (indent 2) apply; as committed this file is no longer formatter-stable, so nix flake check will fail its formatting check, and the churn also buries the real change (one new key + re-encrypted MAC). Run nix fmt after the sops edit — re-indenting a sops file is safe (the MAC covers values, not whitespace).\n\nNotes:\n- The env file must provide exactly VIKUNJA_AUTH_OPENID_PROVIDERS_KEYCLOAK_CLIENTSECRET (the provider key is keycloak). Vikunja treats clientsecret as a required provider key, and with auth.local.enabled = false a wrong or missing name means no login method at all — worth a runtime smoke test, since the build can't catch it.\n- The PR description says the OIDC client-secret file is excluded/user-managed, but vikunja/oidc-environment is present in secrets.yaml (encrypted, correct recipients). Align the description.\n- Remaining out-of-band step: creating the Keycloak client vikunja with redirect URI https://tasks.martials.no/auth/openid/keycloak (and post-logout redirect URI).",
"verdict": "changes",
"comments": [
{
"file": "shared/secrets/secrets.yaml",
"line": 2,
"side": "new",
"severity": "style",
"message": "The whole file was re-indented from 2 to 4 spaces. The repo has no yamlfmt config, so treefmt applies yamlfmt's default indent: 2 — as committed this file will fail nix flake check's formatting check, and the churn hides the actual one-key addition. Please run nix fmt/just fmt after the sops edit; reformatting is safe for sops (the MAC covers values, not whitespace)."
},
{
"file": "shared/base/modules/services/vikunja.nix",
"line": 43,
"side": "new",
"severity": "bug",
"message": "service.secret is not set anywhere, so Vikunja generates a random one on every boot — upstream docs warn that all issued JWTs (and refresh flows, for clients that don't recover) become invalid after each restart. Since every nixos-rebuild switch restarts the service, users get their sessions invalidated on every deploy. Consider adding VIKUNJA_SERVICE_SECRET (stable 32-byte value) to the same user-managed vikunja/oidc-environment file so sessions survive restarts."
},
{
"file": "shared/base/modules/services/vikunja.nix",
"line": 61,
"side": "new",
"severity": "style",
"message": "This module configures ensureDatabases/ensureUsers but never sets services.postgresql.enable = true. It works on nidaros only because hosts/nidaros/postgres.nix (and keycloak's createLocally) enable PostgreSQL elsewhere. freshrss.nix enables it inside its own module — doing the same here keeps the module self-contained if it's ever enabled on another host."
},
{
"file": "shared/base/modules/services/vikunja.nix",
"line": 71,
"side": "new",
"severity": "bug",
"message": "The SOPS secret is read only via EnvironmentFile, so rotating the OIDC client secret deploys the new file but never reaches the running process — the old value stays in the service environment until the next unrelated restart. Add restartUnits = [ \"vikunja.service\" ] (sops-nix option) so secret updates take effect."
}
]
}

Objective

  • Produce a balanced code review of PR #31 (feat(vikunja): deploy Vikunja with Keycloak SSO and PostgreSQL) in martin/nixos-configuration, output as a single JSON object with summary, verdict, and comments fields only (no markdown fences, no extra text).

Important Details

  • Output schema: {summary, verdict ("approve"|"changes"|"comment"), comments:[{file, line, side, severity ("bug"|"security"|"performance"|"style"), message}]}; omit side for new-file lines; empty comments array if no findings.
  • Review style: balanced; must not repeat findings from existing PR context (PR description present, but "Review by clank-bot:" section empty — no prior findings).
  • PR summary: Vikunja on nidaros, dedicated PostgreSQL DB/role, Keycloak OIDC with local login disabled, reverse proxy on pi4, port 8094, domain tasks.martials.no.
  • PR description claims the SOPS OIDC client-secret file is "intentionally excluded and user-managed", but shared/secrets/secrets.yaml DOES include vikunja/oidc-environment (encrypted) — description mismatch noted.
  • Key verified facts:
    • treefmt.nix has yamlfmt.enable = true with no config → yamlfmt default indent: 2; new secrets.yaml uses 4-space indent → nix flake check formatting check will fail.
    • Repo root has no yamlfmt config file; no .gitignore exclusions for secrets.
    • Port 8094 unused elsewhere (8093 = woodpeckerMetrics, 8095 = recurring-events hardcoded in nginx).
    • hosts/nidaros/security/firewall.nix accepts all input from 192.168.10.0/24 (common.localIpRange), nftables enabled → port 8094 reachable from pi4 without opening it; systems.nix: pi4 = 192.168.10.188, nidaros = 192.168.10.228.
    • hosts/nidaros/postgres.nix: services.postgresql.enable = true, auth local all all trust → unix-socket DB connection works without password.
    • shared/base/modules/services/vikunja.nix does NOT set services.postgresql.enable = true (unlike freshrss.nix), relies on host config — portability nit.
    • sops.secrets."vikunja/oidc-environment" = { }; (line 71) lacks restartUnits = [ "vikunja.service" ] — secret rotation won't reach the running process (env file only read at start).
    • Root-owned SOPS secret for a systemd EnvironmentFile is correct for a DynamicUser service — NOT a bug (confirmed via research).
    • nixpkgs services.vikunja module (fetched from GitHub): options environmentFiles (list of path), frontendScheme, frontendHostname, address, port, settings, database.type enum sqlite/mysql/postgres, database.host/user/database, DynamicUser = true.
    • Vikunja setConfigFromEnv splits VIKUNJA_ keys on _ → env var must be exactly VIKUNJA_AUTH_OPENID_PROVIDERS_KEYCLOAK_CLIENTSECRET; provider key keycloak exists in config file (required for env-based provider values).
    • Vikunja treats clientsecret as required per provider (getProviderFromMap); with auth.local.enabled = false, missing secret = no login method at all — runtime smoke test needed.
    • service.secret not set → random per-boot secret → all issued JWTs invalid after every restart/switch.
    • Vikunja OIDC config matches official Keycloak example docs (authurl = realm URL for discovery, logouturl correct, redirect URI /auth/openid/keycloak).
    • AGENTS.md "Adding a New Self-Hosted Service" checklist fully satisfied (registry, module, import, secret, enable, pi4 proxy).
    • Environment: bash, grep, glob tools all broken (ChildProcess.spawn NotFound / ripgrep execution failed); read and webfetch/websearch work; a task (explore subagent) was used successfully.

Work State

Completed

  • Full review research completed (module conventions, firewall, ports, sops, yamlfmt, nixpkgs vikunja module, upstream Vikunja config/OIDC docs).
  • Final JSON review delivered with verdict: "changes" and 4 comments:
    1. shared/secrets/secrets.yaml:2 (style) — whole-file 2→4 space re-indent fails nix flake check fmt check; run nix fmt.
    2. shared/base/modules/services/vikunja.nix:43 (bug) — no service.secret → JWTs invalidated every restart; suggest VIKUNJA_SERVICE_SECRET in the env file.
    3. shared/base/modules/services/vikunja.nix:61 (style) — missing services.postgresql.enable = true (freshrss.nix sets it; only works on nidaros via postgres.nix).
    4. shared/base/modules/services/vikunja.nix:71 (bug) — SOPS secret lacks restartUnits = [ "vikunja.service" ]; rotations never reach the running process.
  • Summary notes included: exact env var name requirement, PR description/secret mismatch, out-of-band Keycloak client creation, root-owned env file correctness.

Active

  • (none) — review output already produced.

Blocked

  • (none)

Next Move

  1. (none) — the JSON review response is the terminal deliverable and has been emitted.
  2. If re-invoked, re-emit the same JSON (or regenerate if PR context changed).

Relevant Files

  • /woodpecker/src/code.martials.no/martin/nixos-configuration/.tmpnToj7n/pr.diff: the PR diff under review (6 files).
  • shared/base/modules/services/vikunja.nix: new module; lines 43 (environmentFiles), 61 (services.postgresql), 71 (sops.secrets declaration) — comment anchors.
  • shared/secrets/secrets.yaml: whole-file re-indentation; comment anchor line 2.
  • treefmt.nix: confirms yamlfmt enabled with no indent config → default 2.
  • hosts/nidaros/security/firewall.nix: LAN range accept rule → port reachability verified.
  • hosts/nidaros/postgres.nix: PostgreSQL enabled on nidaros with local ... trust.
  • web-services.nix: vikunja entry (domain tasks.martials.no, port 8094).
  • hosts/pi4/nginx.nix: mkWsReverseProxy webServices.vikunja → tasks.${domain}.
  • shared/base/modules/services/default.nix: import list (alphabetical, ./vikunja.nix added).
  • shared/base/modules/services/freshrss.nix: reference pattern (sets postgresql.enable = true).
  • .sops.yaml: creation rule for shared/secrets/secrets.yaml with 4 age recipients.
    Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.
    The PR #31 review is complete — final JSON (verdict: changes, 4 comments) was already delivered. Nothing more to do. Let me know if you want it re-run or a comment posted to Forgejo.

*🤖 Review by opencode (opencode/mimo-v2.6-flash-free) — verdict: comment

{ "summary": "# Review Summary\n\nWell-integrated addition that follows the repo's conventions: `vikunja.nix` matches the AGENTS.md module template (`cfg` alias, `mkEnableOption`, `lib.mkIf`, camelCase, sentence descriptions), and the \"Adding a New Self-Hosted Service\" checklist is fully covered — registry entry, module import, host enable, SOPS secret, and the pi4 reverse proxy. Reachability checks out: Vikunja binds `systemConfig.address.private`, pi4's `localAddress` proxies to that same IP, port 8094 is unused anywhere else, and nidaros' firewall already accepts the whole `192.168.10.0/24` range, so no port opening is needed. Registering the domain in `web-services.nix` also auto-enrolls a blackbox health probe. The root-owned SOPS secret used as a systemd `EnvironmentFile` is correct for a `DynamicUser` service (systemd reads it as root; setting a service owner would break activation).\n\n**Blocking:** `shared/secrets/secrets.yaml` is re-indented from 2 to 4 spaces across the entire file. The repo has no yamlfmt config, so treefmt's yamlfmt defaults (indent 2) apply; as committed this file is no longer formatter-stable, so `nix flake check` will fail its formatting check, and the churn also buries the real change (one new key + re-encrypted MAC). Run `nix fmt` after the sops edit — re-indenting a sops file is safe (the MAC covers values, not whitespace).\n\n**Notes:**\n- The env file must provide exactly `VIKUNJA_AUTH_OPENID_PROVIDERS_KEYCLOAK_CLIENTSECRET` (the provider key is `keycloak`). Vikunja treats `clientsecret` as a *required* provider key, and with `auth.local.enabled = false` a wrong or missing name means no login method at all — worth a runtime smoke test, since the build can't catch it.\n- The PR description says the OIDC client-secret file is excluded/user-managed, but `vikunja/oidc-environment` is present in `secrets.yaml` (encrypted, correct recipients). Align the description.\n- Remaining out-of-band step: creating the Keycloak client `vikunja` with redirect URI `https://tasks.martials.no/auth/openid/keycloak` (and post-logout redirect URI).", "verdict": "changes", "comments": [ { "file": "shared/secrets/secrets.yaml", "line": 2, "side": "new", "severity": "style", "message": "The whole file was re-indented from 2 to 4 spaces. The repo has no yamlfmt config, so treefmt applies yamlfmt's default `indent: 2` — as committed this file will fail `nix flake check`'s formatting check, and the churn hides the actual one-key addition. Please run `nix fmt`/`just fmt` after the sops edit; reformatting is safe for sops (the MAC covers values, not whitespace)." }, { "file": "shared/base/modules/services/vikunja.nix", "line": 43, "side": "new", "severity": "bug", "message": "`service.secret` is not set anywhere, so Vikunja generates a random one on every boot — upstream docs warn that all issued JWTs (and refresh flows, for clients that don't recover) become invalid after each restart. Since every `nixos-rebuild switch` restarts the service, users get their sessions invalidated on every deploy. Consider adding `VIKUNJA_SERVICE_SECRET` (stable 32-byte value) to the same user-managed `vikunja/oidc-environment` file so sessions survive restarts." }, { "file": "shared/base/modules/services/vikunja.nix", "line": 61, "side": "new", "severity": "style", "message": "This module configures `ensureDatabases`/`ensureUsers` but never sets `services.postgresql.enable = true`. It works on nidaros only because `hosts/nidaros/postgres.nix` (and keycloak's `createLocally`) enable PostgreSQL elsewhere. `freshrss.nix` enables it inside its own module — doing the same here keeps the module self-contained if it's ever enabled on another host." }, { "file": "shared/base/modules/services/vikunja.nix", "line": 71, "side": "new", "severity": "bug", "message": "The SOPS secret is read only via `EnvironmentFile`, so rotating the OIDC client secret deploys the new file but never reaches the running process — the old value stays in the service environment until the next unrelated restart. Add `restartUnits = [ \"vikunja.service\" ]` (sops-nix option) so secret updates take effect." } ] } ## Objective - Produce a balanced code review of PR #31 (`feat(vikunja): deploy Vikunja with Keycloak SSO and PostgreSQL`) in `martin/nixos-configuration`, output as a single JSON object with `summary`, `verdict`, and `comments` fields only (no markdown fences, no extra text). ## Important Details - Output schema: `{summary, verdict ("approve"|"changes"|"comment"), comments:[{file, line, side, severity ("bug"|"security"|"performance"|"style"), message}]}`; omit `side` for new-file lines; empty `comments` array if no findings. - Review style: balanced; must not repeat findings from existing PR context (PR description present, but "Review by clank-bot:" section empty — no prior findings). - PR summary: Vikunja on `nidaros`, dedicated PostgreSQL DB/role, Keycloak OIDC with local login disabled, reverse proxy on `pi4`, port `8094`, domain `tasks.martials.no`. - PR description claims the SOPS OIDC client-secret file is "intentionally excluded and user-managed", but `shared/secrets/secrets.yaml` DOES include `vikunja/oidc-environment` (encrypted) — description mismatch noted. - Key verified facts: - `treefmt.nix` has `yamlfmt.enable = true` with no config → yamlfmt default `indent: 2`; new `secrets.yaml` uses 4-space indent → `nix flake check` formatting check will fail. - Repo root has no yamlfmt config file; no `.gitignore` exclusions for secrets. - Port `8094` unused elsewhere (8093 = woodpeckerMetrics, 8095 = recurring-events hardcoded in nginx). - `hosts/nidaros/security/firewall.nix` accepts all input from `192.168.10.0/24` (`common.localIpRange`), nftables enabled → port 8094 reachable from pi4 without opening it; `systems.nix`: pi4 = `192.168.10.188`, nidaros = `192.168.10.228`. - `hosts/nidaros/postgres.nix`: `services.postgresql.enable = true`, auth `local all all trust` → unix-socket DB connection works without password. - `shared/base/modules/services/vikunja.nix` does NOT set `services.postgresql.enable = true` (unlike `freshrss.nix`), relies on host config — portability nit. - `sops.secrets."vikunja/oidc-environment" = { };` (line 71) lacks `restartUnits = [ "vikunja.service" ]` — secret rotation won't reach the running process (env file only read at start). - Root-owned SOPS secret for a systemd `EnvironmentFile` is correct for a `DynamicUser` service — NOT a bug (confirmed via research). - nixpkgs `services.vikunja` module (fetched from GitHub): options `environmentFiles` (list of path), `frontendScheme`, `frontendHostname`, `address`, `port`, `settings`, `database.type` enum sqlite/mysql/postgres, `database.host/user/database`, `DynamicUser = true`. - Vikunja `setConfigFromEnv` splits `VIKUNJA_` keys on `_` → env var must be exactly `VIKUNJA_AUTH_OPENID_PROVIDERS_KEYCLOAK_CLIENTSECRET`; provider key `keycloak` exists in config file (required for env-based provider values). - Vikunja treats `clientsecret` as required per provider (`getProviderFromMap`); with `auth.local.enabled = false`, missing secret = no login method at all — runtime smoke test needed. - `service.secret` not set → random per-boot secret → all issued JWTs invalid after every restart/switch. - Vikunja OIDC config matches official Keycloak example docs (authurl = realm URL for discovery, logouturl correct, redirect URI `/auth/openid/keycloak`). - AGENTS.md "Adding a New Self-Hosted Service" checklist fully satisfied (registry, module, import, secret, enable, pi4 proxy). - Environment: `bash`, `grep`, `glob` tools all broken (`ChildProcess.spawn` NotFound / `ripgrep execution failed`); `read` and `webfetch`/`websearch` work; a `task` (explore subagent) was used successfully. ## Work State ### Completed - Full review research completed (module conventions, firewall, ports, sops, yamlfmt, nixpkgs vikunja module, upstream Vikunja config/OIDC docs). - Final JSON review delivered with `verdict: "changes"` and 4 comments: 1. `shared/secrets/secrets.yaml:2` (style) — whole-file 2→4 space re-indent fails `nix flake check` fmt check; run `nix fmt`. 2. `shared/base/modules/services/vikunja.nix:43` (bug) — no `service.secret` → JWTs invalidated every restart; suggest `VIKUNJA_SERVICE_SECRET` in the env file. 3. `shared/base/modules/services/vikunja.nix:61` (style) — missing `services.postgresql.enable = true` (freshrss.nix sets it; only works on nidaros via `postgres.nix`). 4. `shared/base/modules/services/vikunja.nix:71` (bug) — SOPS secret lacks `restartUnits = [ "vikunja.service" ]`; rotations never reach the running process. - Summary notes included: exact env var name requirement, PR description/secret mismatch, out-of-band Keycloak client creation, root-owned env file correctness. ### Active - (none) — review output already produced. ### Blocked - (none) ## Next Move 1. (none) — the JSON review response is the terminal deliverable and has been emitted. 2. If re-invoked, re-emit the same JSON (or regenerate if PR context changed). ## Relevant Files - `/woodpecker/src/code.martials.no/martin/nixos-configuration/.tmpnToj7n/pr.diff`: the PR diff under review (6 files). - `shared/base/modules/services/vikunja.nix`: new module; lines 43 (`environmentFiles`), 61 (`services.postgresql`), 71 (`sops.secrets` declaration) — comment anchors. - `shared/secrets/secrets.yaml`: whole-file re-indentation; comment anchor line 2. - `treefmt.nix`: confirms yamlfmt enabled with no indent config → default 2. - `hosts/nidaros/security/firewall.nix`: LAN range accept rule → port reachability verified. - `hosts/nidaros/postgres.nix`: PostgreSQL enabled on nidaros with `local ... trust`. - `web-services.nix`: vikunja entry (domain `tasks.martials.no`, port 8094). - `hosts/pi4/nginx.nix`: `mkWsReverseProxy webServices.vikunja` → `tasks.${domain}`. - `shared/base/modules/services/default.nix`: import list (alphabetical, `./vikunja.nix` added). - `shared/base/modules/services/freshrss.nix`: reference pattern (sets `postgresql.enable = true`). - `.sops.yaml`: creation rule for `shared/secrets/secrets.yaml` with 4 age recipients. Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed. The PR #31 review is complete — final JSON (verdict: `changes`, 4 comments) was already delivered. Nothing more to do. Let me know if you want it re-run or a comment posted to Forgejo. --- *🤖 Review by opencode (opencode/mimo-v2.6-flash-free) — verdict: **comment** <!-- forgejo-opencode-review -->
All checks were successful
ci/woodpecker/pr/pr-review Pipeline was successful
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/vikunja-keycloak-sso:feat/vikunja-keycloak-sso
git switch feat/vikunja-keycloak-sso

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff feat/vikunja-keycloak-sso
git switch feat/vikunja-keycloak-sso
git rebase main
git switch main
git merge --ff-only feat/vikunja-keycloak-sso
git switch feat/vikunja-keycloak-sso
git rebase main
git switch main
git merge --no-ff feat/vikunja-keycloak-sso
git switch main
git merge --squash feat/vikunja-keycloak-sso
git switch main
git merge --ff-only feat/vikunja-keycloak-sso
git switch main
git merge feat/vikunja-keycloak-sso
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
martin/nixos-configuration!31
No description provided.