at 23.11-pre 16 kB view raw
1{ config, lib, options, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.privacyidea; 7 opt = options.services.privacyidea; 8 9 uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python310; }; 10 python = uwsgi.python3; 11 penv = python.withPackages (const [ pkgs.privacyidea ]); 12 logCfg = pkgs.writeText "privacyidea-log.cfg" '' 13 [formatters] 14 keys=detail 15 16 [handlers] 17 keys=stream 18 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 22 23 [handler_stream] 24 class=StreamHandler 25 level=NOTSET 26 formatter=detail 27 args=(sys.stdout,) 28 29 [loggers] 30 keys=root,privacyidea 31 32 [logger_privacyidea] 33 handlers=stream 34 qualname=privacyidea 35 level=INFO 36 37 [logger_root] 38 handlers=stream 39 level=ERROR 40 ''; 41 42 piCfgFile = pkgs.writeText "privacyidea.cfg" '' 43 SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ] 44 SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///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 ''; 53 54 renderValue = x: 55 if isList x then concatMapStringsSep "," (x: ''"${x}"'') x 56 else if isString x && hasInfix "," x then ''"${x}"'' 57 else x; 58 59 ldapProxyConfig = pkgs.writeText "ldap-proxy.ini" 60 (generators.toINI {} 61 (flip mapAttrs cfg.ldap-proxy.settings 62 (const (mapAttrs (const renderValue))))); 63 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 ''; 70in 71 72{ 73 options = { 74 services.privacyidea = { 75 enable = mkEnableOption (lib.mdDoc "PrivacyIDEA"); 76 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 ``` 89 90 The environment-file can now specify the actual secret key: 91 ``` 92 SECRET=veryverytopsecret 93 ``` 94 ''; 95 }; 96 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 }; 104 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 }; 112 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 }; 120 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 }; 128 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 }; 137 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 }; 146 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 }; 155 156 adminPasswordFile = mkOption { 157 type = types.path; 158 description = lib.mdDoc "File containing password for the admin user"; 159 }; 160 161 adminEmail = mkOption { 162 type = types.str; 163 example = "admin@example.com"; 164 description = lib.mdDoc "Mail address for the admin user"; 165 }; 166 167 extraConfig = mkOption { 168 type = types.lines; 169 default = ""; 170 description = lib.mdDoc '' 171 Extra configuration options for pi.cfg. 172 ''; 173 }; 174 175 user = mkOption { 176 type = types.str; 177 default = "privacyidea"; 178 description = lib.mdDoc "User account under which PrivacyIDEA runs."; 179 }; 180 181 group = mkOption { 182 type = types.str; 183 default = "privacyidea"; 184 description = lib.mdDoc "Group account under which PrivacyIDEA runs."; 185 }; 186 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 }; 222 223 ldap-proxy = { 224 enable = mkEnableOption (lib.mdDoc "PrivacyIDEA LDAP Proxy"); 225 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 }; 233 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 }; 239 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 }; 245 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 }; 256 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 }; 268 269 config = mkMerge [ 270 271 (mkIf cfg.enable { 272 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 ]; 282 283 environment.systemPackages = [ pkgs.privacyidea (hiPrio privacyidea-token-janitor) ]; 284 285 services.postgresql.enable = mkDefault true; 286 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 }; 326 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 }; 391 392 users.users.privacyidea = mkIf (cfg.user == "privacyidea") { 393 group = cfg.group; 394 isSystemUser = true; 395 }; 396 397 users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {}; 398 }) 399 400 (mkIf cfg.ldap-proxy.enable { 401 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 ]; 409 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 ]; 413 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 }; 448 449 users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") { 450 group = cfg.ldap-proxy.group; 451 isSystemUser = true; 452 }; 453 454 users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {}; 455 }) 456 ]; 457 458}