My nix-darwin and NixOS config
3
fork

Configure Feed

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

sharkey: rewrite module to use scrub-secrets + sharkey-precise wrappers

Add sharkey-precise.nix (TypeORM/node wrapper derivation) and rewrite
sharkey.nix to disable the upstream nixpkgs module and replace it with
a self-contained implementation using the scrub-secrets credential
extraction pattern. Secrets expressed as `{ file = ...; }` in settings
are stripped from the generated YAML and injected via systemd
LoadCredential as MK_CONFIG_*_FILE env vars. Drop sharkey.env dotenv
secret — database.createLocally uses peer auth over the PostgreSQL unix
socket, no password needed. Meilisearch master key now flows through the
credential path via settings.meilisearch.apiKey. Add AGPLv3 compliance
assertion from upstream module.

+532 -104
+120
modules/server/sharkey-precise.nix
··· 1 + { 2 + lib, 3 + runCommandLocal, 4 + jq, 5 + makeWrapper, 6 + sharkey, 7 + }: 8 + let 9 + inherit (sharkey) nodejs; 10 + in 11 + runCommandLocal "sharkey-precise-${sharkey.version}" 12 + { 13 + 14 + nativeBuildInputs = [ 15 + jq 16 + makeWrapper 17 + ]; 18 + 19 + outputs = [ 20 + "out" 21 + "typeorm" 22 + ]; 23 + 24 + # This package serves to replace the following scripts. 25 + # As such, it is important that the package.json files contain the expected scripts. 26 + # If they change, this package must be updated. 27 + 28 + ensure_toplevel = builtins.toJSON { 29 + "scripts" = { 30 + "check:connect" = "pnpm --filter backend check:connect"; 31 + "migrate" = "pnpm --filter backend migrate"; 32 + "revert" = "pnpm --filter backend revert"; 33 + "migrateandstart" = builtins.concatStringsSep " && " [ 34 + "pnpm migrate" 35 + "pnpm start" 36 + ]; 37 + "start" = builtins.concatStringsSep " && " [ 38 + "pnpm check:connect" 39 + "cd packages/backend" 40 + "MK_WARNED_ABOUT_CONFIG=true node ./built/boot/entry.js" 41 + ]; 42 + }; 43 + }; 44 + ensure_backend = builtins.toJSON { 45 + "scripts" = { 46 + "migrate" = "pnpm typeorm migration:run -d ormconfig.js"; 47 + "revert" = "pnpm typeorm migration:revert -d ormconfig.js"; 48 + "check:connect" = "node ./scripts/check_connect.js"; 49 + }; 50 + }; 51 + 52 + ensure_typeorm = builtins.toJSON { 53 + "bin" = { 54 + "typeorm" = "./cli.js"; 55 + }; 56 + }; 57 + 58 + jqCheck = '' 59 + [ 60 + 61 + $expected | path(.. | scalars) as $path | 62 + 63 + if ($actual | getpath($path)) == ($expected | getpath($path)) 64 + then 65 + empty 66 + else 67 + { 68 + path: $path, 69 + actual: $actual | getpath($path), 70 + expected: $expected | getpath($path), 71 + } 72 + end 73 + 74 + ] | select (length > 0) | 75 + 76 + " 77 + 78 + package assertions failed in \($filename):\n\(map(" 79 + at \(.path | map(".[\(tojson)]") | join("")): 80 + expected: \(.expected | tojson), 81 + found: \(.actual | tojson) 82 + ") | join("")) 83 + 84 + 85 + " | halt_error 86 + ''; 87 + 88 + sharkey = "${sharkey}/Sharkey"; 89 + 90 + node = lib.getExe nodejs; 91 + } 92 + '' 93 + verify() { 94 + jq -n "$jqCheck" --argjson expected "$1" --arg filename "$2" --argjson actual "$(cat "$2")" 95 + } 96 + 97 + verify "$ensure_toplevel" "$sharkey/package.json" 98 + verify "$ensure_backend" "$sharkey/packages/backend/package.json" 99 + verify "$ensure_typeorm" "$sharkey/packages/backend/node_modules/typeorm/package.json" 100 + 101 + mkdir -p $out/bin 102 + mkdir -p $typeorm/bin 103 + 104 + makeWrapper $node $typeorm/bin/typeorm \ 105 + --add-flags "$sharkey/packages/backend/node_modules/typeorm/cli.js" 106 + 107 + makeWrapper $typeorm/bin/typeorm $out/bin/sharkey-migrate \ 108 + --set-default NODE_ENV production \ 109 + --chdir $sharkey/packages/backend \ 110 + --add-flags "migration:run -d $sharkey/packages/backend/ormconfig.js" 111 + 112 + makeWrapper $typeorm/bin/typeorm $out/bin/sharkey-revert \ 113 + --set-default NODE_ENV production \ 114 + --chdir $sharkey/packages/backend \ 115 + --add-flags "migration:revert -d $sharkey/packages/backend/ormconfig.js" 116 + 117 + makeWrapper $node $out/bin/sharkey-start \ 118 + --set-default NODE_ENV production \ 119 + --add-flags "$sharkey/packages/backend/built/boot/entry.js" 120 + ''
+412 -104
modules/server/sharkey.nix
··· 12 12 # WebFinger redirect at ewancroft.uk (Vercel) still points to 13 13 # ap.ewancroft.uk, so handles resolve as @ewan@ewancroft.uk unchanged. 14 14 # 15 - # PostgreSQL + Redis + Meilisearch: 16 - # services.sharkey.setupPostgresql, setupRedis, and setupMeilisearch are all 17 - # enabled. setupMeilisearch starts a local Meilisearch instance on port 7700 18 - # and wires it into Sharkey's config automatically via the NixOS module. 15 + # Service startup: 16 + # sharkey-migrate.service runs TypeORM migrations first, then 17 + # sharkey.service starts the main process via sharkey-precise.nix wrappers. 19 18 # 20 - # Secrets (secrets/sharkey.env — sops dotenv): 21 - # MK_CONFIG_DB_PASS=... PostgreSQL password for the sharkey user 19 + # Secrets (via sops-nix): 20 + # secrets/meilisearch-master-key — raw binary key, owner meilisearch. 21 + # No DB password needed: database.createLocally uses peer auth over 22 + # the PostgreSQL unix socket at /run/postgresql. 22 23 # 23 - # SMTP is configured via Admin → Settings → Email in the Sharkey web UI, 24 - # NOT via the YAML config. There is no smtp key in the config schema. 24 + # Credentials in settings: 25 + # Any settings value of the form `{ file = /path/to/secret; }` is 26 + # extracted by scrub-secrets, injected via systemd LoadCredential, and 27 + # exposed to the process as MK_CONFIG_<UPPER_DOTTED_PATH>_FILE=<path>. 25 28 # 26 29 # First-run: 27 30 # Open https://ap.ewancroft.uk — Sharkey prompts for initial setup. 28 - # Then run the migration script to inject the old GTS RSA keypair. 31 + # 32 + # Upstream nixpkgs sharkey module is disabled in favour of this one, 33 + # which uses sharkey-precise.nix wrappers rather than the pnpm run shim. 29 34 ############################################################################## 30 35 { 36 + options, 31 37 config, 32 38 lib, 39 + pkgs, 40 + modulesPath, 33 41 ... 34 42 }: 35 43 let 36 - cfg = config.myConfig; 37 - sk = cfg.sharkey; 44 + myCfg = config.myConfig; 45 + sk = myCfg.sharkey; 38 46 skPort = toString sk.port; 39 47 caddyPort = toString sk.caddyPort; 40 - in 41 - lib.mkIf cfg.services.sharkey.enable { 42 48 43 - # Declare the sharkey user/group statically so sops-nix can resolve the 44 - # owner at activation time. The nixpkgs sharkey module creates this user 45 - # during service activation, which is too late for sops secret ownership. 46 - users.users.sharkey = { 47 - isSystemUser = true; 48 - group = "sharkey"; 49 - }; 50 - users.groups.sharkey = { }; 49 + skcfg = config.services.sharkey; 51 50 52 - # Meilisearch master key — file must contain the raw key value only (no KEY= prefix). 53 - # Generate and encrypt: 54 - # openssl rand -base64 32 > secrets/meilisearch-master-key 55 - # SOPS_AGE_KEY_FILE=~/.config/age/keys.txt sops --encrypt --in-place --input-type binary --output-type binary secrets/meilisearch-master-key 56 - sops.secrets."meilisearch-master-key" = { 57 - sopsFile = ../../secrets/meilisearch-master-key; 58 - format = "binary"; 59 - owner = "meilisearch"; 60 - group = "meilisearch"; 61 - mode = "0400"; 51 + sharkey-precise = pkgs.callPackage ./sharkey-precise.nix { 52 + sharkey = skcfg.package; 62 53 }; 63 54 64 - # Declare meilisearch user/group statically so sops-nix can resolve the 65 - # owner at activation time (same pattern as sharkey above). 66 - users.users.meilisearch = { 67 - isSystemUser = true; 68 - group = "meilisearch"; 69 - }; 70 - users.groups.meilisearch = { }; 55 + sharkey-migrate = lib.getExe' sharkey-precise "sharkey-migrate"; 56 + sharkey-start = lib.getExe' sharkey-precise "sharkey-start"; 71 57 72 - services.meilisearch = { 73 - masterKeyFile = config.sops.secrets."meilisearch-master-key".path; 74 - listenAddress = "127.0.0.1"; 75 - settings = { 76 - env = "production"; 77 - no_analytics = true; 78 - }; 79 - }; 58 + settingsFormat = pkgs.formats.yaml { }; 80 59 81 - sops.secrets."sharkey.env" = { 82 - sopsFile = ../../secrets/sharkey.env; 83 - format = "dotenv"; 84 - owner = "sharkey"; 85 - group = "sharkey"; 86 - mode = "0400"; 87 - }; 60 + # Recursively walks the settings attrset. Any leaf of the form 61 + # `{ file = /path/to/secret; }` is treated as a secret: stripped from 62 + # the generated YAML and instead injected via systemd LoadCredential, 63 + # surfaced to the process as MK_CONFIG_<UPPER_PATH>_FILE=<cred-path>. 64 + scrub-secrets = 65 + loc: this: 66 + ( 67 + if builtins.typeOf this == "set" then 68 + if builtins.attrNames this == [ "file" ] then 69 + { 70 + leaf = "secret"; 71 + value = ""; 72 + secret = 73 + if lib.types.path.check this.file then 74 + this.file 75 + else 76 + throw '' 77 + The value for the option `${ 78 + lib.showOption (loc ++ [ "file" ]) 79 + }` is not of type `${lib.types.path.description}`. Value: ${ 80 + lib.generators.toPretty { } ( 81 + lib.generators.withRecursion { 82 + depthLimit = 10; 83 + throwOnDepthLimit = false; 84 + } this.file 85 + ) 86 + } 87 + ''; 88 + } 89 + else 90 + let 91 + scrubbed = builtins.mapAttrs (k: scrub-secrets (loc ++ [ k ])) this; 92 + except-leaves = kind: lib.filterAttrs (_: v: v.leaf or null != kind); 93 + except-empty = lib.filterAttrs (_: v: v != { } && v != [ ]); 94 + in 95 + { 96 + value = lib.pipe scrubbed [ 97 + (except-leaves "secret") 98 + (builtins.mapAttrs (_: v: v.value)) 99 + ]; 100 + secret = lib.pipe scrubbed [ 101 + (except-leaves "value") 102 + (builtins.mapAttrs (_: v: v.secret)) 103 + except-empty 104 + ]; 105 + } 88 106 89 - services.sharkey = { 90 - enable = true; 91 - environmentFiles = [ config.sops.secrets."sharkey.env".path ]; 107 + else if lib.typeOf this == "list" then 108 + let 109 + scrubbed = lib.imap0 (i: v: scrub-secrets (loc ++ [ "[index ${toString i}]" ]) v) this; 110 + in 111 + { 112 + value = builtins.map (builtins.getAttr "value") scrubbed; 113 + secret = builtins.map (builtins.getAttr "secret") scrubbed; 114 + } 115 + else 116 + { 117 + leaf = "value"; 118 + value = this; 119 + secret = { }; 120 + } 121 + ); 92 122 93 - # Automatically provision a local PostgreSQL database and Redis instance. 94 - setupPostgresql = true; 95 - setupRedis = true; 123 + scrubbed-settings = scrub-secrets (options.services.sharkey.settings.loc) skcfg.settings; 124 + 125 + configFile = settingsFormat.generate "sharkey-config.yml" scrubbed-settings.value; 126 + 127 + make-credentials = 128 + loc: this: 129 + if builtins.typeOf this == "set" then 130 + lib.mapAttrsToList (name: make-credentials (loc ++ [ (lib.strings.toUpper name) ])) this 131 + else if builtins.typeOf this == "list" then 132 + lib.imap0 (i: make-credentials (loc ++ [ (toString i) ])) this 133 + else 134 + { 135 + name = "MK_CONFIG_${builtins.concatStringsSep "_" loc}_FILE"; 136 + value = lib.mkDefinition { 137 + file = lib.unknownModule; 138 + value = this; 139 + }; 140 + }; 141 + 142 + extracted-credentials = builtins.listToAttrs ( 143 + lib.flatten (make-credentials [ ] scrubbed-settings.secret) 144 + ); 96 145 97 - # Meilisearch full-text search — local instance managed by the NixOS module. 98 - # Starts services.meilisearch on localhost:7700 and wires the API key automatically. 99 - setupMeilisearch = true; 146 + in 147 + { 148 + # Replace the upstream nixpkgs sharkey module with this one. 149 + disabledModules = [ "${modulesPath}/services/web-apps/sharkey.nix" ]; 100 150 101 - openFirewall = false; 151 + # ── Option declarations ──────────────────────────────────────────────────── 152 + options.services.sharkey = { 153 + enable = lib.mkEnableOption "sharkey"; 102 154 103 - settings = { 104 - # Public-facing URL — do NOT change after initial setup. 105 - url = "https://${sk.hostname}/"; 155 + package = lib.mkOption { 156 + type = lib.types.package; 157 + default = pkgs.sharkey; 158 + defaultText = lib.literalExpression "pkgs.sharkey"; 159 + description = "Sharkey package to use."; 160 + }; 106 161 107 - port = sk.port; 108 - address = "127.0.0.1"; 162 + database.createLocally = lib.mkOption { 163 + type = lib.types.bool; 164 + default = false; 165 + description = '' 166 + Create the PostgreSQL database locally and configure Sharkey to use it. 167 + Uses peer authentication over the unix socket — no password required. 168 + ''; 169 + }; 109 170 110 - # ID generation algorithm — do NOT change after initial setup. 111 - # Changing this after data exists will corrupt existing record IDs. 112 - id = "aidx"; 171 + redis.createLocally = lib.mkOption { 172 + type = lib.types.bool; 173 + default = false; 174 + description = "Create the Redis server locally and configure Sharkey to use it."; 175 + }; 113 176 114 - # Full-text search via Meilisearch — local instance on localhost:7700. 115 - # setupMeilisearch = true above provisions the service and API key automatically. 116 - fulltextSearch.provider = "meilisearch"; 177 + meilisearch.createLocally = lib.mkOption { 178 + type = lib.types.bool; 179 + default = false; 180 + description = "Create a local Meilisearch instance and wire it into Sharkey's config."; 181 + }; 117 182 118 - # Media storage on /srv — survives rebuilds, same disk as other services. 119 - mediaDirectory = sk.mediaDir; 183 + settings = lib.mkOption { 184 + type = settingsFormat.type; 185 + default = { }; 186 + description = '' 187 + Configuration for Sharkey, see 188 + <link xlink:href="https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/.config/example.yml"/> 189 + for supported settings. 120 190 121 - # NOTE: SMTP / email is NOT configured here. 122 - # Sharkey stores email server settings in the database, configured via 123 - # Admin → Settings → Email in the web UI. There is no smtp key in the 124 - # Sharkey YAML config schema. 191 + Values of the form `{ file = /path/to/secret; }` are treated as credentials: 192 + stripped from the generated YAML and injected via systemd LoadCredential. 193 + ''; 125 194 }; 126 - }; 127 195 128 - # ── Media directory on /srv ─────────────────────────────────────────────── 129 - # Must exist before Sharkey starts — nixpkgs bind-mounts mediaDirectory into 130 - # the service namespace and fails with NAMESPACE (226) if the path is absent. 131 - systemd.tmpfiles.rules = [ 132 - "d ${sk.mediaDir} 0750 sharkey sharkey -" 133 - ]; 196 + scrubbed-settings = lib.mkOption { 197 + default = scrubbed-settings; 198 + description = "Internal: settings with secrets extracted (not for direct use)."; 199 + }; 134 200 135 - # ── Systemd service tweaks ──────────────────────────────────────────────── 136 - systemd.services.sharkey = { 137 - after = [ "srv.mount" ]; 138 - wants = [ "srv.mount" ]; 139 - serviceConfig = { 140 - Restart = lib.mkForce "always"; 141 - RestartSec = cfg.server.servicePolicy.restartSec; 201 + credentials = lib.mkOption { 202 + type = lib.types.attrsOf lib.types.path; 203 + default = { }; 204 + description = '' 205 + Credentials injected via systemd LoadCredential. 206 + Keys must be of the form MK_CONFIG_*_FILE. 207 + Populated automatically from any `{ file = ...; }` values in settings. 208 + ''; 142 209 }; 143 210 }; 144 211 145 - # ── Caddy vhost — same pattern as every other CF-tunnel service ─────────── 146 - services.caddy.virtualHosts."http://${sk.hostname}:${caddyPort}" = { 147 - extraConfig = '' 148 - handle { 149 - reverse_proxy http://127.0.0.1:${skPort} { 150 - # Cloudflare tunnel passes CF-Connecting-IP with the real client IP. 151 - header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} 152 - header_up X-Real-IP {http.request.header.CF-Connecting-IP} 212 + # ── Configuration ────────────────────────────────────────────────────────── 213 + config = lib.mkIf myCfg.services.sharkey.enable ( 214 + lib.mkMerge [ 215 + 216 + # ── AGPLv3 compliance assertion ────────────────────────────────────────── 217 + { 218 + assertions = [ 219 + ( 220 + let 221 + package-is-very-likely-unmodified = 222 + skcfg.package.src.gitRepoUrl or null == "https://activitypub.software/TransFem-org/Sharkey.git" 223 + && skcfg.package.patches or [ ] == [ ]; 224 + 225 + has-git-repo-url = skcfg.package.src.gitRepoUrl or null != null; 226 + has-patches = skcfg.package.patches or [ ] != [ ]; 227 + 228 + user-probably-knows-what-they're-doing = 229 + skcfg.settings ? publishTarballInsteadOfProvideRepositoryUrl; 230 + in 231 + { 232 + assertion = package-is-very-likely-unmodified || user-probably-knows-what-they're-doing; 233 + message = '' 234 + The Sharkey setting `publishTarballInsteadOfProvideRepositoryUrl` must be explicitly set. 235 + Please read its documentation to avoid violating the AGPLv3 license that Sharkey is distributed under. 236 + https://activitypub.software/TransFem-org/Sharkey/-/blob/05a499ac55f13d654453eb3419ddae2c8eab1a34/.config/example.yml#L5-60 237 + '' 238 + + lib.optionalString (has-git-repo-url && !has-patches) '' 239 + note: you probably need to ensure the repository in the settings is ${skcfg.package.src.gitRepoUrl} 240 + ''; 241 + } 242 + ) 243 + ]; 244 + } 245 + 246 + # ── Core service setup ──────────────────────────────────────────────────── 247 + { 248 + services.sharkey.enable = true; 249 + 250 + users.users.sharkey = { 251 + group = "sharkey"; 252 + isSystemUser = true; 253 + home = "/run/sharkey"; 254 + packages = [ skcfg.package ]; 255 + }; 256 + users.groups.sharkey = { }; 257 + 258 + services.sharkey.settings.mediaDirectory = lib.mkForce sk.mediaDir; 259 + 260 + systemd.services.sharkey-migrate = { 261 + environment.MISSKEY_CONFIG_YML = "${configFile}"; 262 + serviceConfig = { 263 + Type = "oneshot"; 264 + User = "sharkey"; 265 + ExecStart = sharkey-migrate; 266 + StandardOutput = "journal"; 267 + StandardError = "journal"; 268 + SyslogIdentifier = "sharkey-migrate"; 269 + }; 270 + }; 271 + 272 + systemd.services.sharkey = { 273 + requires = [ "sharkey-migrate.service" ]; 274 + after = [ 275 + "sharkey-migrate.service" 276 + "srv.mount" 277 + ]; 278 + wants = [ "srv.mount" ]; 279 + 280 + environment.MISSKEY_CONFIG_YML = "${configFile}"; 281 + serviceConfig = { 282 + Type = "simple"; 283 + User = "sharkey"; 284 + StateDirectory = "sharkey"; 285 + StateDirectoryMode = "0700"; 286 + RuntimeDirectory = "sharkey"; 287 + RuntimeDirectoryMode = "0700"; 288 + ExecStart = sharkey-start; 289 + TimeoutSec = 60; 290 + Restart = lib.mkForce "always"; 291 + RestartSec = myCfg.server.servicePolicy.restartSec; 292 + StandardOutput = "journal"; 293 + StandardError = "journal"; 294 + SyslogIdentifier = "sharkey"; 295 + }; 296 + }; 297 + } 298 + 299 + # ── Credentials assertion + auto-extraction ─────────────────────────────── 300 + { 301 + assertions = [ 302 + ( 303 + let 304 + badly-named-credentials = builtins.filter (env: builtins.match "^MK_CONFIG_.*_FILE$" env == null) ( 305 + builtins.attrNames skcfg.credentials 306 + ); 307 + in 308 + { 309 + assertion = badly-named-credentials == [ ]; 310 + message = '' 311 + services.sharkey.credentials contains invalid environment variables: ${builtins.concatStringsSep ", " badly-named-credentials} 312 + They should all be of the form MK_CONFIG_*_FILE. 313 + ''; 314 + } 315 + ) 316 + ]; 317 + 318 + services.sharkey.credentials = extracted-credentials; 319 + } 320 + 321 + # ── LoadCredential wiring ───────────────────────────────────────────────── 322 + ( 323 + let 324 + credentials' = lib.imap0 (i: env: { 325 + identifier = "sharkey-cred-${toString i}"; 326 + inherit env; 327 + path = skcfg.credentials.${env}; 328 + }) (builtins.attrNames skcfg.credentials); 329 + 330 + service = { 331 + serviceConfig.LoadCredential = map (cred: "${cred.identifier}:${cred.path}") credentials'; 332 + environment = lib.mkMerge (map (cred: { ${cred.env} = "%d/${cred.identifier}"; }) credentials'); 333 + }; 334 + in 335 + { 336 + systemd.services.sharkey-migrate = service; 337 + systemd.services.sharkey = service; 153 338 } 339 + ) 340 + 341 + # ── myConfig wiring ─────────────────────────────────────────────────────── 342 + # Wire myConfig.sharkey.* values into services.sharkey.settings. 343 + # publishTarballInsteadOfProvideRepositoryUrl satisfies the AGPLv3 assertion 344 + # above; set to false to expose the upstream git repo URL instead. 345 + { 346 + services.sharkey.settings = { 347 + url = "https://${sk.hostname}/"; 348 + port = sk.port; 349 + address = "127.0.0.1"; 350 + id = "aidx"; 351 + publishTarballInsteadOfProvideRepositoryUrl = false; 352 + }; 154 353 } 155 - ''; 156 - }; 354 + 355 + # ── PostgreSQL ───────────────────────────────────────────────────────────── 356 + (lib.mkIf skcfg.database.createLocally { 357 + systemd.services.sharkey-migrate.bindsTo = [ "postgresql.service" ]; 358 + systemd.services.sharkey-migrate.after = [ "postgresql.service" ]; 359 + systemd.services.sharkey.bindsTo = [ "postgresql.service" ]; 360 + systemd.services.sharkey.after = [ "postgresql.service" ]; 361 + services.postgresql = { 362 + enable = true; 363 + ensureDatabases = [ "sharkey" ]; 364 + ensureUsers = [ 365 + { 366 + name = "sharkey"; 367 + ensureDBOwnership = true; 368 + } 369 + ]; 370 + }; 371 + services.sharkey.settings = { 372 + db.host = lib.mkDefault "/run/postgresql"; 373 + db.port = lib.mkDefault config.services.postgresql.settings.port; 374 + db.db = lib.mkDefault "sharkey"; 375 + db.user = lib.mkDefault "sharkey"; 376 + }; 377 + }) 378 + 379 + # ── Redis ────────────────────────────────────────────────────────────────── 380 + (lib.mkIf skcfg.redis.createLocally { 381 + services.redis.servers.sharkey.enable = true; 382 + systemd.services.sharkey = { 383 + after = [ "redis-sharkey.service" ]; 384 + serviceConfig.SupplementaryGroups = [ config.services.redis.servers.sharkey.group ]; 385 + }; 386 + services.sharkey.settings = { 387 + redis.path = lib.mkDefault config.services.redis.servers.sharkey.unixSocket; 388 + }; 389 + }) 390 + 391 + # ── Meilisearch ──────────────────────────────────────────────────────────── 392 + # Meilisearch master key — file must contain the raw key value only (no KEY= prefix). 393 + # Generate and encrypt: 394 + # openssl rand -base64 32 > secrets/meilisearch-master-key 395 + # SOPS_AGE_KEY_FILE=~/.config/age/keys.txt sops --encrypt --in-place \ 396 + # --input-type binary --output-type binary secrets/meilisearch-master-key 397 + (lib.mkIf skcfg.meilisearch.createLocally { 398 + users.users.meilisearch = { 399 + isSystemUser = true; 400 + group = "meilisearch"; 401 + }; 402 + users.groups.meilisearch = { }; 403 + 404 + sops.secrets."meilisearch-master-key" = { 405 + sopsFile = ../../secrets/meilisearch-master-key; 406 + format = "binary"; 407 + owner = "meilisearch"; 408 + group = "meilisearch"; 409 + mode = "0400"; 410 + }; 411 + 412 + systemd.services.sharkey.after = [ "meilisearch.service" ]; 413 + services.meilisearch = { 414 + enable = true; 415 + listenAddress = "127.0.0.1"; 416 + masterKeyFile = config.sops.secrets."meilisearch-master-key".path; 417 + settings = { 418 + env = lib.mkDefault "production"; 419 + no_analytics = true; 420 + }; 421 + }; 422 + services.sharkey.settings = { 423 + fulltextSearch.provider = lib.mkDefault "meilisearch"; 424 + meilisearch.host = lib.mkDefault "localhost"; 425 + meilisearch.port = lib.mkDefault config.services.meilisearch.listenPort; 426 + meilisearch.index = lib.mkDefault "sharkey"; 427 + meilisearch.apiKey = lib.mkIf (config.services.meilisearch.masterKeyFile != null) ( 428 + lib.mkDefault { file = config.services.meilisearch.masterKeyFile; } 429 + ); 430 + }; 431 + }) 432 + 433 + # ── createLocally convenience defaults ───────────────────────────────────── 434 + { 435 + services.sharkey.database.createLocally = lib.mkDefault true; 436 + services.sharkey.redis.createLocally = lib.mkDefault true; 437 + services.sharkey.meilisearch.createLocally = lib.mkDefault true; 438 + } 439 + 440 + # ── Media directory on /srv ──────────────────────────────────────────────── 441 + # Must exist before Sharkey starts — the service fails with NAMESPACE (226) 442 + # if the path is absent. 443 + { 444 + systemd.tmpfiles.rules = [ 445 + "d ${sk.mediaDir} 0750 sharkey sharkey -" 446 + ]; 447 + } 448 + 449 + # ── Caddy vhost — same pattern as every other CF-tunnel service ─────────── 450 + { 451 + services.caddy.virtualHosts."http://${sk.hostname}:${caddyPort}" = { 452 + extraConfig = '' 453 + handle { 454 + reverse_proxy http://127.0.0.1:${skPort} { 455 + # Cloudflare tunnel passes CF-Connecting-IP with the real client IP. 456 + header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} 457 + header_up X-Real-IP {http.request.header.CF-Connecting-IP} 458 + } 459 + } 460 + ''; 461 + }; 462 + } 463 + ] 464 + ); 157 465 }