diff --git a/checks/matrix-synapse/default.nix b/checks/matrix-synapse/default.nix index 0e879e3d..10395d26 100644 --- a/checks/matrix-synapse/default.nix +++ b/checks/matrix-synapse/default.nix @@ -4,7 +4,12 @@ name = "matrix-synapse"; nodes.machine = - { self, lib, ... }: + { + config, + self, + lib, + ... + }: { imports = [ self.clanModules.matrix-synapse @@ -12,17 +17,48 @@ { clanCore.machineName = "machine"; clanCore.clanDir = ./.; - clan.matrix-synapse = { - domain = "clan.test"; - }; - } - { - # secret override - clanCore.facts.services.matrix-synapse.secret.synapse-registration_shared_secret.path = "${./synapse-registration_shared_secret}"; + services.nginx.virtualHosts."matrix.clan.test" = { enableACME = lib.mkForce false; forceSSL = lib.mkForce false; }; + clan.matrix-synapse.domain = "clan.test"; + clan.matrix-synapse.users.admin.admin = true; + clan.matrix-synapse.users.someuser = { }; + + clanCore.facts.secretStore = "vm"; + + # because we use systemd-tmpfiles to copy the secrets, we need to a seperate systemd-tmpfiles call to provison them. + boot.postBootCommands = "${config.systemd.package}/bin/systemd-tmpfiles --create /etc/tmpfiles.d/00-vmsecrets.conf"; + + systemd.tmpfiles.settings."00-vmsecrets" = { + # run before 00-nixos.conf + "/etc/secrets" = { + d.mode = "0700"; + z.mode = "0700"; + }; + "/etc/secrets/synapse-registration_shared_secret" = { + f.argument = "registration_shared_secret: supersecret"; + z = { + mode = "0400"; + user = "root"; + }; + }; + "/etc/secrets/matrix-password-admin" = { + f.argument = "matrix-password1"; + z = { + mode = "0400"; + user = "root"; + }; + }; + "/etc/secrets/matrix-password-someuser" = { + f.argument = "matrix-password2"; + z = { + mode = "0400"; + user = "root"; + }; + }; + }; } ]; }; @@ -31,6 +67,12 @@ machine.wait_for_unit("matrix-synapse") machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 8008") machine.succeed("${pkgs.curl}/bin/curl -Ssf -L http://localhost/_matrix/static/ -H 'Host: matrix.clan.test'") + + machine.systemctl("restart matrix-synapse >&2") # check if user creation is idempotent + machine.execute("journalctl -u matrix-synapse --no-pager >&2") + machine.wait_for_unit("matrix-synapse") + machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 8008") + machine.succeed("${pkgs.curl}/bin/curl -Ssf -L http://localhost/_matrix/static/ -H 'Host: matrix.clan.test'") ''; } ) diff --git a/clanModules/matrix-synapse/0001-register_new_matrix_user-add-password-file-flag.patch b/clanModules/matrix-synapse/0001-register_new_matrix_user-add-password-file-flag.patch new file mode 100644 index 00000000..39e9ea2c --- /dev/null +++ b/clanModules/matrix-synapse/0001-register_new_matrix_user-add-password-file-flag.patch @@ -0,0 +1,84 @@ +From df45634a92944dcab4edb02fb5e478911c58fdd6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= +Date: Tue, 11 Jun 2024 11:40:47 +0200 +Subject: [PATCH] register_new_matrix_user: add password-file flag +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +getpass in python exist on stdin to be a tty, hence we cannot just pipe +into register_new_matrix_user. --password-file instead works better and +it would also allow the use of stdin if /dev/stdin is passed. + +Signed-off-by: Jörg Thalheim +--- + debian/register_new_matrix_user.ronn | 6 +++++- + synapse/_scripts/register_new_matrix_user.py | 16 ++++++++++++++-- + 2 files changed, 19 insertions(+), 3 deletions(-) + +diff --git a/debian/register_new_matrix_user.ronn b/debian/register_new_matrix_user.ronn +index 0410b1f4c..e39bef448 100644 +--- a/debian/register_new_matrix_user.ronn ++++ b/debian/register_new_matrix_user.ronn +@@ -32,7 +32,11 @@ A sample YAML file accepted by `register_new_matrix_user` is described below: + + * `-p`, `--password`: + New password for user. Will prompt if omitted. Supplying the password +- on the command line is not recommended. Use the STDIN instead. ++ on the command line is not recommended. Use password-file if possible. ++ ++ * `--password-file`: ++ File containing the new password for user. Will prompt if omitted. ++ This is a more secure alternative to specifying the password on the command line. + + * `-a`, `--admin`: + Register new user as an admin. Will prompt if omitted. +diff --git a/synapse/_scripts/register_new_matrix_user.py b/synapse/_scripts/register_new_matrix_user.py +index 77a7129ee..f067e6832 100644 +--- a/synapse/_scripts/register_new_matrix_user.py ++++ b/synapse/_scripts/register_new_matrix_user.py +@@ -173,12 +173,18 @@ def main() -> None: + default=None, + help="Local part of the new user. Will prompt if omitted.", + ) +- parser.add_argument( ++ password_group = parser.add_mutually_exclusive_group() ++ password_group.add_argument( + "-p", + "--password", + default=None, + help="New password for user. Will prompt if omitted.", + ) ++ password_group.add_argument( ++ "--password-file", ++ default=None, ++ help="File containing the new password for user. Will prompt if omitted.", ++ ) + parser.add_argument( + "-t", + "--user_type", +@@ -247,6 +253,12 @@ def main() -> None: + print(_NO_SHARED_SECRET_OPTS_ERROR, file=sys.stderr) + sys.exit(1) + ++ password = "" ++ if args.password_file: ++ password = _read_file(args.password_file, "password-file").strip() ++ else: ++ password = args.password ++ + if args.server_url: + server_url = args.server_url + elif config is not None: +@@ -270,7 +282,7 @@ def main() -> None: + admin = args.admin + + register_new_user( +- args.user, args.password, server_url, secret, admin, args.user_type ++ args.user, password, server_url, secret, admin, args.user_type + ) + + +-- +2.44.1 + diff --git a/clanModules/matrix-synapse/default.nix b/clanModules/matrix-synapse/default.nix index 6a06708d..3c94d2e0 100644 --- a/clanModules/matrix-synapse/default.nix +++ b/clanModules/matrix-synapse/default.nix @@ -16,16 +16,58 @@ let > $out/config.json < ${pkgs.element-web}/config.json ln -s $out/config.json $out/config.${nginx-vhost}.json ''; + + # FIXME: This was taken from upstream. Drop this when our patch is upstream + synapseCfg = config.services.matrix-synapse; + wantedExtras = + synapseCfg.extras + ++ lib.optional (synapseCfg.settings ? oidc_providers) "oidc" + ++ lib.optional (synapseCfg.settings ? jwt_config) "jwt" + ++ lib.optional (synapseCfg.settings ? saml2_config) "saml2" + ++ lib.optional (synapseCfg.settings ? redis) "redis" + ++ lib.optional (synapseCfg.settings ? sentry) "sentry" + ++ lib.optional (synapseCfg.settings ? user_directory) "user-search" + ++ lib.optional (synapseCfg.settings.url_preview_enabled) "url-preview" + ++ lib.optional (synapseCfg.settings.database.name == "psycopg2") "postgres"; + in { + options.services.matrix-synapse.package = lib.mkOption { readOnly = false; }; options.clan.matrix-synapse = { domain = lib.mkOption { type = lib.types.str; description = "The domain name of the matrix server"; example = "example.com"; }; + users = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + name = lib.mkOption { + type = lib.types.str; + default = name; + description = "The name of the user"; + }; + + admin = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether the user should be an admin"; + }; + }; + } + ) + ); + description = "A list of users. Not that only new users will be created and existing ones are not modified."; + example.alice = { + admin = true; + }; + }; }; imports = [ + ../postgresql (lib.mkRemovedOptionModule [ "clan" "matrix-synapse" @@ -36,6 +78,18 @@ in ]; config = { services.matrix-synapse = { + package = lib.mkForce ( + pkgs.matrix-synapse.override { + matrix-synapse-unwrapped = pkgs.matrix-synapse.unwrapped.overrideAttrs (_old: { + doInstallCheck = false; # too slow, nixpkgs maintainer already run this. + # see: https://github.com/element-hq/synapse/pull/17294 + patches = [ ./0001-register_new_matrix_user-add-password-file-flag.patch ]; + }); + extras = wantedExtras; + plugins = synapseCfg.plugins; + } + ); + enable = true; settings = { server_name = cfg.domain; @@ -70,7 +124,10 @@ in }; extraConfigFiles = [ "/run/synapse-registration-shared-secret.yaml" ]; }; - systemd.tmpfiles.settings."synapse" = { + + security.wrappers = lib.mkForce { }; # unsupported in unprivileged containers + + systemd.tmpfiles.settings."01-matrix" = { "/run/synapse-registration-shared-secret.yaml" = { C.argument = config.clanCore.facts.services.matrix-synapse.secret.synapse-registration_shared_secret.path; @@ -90,16 +147,64 @@ in OWNER = "matrix-synapse"; }; - clanCore.facts.services."matrix-synapse" = { - secret."synapse-registration_shared_secret" = { }; - generator.path = with pkgs; [ - coreutils - pwgen - ]; - generator.script = '' - echo "registration_shared_secret: $(pwgen -s 32 1)" > "$secrets"/synapse-registration_shared_secret - ''; - }; + clanCore.facts.services = + { + "matrix-synapse" = { + secret."synapse-registration_shared_secret" = { }; + generator.path = with pkgs; [ + coreutils + pwgen + ]; + generator.script = '' + echo "registration_shared_secret: $(pwgen -s 32 1)" > "$secrets"/synapse-registration_shared_secret + ''; + }; + } + // lib.mapAttrs' ( + name: user: + lib.nameValuePair "matrix-password-${user.name}" { + secret."matrix-password-${user.name}" = { }; + generator.path = with pkgs; [ + coreutils + pwgen + ]; + generator.script = '' + xkcdpass -n 4 -d - > "$secrets"/${lib.escapeShellArg "matrix-password-${user.name}"} + ''; + } + ) cfg.users; + + systemd.services.matrix-synapse = + let + usersScript = + '' + while ! ${pkgs.netcat}/bin/nc -z -v ::1 8008; do + if ! kill -0 "$MAINPID"; then exit 1; fi + sleep 1; + done + + headers=$(mktemp) + trap 'rm -f "$headers"' EXIT + + cat > "$headers" <&2; then + /run/current-system/sw/bin/matrix-synapse-register_new_matrix_user --password-file ${ + config.clanCore.facts.services."matrix-password-${user.name}".secret."matrix-password-${user.name}".path + } --user "${user.name}" ${if user.admin then "--admin" else "--no-admin"} + fi + '') (lib.attrValues cfg.users); + in + { + path = [ pkgs.curl ]; + serviceConfig.ExecStartPost = [ + (''+${pkgs.writeShellScript "matrix-synapse-create-users" usersScript}'') + ]; + }; services.nginx = { enable = true;