about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorMartin Weinelt <mweinelt@users.noreply.github.com>2022-06-24 21:38:22 +0200
committerGitHub <noreply@github.com>2022-06-24 21:38:22 +0200
commita24431e56f3cd4b01db7d71086a782ba29ad87ab (patch)
tree80f0d3540102eb29600d72dc8099f020043e53e3 /nixos
parent5c926654849e6bdc99259d118405358a12f52b73 (diff)
parente608c54d58daa466c494a6e09daff8d4cfbed942 (diff)
Merge pull request #162808 from mweinelt/schleuder
schleuder: init
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2211.section.xml7
-rw-r--r--nixos/doc/manual/release-notes/rl-2211.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/mail/schleuder.nix162
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/schleuder.nix128
6 files changed, 301 insertions, 0 deletions
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
index 8b1e402fc6f44..097c2b5e77c65 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -144,6 +144,13 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://schleuder.org/">schleuder</link>, a
+          mailing list manager with PGP support. Enable using
+          <link linkend="opt-services.schleuder.enable">services.schleuder</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://www.expressvpn.com">expressvpn</link>,
           the CLI client for ExpressVPN. Available as
           <link linkend="opt-services.expressvpn.enable">services.expressvpn</link>.
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
index c2626998843bf..f79d9117abb25 100644
--- a/nixos/doc/manual/release-notes/rl-2211.section.md
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -60,6 +60,8 @@ In addition to numerous new and upgraded packages, this release has the followin
   Available as [services.infnoise](options.html#opt-services.infnoise.enable).
 - [persistent-evdev](https://github.com/aiberia/persistent-evdev), a daemon to add virtual proxy devices that mirror a physical input device but persist even if the underlying hardware is hot-plugged. Available as [services.persistent-evdev](#opt-services.persistent-evdev.enable).
 
+- [schleuder](https://schleuder.org/), a mailing list manager with PGP support. Enable using [services.schleuder](#opt-services.schleuder.enable).
+
 - [expressvpn](https://www.expressvpn.com), the CLI client for ExpressVPN. Available as [services.expressvpn](#opt-services.expressvpn.enable).
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 7af6d826a78f5..b0fd06e66e02f 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -515,6 +515,7 @@
   ./services/mail/rspamd.nix
   ./services/mail/rss2email.nix
   ./services/mail/roundcube.nix
+  ./services/mail/schleuder.nix
   ./services/mail/sympa.nix
   ./services/mail/nullmailer.nix
   ./services/matrix/appservice-discord.nix
diff --git a/nixos/modules/services/mail/schleuder.nix b/nixos/modules/services/mail/schleuder.nix
new file mode 100644
index 0000000000000..7ba15f1070bde
--- /dev/null
+++ b/nixos/modules/services/mail/schleuder.nix
@@ -0,0 +1,162 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.schleuder;
+  settingsFormat = pkgs.formats.yaml { };
+  postfixMap = entries: lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${value}") entries);
+  writePostfixMap = name: entries: pkgs.writeText name (postfixMap entries);
+  configScript = pkgs.writeScript "schleuder-cfg" ''
+    #!${pkgs.runtimeShell}
+    set -exuo pipefail
+    umask 0077
+    ${pkgs.yq}/bin/yq \
+      --slurpfile overrides <(${pkgs.yq}/bin/yq . <${lib.escapeShellArg cfg.extraSettingsFile}) \
+      < ${settingsFormat.generate "schleuder.yml" cfg.settings} \
+      '. * $overrides[0]' \
+      > /etc/schleuder/schleuder.yml
+    chown schleuder: /etc/schleuder/schleuder.yml
+  '';
+in
+{
+  options.services.schleuder = {
+    enable = lib.mkEnableOption "Schleuder secure remailer";
+    enablePostfix = lib.mkEnableOption "automatic postfix integration" // { default = true; };
+    lists = lib.mkOption {
+      description = ''
+        List of list addresses that should be handled by Schleuder.
+
+        Note that this is only handled by the postfix integration, and
+        the setup of the lists, their members and their keys has to be
+        performed separately via schleuder's API, using a tool such as
+        schleuder-cli.
+      '';
+      type = lib.types.listOf lib.types.str;
+      default = [ ];
+      example = [ "widget-team@example.com" "security@example.com" ];
+    };
+    /* maybe one day....
+      domains = lib.mkOption {
+      description = "Domains for which all mail should be handled by Schleuder.";
+      type = lib.types.listOf lib.types.str;
+      default = [];
+      example = ["securelists.example.com"];
+      };
+    */
+    settings = lib.mkOption {
+      description = ''
+        Settings for schleuder.yml.
+
+        Check the <link xlink:href="https://0xacab.org/schleuder/schleuder/blob/master/etc/schleuder.yml">example configuration</link> for possible values.
+      '';
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        options.keyserver = lib.mkOption {
+          type = lib.types.str;
+          description = ''
+            Key server from which to fetch and update keys.
+
+            Note that NixOS uses a different default from upstream, since the upstream default sks-keyservers.net is deprecated.
+          '';
+          default = "keys.openpgp.org";
+        };
+      };
+      default = { };
+    };
+    extraSettingsFile = lib.mkOption {
+      description = "YAML file to merge into the schleuder config at runtime. This can be used for secrets such as API keys.";
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+    };
+    listDefaults = lib.mkOption {
+      description = ''
+        Default settings for lists (list-defaults.yml).
+
+        Check the <link xlink:href="https://0xacab.org/schleuder/schleuder/-/blob/master/etc/list-defaults.yml">example configuration</link> for possible values.
+      '';
+      type = settingsFormat.type;
+      default = { };
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !(cfg.settings.api ? valid_api_keys);
+        message = ''
+          services.schleuder.settings.api.valid_api_keys is set. Defining API keys via NixOS config results in them being copied to the world-readable Nix store. Please use the extraSettingsFile option to store API keys in a non-public location.
+        '';
+      }
+      {
+        assertion = !(lib.any (db: db ? password) (lib.attrValues cfg.settings.database or {}));
+        message = ''
+          A password is defined for at least one database in services.schleuder.settings.database. Defining passwords via NixOS config results in them being copied to the world-readable Nix store. Please use the extraSettingsFile option to store database passwords in a non-public location.
+        '';
+      }
+    ];
+    users.users.schleuder.isSystemUser = true;
+    users.users.schleuder.group = "schleuder";
+    users.groups.schleuder = {};
+    environment.systemPackages = [
+      pkgs.schleuder-cli
+    ];
+    services.postfix = lib.mkIf cfg.enablePostfix {
+      extraMasterConf = ''
+        schleuder  unix  -       n       n       -       -       pipe
+          flags=DRhu user=schleuder argv=/${pkgs.schleuder}/bin/schleuder work ''${recipient}
+      '';
+      transport = lib.mkIf (cfg.lists != [ ]) (postfixMap (lib.genAttrs cfg.lists (_: "schleuder:")));
+      extraConfig = ''
+        schleuder_destination_recipient_limit = 1
+      '';
+      # review: does this make sense?
+      localRecipients = lib.mkIf (cfg.lists != [ ]) cfg.lists;
+    };
+    systemd.services = let commonServiceConfig = {
+      # We would have liked to use DynamicUser, but since the default
+      # database is SQLite and lives in StateDirectory, and that same
+      # database needs to be readable from the postfix service, this
+      # isn't trivial to do.
+      User = "schleuder";
+      StateDirectory = "schleuder";
+      StateDirectoryMode = "0700";
+    }; in
+      {
+        schleuder-init = {
+          serviceConfig = commonServiceConfig // {
+            ExecStartPre = lib.mkIf (cfg.extraSettingsFile != null) [
+              "+${configScript}"
+            ];
+            ExecStart = [ "${pkgs.schleuder}/bin/schleuder install" ];
+            Type = "oneshot";
+          };
+        };
+        schleuder-api-daemon = {
+          after = [ "local-fs.target" "network.target" "schleuder-init.service" ];
+          wantedBy = [ "multi-user.target" ];
+          requires = [ "schleuder-init.service" ];
+          serviceConfig = commonServiceConfig // {
+            ExecStart = [ "${pkgs.schleuder}/bin/schleuder-api-daemon" ];
+          };
+        };
+        schleuder-weekly-key-maintenance = {
+          after = [ "local-fs.target" "network.target" ];
+          startAt = "weekly";
+          serviceConfig = commonServiceConfig // {
+            ExecStart = [
+              "${pkgs.schleuder}/bin/schleuder refresh_keys"
+              "${pkgs.schleuder}/bin/schleuder check_keys"
+            ];
+          };
+        };
+      };
+
+    environment.etc."schleuder/schleuder.yml" = lib.mkIf (cfg.extraSettingsFile == null) {
+      source = settingsFormat.generate "schleuder.yml" cfg.settings;
+    };
+    environment.etc."schleuder/list-defaults.yml".source = settingsFormat.generate "list-defaults.yml" cfg.listDefaults;
+
+    services.schleuder = {
+      #lists_dir = "/var/lib/schleuder.lists";
+      settings.filters_dir = lib.mkDefault "/var/lib/schleuder/filters";
+      settings.keyword_handlers_dir = lib.mkDefault "/var/lib/schleuder/keyword_handlers";
+    };
+  };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index fa88ad524070c..5e4325a1739fc 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -485,6 +485,7 @@ in {
   samba = handleTest ./samba.nix {};
   samba-wsdd = handleTest ./samba-wsdd.nix {};
   sanoid = handleTest ./sanoid.nix {};
+  schleuder = handleTest ./schleuder.nix {};
   sddm = handleTest ./sddm.nix {};
   seafile = handleTest ./seafile.nix {};
   searx = handleTest ./searx.nix {};
diff --git a/nixos/tests/schleuder.nix b/nixos/tests/schleuder.nix
new file mode 100644
index 0000000000000..a9e4cc325bc76
--- /dev/null
+++ b/nixos/tests/schleuder.nix
@@ -0,0 +1,128 @@
+let
+  certs = import ./common/acme/server/snakeoil-certs.nix;
+  domain = certs.domain;
+in
+import ./make-test-python.nix {
+  name = "schleuder";
+  nodes.machine = { pkgs, ... }: {
+    imports = [ ./common/user-account.nix ];
+    services.postfix = {
+      enable = true;
+      enableSubmission = true;
+      tlsTrustedAuthorities = "${certs.ca.cert}";
+      sslCert = "${certs.${domain}.cert}";
+      sslKey = "${certs.${domain}.key}";
+      inherit domain;
+      destination = [ domain ];
+      localRecipients = [ "root" "alice" "bob" ];
+    };
+    services.schleuder = {
+      enable = true;
+      # Don't do it like this in production! The point of this setting
+      # is to allow loading secrets from _outside_ the world-readable
+      # Nix store.
+      extraSettingsFile = pkgs.writeText "schleuder-api-keys.yml" ''
+        api:
+          valid_api_keys:
+            - fnord
+      '';
+      lists = [ "security@${domain}" ];
+      settings.api = {
+        tls_cert_file = "${certs.${domain}.cert}";
+        tls_key_file = "${certs.${domain}.key}";
+      };
+    };
+
+    environment.systemPackages = [
+      pkgs.gnupg
+      pkgs.msmtp
+      (pkgs.writeScriptBin "do-test" ''
+        #!${pkgs.runtimeShell}
+        set -exuo pipefail
+
+        # Generate a GPG key with no passphrase and export it
+        sudo -u alice gpg --passphrase-fd 0 --batch --yes --quick-generate-key 'alice@${domain}' rsa4096 sign,encr < <(echo)
+        sudo -u alice gpg --armor --export alice@${domain} > alice.asc
+        # Create a new mailing list with alice as the owner, and alice's key
+        schleuder-cli list new security@${domain} alice@${domain} alice.asc
+
+        # Send an email from a non-member of the list. Use --auto-from so we don't have to specify who it's from twice.
+        msmtp --auto-from security@${domain} --host=${domain} --port=25 --tls --tls-starttls <<EOF
+          Subject: really big security issue!!
+          From: root@${domain}
+
+          I found a big security problem!
+        EOF
+
+        # Wait for delivery
+        (set +o pipefail; journalctl -f -n 1000 -u postfix | grep -m 1 'delivered to maildir')
+
+        # There should be exactly one email
+        mail=(/var/spool/mail/alice/new/*)
+        [[ "''${#mail[@]}" = 1 ]]
+
+        # Find the fingerprint of the mailing list key
+        read list_key_fp address < <(schleuder-cli keys list security@${domain} | grep security@)
+        schleuder-cli keys export security@${domain} $list_key_fp > list.asc
+
+        # Import the key into alice's keyring, so we can verify it as well as decrypting
+        sudo -u alice gpg --import <list.asc
+        # And perform the decryption.
+        sudo -u alice gpg -d $mail >decrypted
+        # And check that the text matches.
+        grep "big security problem" decrypted
+      '')
+
+      # For debugging:
+      # pkgs.vim pkgs.openssl pkgs.sqliteinteractive
+    ];
+
+    security.pki.certificateFiles = [ certs.ca.cert ];
+
+    # Since we don't have internet here, use dnsmasq to provide MX records from /etc/hosts
+    services.dnsmasq = {
+      enable = true;
+      extraConfig = ''
+        selfmx
+      '';
+    };
+
+    networking.extraHosts = ''
+      127.0.0.1 ${domain}
+    '';
+
+    # schleuder-cli's config is not quite optimal in several ways:
+    # - A fingerprint _must_ be pinned, it doesn't even have an option
+    #   to trust the PKI
+    # - It compares certificate fingerprints rather than key
+    #   fingerprints, so renewals break the pin (though that's not
+    #   relevant for this test)
+    # - It compares them as strings, which means we need to match the
+    #   expected format exactly. This means removing the :s and
+    #   lowercasing it.
+    # Refs:
+    # https://0xacab.org/schleuder/schleuder-cli/-/issues/16
+    # https://0xacab.org/schleuder/schleuder-cli/-/blob/f8895b9f47083d8c7b99a2797c93f170f3c6a3c0/lib/schleuder-cli/helper.rb#L230-238
+    systemd.tmpfiles.rules = let cliconfig = pkgs.runCommand "schleuder-cli.yml"
+      {
+        nativeBuildInputs = [ pkgs.jq pkgs.openssl ];
+      } ''
+      fp=$(openssl x509 -in ${certs.${domain}.cert} -noout -fingerprint -sha256 | cut -d = -f 2 | tr -d : | tr 'A-Z' 'a-z')
+      cat > $out <<EOF
+      host: localhost
+      port: 4443
+      tls_fingerprint: "$fp"
+      api_key: fnord
+      EOF
+    ''; in
+      [
+        "L+ /root/.schleuder-cli/schleuder-cli.yml - - - - ${cliconfig}"
+      ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_until_succeeds("nc -z localhost 4443")
+    machine.succeed("do-test")
+  '';
+}