diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index df08ac2959ed..bc0686f578c0 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -792,6 +792,7 @@ ./services/security/nginx-sso.nix ./services/security/oauth2_proxy.nix ./services/security/oauth2_proxy_nginx.nix + ./services/security/privacyidea.nix ./services/security/physlock.nix ./services/security/shibboleth-sp.nix ./services/security/sks.nix diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix new file mode 100644 index 000000000000..49c03afdf8c9 --- /dev/null +++ b/nixos/modules/services/security/privacyidea.nix @@ -0,0 +1,296 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.privacyidea; + + uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; }; + python = uwsgi.python3; + penv = python.withPackages (ps: [ ps.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 = '${cfg.databaseURI}' + 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} + ''; + +in + +{ + options = { + services.privacyidea = { + enable = mkEnableOption "PrivacyIDEA"; + + stateDir = mkOption { + type = types.str; + default = "/var/lib/privacyidea"; + description = '' + Directory where all PrivacyIDEA files will be placed by default. + ''; + }; + + runDir = mkOption { + type = types.str; + default = "/run/privacyidea"; + description = '' + Directory where all PrivacyIDEA files will be placed by default. + ''; + }; + + superuserRealm = mkOption { + type = types.listOf types.str; + default = [ "super" "administrators" ]; + description = '' + The realm where users are allowed to login as administrators. + ''; + }; + + databaseURI = mkOption { + type = types.str; + default = "postgresql:///privacyidea"; + description = '' + Database as SQLAlchemy URI to use for PrivacyIDEA. + ''; + }; + + secretKey = mkOption { + type = types.str; + example = "t0p s3cr3t"; + description = '' + This is used to encrypt the auth_token. + ''; + }; + + pepper = mkOption { + type = types.str; + example = "Never know..."; + description = '' + This is used to encrypt the admin passwords. + ''; + }; + + encFile = mkOption { + type = types.str; + default = "${cfg.stateDir}/enckey"; + description = '' + This is used to encrypt the token data and token passwords + ''; + }; + + auditKeyPrivate = mkOption { + type = types.str; + default = "${cfg.stateDir}/private.pem"; + description = '' + Private Key for signing the audit log. + ''; + }; + + auditKeyPublic = mkOption { + type = types.str; + default = "${cfg.stateDir}/public.pem"; + description = '' + Public key for checking signatures of the audit log. + ''; + }; + + adminPassword = mkOption { + type = types.str; + description = "Password for the admin user"; + }; + + adminEmail = mkOption { + type = types.str; + example = "admin@example.com"; + description = "Mail address for the admin user"; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration options for pi.cfg. + ''; + }; + + user = mkOption { + type = types.str; + default = "privacyidea"; + description = "User account under which PrivacyIDEA runs."; + }; + + group = mkOption { + type = types.str; + default = "privacyidea"; + description = "Group account under which PrivacyIDEA runs."; + }; + + ldap-proxy = { + enable = mkEnableOption "PrivacyIDEA LDAP Proxy"; + + configFile = mkOption { + type = types.path; + default = ""; + description = '' + Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini). + ''; + }; + + user = mkOption { + type = types.str; + default = "pi-ldap-proxy"; + description = "User account under which PrivacyIDEA LDAP proxy runs."; + }; + + group = mkOption { + type = types.str; + default = "pi-ldap-proxy"; + description = "Group account under which PrivacyIDEA LDAP proxy runs."; + }; + }; + }; + }; + + config = mkMerge [ + + (mkIf cfg.enable { + + environment.systemPackages = [ python.pkgs.privacyidea ]; + + services.postgresql.enable = mkDefault true; + + systemd.services.privacyidea = let + piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON { + uwsgi = { + plugins = [ "python3" ]; + pythonpath = "${penv}/${uwsgi.python3.sitePackages}"; + socket = "${cfg.runDir}/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 = "${cfg.runDir}/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 = piCfgFile; + preStart = let + pi-manage = "${pkgs.sudo}/bin/sudo -u privacyidea -H PRIVACYIDEA_CONFIGFILE=${piCfgFile} ${penv}/bin/pi-manage"; + pgsu = config.services.postgresql.superUser; + psql = config.services.postgresql.package; + in '' + mkdir -p ${cfg.stateDir} ${cfg.runDir} + chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} ${cfg.runDir} + ln -sf ${piCfgFile} ${cfg.stateDir}/privacyidea.cfg + if ! test -e "${cfg.stateDir}/db-created"; then + ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user} + ${pkgs.sudo}/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 ${cfg.adminPassword} + ${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"; + ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; + NotifyAccess = "main"; + KillSignal = "SIGQUIT"; + StandardError = "syslog"; + }; + }; + + users.extraUsers.privacyidea = mkIf (cfg.user == "privacyidea") { + group = cfg.group; + }; + + users.extraGroups.privacyidea = mkIf (cfg.group == "privacyidea") {}; + }) + + (mkIf cfg.ldap-proxy.enable { + + systemd.services.privacyidea-ldap-proxy = let + ldap-proxy-env = pkgs.python2.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; + ExecStart = '' + ${ldap-proxy-env}/bin/twistd \ + --nodaemon \ + --pidfile= \ + -u ${cfg.ldap-proxy.user} \ + -g ${cfg.ldap-proxy.group} \ + ldap-proxy \ + -c ${cfg.ldap-proxy.configFile} + ''; + Restart = "always"; + }; + }; + + users.extraUsers.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") { + group = cfg.ldap-proxy.group; + }; + + users.extraGroups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {}; + }) + ]; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 30229a3a5b2f..245f8497d05e 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -255,6 +255,7 @@ in pppd = handleTest ./pppd.nix {}; predictable-interface-names = handleTest ./predictable-interface-names.nix {}; printing = handleTest ./printing.nix {}; + privacyidea = handleTest ./privacyidea.nix {}; prometheus = handleTest ./prometheus.nix {}; prometheus-exporters = handleTest ./prometheus-exporters.nix {}; prosody = handleTest ./xmpp/prosody.nix {}; diff --git a/nixos/tests/privacyidea.nix b/nixos/tests/privacyidea.nix new file mode 100644 index 000000000000..fcca1937385c --- /dev/null +++ b/nixos/tests/privacyidea.nix @@ -0,0 +1,36 @@ +# Miscellaneous small tests that don't warrant their own VM run. + +import ./make-test-python.nix ({ pkgs, ...} : rec { + name = "privacyidea"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ fpletz ]; + }; + + machine = { ... }: { + virtualisation.cores = 2; + virtualisation.memorySize = 512; + + services.privacyidea = { + enable = true; + secretKey = "testing"; + pepper = "testing"; + adminPassword = "testing"; + adminEmail = "root@localhost"; + }; + services.nginx = { + enable = true; + virtualHosts."_".locations."/".extraConfig = '' + uwsgi_pass unix:/run/privacyidea/socket; + ''; + }; + }; + + testScript = '' + machine.start() + machine.wait_for_unit("multi-user.target") + machine.succeed("curl --fail http://localhost | grep privacyIDEA") + machine.succeed( + "curl --fail http://localhost/auth -F username=admin -F password=testing | grep token" + ) + ''; +})