pumpio service: don't keep secrets in nix store

Added extra config options to allow reading passwords from file rather
than the world-readable nix store.

The full config.json file is created at service startup.

Relevant to #18881

Changed files
+149 -70
nixos
modules
services
+23
nixos/modules/services/web-apps/pump.io-configure.js
···
+
var fs = require('fs');
+
+
var opts = JSON.parse(fs.readFileSync("/dev/stdin").toString());
+
var config = opts.config;
+
+
var readSecret = function(filename) {
+
return fs.readFileSync(filename).toString().trim();
+
};
+
+
if (opts.secretFile) {
+
config.secret = readSecret(opts.secretFile);
+
}
+
if (opts.dbPasswordFile) {
+
config.params.dbpass = readSecret(opts.dbPasswordFile);
+
}
+
if (opts.smtpPasswordFile) {
+
config.smtppass = readSecret(opts.smtpPasswordFile);
+
}
+
if (opts.spamClientSecretFile) {
+
config.spamclientsecret = readSecret(opts.opts.spamClientSecretFile);
+
}
+
+
fs.writeFileSync(opts.outputFile, JSON.stringify(config));
+126 -70
nixos/modules/services/web-apps/pump.io.nix
···
let
cfg = config.services.pumpio;
dataDir = "/var/lib/pump.io";
+
runDir = "/run/pump.io";
user = "pumpio";
+
optionalSet = condition: value: if condition then value else {};
+
+
configScript = ./pump.io-configure.js;
configOptions = {
-
driver = if cfg.driver == "disk" then null else cfg.driver;
-
params = ({ } //
-
(if cfg.driver == "disk" then {
-
dir = dataDir;
-
} else { }) //
-
(if cfg.driver == "mongodb" || cfg.driver == "redis" then {
-
host = cfg.dbHost;
-
port = cfg.dbPort;
-
dbname = cfg.dbName;
-
dbuser = cfg.dbUser;
-
dbpass = cfg.dbPassword;
-
} else { }) //
-
(if cfg.driver == "memcached" then {
-
host = cfg.dbHost;
-
port = cfg.dbPort;
-
} else { }) //
-
cfg.driverParams);
+
outputFile = "${runDir}/config.json";
+
config =
+
(optionalSet (cfg.driver != "disk") {
+
driver = cfg.driver;
+
}) //
+
{
+
params = (optionalSet (cfg.driver == "disk") { dir = dataDir; }) //
+
(optionalSet (cfg.driver == "mongodb" || cfg.driver == "redis") {
+
host = cfg.dbHost;
+
port = cfg.dbPort;
+
dbname = cfg.dbName;
+
dbuser = cfg.dbUser;
+
dbpass = cfg.dbPassword;
+
}) //
+
(optionalSet (cfg.driver == "memcached") {
+
host = cfg.dbHost;
+
port = cfg.dbPort;
+
}) // cfg.driverParams;
+
secret = cfg.secret;
-
secret = cfg.secret;
-
-
address = cfg.address;
-
port = cfg.port;
-
-
noweb = false;
-
urlPort = cfg.urlPort;
-
hostname = cfg.hostname;
-
favicon = cfg.favicon;
+
address = cfg.address;
+
port = cfg.port;
-
site = cfg.site;
-
owner = cfg.owner;
-
ownerURL = cfg.ownerURL;
+
noweb = false;
+
urlPort = cfg.urlPort;
+
hostname = cfg.hostname;
+
favicon = cfg.favicon;
-
key = cfg.sslKey;
-
cert = cfg.sslCert;
-
bounce = false;
+
site = cfg.site;
+
owner = cfg.owner;
+
ownerURL = cfg.ownerURL;
-
spamhost = cfg.spamHost;
-
spamclientid = cfg.spamClientId;
-
spamclientsecret = cfg.spamClientSecret;
+
key = cfg.sslKey;
+
cert = cfg.sslCert;
+
bounce = false;
-
requireEmail = cfg.requireEmail;
-
smtpserver = cfg.smtpHost;
-
smtpport = cfg.smtpPort;
-
smtpuser = cfg.smtpUser;
-
smtppass = cfg.smtpPassword;
-
smtpusessl = cfg.smtpUseSSL;
-
smtpfrom = cfg.smtpFrom;
+
spamhost = cfg.spamHost;
+
spamclientid = cfg.spamClientId;
+
spamclientsecret = cfg.spamClientSecret;
-
nologger = false;
-
enableUploads = cfg.enableUploads;
-
datadir = dataDir;
-
debugClient = false;
-
firehose = cfg.firehose;
-
disableRegistration = cfg.disableRegistration;
-
} //
-
(if cfg.port < 1024 then {
-
serverUser = user; # have pump.io listen then drop privileges
-
} else { }) //
-
cfg.extraConfig;
+
requireEmail = cfg.requireEmail;
+
smtpserver = cfg.smtpHost;
+
smtpport = cfg.smtpPort;
+
smtpuser = cfg.smtpUser;
+
smtppass = cfg.smtpPassword;
+
smtpusessl = cfg.smtpUseSSL;
+
smtpfrom = cfg.smtpFrom;
-
in
+
nologger = false;
+
enableUploads = cfg.enableUploads;
+
datadir = dataDir;
+
debugClient = false;
+
firehose = cfg.firehose;
+
disableRegistration = cfg.disableRegistration;
-
{
+
inherit (cfg) secretFile dbPasswordFile smtpPasswordFile spamClientSecretFile;
+
} //
+
(optionalSet (cfg.port < 1024) {
+
serverUser = user; # have pump.io listen then drop privileges
+
}) // cfg.extraConfig;
+
}; in {
options = {
services.pumpio = {
···
enable = mkEnableOption "Pump.io social streams server";
secret = mkOption {
-
type = types.str;
+
type = types.nullOr types.str;
+
default = null;
example = "my dog has fleas";
description = ''
A session-generating secret, server-wide password. Warning:
···
'';
};
+
secretFile = mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
example = "/run/keys/pump.io-secret";
+
description = ''
+
A file containing the session-generating secret,
+
server-wide password.
+
'';
+
};
+
site = mkOption {
type = types.str;
example = "Awesome Sauce";
···
hostname = mkOption {
type = types.nullOr types.str;
-
default = null;
+
default = "localhost";
description = ''
The hostname of the server, used for generating
URLs. Defaults to "localhost" which doesn't do much for you.
···
'';
};
+
dbPasswordFile = mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
example = "/run/keys/pump.io-dbpassword";
+
description = ''
+
A file containing the password corresponding to dbUser.
+
'';
+
};
+
smtpHost = mkOption {
type = types.nullOr types.str;
default = null;
···
cleartext in the Nix store!
'';
};
+
+
smtpPasswordFile = mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
example = "/run/keys/pump.io-smtppassword";
+
description = ''
+
A file containing the password used to connect to SMTP
+
server. Might not be necessary for some servers.
+
'';
+
};
+
smtpUseSSL = mkOption {
type = types.bool;
···
stored in cleartext in the Nix store!
'';
};
+
spamClientSecretFile = mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
example = "/run/keys/pump.io-spamclientsecret";
+
description = ''
+
A file containing the OAuth key for the spam server.
+
'';
+
};
};
};
config = mkIf cfg.enable {
+
warnings = let warn = k: optional (cfg.${k} != null)
+
"config.services.pumpio.${k} is insecure. Use ${k}File instead.";
+
in concatMap warn [ "secret" "dbPassword" "smtpPassword" "spamClientSecret" ];
+
+
assertions = [
+
{ assertion = !(isNull cfg.secret && isNull cfg.secretFile);
+
message = "pump.io needs a secretFile configured";
+
}
+
];
+
systemd.services."pump.io" =
-
{ description = "pump.io social network stream server";
+
{ description = "Pump.io - stream server that does most of what people really want from a social network";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -p ${dataDir}/uploads
-
chown pumpio:pumpio ${dataDir}/uploads
-
chmod 770 ${dataDir}/uploads
-
'';
+
mkdir -p ${runDir}
+
chown pumpio:pumpio ${dataDir}/uploads ${runDir}
+
chmod 770 ${dataDir}/uploads ${runDir}
+
+
${pkgs.nodejs}/bin/node ${configScript} <<EOF
+
${builtins.toJSON configOptions}
+
EOF
-
serviceConfig.ExecStart = "${pkgs.pumpio}/bin/pump -c /etc/pump.io.json";
-
PermissionsStartOnly = true;
-
serviceConfig.User = if cfg.port < 1024 then "root" else user;
-
serviceConfig.Group = user;
-
};
+
chgrp pumpio ${configOptions.outputFile}
+
chmod 640 ${configOptions.outputFile}
+
'';
-
environment.etc."pump.io.json" = {
-
mode = "0440";
-
gid = config.ids.gids.pumpio;
-
text = builtins.toJSON configOptions;
+
serviceConfig = {
+
ExecStart = "${pkgs.pumpio}/bin/pump -c ${configOptions.outputFile}";
+
PermissionsStartOnly = true;
+
User = if cfg.port < 1024 then "root" else user;
+
Group = user;
+
};
+
environment = { NODE_ENV = "production"; };
};
users.extraGroups.pumpio.gid = config.ids.gids.pumpio;