From 2e1c865ebffd6ee9d05360aeae3200426b779bab Mon Sep 17 00:00:00 2001 From: stuebinm Date: Mon, 15 Mar 2021 17:04:27 +0100 Subject: [PATCH] Patch mattermost module to allow secrets outside the nix store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a custom mattermost module (`services.mattermost-patched`) which is identical to the one in nixpkgs except that it also has an option `secretConfig`, which should point to a file containing all secret parts of the mattermost config (e.g. mailserver password), and which is merged with the config genereated from the module at startup time. This allows us to have a (almost) immutable config without having secrets in the nix store. Before deploying this, add a secrets file at /var/lib/mattermost/screts.json (on the host — there is a bind mount in place so we won't have to enter the container each time to change something). --- hosts/hainich/services/mattermost.nix | 18 +- modules/mattermost.nix | 257 ++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 modules/mattermost.nix diff --git a/hosts/hainich/services/mattermost.nix b/hosts/hainich/services/mattermost.nix index 76e4914..eb3fb88 100644 --- a/hosts/hainich/services/mattermost.nix +++ b/hosts/hainich/services/mattermost.nix @@ -7,14 +7,28 @@ hostAddress6 = "fd00::42:24"; localAddress6 = "fd00::42:25"; + bindMounts."/secrets" = { + hostPath = "/var/lib/mattermost/"; + isReadOnly = true; + }; + config = {pkgs, config, ...}: { - services.mattermost = { + + # have to import these here, since container's dont + # inherit imports of their environment. + imports = [ ../../../modules/mattermost.nix ]; + + # couldn't figure out how to actually overwrite modules, so now + # there's two mattermost modules ... + services.mattermost-patched = { enable = true; siteUrl = "https://mattermost.infra4future.de"; siteName = "Mattermost - Blabla for Future"; listenAddress = "[::]:3000"; mutableConfig = false; + secretConfig = "/secrets/secrets.json"; + extraConfig = { ServiceSettings = { TrustedProxyIPHeader = [ "X-Forwarded-For" "X-Real-Ip" ]; @@ -84,7 +98,6 @@ FeedbackOrganization = "∆infra4future.de"; EnableSMTPAuth = true; SMTPUsername = "noreply@infra4future.de"; - SMTPPassword = ""; # TODO: how to best read in these? SMTPServer = "mail.hacc.space"; }; RateLimitSettings.Enable = false; @@ -103,7 +116,6 @@ AnnouncementSettings.EnableBanner = false; GitLabSettings = { Enable = true; - Secret = ""; # TODO: how to do secrets? Id = "mattermost"; Scope = ""; AuthEndpoint = "https://auth.infra4future.de/auth/realms/forfuture/protocol/openid-connect/auth"; diff --git a/modules/mattermost.nix b/modules/mattermost.nix new file mode 100644 index 0000000..0d35f08 --- /dev/null +++ b/modules/mattermost.nix @@ -0,0 +1,257 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + + cfg = config.services.mattermost-patched; + + defaultConfig = builtins.fromJSON (builtins.replaceStrings [ "\\u0026" ] [ "&" ] + (readFile "${pkgs.mattermost}/config/config.json") + ); + + database = "postgres://${cfg.localDatabaseUser}:${cfg.localDatabasePassword}@localhost:5432/${cfg.localDatabaseName}?sslmode=disable&connect_timeout=10"; + + mattermostConf = foldl recursiveUpdate defaultConfig + [ { ServiceSettings.SiteURL = cfg.siteUrl; + ServiceSettings.ListenAddress = cfg.listenAddress; + TeamSettings.SiteName = cfg.siteName; + SqlSettings.DriverName = "postgres"; + SqlSettings.DataSource = database; + } + cfg.extraConfig + ]; + + mattermostConfJSON = pkgs.writeText "mattermost-config-raw.json" (builtins.toJSON mattermostConf); + +in + +{ + options = { + services.mattermost-patched = { + enable = mkEnableOption "Mattermost chat server"; + + statePath = mkOption { + type = types.str; + default = "/var/lib/mattermost"; + description = "Mattermost working directory"; + }; + + siteUrl = mkOption { + type = types.str; + example = "https://chat.example.com"; + description = '' + URL this Mattermost instance is reachable under, without trailing slash. + ''; + }; + + siteName = mkOption { + type = types.str; + default = "Mattermost"; + description = "Name of this Mattermost site."; + }; + + listenAddress = mkOption { + type = types.str; + default = ":8065"; + example = "[::1]:8065"; + description = '' + Address and port this Mattermost instance listens to. + ''; + }; + + mutableConfig = mkOption { + type = types.bool; + default = false; + description = '' + Whether the Mattermost config.json is writeable by Mattermost. + + Most of the settings can be edited in the system console of + Mattermost if this option is enabled. A template config using + the options specified in services.mattermost will be generated + but won't be overwritten on changes or rebuilds. + + If this option is disabled, changes in the system console won't + be possible (default). If an config.json is present, it will be + overwritten! + ''; + }; + + extraConfig = mkOption { + type = types.attrs; + default = { }; + description = '' + Addtional configuration options as Nix attribute set in config.json schema. + ''; + }; + + secretConfig = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Path to a json file containing secret config values, which should + not be written into the Nix store. If it is not null (the default) + and mutableConfig is set to false, then the mattermost service will + join the file at this path into its config. + + Note that this file cannot be used to overwrite values already + specified by the other options of this module. + ''; + }; + + localDatabaseCreate = mkOption { + type = types.bool; + default = true; + description = '' + Create a local PostgreSQL database for Mattermost automatically. + ''; + }; + + localDatabaseName = mkOption { + type = types.str; + default = "mattermost"; + description = '' + Local Mattermost database name. + ''; + }; + + localDatabaseUser = mkOption { + type = types.str; + default = "mattermost"; + description = '' + Local Mattermost database username. + ''; + }; + + localDatabasePassword = mkOption { + type = types.str; + default = "mmpgsecret"; + description = '' + Password for local Mattermost database user. + ''; + }; + + user = mkOption { + type = types.str; + default = "mattermost"; + description = '' + User which runs the Mattermost service. + ''; + }; + + group = mkOption { + type = types.str; + default = "mattermost"; + description = '' + Group which runs the Mattermost service. + ''; + }; + + matterircd = { + enable = mkEnableOption "Mattermost IRC bridge"; + parameters = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "-mmserver chat.example.com" "-bind [::]:6667" ]; + description = '' + Set commandline parameters to pass to matterircd. See + https://github.com/42wim/matterircd#usage for more information. + ''; + }; + }; + }; + }; + + config = mkMerge [ + (mkIf cfg.enable { + users.users = optionalAttrs (cfg.user == "mattermost") { + mattermost = { + group = cfg.group; + uid = config.ids.uids.mattermost; + home = cfg.statePath; + }; + }; + + users.groups = optionalAttrs (cfg.group == "mattermost") { + mattermost.gid = config.ids.gids.mattermost; + }; + + services.postgresql.enable = cfg.localDatabaseCreate; + + # The systemd service will fail to execute the preStart hook + # if the WorkingDirectory does not exist + system.activationScripts.mattermost = '' + mkdir -p ${cfg.statePath} + ''; + + systemd.services.mattermost = { + description = "Mattermost chat service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "postgresql.service" ]; + + preStart = '' + mkdir -p ${cfg.statePath}/{data,config,logs} + ln -sf ${pkgs.mattermost}/{bin,fonts,i18n,templates,client} ${cfg.statePath} + '' + lib.optionalString (!cfg.mutableConfig) '' + rm -f ${cfg.statePath}/config/config.json + '' + (if cfg.secretConfig == null + then '' + cp ${mattermostConfJSON} ${cfg.statePath}/config/config.json + '' + else '' + ${pkgs.jq}/bin/jq -s ".[1] * .[0]" ${cfg.secretConfig} ${mattermostConfJSON} > ${cfg.statePath}/config/config.json + '') + + '' + ${pkgs.mattermost}/bin/mattermost config migrate ${cfg.statePath}/config/config.json ${database} + '' + lib.optionalString cfg.mutableConfig '' + if ! test -e "${cfg.statePath}/config/.initial-created"; then + rm -f ${cfg.statePath}/config/config.json + cp ${mattermostConfJSON} ${cfg.statePath}/config/config.json + touch ${cfg.statePath}/config/.initial-created + fi + '' + lib.optionalString cfg.localDatabaseCreate '' + if ! test -e "${cfg.statePath}/.db-created"; then + ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \ + ${config.services.postgresql.package}/bin/psql postgres -c \ + "CREATE ROLE ${cfg.localDatabaseUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${cfg.localDatabasePassword}'" + ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \ + ${config.services.postgresql.package}/bin/createdb \ + --owner ${cfg.localDatabaseUser} ${cfg.localDatabaseName} + touch ${cfg.statePath}/.db-created + fi + '' + '' + chown ${cfg.user}:${cfg.group} -R ${cfg.statePath} + chmod u+rw,g+r,o-rwx -R ${cfg.statePath} + ''; + + serviceConfig = { + PermissionsStartOnly = true; + User = cfg.user; + Group = cfg.group; + ExecStart = "${pkgs.mattermost}/bin/mattermost" + + (lib.optionalString (!cfg.mutableConfig) " -c ${database}"); + WorkingDirectory = "${cfg.statePath}"; + Restart = "always"; + RestartSec = "10"; + LimitNOFILE = "49152"; + }; + unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service"; + }; + }) + (mkIf cfg.matterircd.enable { + systemd.services.matterircd = { + description = "Mattermost IRC bridge service"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "nobody"; + Group = "nogroup"; + ExecStart = "${pkgs.matterircd}/bin/matterircd ${concatStringsSep " " cfg.matterircd.parameters}"; + WorkingDirectory = "/tmp"; + PrivateTmp = true; + Restart = "always"; + RestartSec = "5"; + }; + }; + }) + ]; +}