Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/roundcube: security improvements #77532

Merged
merged 3 commits into from
Jan 22, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 59 additions & 20 deletions nixos/modules/services/mail/roundcube.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ with lib;
let
cfg = config.services.roundcube;
fpm = config.services.phpfpm.pools.roundcube;
localDB = cfg.database.host == "localhost";
aanderse marked this conversation as resolved.
Show resolved Hide resolved
user = cfg.database.username;
in
{
options.services.roundcube = {
Expand Down Expand Up @@ -44,7 +46,10 @@ in
username = mkOption {
type = types.str;
default = "roundcube";
description = "Username for the postgresql connection";
description = ''
Username for the postgresql connection.
If <literal>database.host</literal> is set to <literal>localhost</literal>, a unix user and group of the same name will be created as well.
'';
};
host = mkOption {
type = types.str;
Expand All @@ -58,7 +63,12 @@ in
};
password = mkOption {
type = types.str;
description = "Password for the postgresql connection";
description = "Password for the postgresql connection. Do not use: the password will be stored world readable in the store; use <literal>passwordFile</literal> instead.";
default = "";
};
passwordFile = mkOption {
type = types.str;
description = "Password file for the postgresql connection. Must be readable by user <literal>nginx</literal>. Ignored if <literal>database.host</literal> is set to <literal>localhost</literal>, as peer authentication will be used.";
};
dbname = mkOption {
type = types.str;
Expand All @@ -83,14 +93,22 @@ in
};

config = mkIf cfg.enable {
# backward compatibility: if password is set but not passwordFile, make one.
services.roundcube.database.passwordFile = mkIf (!localDB && cfg.database.password != "") (mkDefault ("${pkgs.writeText "roundcube-password" cfg.database.password}"));
warnings = lib.optional (!localDB && cfg.database.password != "") "services.roundcube.database.password is deprecated and insecure; use services.roundcube.database.passwordFile instead";

environment.etc."roundcube/config.inc.php".text = ''
<?php

${lib.optionalString (!localDB) "$password = file_get_contents('${cfg.database.passwordFile}');"}

$config = array();
$config['db_dsnw'] = 'pgsql://${cfg.database.username}:${cfg.database.password}@${cfg.database.host}/${cfg.database.dbname}';
$config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
$config['log_driver'] = 'syslog';
$config['max_message_size'] = '25M';
$config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
$config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
$config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
${cfg.extraConfig}
'';

Expand All @@ -116,12 +134,26 @@ in
};
};

services.postgresql = mkIf (cfg.database.host == "localhost") {
services.postgresql = mkIf localDB {
enable = true;
ensureDatabases = [ cfg.database.dbname ];
ensureUsers = [ {
name = cfg.database.username;
ensurePermissions = {
"DATABASE ${cfg.database.username}" = "ALL PRIVILEGES";
};
} ];
};

users.users.${user} = mkIf localDB {
aanderse marked this conversation as resolved.
Show resolved Hide resolved
group = user;
isSystemUser = true;
createHome = false;
};
users.groups.${user} = mkIf localDB {};

services.phpfpm.pools.roundcube = {
user = "nginx";
user = if localDB then user else "nginx";
phpOptions = ''
error_log = 'stderr'
log_errors = on
Expand All @@ -143,32 +175,39 @@ in
};
systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];

systemd.services.roundcube-setup = let
pgSuperUser = config.services.postgresql.superUser;
in mkMerge [
systemd.services.roundcube-setup = mkMerge [
(mkIf (cfg.database.host == "localhost") {
requires = [ "postgresql.service" ];
after = [ "postgresql.service" ];
path = [ config.services.postgresql.package ];
})
{
wantedBy = [ "multi-user.target" ];
script = ''
mkdir -p /var/lib/roundcube
if [ ! -f /var/lib/roundcube/db-created ]; then
if [ "${cfg.database.host}" = "localhost" ]; then
${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create role ${cfg.database.username} with login password '${cfg.database.password}'";
${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create database ${cfg.database.dbname} with owner ${cfg.database.username}";
fi
PGPASSWORD="${cfg.database.password}" ${pkgs.postgresql}/bin/psql -U ${cfg.database.username} \
-f ${cfg.package}/SQL/postgres.initial.sql \
-h ${cfg.database.host} ${cfg.database.dbname}
touch /var/lib/roundcube/db-created
script = let
psql = "${lib.optionalString (!localDB) "PGPASSFILE=${cfg.database.passwordFile}"} ${pkgs.postgresql}/bin/psql ${lib.optionalString (!localDB) "-h ${cfg.database.host} -U ${cfg.database.username} "} ${cfg.database.dbname}";
in
''
version="$(${psql} -t <<< "select value from system where name = 'roundcube-version';" || true)"
if ! (grep -E '[a-zA-Z0-9]' <<< "$version"); then
${psql} -f ${cfg.package}/SQL/postgres.initial.sql
fi

if [ ! -f /var/lib/roundcube/des_key ]; then
base64 /dev/urandom | head -c 24 > /var/lib/roundcube/des_key;
# we need to log out everyone in case change the des_key
# from the default when upgrading from nixos 19.09
${psql} <<< 'TRUNCATE TABLE session;'
fi

${pkgs.php}/bin/php ${cfg.package}/bin/update.sh
'';
serviceConfig.Type = "oneshot";
serviceConfig = {
Type = "oneshot";
StateDirectory = "roundcube";
User = if localDB then user else "nginx";
# so that the des_key is not world readable
StateDirectoryMode = "0700";
};
}
];
};
Expand Down