mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-12-17 03:03:37 +00:00
918c22bd5f
The previous changes for the 3.8 update are ready, but staging got merged into master, so there are a few more challenges to tackle: * Use python 3.10 now since it's actually supported and less effort to build (3.9 isn't recursed into anymore). * sphinx doesn't build with these overrides, so patch it out entirely (i.e. drop `sphinxHook` where it's causing problems). * backport a few jinja2 fixes for python 3.10 that were fixed in later versions, but break because this env is stuck to 2.11.
459 lines
15 KiB
Nix
459 lines
15 KiB
Nix
{ config, lib, options, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.privacyidea;
|
|
opt = options.services.privacyidea;
|
|
|
|
uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python310; };
|
|
python = uwsgi.python3;
|
|
penv = python.withPackages (const [ pkgs.privacyidea ]);
|
|
logCfg = pkgs.writeText "privacyidea-log.cfg" ''
|
|
[formatters]
|
|
keys=detail
|
|
|
|
[handlers]
|
|
keys=stream
|
|
|
|
[formatter_detail]
|
|
class=privacyidea.lib.log.SecureFormatter
|
|
format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
|
|
|
|
[handler_stream]
|
|
class=StreamHandler
|
|
level=NOTSET
|
|
formatter=detail
|
|
args=(sys.stdout,)
|
|
|
|
[loggers]
|
|
keys=root,privacyidea
|
|
|
|
[logger_privacyidea]
|
|
handlers=stream
|
|
qualname=privacyidea
|
|
level=INFO
|
|
|
|
[logger_root]
|
|
handlers=stream
|
|
level=ERROR
|
|
'';
|
|
|
|
piCfgFile = pkgs.writeText "privacyidea.cfg" ''
|
|
SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
|
|
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///privacyidea'
|
|
SECRET_KEY = '${cfg.secretKey}'
|
|
PI_PEPPER = '${cfg.pepper}'
|
|
PI_ENCFILE = '${cfg.encFile}'
|
|
PI_AUDIT_KEY_PRIVATE = '${cfg.auditKeyPrivate}'
|
|
PI_AUDIT_KEY_PUBLIC = '${cfg.auditKeyPublic}'
|
|
PI_LOGCONFIG = '${logCfg}'
|
|
${cfg.extraConfig}
|
|
'';
|
|
|
|
renderValue = x:
|
|
if isList x then concatMapStringsSep "," (x: ''"${x}"'') x
|
|
else if isString x && hasInfix "," x then ''"${x}"''
|
|
else x;
|
|
|
|
ldapProxyConfig = pkgs.writeText "ldap-proxy.ini"
|
|
(generators.toINI {}
|
|
(flip mapAttrs cfg.ldap-proxy.settings
|
|
(const (mapAttrs (const renderValue)))));
|
|
|
|
privacyidea-token-janitor = pkgs.writeShellScriptBin "privacyidea-token-janitor" ''
|
|
exec -a privacyidea-token-janitor \
|
|
/run/wrappers/bin/sudo -u ${cfg.user} \
|
|
env PRIVACYIDEA_CONFIGFILE=${cfg.stateDir}/privacyidea.cfg \
|
|
${penv}/bin/privacyidea-token-janitor $@
|
|
'';
|
|
in
|
|
|
|
{
|
|
options = {
|
|
services.privacyidea = {
|
|
enable = mkEnableOption (lib.mdDoc "PrivacyIDEA");
|
|
|
|
environmentFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
example = "/root/privacyidea.env";
|
|
description = lib.mdDoc ''
|
|
File to load as environment file. Environment variables
|
|
from this file will be interpolated into the config file
|
|
using `envsubst` which is helpful for specifying
|
|
secrets:
|
|
```
|
|
{ services.privacyidea.secretKey = "$SECRET"; }
|
|
```
|
|
|
|
The environment-file can now specify the actual secret key:
|
|
```
|
|
SECRET=veryverytopsecret
|
|
```
|
|
'';
|
|
};
|
|
|
|
stateDir = mkOption {
|
|
type = types.str;
|
|
default = "/var/lib/privacyidea";
|
|
description = lib.mdDoc ''
|
|
Directory where all PrivacyIDEA files will be placed by default.
|
|
'';
|
|
};
|
|
|
|
superuserRealm = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ "super" "administrators" ];
|
|
description = lib.mdDoc ''
|
|
The realm where users are allowed to login as administrators.
|
|
'';
|
|
};
|
|
|
|
secretKey = mkOption {
|
|
type = types.str;
|
|
example = "t0p s3cr3t";
|
|
description = lib.mdDoc ''
|
|
This is used to encrypt the auth_token.
|
|
'';
|
|
};
|
|
|
|
pepper = mkOption {
|
|
type = types.str;
|
|
example = "Never know...";
|
|
description = lib.mdDoc ''
|
|
This is used to encrypt the admin passwords.
|
|
'';
|
|
};
|
|
|
|
encFile = mkOption {
|
|
type = types.str;
|
|
default = "${cfg.stateDir}/enckey";
|
|
defaultText = literalExpression ''"''${config.${opt.stateDir}}/enckey"'';
|
|
description = lib.mdDoc ''
|
|
This is used to encrypt the token data and token passwords
|
|
'';
|
|
};
|
|
|
|
auditKeyPrivate = mkOption {
|
|
type = types.str;
|
|
default = "${cfg.stateDir}/private.pem";
|
|
defaultText = literalExpression ''"''${config.${opt.stateDir}}/private.pem"'';
|
|
description = lib.mdDoc ''
|
|
Private Key for signing the audit log.
|
|
'';
|
|
};
|
|
|
|
auditKeyPublic = mkOption {
|
|
type = types.str;
|
|
default = "${cfg.stateDir}/public.pem";
|
|
defaultText = literalExpression ''"''${config.${opt.stateDir}}/public.pem"'';
|
|
description = lib.mdDoc ''
|
|
Public key for checking signatures of the audit log.
|
|
'';
|
|
};
|
|
|
|
adminPasswordFile = mkOption {
|
|
type = types.path;
|
|
description = lib.mdDoc "File containing password for the admin user";
|
|
};
|
|
|
|
adminEmail = mkOption {
|
|
type = types.str;
|
|
example = "admin@example.com";
|
|
description = lib.mdDoc "Mail address for the admin user";
|
|
};
|
|
|
|
extraConfig = mkOption {
|
|
type = types.lines;
|
|
default = "";
|
|
description = lib.mdDoc ''
|
|
Extra configuration options for pi.cfg.
|
|
'';
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "privacyidea";
|
|
description = lib.mdDoc "User account under which PrivacyIDEA runs.";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "privacyidea";
|
|
description = lib.mdDoc "Group account under which PrivacyIDEA runs.";
|
|
};
|
|
|
|
tokenjanitor = {
|
|
enable = mkEnableOption (lib.mdDoc "automatic runs of the token janitor");
|
|
interval = mkOption {
|
|
default = "quarterly";
|
|
type = types.str;
|
|
description = lib.mdDoc ''
|
|
Interval in which the cleanup program is supposed to run.
|
|
See {manpage}`systemd.time(7)` for further information.
|
|
'';
|
|
};
|
|
action = mkOption {
|
|
type = types.enum [ "delete" "mark" "disable" "unassign" ];
|
|
description = lib.mdDoc ''
|
|
Which action to take for matching tokens.
|
|
'';
|
|
};
|
|
unassigned = mkOption {
|
|
default = false;
|
|
type = types.bool;
|
|
description = lib.mdDoc ''
|
|
Whether to search for **unassigned** tokens
|
|
and apply [](#opt-services.privacyidea.tokenjanitor.action)
|
|
onto them.
|
|
'';
|
|
};
|
|
orphaned = mkOption {
|
|
default = true;
|
|
type = types.bool;
|
|
description = lib.mdDoc ''
|
|
Whether to search for **orphaned** tokens
|
|
and apply [](#opt-services.privacyidea.tokenjanitor.action)
|
|
onto them.
|
|
'';
|
|
};
|
|
};
|
|
|
|
ldap-proxy = {
|
|
enable = mkEnableOption (lib.mdDoc "PrivacyIDEA LDAP Proxy");
|
|
|
|
configFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
|
|
'';
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "pi-ldap-proxy";
|
|
description = lib.mdDoc "User account under which PrivacyIDEA LDAP proxy runs.";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "pi-ldap-proxy";
|
|
description = lib.mdDoc "Group account under which PrivacyIDEA LDAP proxy runs.";
|
|
};
|
|
|
|
settings = mkOption {
|
|
type = with types; attrsOf (attrsOf (oneOf [ str bool int (listOf str) ]));
|
|
default = {};
|
|
description = lib.mdDoc ''
|
|
Attribute-set containing the settings for `privacyidea-ldap-proxy`.
|
|
It's possible to pass secrets using env-vars as substitutes and
|
|
use the option [](#opt-services.privacyidea.ldap-proxy.environmentFile)
|
|
to inject them via `envsubst`.
|
|
'';
|
|
};
|
|
|
|
environmentFile = mkOption {
|
|
default = null;
|
|
type = types.nullOr types.str;
|
|
description = lib.mdDoc ''
|
|
Environment file containing secrets to be substituted into
|
|
[](#opt-services.privacyidea.ldap-proxy.settings).
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkMerge [
|
|
|
|
(mkIf cfg.enable {
|
|
|
|
assertions = [
|
|
{
|
|
assertion = cfg.tokenjanitor.enable -> (cfg.tokenjanitor.orphaned || cfg.tokenjanitor.unassigned);
|
|
message = ''
|
|
privacyidea-token-janitor has no effect if neither orphaned nor unassigned tokens
|
|
are to be searched.
|
|
'';
|
|
}
|
|
];
|
|
|
|
environment.systemPackages = [ pkgs.privacyidea (hiPrio privacyidea-token-janitor) ];
|
|
|
|
services.postgresql.enable = mkDefault true;
|
|
|
|
systemd.services.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
|
|
environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
|
|
path = [ penv ];
|
|
serviceConfig = {
|
|
CapabilityBoundingSet = [ "" ];
|
|
ExecStart = "${pkgs.writeShellScript "pi-token-janitor" ''
|
|
${optionalString cfg.tokenjanitor.orphaned ''
|
|
echo >&2 "Removing orphaned tokens..."
|
|
privacyidea-token-janitor find \
|
|
--orphaned true \
|
|
--action ${cfg.tokenjanitor.action}
|
|
''}
|
|
${optionalString cfg.tokenjanitor.unassigned ''
|
|
echo >&2 "Removing unassigned tokens..."
|
|
privacyidea-token-janitor find \
|
|
--assigned false \
|
|
--action ${cfg.tokenjanitor.action}
|
|
''}
|
|
''}";
|
|
Group = cfg.group;
|
|
LockPersonality = true;
|
|
MemoryDenyWriteExecute = true;
|
|
ProtectHome = true;
|
|
ProtectHostname = true;
|
|
ProtectKernelLogs = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectSystem = "strict";
|
|
ReadWritePaths = cfg.stateDir;
|
|
Type = "oneshot";
|
|
User = cfg.user;
|
|
WorkingDirectory = cfg.stateDir;
|
|
};
|
|
};
|
|
systemd.timers.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig.OnCalendar = cfg.tokenjanitor.interval;
|
|
timerConfig.Persistent = true;
|
|
};
|
|
|
|
systemd.services.privacyidea = let
|
|
piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
|
|
uwsgi = {
|
|
buffer-size = 8192;
|
|
plugins = [ "python3" ];
|
|
pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
|
|
socket = "/run/privacyidea/socket";
|
|
uid = cfg.user;
|
|
gid = cfg.group;
|
|
chmod-socket = 770;
|
|
chown-socket = "${cfg.user}:nginx";
|
|
chdir = cfg.stateDir;
|
|
wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
|
|
processes = 4;
|
|
harakiri = 60;
|
|
reload-mercy = 8;
|
|
stats = "/run/privacyidea/stats.socket";
|
|
max-requests = 2000;
|
|
limit-as = 1024;
|
|
reload-on-as = 512;
|
|
reload-on-rss = 256;
|
|
no-orphans = true;
|
|
vacuum = true;
|
|
};
|
|
});
|
|
in {
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "postgresql.service" ];
|
|
path = with pkgs; [ openssl ];
|
|
environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
|
|
preStart = let
|
|
pi-manage = "${config.security.sudo.package}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
|
|
pgsu = config.services.postgresql.superUser;
|
|
psql = config.services.postgresql.package;
|
|
in ''
|
|
mkdir -p ${cfg.stateDir} /run/privacyidea
|
|
chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
|
|
umask 077
|
|
${lib.getBin pkgs.envsubst}/bin/envsubst -o ${cfg.stateDir}/privacyidea.cfg \
|
|
-i "${piCfgFile}"
|
|
chown ${cfg.user}:${cfg.group} ${cfg.stateDir}/privacyidea.cfg
|
|
if ! test -e "${cfg.stateDir}/db-created"; then
|
|
${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
|
|
${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
|
|
${pi-manage} create_enckey
|
|
${pi-manage} create_audit_keys
|
|
${pi-manage} createdb
|
|
${pi-manage} admin add admin -e ${cfg.adminEmail} -p "$(cat ${cfg.adminPasswordFile})"
|
|
${pi-manage} db stamp head -d ${penv}/lib/privacyidea/migrations
|
|
touch "${cfg.stateDir}/db-created"
|
|
chmod g+r "${cfg.stateDir}/enckey" "${cfg.stateDir}/private.pem"
|
|
fi
|
|
${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
|
|
'';
|
|
serviceConfig = {
|
|
Type = "notify";
|
|
ExecStart = "${uwsgi}/bin/uwsgi --json ${piuwsgi}";
|
|
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
|
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
|
|
ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
|
|
NotifyAccess = "main";
|
|
KillSignal = "SIGQUIT";
|
|
};
|
|
};
|
|
|
|
users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
|
|
group = cfg.group;
|
|
isSystemUser = true;
|
|
};
|
|
|
|
users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
|
|
})
|
|
|
|
(mkIf cfg.ldap-proxy.enable {
|
|
|
|
assertions = [
|
|
{ assertion = let
|
|
xor = a: b: a && !b || !a && b;
|
|
in xor (cfg.ldap-proxy.settings == {}) (cfg.ldap-proxy.configFile == null);
|
|
message = "configFile & settings are mutually exclusive for services.privacyidea.ldap-proxy!";
|
|
}
|
|
];
|
|
|
|
warnings = mkIf (cfg.ldap-proxy.configFile != null) [
|
|
"Using services.privacyidea.ldap-proxy.configFile is deprecated! Use the RFC42-style settings option instead!"
|
|
];
|
|
|
|
systemd.services.privacyidea-ldap-proxy = let
|
|
ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
|
|
in {
|
|
description = "privacyIDEA LDAP proxy";
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
User = cfg.ldap-proxy.user;
|
|
Group = cfg.ldap-proxy.group;
|
|
StateDirectory = "privacyidea-ldap-proxy";
|
|
EnvironmentFile = mkIf (cfg.ldap-proxy.environmentFile != null)
|
|
[ cfg.ldap-proxy.environmentFile ];
|
|
ExecStartPre =
|
|
"${pkgs.writeShellScript "substitute-secrets-ldap-proxy" ''
|
|
umask 0077
|
|
${pkgs.envsubst}/bin/envsubst \
|
|
-i ${ldapProxyConfig} \
|
|
-o $STATE_DIRECTORY/ldap-proxy.ini
|
|
''}";
|
|
ExecStart = let
|
|
configPath = if cfg.ldap-proxy.settings != {}
|
|
then "%S/privacyidea-ldap-proxy/ldap-proxy.ini"
|
|
else cfg.ldap-proxy.configFile;
|
|
in ''
|
|
${ldap-proxy-env}/bin/twistd \
|
|
--nodaemon \
|
|
--pidfile= \
|
|
-u ${cfg.ldap-proxy.user} \
|
|
-g ${cfg.ldap-proxy.group} \
|
|
ldap-proxy \
|
|
-c ${configPath}
|
|
'';
|
|
Restart = "always";
|
|
};
|
|
};
|
|
|
|
users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
|
|
group = cfg.ldap-proxy.group;
|
|
isSystemUser = true;
|
|
};
|
|
|
|
users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};
|
|
})
|
|
];
|
|
|
|
}
|