nixpkgs: remove merged patches
[sourcephile-nix.git] / nixos / modules / services / misc / sourcehut / default.nix
index 62b5c745c4346d4139bfe71c30ecd21be420cebc..6cea2ce6490cd07572af8674d3447fc91c81f41b 100644 (file)
@@ -24,8 +24,6 @@ let
       if srvMatch == null # Include sections shared by all services
       || head srvMatch == srv # Include sections for the service being configured
       then v
-      # *srht-{dispatch,keys,shell,update-hook} share the same config.ini
-      else if srv == "ssh" && elem (head srvMatch) ["builds" "git" "hg"] && cfg.${head srvMatch}.enable then v
       # Enable Web links and integrations between services.
       else if tail srvMatch == [ null ] && elem (head srvMatch) cfg.services
       then {
@@ -43,12 +41,16 @@ let
       "git.sr.ht".repos = "/var/lib/sourcehut/gitsrht/repos";
       "hg.sr.ht".changegroup-script = "/var/lib/sourcehut/hgsrht/bin/changegroup-script";
       "hg.sr.ht".repos = "/var/lib/sourcehut/hgsrht/repos";
+      # Making this a per service option despite being in a global section,
+      # so that it uses the redis-server used by the service.
+      "sr.ht".redis-host = cfg.${srv}.redis.host;
     })));
   commonServiceSettings = srv: {
     origin = mkOption {
       description = "URL ${srv}.sr.ht is being served at (protocol://domain)";
       type = types.str;
-      default = "https://${srv}.localhost.localdomain";
+      default = "https://${srv}.${domain}";
+      defaultText = "https://${srv}.example.com";
     };
     debug-host = mkOption {
       description = "Address to bind the debug server to.";
@@ -81,6 +83,8 @@ let
   python = pkgs.sourcehut.python.withPackages (ps: with ps; [
     gunicorn
     eventlet
+    # For monitoring Celery: sudo -u listssrht celery --app listssrht.process -b redis+socket:///run/redis-sourcehut/redis.sock?virtual_host=5 flower
+    flower
     # Sourcehut services
     srht
     buildsrht
@@ -140,6 +144,11 @@ in
 
     nginx = {
       enable = mkEnableOption ''local nginx integration'';
+      virtualHost = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Virtual-host configuration merged with all Sourcehut's virtual-hosts.";
+      };
     };
 
     postfix = {
@@ -151,15 +160,7 @@ in
     };
 
     redis = {
-      enable = mkEnableOption ''local redis integration'';
-      firstDatabase = mkOption {
-        type = types.int;
-        default = 0;
-        description = ''
-          Number of the first Redis database to use.
-          At most 9 consecutive databases are currently used.
-        '';
-      };
+      enable = mkEnableOption ''local redis integration in a dedicated redis-server'';
     };
 
     settings = mkOption {
@@ -195,15 +196,6 @@ in
             type = types.str;
             default = "John Doe";
           };
-          redis-host = mkOption {
-            type = with types; nullOr str;
-            description = ''
-              The redis host URL. This is used for caching and temporary storage, and must
-              be shared between nodes (e.g. g - 1it1.sr.ht and git2.sr.ht), but need not be
-              shared between services. It may be shared between services, however, with no
-              ill effect, if this better suits your infrastructure.
-            '';
-          };
           site-blurb = mkOption {
             description = "Blurb for your site.";
             type = types.str;
@@ -326,9 +318,9 @@ in
         options."builds.sr.ht" = commonServiceSettings "builds" // {
           allow-free = mkEnableOption "nonpaying users to submit builds";
           redis = mkOption {
-            description = "The redis connection used for the celery worker.";
+            description = "The Redis connection used for the Celery worker.";
             type = types.str;
-            default = "redis://localhost:6379/3";
+            default = "redis+socket:///run/redis-sourcehut-buildsrht/redis.sock?virtual_host=2";
           };
           shell = mkOption {
             description = ''
@@ -390,8 +382,9 @@ in
             # Git hooks are run relative to their repository's directory,
             # but gitsrht-update-hook looks up ../config.ini
             apply = p: pkgs.writeShellScript "update-hook-wrapper" ''
+              set -e
               test -e "''${PWD%/*}"/config.ini ||
-              ln -s ${users."sshsrht".home}/../config.ini "''${PWD%/*}"/config.ini
+              ln -s /run/sourcehut/gitsrht/config.ini "''${PWD%/*}"/config.ini
               exec -a "$0" '${p}' "$@"
             '';
           };
@@ -405,9 +398,21 @@ in
             default = "/var/lib/sourcehut/gitsrht/repos";
           };
           webhooks = mkOption {
-            description = "The redis connection used for the webhooks worker.";
+            description = "The Redis connection used for the webhooks worker.";
             type = types.str;
-            default = "redis://localhost:6379/1";
+            default = "redis+socket:///run/redis-sourcehut-gitsrht/redis.sock?virtual_host=1";
+          };
+        };
+        options."git.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
           };
         };
 
@@ -423,8 +428,9 @@ in
             # Mercurial's changegroup hooks are run relative to their repository's directory,
             # but hgsrht-hook-changegroup looks up ./config.ini
             apply = p: pkgs.writeShellScript "hook-changegroup-wrapper" ''
+              set -e
               test -e "''$PWD"/config.ini ||
-              ln -s ${users."sshsrht".home}/../config.ini "''$PWD"/config.ini
+              ln -s /run/sourcehut/hgsrht/config.ini "''$PWD"/config.ini
               exec -a "$0" '${p}' "$@"
             '';
           };
@@ -453,9 +459,9 @@ in
             defaultText = "\${pkgs.mercurial}/bin/hg-ssh";
           };
           webhooks = mkOption {
-            description = "The redis connection used for the webhooks worker.";
+            description = "The Redis connection used for the webhooks worker.";
             type = types.str;
-            default = "redis://localhost:6379/8";
+            default = "redis+socket:///run/redis-sourcehut-hgsrht/redis.sock?virtual_host=1";
           };
         };
 
@@ -475,14 +481,14 @@ in
             default = "lists.localhost.localdomain";
           };
           redis = mkOption {
-            description = "The redis connection used for the celery worker.";
+            description = "The Redis connection used for the Celery worker.";
             type = types.str;
-            default = "redis://localhost:6379/4";
+            default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=2";
           };
           webhooks = mkOption {
-            description = "The redis connection used for the webhooks worker.";
+            description = "The Redis connection used for the webhooks worker.";
             type = types.str;
-            default = "redis://localhost:6379/2";
+            default = "redis+socket:///run/redis-sourcehut-listssrht/redis.sock?virtual_host=1";
           };
         };
         options."lists.sr.ht::worker" = {
@@ -529,29 +535,26 @@ in
           api-origin = mkOption {
             description = "Origin URL for API, 100 more than web.";
             type = types.str;
-            default = "http://localhost:5100";
+            default = "http://${cfg.listenAddress}:${toString (cfg.meta.port + 100)}";
+            defaultText = ''http://<xref linkend="opt-services.sourcehut.listenAddress"/>:''${toString (<xref linkend="opt-services.sourcehut.meta.port"/> + 100)}'';
           };
           webhooks = mkOption {
-            description = "The redis connection used for the webhooks worker.";
+            description = "The Redis connection used for the webhooks worker.";
             type = types.str;
-            default = "redis://localhost:6379/6";
+            default = "redis+socket:///run/redis-sourcehut-metasrht/redis.sock?virtual_host=1";
           };
           welcome-emails = mkEnableOption "sending stock sourcehut welcome emails after signup";
         };
-        options."meta.sr.ht::settings" = {
-          registration = mkEnableOption "public registration";
-          onboarding-redirect = mkOption {
-            description = "Where to redirect new users upon registration.";
-            type = types.str;
-            default = "https://meta.localhost.localdomain";
-          };
-          user-invites = mkOption {
+        options."meta.sr.ht::api" = {
+          internal-ipnet = mkOption {
             description = ''
-              How many invites each user is issued upon registration
-              (only applicable if open registration is disabled).
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
             '';
-            type = types.ints.unsigned;
-            default = 5;
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
           };
         };
         options."meta.sr.ht::aliases" = mkOption {
@@ -570,6 +573,22 @@ in
             apply = mapNullable (s: "<" + toString s);
           };
         };
+        options."meta.sr.ht::settings" = {
+          registration = mkEnableOption "public registration";
+          onboarding-redirect = mkOption {
+            description = "Where to redirect new users upon registration.";
+            type = types.str;
+            default = "https://meta.localhost.localdomain";
+          };
+          user-invites = mkOption {
+            description = ''
+              How many invites each user is issued upon registration
+              (only applicable if open registration is disabled).
+            '';
+            type = types.ints.unsigned;
+            default = 5;
+          };
+        };
 
         options."pages.sr.ht" = commonServiceSettings "pages" // {
           gemini-certs = mkOption {
@@ -595,14 +614,19 @@ in
           };
         };
         options."pages.sr.ht::api" = {
+          internal-ipnet = mkOption {
+            description = ''
+              Set of IP subnets which are permitted to utilize internal API
+              authentication. This should be limited to the subnets
+              from which your *.sr.ht services are running.
+              See <xref linkend="opt-services.sourcehut.listenAddress"/>.
+            '';
+            type = with types; listOf str;
+            default = [ "127.0.0.0/8" "::1/128" ];
+          };
         };
 
         options."paste.sr.ht" = commonServiceSettings "paste" // {
-          webhooks = mkOption {
-            description = "The redis connection used for the webhooks worker.";
-            type = types.str;
-            default = "redis://localhost:6379/5";
-          };
         };
 
         options."todo.sr.ht" = commonServiceSettings "todo" // {
@@ -612,9 +636,9 @@ in
             default = "todo-notify@localhost.localdomain";
           };
           webhooks = mkOption {
-            description = "The redis connection used for the webhooks worker.";
+            description = "The Redis connection used for the webhooks worker.";
             type = types.str;
-            default = "redis://localhost:6379/7";
+            default = "redis+socket:///run/redis-sourcehut-todosrht/redis.sock?virtual_host=1";
           };
         };
         options."todo.sr.ht::mail" = {
@@ -684,7 +708,7 @@ in
         '';
       };
       fcgiwrap.preforkProcess = mkOption {
-        description = "Number of processes to prefork.";
+        description = "Number of fcgiwrap processes to prefork.";
         type = types.int;
         default = 4;
       };
@@ -706,6 +730,21 @@ in
         '';
       };
     };
+
+    lists = {
+      process = {
+        extraArgs = mkOption {
+          type = with types; listOf str;
+          default = [ "--loglevel DEBUG" "--pool eventlet" "--without-heartbeat" ];
+          description = "Extra arguments passed to the Celery responsible for processing mails.";
+        };
+        celeryConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Content of the <literal>celeryconfig.py</literal> used by the Celery of <literal>listssrht-process</literal>.";
+        };
+      };
+    };
   };
 
   config = mkIf cfg.enable (mkMerge [
@@ -722,106 +761,123 @@ in
       };
     }
     (mkIf cfg.postgresql.enable {
-      services.postgresql.enable = true;
+      assertions = [
+        { assertion = postgresql.enable;
+          message = "postgresql must be enabled and configured";
+        }
+      ];
     })
     (mkIf cfg.postfix.enable {
-      services.postfix.enable = true;
+      assertions = [
+        { assertion = postfix.enable;
+          message = "postfix must be enabled and configured";
+        }
+      ];
       # Needed for sharing the LMTP sockets with JoinsNamespaceOf=
       systemd.services.postfix.serviceConfig.PrivateTmp = true;
     })
     (mkIf cfg.redis.enable {
-      services.redis.enable = true;
-      services.sourcehut.settings."sr.ht".redis-host = mkDefault ("redis://localhost:6379/" + toString cfg.redis.firstDatabase);
+      services.redis.vmOverCommit = mkDefault true;
     })
     (mkIf cfg.nginx.enable {
-      services.nginx.enable = true;
+      assertions = [
+        { assertion = nginx.enable;
+          message = "nginx must be enabled and configured";
+        }
+      ];
       # For proxyPass= in virtual-hosts for Sourcehut services.
       services.nginx.recommendedProxySettings = mkDefault true;
     })
     (mkIf (cfg.builds.enable || cfg.git.enable || cfg.hg.enable) {
       services.openssh = {
-        # Note that sshd will continue to honor AuthorizedKeysFile
-        authorizedKeysCommand = ''/etc/ssh/srht-dispatch "%u" "%h" "%t" "%k"'';
-        # The sshsrht-dispatch user needs:
-        # 1. to read ${users."sshsrht".home}/../config.ini,
-        # 2. to access the redis server in redis-host,
-        # 3. to access the postgresql server in the service's connection-string,
-        # 4. to query metasrht-api (through the HTTP API).
-        # Note that *srht-{dispatch,keys,shell,update-hook} will likely fail
-        # to write their log on /var/log with that user, and will fallback to stderr,
-        # making their log visible in sshd's log when sshd is in debug mode (-d).
-        # Alternatively, you can touch and chown sshsrht /var/log/gitsrht-{dispatch,keys,shell,update-hook}
-        # during your debug.
-        authorizedKeysCommandUser = users."sshsrht".name;
+        # Note that sshd will continue to honor AuthorizedKeysFile.
+        # Note that you may want automatically rotate
+        # or link to /dev/null the following log files:
+        # - /var/log/gitsrht-dispatch
+        # - /var/log/{build,git,hg}srht-keys
+        # - /var/log/{git,hg}srht-shell
+        # - /var/log/gitsrht-update-hook
+        authorizedKeysCommand = ''/etc/ssh/sourcehut/subdir/srht-dispatch "%u" "%h" "%t" "%k"'';
+        # srht-dispatch will setuid/setgid according to [git.sr.ht::dispatch]
+        authorizedKeysCommandUser = "root";
         extraConfig = ''
           PermitUserEnvironment SRHT_*
         '';
       };
-      environment.etc."ssh/srht-dispatch" = {
+      environment.etc."ssh/sourcehut/config.ini".source =
+        settingsFormat.generate "sourcehut-dispatch-config.ini"
+          (filterAttrs (k: v: k == "git.sr.ht::dispatch")
+          cfg.settings);
+      environment.etc."ssh/sourcehut/subdir/srht-dispatch" = {
         # sshd_config(5): The program must be owned by root, not writable by group or others
         mode = "0755";
         source = pkgs.writeShellScript "srht-dispatch" ''
           set -e
-          cd ${users."sshsrht".home}
-          exec ${cfg.python}/bin/gitsrht-dispatch "$@"
+          cd /etc/ssh/sourcehut/subdir
+          ${cfg.python}/bin/gitsrht-dispatch "$@"
         '';
       };
-      systemd.services.sshd = let configIni = configIniOfService "ssh"; in {
+      systemd.services.sshd = {
         #path = optional cfg.git.enable [ cfg.git.package ];
-        restartTriggers = [ configIni ];
         serviceConfig = {
-          RuntimeDirectory = [ "sourcehut/sshsrht/subdir" ];
           BindReadOnlyPaths =
             # Note that those /usr/bin/* paths are hardcoded in multiple places in *.sr.ht,
-            # for instance to get the user from the [*.sr.ht::dispatch] settings.
+            # for instance to get the user from the [git.sr.ht::dispatch] settings.
+            # *srht-keys needs to:
+            # - access a redis-server in [sr.ht] redis-host,
+            # - access the PostgreSQL server in [*.sr.ht] connection-string,
+            # - query metasrht-api (through the HTTP API).
+            # Using this has the side effect of creating empty files in /usr/bin/
             optionals cfg.builds.enable [
-              "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys:/usr/bin/buildsrht-keys"
+              "${pkgs.writeShellScript "buildsrht-keys-wrapper" ''
+                set -ex
+                cd /run/sourcehut/buildsrht/subdir
+                exec -a "$0" ${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys "$@"
+              ''}:/usr/bin/buildsrht-keys"
               "${pkgs.sourcehut.buildsrht}/bin/master-shell:/usr/bin/master-shell"
               "${pkgs.sourcehut.buildsrht}/bin/runner-shell:/usr/bin/runner-shell"
             ] ++
             optionals cfg.git.enable [
-              "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys:/usr/bin/gitsrht-keys"
-              "${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell:/usr/bin/gitsrht-shell"
+              # /path/to/gitsrht-keys calls /path/to/gitsrht-shell,
+              # or [git.sr.ht] shell= if set.
+              "${pkgs.writeShellScript "gitsrht-keys-wrapper" ''
+                set -ex
+                cd /run/sourcehut/gitsrht/subdir
+                exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys "$@"
+              ''}:/usr/bin/gitsrht-keys"
+              "${pkgs.writeShellScript "gitsrht-shell-wrapper" ''
+                set -e
+                cd /run/sourcehut/gitsrht/subdir
+                exec -a "$0" ${pkgs.sourcehut.gitsrht}/bin/gitsrht-shell "$@"
+              ''}:/usr/bin/gitsrht-shell"
             ] ++
             optionals cfg.hg.enable [
-              "${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys:/usr/bin/htsrht-keys"
-              "${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell:/usr/bin/htsrht-shell"
+              # /path/to/hgsrht-keys calls /path/to/hgsrht-shell,
+              # or [hg.sr.ht] shell= if set.
+              "${pkgs.writeShellScript "hgsrht-keys-wrapper" ''
+                set -ex
+                cd /run/sourcehut/hgsrht/subdir
+                exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-keys "$@"
+              ''}:/usr/bin/hgsrht-keys"
+              ":/usr/bin/hgsrht-shell"
+              "${pkgs.writeShellScript "hgsrht-shell-wrapper" ''
+                set -e
+                cd /run/sourcehut/hgsrht/subdir
+                exec -a "$0" ${pkgs.sourcehut.hgsrht}/bin/hgsrht-shell "$@"
+              ''}:/usr/bin/hgsrht-shell"
             ];
-          ExecStartPre = mkBefore [("+"+pkgs.writeShellScript "sshsrht-credentials" ''
-            # Replace values begining with a '<' by the content of the file whose name is after.
-            ${pkgs.gawk}/bin/gawk '{ if (match($0,/^([^=]+=)<(.+)/,m)) { getline f < m[2]; print m[1] f } else print $0 }' ${configIni} |
-            install -o ${users."sshsrht".name} -g ${groups."sshsrht".name} -m 440 \
-              /dev/stdin ${users."sshsrht".home}/../config.ini
-          '')];
-        };
-      };
-      users = {
-        users."sshsrht" = {
-          isSystemUser = true;
-          # srht-dispatch, *srht-keys, and *srht-shell
-          # look up in ../config.ini from this directory;
-          # that config.ini being set in *srht.service's ExecStartPre=
-          home = "/run/sourcehut/sshsrht/subdir";
-          group =
-            # Unfortunately, AuthorizedKeysCommandUser does not honor supplementary groups,
-            # hence the main group is used.
-            if cfg.postgresql.enable
-            && hasSuffix "0" (postgresql.settings.unix_socket_permissions or "")
-            then groups.postgres.name
-            else groups.nogroup.name;
-          description = "sourcehut user for sshd's AuthorizedKeysCommandUser";
         };
-        groups."sshsrht" = {};
       };
     })
   ]);
 
   imports = [
+
     (import ./service.nix "builds" {
       inherit configIniOfService;
       srvsrht = "buildsrht";
       port = 5002;
-      redisDatabase = 3;
+      # TODO: a celery worker on the master and worker are apparently needed
       extraServices.buildsrht-worker = let
         qemuPackage = pkgs.qemu_kvm;
         serviceName = "buildsrht-worker";
@@ -877,16 +933,13 @@ in
         '';
         in mkMerge [
         {
-          users.users.${cfg.builds.user} = {
-            shell = pkgs.bash;
-            # Allow reading of ${users."sshsrht".home}/../config.ini
-            extraGroups = [ groups."sshsrht".name ];
-          };
+          users.users.${cfg.builds.user}.shell = pkgs.bash;
 
           virtualisation.docker.enable = true;
 
           services.sourcehut.settings = mkMerge [
-            { # Register the builds.sr.ht dispatcher
+            { # Note that git.sr.ht::dispatch is not a typo,
+              # gitsrht-dispatch always use this section
               "git.sr.ht::dispatch"."/usr/bin/buildsrht-keys" =
                 mkDefault "${cfg.builds.user}:${cfg.builds.group}";
             }
@@ -908,7 +961,7 @@ in
           systemd.services.nginx = {
             serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."builds.sr.ht::worker".buildlogs}:/var/log/nginx/buildsrht/logs" ];
           };
-          services.nginx.virtualHosts."logs.${domain}" = {
+          services.nginx.virtualHosts."logs.${domain}" = mkMerge [ {
             /* FIXME: is a listen needed?
             listen = with builtins;
               # FIXME: not compatible with IPv6
@@ -916,46 +969,39 @@ in
               [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
             */
             locations."/logs/".alias = "/var/log/nginx/buildsrht/logs/";
-          };
+          } cfg.nginx.virtualHost ];
         })
       ];
     })
+
     (import ./service.nix "dispatch" {
       inherit configIniOfService;
       port = 5005;
     })
+
     (import ./service.nix "git" (let
       baseService = {
         path = [ cfg.git.package ];
         serviceConfig.BindPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
         serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".post-update-script}:/var/lib/sourcehut/gitsrht/bin/post-update-script" ];
       };
-      mainService = mkMerge [ baseService {
-        serviceConfig.StateDirectory = [ "sourcehut/gitsrht" ];
-      } ];
       in {
       inherit configIniOfService;
-      inherit mainService;
+      mainService = mkMerge [ baseService {
+        serviceConfig.StateDirectory = [ "sourcehut/gitsrht" "sourcehut/gitsrht/repos" ];
+      } ];
       port = 5001;
-      webhooks.redisDatabase = 1;
+      webhooks = true;
       extraTimers.gitsrht-periodic = {
-        service = mainService;
-        timerConfig = {
-          OnCalendar = ["20min"];
-        };
+        service = baseService;
+        timerConfig.OnCalendar = ["20min"];
       };
       extraConfig = mkMerge [
         {
-          users.users.${cfg.git.user} = {
-            # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
-            # Probably could use gitsrht-shell if output is restricted to just parameters...
-            shell = pkgs.bash;
-            # Allow reading of ${users."sshsrht".home}/../config.ini
-            extraGroups = [ groups."sshsrht".name ];
-            home = users.sshsrht.home;
-          };
+          # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
+          # Probably could use gitsrht-shell if output is restricted to just parameters...
+          users.users.${cfg.git.user}.shell = pkgs.bash;
           services.sourcehut.settings = {
-            # Register the git.sr.ht dispatcher
             "git.sr.ht::dispatch"."/usr/bin/gitsrht-keys" =
               mkDefault "${cfg.git.user}:${cfg.git.group}";
           };
@@ -999,7 +1045,7 @@ in
       ];
       extraServices.gitsrht-fcgiwrap = mkIf cfg.nginx.enable {
         serviceConfig = {
-          # Socket is passed by systemd.sockets.gitsrht-fcgiwrap
+          # Socket is passed by gitsrht-fcgiwrap.socket
           ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${toString cfg.git.fcgiwrap.preforkProcess}";
           # No need for config.ini
           ExecStartPre = mkForce [];
@@ -1007,7 +1053,7 @@ in
           DynamicUser = true;
           BindReadOnlyPaths = [ "${cfg.settings."git.sr.ht".repos}:/var/lib/sourcehut/gitsrht/repos" ];
           IPAddressDeny = "any";
-          InaccessiblePaths = [ "-+/run/postgresql" "-+/run/redis" ];
+          InaccessiblePaths = [ "-+/run/postgresql" "-+/run/redis-sourcehut" ];
           PrivateNetwork = true;
           RestrictAddressFamilies = mkForce [ "none" ];
           SystemCallFilter = mkForce [
@@ -1018,43 +1064,36 @@ in
         };
       };
     }))
+
     (import ./service.nix "hg" (let
       baseService = {
         path = [ cfg.hg.package ];
         serviceConfig.BindPaths = [ "${cfg.settings."hg.sr.ht".repos}:/var/lib/sourcehut/hgsrht/repos" ];
         serviceConfig.BindReadOnlyPaths = [ "${cfg.settings."ht.sr.ht".changegroup-script}:/var/lib/sourcehut/hgsrht/bin/changegroup-script" ];
       };
-      mainService = mkMerge [ baseService {
-        serviceConfig.StateDirectory = [ "sourcehut/hgsrht" ];
-      } ];
       in {
       inherit configIniOfService;
-      inherit mainService;
+      mainService = mkMerge [ baseService {
+        serviceConfig.StateDirectory = [ "sourcehut/hgsrht" "sourcehut/hgsrht/repos" ];
+      } ];
       port = 5010;
-      webhooks.redisDatabase = 8;
+      webhooks = true;
       extraTimers.hgsrht-periodic = {
-        service = mainService;
-        timerConfig = {
-          OnCalendar = ["20min"];
-        };
+        service = baseService;
+        timerConfig.OnCalendar = ["20min"];
       };
       extraTimers.hgsrht-clonebundles = mkIf cfg.hg.cloneBundles {
-        service = mainService;
-        timerConfig = {
-          OnCalendar = ["daily"];
-          AccuracySec = "1h";
-        };
+        service = baseService;
+        timerConfig.OnCalendar = ["daily"];
+        timerConfig.AccuracySec = "1h";
       };
       extraConfig = mkMerge [
         {
-          users.users.${cfg.hg.user} = {
-            shell = pkgs.bash;
-            # Allow reading of ${users."sshsrht".home}/../config.ini
-            extraGroups = [ groups."sshsrht".name ];
-          };
+          users.users.${cfg.hg.user}.shell = pkgs.bash;
           services.sourcehut.settings = {
-            # Register the hg.sr.ht dispatcher
-            "hg.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
+            # Note that git.sr.ht::dispatch is not a typo,
+            # gitsrht-dispatch always uses this section.
+            "git.sr.ht::dispatch"."/usr/bin/hgsrht-keys" =
               mkDefault "${cfg.hg.user}:${cfg.hg.group}";
           };
           systemd.services.sshd = baseService;
@@ -1089,50 +1128,73 @@ in
         })
       ];
     }))
+
     (import ./service.nix "hub" {
       inherit configIniOfService;
       port = 5014;
       extraConfig = {
         services.nginx = mkIf cfg.nginx.enable {
-          virtualHosts."hub.${domain}" = {
+          virtualHosts."hub.${domain}" = mkMerge [ {
             serverAliases = [ domain ];
-          };
+          } cfg.nginx.virtualHost ];
         };
       };
     })
-    (import ./service.nix "lists" {
+
+    (import ./service.nix "lists" (let
+      srvsrht = "listssrht";
+      in {
       inherit configIniOfService;
       port = 5006;
-      redisDatabase = 4;
-      webhooks.redisDatabase = 2;
+      webhooks = true;
+      # Receive the mail from Postfix and enqueue them into Redis and PostgreSQL
       extraServices.listssrht-lmtp = {
-        requires = [ "postfix.service" ];
+        wants = [ "postfix.service" ];
         unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
         serviceConfig.ExecStart = "${cfg.python}/bin/listssrht-lmtp";
         # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
         serviceConfig.PrivateUsers = mkForce false;
       };
+      # Dequeue the mails from Redis and dispatch them
       extraServices.listssrht-process = {
-        serviceConfig.ExecStart = "${cfg.python}/bin/celery -A listssrht.process worker --loglevel INFO --pool eventlet";
-        # Avoid crashing: os.getloadavg()
-        serviceConfig.ProcSubset = mkForce "all";
+        serviceConfig = {
+          preStart = ''
+            cp ${pkgs.writeText "${srvsrht}-webhooks-celeryconfig.py" cfg.lists.process.celeryConfig} \
+               /run/sourcehut/${srvsrht}-webhooks/celeryconfig.py
+          '';
+          ExecStart = "${cfg.python}/bin/celery --app listssrht.process worker --hostname listssrht-process@%%h " + concatStringsSep " " cfg.lists.process.extraArgs;
+          # Avoid crashing: os.getloadavg()
+          ProcSubset = mkForce "all";
+        };
       };
       extraConfig = mkIf cfg.postfix.enable {
         users.groups.${postfix.group}.members = [ cfg.lists.user ];
         services.sourcehut.settings."lists.sr.ht::mail".sock-group = postfix.group;
-        services.postfix.transport = ''
-          lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
-        '';
+        services.postfix = {
+          destination = [ "lists.${domain}" ];
+          # FIXME: an accurate recipient list should be queried
+          # from the lists.sr.ht PostgreSQL database to avoid backscattering.
+          # But usernames are unfortunately not in that database but in meta.sr.ht.
+          # Note that two syntaxes are allowed:
+          # - ~username/list-name@lists.${domain}
+          # - u.username.list-name@lists.${domain}
+          localRecipients = [ "@lists.${domain}" ];
+          transport = ''
+            lists.${domain} lmtp:unix:${cfg.settings."lists.sr.ht::worker".sock}
+          '';
+        };
       };
-    })
+    }))
+
     (import ./service.nix "man" {
       inherit configIniOfService;
       port = 5004;
     })
+
     (import ./service.nix "meta" {
       inherit configIniOfService;
       port = 5000;
-      webhooks.redisDatabase = 6;
+      webhooks = true;
       extraServices.metasrht-api = {
         serviceConfig.Restart = "always";
         serviceConfig.RestartSec = "2s";
@@ -1153,32 +1215,60 @@ in
         OnCalendar = ["daily"];
         AccuracySec = "1h";
       };
-      extraConfig = {
-        assertions = [
-          { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
-                        s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
-            message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
-          }
-        ];
-        environment.systemPackages = [
-          (pkgs.writeShellScriptBin "metasrht-manageuser" ''
-            set -eux
-            test "$(${pkgs.coreutils}/bin/id -n -u)" = '${cfg.meta.user}' ||
-            sudo -u '${cfg.meta.user}' "$0" "$@"
-            # In order to load config.ini
-            cd /run/sourcehut/metasrht ||
-            cat <<EOF
-            Please run: sudo systemctl start metasrht
-            EOF
-            ${cfg.python}/bin/metasrht-manageuser "$@"
-          '')
-        ];
-      };
+      extraConfig = mkMerge [
+        {
+          assertions = [
+            { assertion = let s = cfg.settings."meta.sr.ht::billing"; in
+                          s.enabled == "yes" -> (s.stripe-public-key != null && s.stripe-secret-key != null);
+              message = "If meta.sr.ht::billing is enabled, the keys must be defined.";
+            }
+          ];
+          environment.systemPackages = optional cfg.meta.enable
+            (pkgs.writeShellScriptBin "metasrht-manageuser" ''
+              set -eux
+              if test "$(${pkgs.coreutils}/bin/id -n -u)" != '${cfg.meta.user}'
+              then exec sudo -u '${cfg.meta.user}' "$0" "$@"
+              else
+                # In order to load config.ini
+                if cd /run/sourcehut/metasrht
+                then exec ${cfg.python}/bin/metasrht-manageuser "$@"
+                else cat <<EOF
+                  Please run: sudo systemctl start metasrht
+              EOF
+                  exit 1
+                fi
+              fi
+            '');
+        }
+        (mkIf cfg.nginx.enable {
+          services.nginx.virtualHosts."meta.${domain}" = {
+            locations."/query" = {
+              proxyPass = cfg.settings."meta.sr.ht".api-origin;
+              extraConfig = ''
+                if ($request_method = 'OPTIONS') {
+                  add_header 'Access-Control-Allow-Origin' '*';
+                  add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+                  add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+                  add_header 'Access-Control-Max-Age' 1728000;
+                  add_header 'Content-Type' 'text/plain; charset=utf-8';
+                  add_header 'Content-Length' 0;
+                  return 204;
+                }
+
+                add_header 'Access-Control-Allow-Origin' '*';
+                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+                add_header 'Access-Control-Allow-Headers' 'User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
+                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
+              '';
+            };
+          };
+        })
+      ];
     })
+
     (import ./service.nix "pages" {
       inherit configIniOfService;
       port = 5112;
-      #webhooks.redisDatabase = 9;
       mainService = let
         srvsrht = "pagessrht";
         version = pkgs.sourcehut.${srvsrht}.version;
@@ -1211,17 +1301,18 @@ in
         };
       };
     })
+
     (import ./service.nix "paste" {
       inherit configIniOfService;
       port = 5011;
-      webhooks.redisDatabase = 5;
     })
+
     (import ./service.nix "todo" {
       inherit configIniOfService;
       port = 5003;
-      webhooks.redisDatabase = 7;
+      webhooks = true;
       extraServices.todosrht-lmtp = {
-        requires = [ "postfix.service" ];
+        wants = [ "postfix.service" ];
         unitConfig.JoinsNamespaceOf = optional cfg.postfix.enable "postfix.service";
         serviceConfig.ExecStart = "${cfg.python}/bin/todosrht-lmtp";
         # Avoid crashing: os.chown(sock, os.getuid(), sock_gid)
@@ -1230,15 +1321,27 @@ in
       extraConfig = mkIf cfg.postfix.enable {
         users.groups.${postfix.group}.members = [ cfg.todo.user ];
         services.sourcehut.settings."todo.sr.ht::mail".sock-group = postfix.group;
-        services.postfix.transport = ''
-          todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
-        '';
+        services.postfix = {
+          destination = [ "todo.${domain}" ];
+          # FIXME: an accurate recipient list should be queried
+          # from the todo.sr.ht PostgreSQL database to avoid backscattering.
+          # But usernames are unfortunately not in that database but in meta.sr.ht.
+          # Note that two syntaxes are allowed:
+          # - ~username/tracker-name@todo.${domain}
+          # - u.username.tracker-name@todo.${domain}
+          localRecipients = [ "@todo.${domain}" ];
+          transport = ''
+            todo.${domain} lmtp:unix:${cfg.settings."todo.sr.ht::mail".sock}
+          '';
+        };
       };
     })
+
     (mkRenamedOptionModule [ "services" "sourcehut" "originBase" ]
                            [ "services" "sourcehut" "settings" "sr.ht" "global-domain" ])
     (mkRenamedOptionModule [ "services" "sourcehut" "address" ]
                            [ "services" "sourcehut" "listenAddress" ])
+
   ];
 
   meta.doc = ./sourcehut.xml;