1 { config, lib, options, pkgs, ... }:
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" ''
20 class=privacyidea.lib.log.SecureFormatter
21 format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
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}'
55 if isList x then concatMapStringsSep "," (x: ''"${x}"'') x
56 else if isString x && hasInfix "," x then ''"${x}"''
59 ldapProxyConfig = pkgs.writeText "ldap-proxy.ini"
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 $@
74 services.privacyidea = {
75 enable = mkEnableOption (lib.mdDoc "PrivacyIDEA");
77 environmentFile = mkOption {
78 type = types.nullOr types.path;
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
87 { services.privacyidea.secretKey = "$SECRET"; }
90 The environment-file can now specify the actual secret key:
92 SECRET=veryverytopsecret
99 default = "/var/lib/privacyidea";
100 description = lib.mdDoc ''
101 Directory where all PrivacyIDEA files will be placed by default.
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.
113 secretKey = mkOption {
115 example = "t0p s3cr3t";
116 description = lib.mdDoc ''
117 This is used to encrypt the auth_token.
123 example = "Never know...";
124 description = lib.mdDoc ''
125 This is used to encrypt the admin passwords.
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
138 auditKeyPrivate = mkOption {
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.
147 auditKeyPublic = mkOption {
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.
156 adminPasswordFile = mkOption {
158 description = lib.mdDoc "File containing password for the admin user";
161 adminEmail = mkOption {
163 example = "admin@example.com";
164 description = lib.mdDoc "Mail address for the admin user";
167 extraConfig = mkOption {
170 description = lib.mdDoc ''
171 Extra configuration options for pi.cfg.
177 default = "privacyidea";
178 description = lib.mdDoc "User account under which PrivacyIDEA runs.";
183 default = "privacyidea";
184 description = lib.mdDoc "Group account under which PrivacyIDEA runs.";
188 enable = mkEnableOption (lib.mdDoc "automatic runs of the token janitor");
189 interval = mkOption {
190 default = "quarterly";
192 description = lib.mdDoc ''
193 Interval in which the cleanup program is supposed to run.
194 See {manpage}`systemd.time(7)` for further information.
198 type = types.enum [ "delete" "mark" "disable" "unassign" ];
199 description = lib.mdDoc ''
200 Which action to take for matching tokens.
203 unassigned = mkOption {
206 description = lib.mdDoc ''
207 Whether to search for **unassigned** tokens
208 and apply [](#opt-services.privacyidea.tokenjanitor.action)
212 orphaned = mkOption {
215 description = lib.mdDoc ''
216 Whether to search for **orphaned** tokens
217 and apply [](#opt-services.privacyidea.tokenjanitor.action)
224 enable = mkEnableOption (lib.mdDoc "PrivacyIDEA LDAP Proxy");
226 configFile = mkOption {
227 type = types.nullOr types.path;
229 description = lib.mdDoc ''
230 Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
236 default = "pi-ldap-proxy";
237 description = lib.mdDoc "User account under which PrivacyIDEA LDAP proxy runs.";
242 default = "pi-ldap-proxy";
243 description = lib.mdDoc "Group account under which PrivacyIDEA LDAP proxy runs.";
246 settings = mkOption {
247 type = with types; attrsOf (attrsOf (oneOf [ str bool int (listOf str) ]));
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`.
257 environmentFile = mkOption {
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).
275 assertion = cfg.tokenjanitor.enable -> (cfg.tokenjanitor.orphaned || cfg.tokenjanitor.unassigned);
277 privacyidea-token-janitor has no effect if neither orphaned nor unassigned tokens
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";
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 \
297 --action ${cfg.tokenjanitor.action}
299 ${optionalString cfg.tokenjanitor.unassigned ''
300 echo >&2 "Removing unassigned tokens..."
301 privacyidea-token-janitor find \
303 --action ${cfg.tokenjanitor.action}
307 LockPersonality = true;
308 MemoryDenyWriteExecute = true;
310 ProtectHostname = true;
311 ProtectKernelLogs = true;
312 ProtectKernelModules = true;
313 ProtectKernelTunables = true;
314 ProtectSystem = "strict";
315 ReadWritePaths = cfg.stateDir;
318 WorkingDirectory = cfg.stateDir;
321 systemd.timers.privacyidea-tokenjanitor = mkIf cfg.tokenjanitor.enable {
322 wantedBy = [ "timers.target" ];
323 timerConfig.OnCalendar = cfg.tokenjanitor.interval;
324 timerConfig.Persistent = true;
327 systemd.services.privacyidea = let
328 piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
331 plugins = [ "python3" ];
332 pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
333 socket = "/run/privacyidea/socket";
337 chown-socket = "${cfg.user}:nginx";
338 chdir = cfg.stateDir;
339 wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
343 stats = "/run/privacyidea/stats.socket";
353 wantedBy = [ "multi-user.target" ];
354 after = [ "postgresql.service" ];
355 path = with pkgs; [ openssl ];
356 environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
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;
362 mkdir -p ${cfg.stateDir} /run/privacyidea
363 chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
365 ${lib.getBin pkgs.envsubst}/bin/envsubst -o ${cfg.stateDir}/privacyidea.cfg \
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"
379 ${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
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";
392 users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
397 users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
400 (mkIf cfg.ldap-proxy.enable {
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!";
410 warnings = mkIf (cfg.ldap-proxy.configFile != null) [
411 "Using services.privacyidea.ldap-proxy.configFile is deprecated! Use the RFC42-style settings option instead!"
414 systemd.services.privacyidea-ldap-proxy = let
415 ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
417 description = "privacyIDEA LDAP proxy";
418 wantedBy = [ "multi-user.target" ];
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 ];
426 "${pkgs.writeShellScript "substitute-secrets-ldap-proxy" ''
428 ${pkgs.envsubst}/bin/envsubst \
429 -i ${ldapProxyConfig} \
430 -o $STATE_DIRECTORY/ldap-proxy.ini
433 configPath = if cfg.ldap-proxy.settings != {}
434 then "%S/privacyidea-ldap-proxy/ldap-proxy.ini"
435 else cfg.ldap-proxy.configFile;
437 ${ldap-proxy-env}/bin/twistd \
440 -u ${cfg.ldap-proxy.user} \
441 -g ${cfg.ldap-proxy.group} \
449 users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
450 group = cfg.ldap-proxy.group;
454 users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};