Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)

nixos/schleuder: init module and accompanying test

Co-Authored-By: Martin Weinelt <hexa@darmstadt.ccc.de>
Co-Authored-By: Cole Helbling <cole.helbling@determinate.systems>

+296
+1
nixos/modules/module-list.nix
··· 516 516 ./services/mail/rspamd.nix 517 517 ./services/mail/rss2email.nix 518 518 ./services/mail/roundcube.nix 519 + ./services/mail/schleuder.nix 519 520 ./services/mail/sympa.nix 520 521 ./services/mail/nullmailer.nix 521 522 ./services/matrix/appservice-discord.nix
+162
nixos/modules/services/mail/schleuder.nix
··· 1 + { config, pkgs, lib, ... }: 2 + let 3 + cfg = config.services.schleuder; 4 + settingsFormat = pkgs.formats.yaml { }; 5 + postfixMap = entries: lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${value}") entries); 6 + writePostfixMap = name: entries: pkgs.writeText name (postfixMap entries); 7 + configScript = pkgs.writeScript "schleuder-cfg" '' 8 + #!${pkgs.runtimeShell} 9 + set -exuo pipefail 10 + umask 0077 11 + ${pkgs.yq}/bin/yq \ 12 + --slurpfile overrides <(${pkgs.yq}/bin/yq . <${lib.escapeShellArg cfg.extraSettingsFile}) \ 13 + < ${settingsFormat.generate "schleuder.yml" cfg.settings} \ 14 + '. * $overrides[0]' \ 15 + > /etc/schleuder/schleuder.yml 16 + chown schleuder: /etc/schleuder/schleuder.yml 17 + ''; 18 + in 19 + { 20 + options.services.schleuder = { 21 + enable = lib.mkEnableOption "Schleuder secure remailer"; 22 + enablePostfix = lib.mkEnableOption "automatic postfix integration" // { default = true; }; 23 + lists = lib.mkOption { 24 + description = '' 25 + List of list addresses that should be handled by Schleuder. 26 + 27 + Note that this is only handled by the postfix integration, and 28 + the setup of the lists, their members and their keys has to be 29 + performed separately via schleuder's API, using a tool such as 30 + schleuder-cli. 31 + ''; 32 + type = lib.types.listOf lib.types.str; 33 + default = [ ]; 34 + example = [ "widget-team@example.com" "security@example.com" ]; 35 + }; 36 + /* maybe one day.... 37 + domains = lib.mkOption { 38 + description = "Domains for which all mail should be handled by Schleuder."; 39 + type = lib.types.listOf lib.types.str; 40 + default = []; 41 + example = ["securelists.example.com"]; 42 + }; 43 + */ 44 + settings = lib.mkOption { 45 + description = '' 46 + Settings for schleuder.yml. 47 + 48 + Check the <link xlink:href="https://0xacab.org/schleuder/schleuder/blob/master/etc/schleuder.yml">example configuration</link> for possible values. 49 + ''; 50 + type = lib.types.submodule { 51 + freeformType = settingsFormat.type; 52 + options.keyserver = lib.mkOption { 53 + type = lib.types.str; 54 + description = '' 55 + Key server from which to fetch and update keys. 56 + 57 + Note that NixOS uses a different default from upstream, since the upstream default sks-keyservers.net is deprecated. 58 + ''; 59 + default = "keys.openpgp.org"; 60 + }; 61 + }; 62 + default = { }; 63 + }; 64 + extraSettingsFile = lib.mkOption { 65 + description = "YAML file to merge into the schleuder config at runtime. This can be used for secrets such as API keys."; 66 + type = lib.types.nullOr lib.types.path; 67 + default = null; 68 + }; 69 + listDefaults = lib.mkOption { 70 + description = '' 71 + Default settings for lists (list-defaults.yml). 72 + 73 + Check the <link xlink:href="https://0xacab.org/schleuder/schleuder/-/blob/master/etc/list-defaults.yml">example configuration</link> for possible values. 74 + ''; 75 + type = settingsFormat.type; 76 + default = { }; 77 + }; 78 + }; 79 + config = lib.mkIf cfg.enable { 80 + assertions = [ 81 + { 82 + assertion = !(cfg.settings.api ? valid_api_keys); 83 + message = '' 84 + 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. 85 + ''; 86 + } 87 + { 88 + assertion = !(lib.any (db: db ? password) (lib.attrValues cfg.settings.database or {})); 89 + message = '' 90 + 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. 91 + ''; 92 + } 93 + ]; 94 + users.users.schleuder.isSystemUser = true; 95 + users.users.schleuder.group = "schleuder"; 96 + users.groups.schleuder = {}; 97 + environment.systemPackages = [ 98 + pkgs.schleuder-cli 99 + ]; 100 + services.postfix = lib.mkIf cfg.enablePostfix { 101 + extraMasterConf = '' 102 + schleuder unix - n n - - pipe 103 + flags=DRhu user=schleuder argv=/${pkgs.schleuder}/bin/schleuder work ''${recipient} 104 + ''; 105 + transport = lib.mkIf (cfg.lists != [ ]) (postfixMap (lib.genAttrs cfg.lists (_: "schleuder:"))); 106 + extraConfig = '' 107 + schleuder_destination_recipient_limit = 1 108 + ''; 109 + # review: does this make sense? 110 + localRecipients = lib.mkIf (cfg.lists != [ ]) cfg.lists; 111 + }; 112 + systemd.services = let commonServiceConfig = { 113 + # We would have liked to use DynamicUser, but since the default 114 + # database is SQLite and lives in StateDirectory, and that same 115 + # database needs to be readable from the postfix service, this 116 + # isn't trivial to do. 117 + User = "schleuder"; 118 + StateDirectory = "schleuder"; 119 + StateDirectoryMode = "0700"; 120 + }; in 121 + { 122 + schleuder-init = { 123 + serviceConfig = commonServiceConfig // { 124 + ExecStartPre = lib.mkIf (cfg.extraSettingsFile != null) [ 125 + "+${configScript}" 126 + ]; 127 + ExecStart = [ "${pkgs.schleuder}/bin/schleuder install" ]; 128 + Type = "oneshot"; 129 + }; 130 + }; 131 + schleuder-api-daemon = { 132 + after = [ "local-fs.target" "network.target" "schleuder-init.service" ]; 133 + wantedBy = [ "multi-user.target" ]; 134 + requires = [ "schleuder-init.service" ]; 135 + serviceConfig = commonServiceConfig // { 136 + ExecStart = [ "${pkgs.schleuder}/bin/schleuder-api-daemon" ]; 137 + }; 138 + }; 139 + schleuder-weekly-key-maintenance = { 140 + after = [ "local-fs.target" "network.target" ]; 141 + startAt = "weekly"; 142 + serviceConfig = commonServiceConfig // { 143 + ExecStart = [ 144 + "${pkgs.schleuder}/bin/schleuder refresh_keys" 145 + "${pkgs.schleuder}/bin/schleuder check_keys" 146 + ]; 147 + }; 148 + }; 149 + }; 150 + 151 + environment.etc."schleuder/schleuder.yml" = lib.mkIf (cfg.extraSettingsFile == null) { 152 + source = settingsFormat.generate "schleuder.yml" cfg.settings; 153 + }; 154 + environment.etc."schleuder/list-defaults.yml".source = settingsFormat.generate "list-defaults.yml" cfg.listDefaults; 155 + 156 + services.schleuder = { 157 + #lists_dir = "/var/lib/schleuder.lists"; 158 + settings.filters_dir = lib.mkDefault "/var/lib/schleuder/filters"; 159 + settings.keyword_handlers_dir = lib.mkDefault "/var/lib/schleuder/keyword_handlers"; 160 + }; 161 + }; 162 + }
+1
nixos/tests/all-tests.nix
··· 485 485 samba = handleTest ./samba.nix {}; 486 486 samba-wsdd = handleTest ./samba-wsdd.nix {}; 487 487 sanoid = handleTest ./sanoid.nix {}; 488 + schleuder = handleTest ./schleuder.nix {}; 488 489 sddm = handleTest ./sddm.nix {}; 489 490 seafile = handleTest ./seafile.nix {}; 490 491 searx = handleTest ./searx.nix {};
+128
nixos/tests/schleuder.nix
··· 1 + let 2 + certs = import ./common/acme/server/snakeoil-certs.nix; 3 + domain = certs.domain; 4 + in 5 + import ./make-test-python.nix { 6 + name = "schleuder"; 7 + nodes.machine = { pkgs, ... }: { 8 + imports = [ ./common/user-account.nix ]; 9 + services.postfix = { 10 + enable = true; 11 + enableSubmission = true; 12 + tlsTrustedAuthorities = "${certs.ca.cert}"; 13 + sslCert = "${certs.${domain}.cert}"; 14 + sslKey = "${certs.${domain}.key}"; 15 + inherit domain; 16 + destination = [ domain ]; 17 + localRecipients = [ "root" "alice" "bob" ]; 18 + }; 19 + services.schleuder = { 20 + enable = true; 21 + # Don't do it like this in production! The point of this setting 22 + # is to allow loading secrets from _outside_ the world-readable 23 + # Nix store. 24 + extraSettingsFile = pkgs.writeText "schleuder-api-keys.yml" '' 25 + api: 26 + valid_api_keys: 27 + - fnord 28 + ''; 29 + lists = [ "security@${domain}" ]; 30 + settings.api = { 31 + tls_cert_file = "${certs.${domain}.cert}"; 32 + tls_key_file = "${certs.${domain}.key}"; 33 + }; 34 + }; 35 + 36 + environment.systemPackages = [ 37 + pkgs.gnupg 38 + pkgs.msmtp 39 + (pkgs.writeScriptBin "do-test" '' 40 + #!${pkgs.runtimeShell} 41 + set -exuo pipefail 42 + 43 + # Generate a GPG key with no passphrase and export it 44 + sudo -u alice gpg --passphrase-fd 0 --batch --yes --quick-generate-key 'alice@${domain}' rsa4096 sign,encr < <(echo) 45 + sudo -u alice gpg --armor --export alice@${domain} > alice.asc 46 + # Create a new mailing list with alice as the owner, and alice's key 47 + schleuder-cli list new security@${domain} alice@${domain} alice.asc 48 + 49 + # 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. 50 + msmtp --auto-from security@${domain} --host=${domain} --port=25 --tls --tls-starttls <<EOF 51 + Subject: really big security issue!! 52 + From: root@${domain} 53 + 54 + I found a big security problem! 55 + EOF 56 + 57 + # Wait for delivery 58 + (set +o pipefail; journalctl -f -n 1000 -u postfix | grep -m 1 'delivered to maildir') 59 + 60 + # There should be exactly one email 61 + mail=(/var/spool/mail/alice/new/*) 62 + [[ "''${#mail[@]}" = 1 ]] 63 + 64 + # Find the fingerprint of the mailing list key 65 + read list_key_fp address < <(schleuder-cli keys list security@${domain} | grep security@) 66 + schleuder-cli keys export security@${domain} $list_key_fp > list.asc 67 + 68 + # Import the key into alice's keyring, so we can verify it as well as decrypting 69 + sudo -u alice gpg --import <list.asc 70 + # And perform the decryption. 71 + sudo -u alice gpg -d $mail >decrypted 72 + # And check that the text matches. 73 + grep "big security problem" decrypted 74 + '') 75 + 76 + # For debugging: 77 + # pkgs.vim pkgs.openssl pkgs.sqliteinteractive 78 + ]; 79 + 80 + security.pki.certificateFiles = [ certs.ca.cert ]; 81 + 82 + # Since we don't have internet here, use dnsmasq to provide MX records from /etc/hosts 83 + services.dnsmasq = { 84 + enable = true; 85 + extraConfig = '' 86 + selfmx 87 + ''; 88 + }; 89 + 90 + networking.extraHosts = '' 91 + 127.0.0.1 ${domain} 92 + ''; 93 + 94 + # schleuder-cli's config is not quite optimal in several ways: 95 + # - A fingerprint _must_ be pinned, it doesn't even have an option 96 + # to trust the PKI 97 + # - It compares certificate fingerprints rather than key 98 + # fingerprints, so renewals break the pin (though that's not 99 + # relevant for this test) 100 + # - It compares them as strings, which means we need to match the 101 + # expected format exactly. This means removing the :s and 102 + # lowercasing it. 103 + # Refs: 104 + # https://0xacab.org/schleuder/schleuder-cli/-/issues/16 105 + # https://0xacab.org/schleuder/schleuder-cli/-/blob/f8895b9f47083d8c7b99a2797c93f170f3c6a3c0/lib/schleuder-cli/helper.rb#L230-238 106 + systemd.tmpfiles.rules = let cliconfig = pkgs.runCommand "schleuder-cli.yml" 107 + { 108 + nativeBuildInputs = [ pkgs.jq pkgs.openssl ]; 109 + } '' 110 + fp=$(openssl x509 -in ${certs.${domain}.cert} -noout -fingerprint -sha256 | cut -d = -f 2 | tr -d : | tr 'A-Z' 'a-z') 111 + cat > $out <<EOF 112 + host: localhost 113 + port: 4443 114 + tls_fingerprint: "$fp" 115 + api_key: fnord 116 + EOF 117 + ''; in 118 + [ 119 + "L+ /root/.schleuder-cli/schleuder-cli.yml - - - - ${cliconfig}" 120 + ]; 121 + }; 122 + 123 + testScript = '' 124 + machine.wait_for_unit("multi-user.target") 125 + machine.wait_until_succeeds("nc -z localhost 4443") 126 + machine.succeed("do-test") 127 + ''; 128 + }
+4
pkgs/tools/security/schleuder/default.nix
··· 3 3 , ruby 4 4 , bundlerUpdateScript 5 5 , defaultGemConfig 6 + , nixosTests 6 7 }: 7 8 8 9 bundlerApp { ··· 18 19 ]; 19 20 20 21 passthru.updateScript = bundlerUpdateScript "schleuder"; 22 + passthru.tests = { 23 + inherit (nixosTests) schleuder; 24 + }; 21 25 22 26 meta = with lib; { 23 27 description = "Schleuder is an encrypting mailing list manager with remailing-capabilities";