python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / security / privacyidea.nix
blobe446e606cad8bd883cc7f1c4601db5556d195803
1 { config, lib, options, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.privacyidea;
7   opt = options.services.privacyidea;
9   uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python39; };
10   python = uwsgi.python3;
11   penv = python.withPackages (const [ pkgs.privacyidea ]);
12   logCfg = pkgs.writeText "privacyidea-log.cfg" ''
13     [formatters]
14     keys=detail
16     [handlers]
17     keys=stream
19     [formatter_detail]
20     class=privacyidea.lib.log.SecureFormatter
21     format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
23     [handler_stream]
24     class=StreamHandler
25     level=NOTSET
26     formatter=detail
27     args=(sys.stdout,)
29     [loggers]
30     keys=root,privacyidea
32     [logger_privacyidea]
33     handlers=stream
34     qualname=privacyidea
35     level=INFO
37     [logger_root]
38     handlers=stream
39     level=ERROR
40   '';
42   piCfgFile = pkgs.writeText "privacyidea.cfg" ''
43     SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
44     SQLALCHEMY_DATABASE_URI = 'postgresql:///privacyidea'
45     SECRET_KEY = '${cfg.secretKey}'
46     PI_PEPPER = '${cfg.pepper}'
47     PI_ENCFILE = '${cfg.encFile}'
48     PI_AUDIT_KEY_PRIVATE = '${cfg.auditKeyPrivate}'
49     PI_AUDIT_KEY_PUBLIC = '${cfg.auditKeyPublic}'
50     PI_LOGCONFIG = '${logCfg}'
51     ${cfg.extraConfig}
52   '';
54   renderValue = x:
55     if isList x then concatMapStringsSep "," (x: ''"${x}"'') x
56     else if isString x && hasInfix "," x then ''"${x}"''
57     else x;
59   ldapProxyConfig = pkgs.writeText "ldap-proxy.ini"
60     (generators.toINI {}
61       (flip mapAttrs cfg.ldap-proxy.settings
62         (const (mapAttrs (const renderValue)))));
64   privacyidea-token-janitor = pkgs.writeShellScriptBin "privacyidea-token-janitor" ''
65     exec -a privacyidea-token-janitor \
66       /run/wrappers/bin/sudo -u ${cfg.user} \
67       env PRIVACYIDEA_CONFIGFILE=${cfg.stateDir}/privacyidea.cfg \
68       ${penv}/bin/privacyidea-token-janitor $@
69   '';
73   options = {
74     services.privacyidea = {
75       enable = mkEnableOption (lib.mdDoc "PrivacyIDEA");
77       environmentFile = mkOption {
78         type = types.nullOr types.path;
79         default = null;
80         example = "/root/privacyidea.env";
81         description = lib.mdDoc ''
82           File to load as environment file. Environment variables
83           from this file will be interpolated into the config file
84           using `envsubst` which is helpful for specifying
85           secrets:
86           ```
87           { services.privacyidea.secretKey = "$SECRET"; }
88           ```
90           The environment-file can now specify the actual secret key:
91           ```
92           SECRET=veryverytopsecret
93           ```
94         '';
95       };
97       stateDir = mkOption {
98         type = types.str;
99         default = "/var/lib/privacyidea";
100         description = lib.mdDoc ''
101           Directory where all PrivacyIDEA files will be placed by default.
102         '';
103       };
105       superuserRealm = mkOption {
106         type = types.listOf types.str;
107         default = [ "super" "administrators" ];
108         description = lib.mdDoc ''
109           The realm where users are allowed to login as administrators.
110         '';
111       };
113       secretKey = mkOption {
114         type = types.str;
115         example = "t0p s3cr3t";
116         description = lib.mdDoc ''
117           This is used to encrypt the auth_token.
118         '';
119       };
121       pepper = mkOption {
122         type = types.str;
123         example = "Never know...";
124         description = lib.mdDoc ''
125           This is used to encrypt the admin passwords.
126         '';
127       };
129       encFile = mkOption {
130         type = types.str;
131         default = "${cfg.stateDir}/enckey";
132         defaultText = literalExpression ''"''${config.${opt.stateDir}}/enckey"'';
133         description = lib.mdDoc ''
134           This is used to encrypt the token data and token passwords
135         '';
136       };
138       auditKeyPrivate = mkOption {
139         type = types.str;
140         default = "${cfg.stateDir}/private.pem";
141         defaultText = literalExpression ''"''${config.${opt.stateDir}}/private.pem"'';
142         description = lib.mdDoc ''
143           Private Key for signing the audit log.
144         '';
145       };
147       auditKeyPublic = mkOption {
148         type = types.str;
149         default = "${cfg.stateDir}/public.pem";
150         defaultText = literalExpression ''"''${config.${opt.stateDir}}/public.pem"'';
151         description = lib.mdDoc ''
152           Public key for checking signatures of the audit log.
153         '';
154       };
156       adminPasswordFile = mkOption {
157         type = types.path;
158         description = lib.mdDoc "File containing password for the admin user";
159       };
161       adminEmail = mkOption {
162         type = types.str;
163         example = "admin@example.com";
164         description = lib.mdDoc "Mail address for the admin user";
165       };
167       extraConfig = mkOption {
168         type = types.lines;
169         default = "";
170         description = lib.mdDoc ''
171           Extra configuration options for pi.cfg.
172         '';
173       };
175       user = mkOption {
176         type = types.str;
177         default = "privacyidea";
178         description = lib.mdDoc "User account under which PrivacyIDEA runs.";
179       };
181       group = mkOption {
182         type = types.str;
183         default = "privacyidea";
184         description = lib.mdDoc "Group account under which PrivacyIDEA runs.";
185       };
187       tokenjanitor = {
188         enable = mkEnableOption (lib.mdDoc "automatic runs of the token janitor");
189         interval = mkOption {
190           default = "quarterly";
191           type = types.str;
192           description = lib.mdDoc ''
193             Interval in which the cleanup program is supposed to run.
194             See {manpage}`systemd.time(7)` for further information.
195           '';
196         };
197         action = mkOption {
198           type = types.enum [ "delete" "mark" "disable" "unassign" ];
199           description = lib.mdDoc ''
200             Which action to take for matching tokens.
201           '';
202         };
203         unassigned = mkOption {
204           default = false;
205           type = types.bool;
206           description = lib.mdDoc ''
207             Whether to search for **unassigned** tokens
208             and apply [](#opt-services.privacyidea.tokenjanitor.action)
209             onto them.
210           '';
211         };
212         orphaned = mkOption {
213           default = true;
214           type = types.bool;
215           description = lib.mdDoc ''
216             Whether to search for **orphaned** tokens
217             and apply [](#opt-services.privacyidea.tokenjanitor.action)
218             onto them.
219           '';
220         };
221       };
223       ldap-proxy = {
224         enable = mkEnableOption (lib.mdDoc "PrivacyIDEA LDAP Proxy");
226         configFile = mkOption {
227           type = types.nullOr types.path;
228           default = null;
229           description = lib.mdDoc ''
230             Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
231           '';
232         };
234         user = mkOption {
235           type = types.str;
236           default = "pi-ldap-proxy";
237           description = lib.mdDoc "User account under which PrivacyIDEA LDAP proxy runs.";
238         };
240         group = mkOption {
241           type = types.str;
242           default = "pi-ldap-proxy";
243           description = lib.mdDoc "Group account under which PrivacyIDEA LDAP proxy runs.";
244         };
246         settings = mkOption {
247           type = with types; attrsOf (attrsOf (oneOf [ str bool int (listOf str) ]));
248           default = {};
249           description = lib.mdDoc ''
250             Attribute-set containing the settings for `privacyidea-ldap-proxy`.
251             It's possible to pass secrets using env-vars as substitutes and
252             use the option [](#opt-services.privacyidea.ldap-proxy.environmentFile)
253             to inject them via `envsubst`.
254           '';
255         };
257         environmentFile = mkOption {
258           default = null;
259           type = types.nullOr types.str;
260           description = lib.mdDoc ''
261             Environment file containing secrets to be substituted into
262             [](#opt-services.privacyidea.ldap-proxy.settings).
263           '';
264         };
265       };
266     };
267   };
269   config = mkMerge [
271     (mkIf cfg.enable {
273       assertions = [
274         {
275           assertion = cfg.tokenjanitor.enable -> (cfg.tokenjanitor.orphaned || cfg.tokenjanitor.unassigned);
276           message = ''
277             privacyidea-token-janitor has no effect if neither orphaned nor unassigned tokens
278             are to be searched.
279           '';
280         }
281       ];
283       environment.systemPackages = [ pkgs.privacyidea (hiPrio privacyidea-token-janitor) ];
285       services.postgresql.enable = mkDefault true;
287       systemd.services.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
288         environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
289         path = [ penv ];
290         serviceConfig = {
291           CapabilityBoundingSet = [ "" ];
292           ExecStart = "${pkgs.writeShellScript "pi-token-janitor" ''
293             ${optionalString cfg.tokenjanitor.orphaned ''
294               echo >&2 "Removing orphaned tokens..."
295               privacyidea-token-janitor find \
296                 --orphaned true \
297                 --action ${cfg.tokenjanitor.action}
298             ''}
299             ${optionalString cfg.tokenjanitor.unassigned ''
300               echo >&2 "Removing unassigned tokens..."
301               privacyidea-token-janitor find \
302                 --assigned false \
303                 --action ${cfg.tokenjanitor.action}
304             ''}
305           ''}";
306           Group = cfg.group;
307           LockPersonality = true;
308           MemoryDenyWriteExecute = true;
309           ProtectHome = true;
310           ProtectHostname = true;
311           ProtectKernelLogs = true;
312           ProtectKernelModules = true;
313           ProtectKernelTunables = true;
314           ProtectSystem = "strict";
315           ReadWritePaths = cfg.stateDir;
316           Type = "oneshot";
317           User = cfg.user;
318           WorkingDirectory = cfg.stateDir;
319         };
320       };
321       systemd.timers.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
322         wantedBy = [ "timers.target" ];
323         timerConfig.OnCalendar = cfg.tokenjanitor.interval;
324         timerConfig.Persistent = true;
325       };
327       systemd.services.privacyidea = let
328         piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
329           uwsgi = {
330             buffer-size = 8192;
331             plugins = [ "python3" ];
332             pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
333             socket = "/run/privacyidea/socket";
334             uid = cfg.user;
335             gid = cfg.group;
336             chmod-socket = 770;
337             chown-socket = "${cfg.user}:nginx";
338             chdir = cfg.stateDir;
339             wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
340             processes = 4;
341             harakiri = 60;
342             reload-mercy = 8;
343             stats = "/run/privacyidea/stats.socket";
344             max-requests = 2000;
345             limit-as = 1024;
346             reload-on-as = 512;
347             reload-on-rss = 256;
348             no-orphans = true;
349             vacuum = true;
350           };
351         });
352       in {
353         wantedBy = [ "multi-user.target" ];
354         after = [ "postgresql.service" ];
355         path = with pkgs; [ openssl ];
356         environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
357         preStart = let
358           pi-manage = "${config.security.sudo.package}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
359           pgsu = config.services.postgresql.superUser;
360           psql = config.services.postgresql.package;
361         in ''
362           mkdir -p ${cfg.stateDir} /run/privacyidea
363           chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
364           umask 077
365           ${lib.getBin pkgs.envsubst}/bin/envsubst -o ${cfg.stateDir}/privacyidea.cfg \
366                                                    -i "${piCfgFile}"
367           chown ${cfg.user}:${cfg.group} ${cfg.stateDir}/privacyidea.cfg
368           if ! test -e "${cfg.stateDir}/db-created"; then
369             ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
370             ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
371             ${pi-manage} create_enckey
372             ${pi-manage} create_audit_keys
373             ${pi-manage} createdb
374             ${pi-manage} admin add admin -e ${cfg.adminEmail} -p "$(cat ${cfg.adminPasswordFile})"
375             ${pi-manage} db stamp head -d ${penv}/lib/privacyidea/migrations
376             touch "${cfg.stateDir}/db-created"
377             chmod g+r "${cfg.stateDir}/enckey" "${cfg.stateDir}/private.pem"
378           fi
379           ${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
380         '';
381         serviceConfig = {
382           Type = "notify";
383           ExecStart = "${uwsgi}/bin/uwsgi --json ${piuwsgi}";
384           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
385           EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
386           ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
387           NotifyAccess = "main";
388           KillSignal = "SIGQUIT";
389         };
390       };
392       users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
393         group = cfg.group;
394         isSystemUser = true;
395       };
397       users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
398     })
400     (mkIf cfg.ldap-proxy.enable {
402       assertions = [
403         { assertion = let
404             xor = a: b: a && !b || !a && b;
405           in xor (cfg.ldap-proxy.settings == {}) (cfg.ldap-proxy.configFile == null);
406           message = "configFile & settings are mutually exclusive for services.privacyidea.ldap-proxy!";
407         }
408       ];
410       warnings = mkIf (cfg.ldap-proxy.configFile != null) [
411         "Using services.privacyidea.ldap-proxy.configFile is deprecated! Use the RFC42-style settings option instead!"
412       ];
414       systemd.services.privacyidea-ldap-proxy = let
415         ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
416       in {
417         description = "privacyIDEA LDAP proxy";
418         wantedBy = [ "multi-user.target" ];
419         serviceConfig = {
420           User = cfg.ldap-proxy.user;
421           Group = cfg.ldap-proxy.group;
422           StateDirectory = "privacyidea-ldap-proxy";
423           EnvironmentFile = mkIf (cfg.ldap-proxy.environmentFile != null)
424             [ cfg.ldap-proxy.environmentFile ];
425           ExecStartPre =
426             "${pkgs.writeShellScript "substitute-secrets-ldap-proxy" ''
427               umask 0077
428               ${pkgs.envsubst}/bin/envsubst \
429                 -i ${ldapProxyConfig} \
430                 -o $STATE_DIRECTORY/ldap-proxy.ini
431             ''}";
432           ExecStart = let
433             configPath = if cfg.ldap-proxy.settings != {}
434               then "%S/privacyidea-ldap-proxy/ldap-proxy.ini"
435               else cfg.ldap-proxy.configFile;
436           in ''
437             ${ldap-proxy-env}/bin/twistd \
438               --nodaemon \
439               --pidfile= \
440               -u ${cfg.ldap-proxy.user} \
441               -g ${cfg.ldap-proxy.group} \
442               ldap-proxy \
443               -c ${configPath}
444           '';
445           Restart = "always";
446         };
447       };
449       users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
450         group = cfg.ldap-proxy.group;
451         isSystemUser = true;
452       };
454       users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};
455     })
456   ];