diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix
index a8c47dbdbfdf..f95592557a95 100644
--- a/maintainers/maintainer-list.nix
+++ b/maintainers/maintainer-list.nix
@@ -7956,6 +7956,12 @@
githubId = 31056089;
name = "Tom Ho";
};
+ majewsky = {
+ email = "majewsky@gmx.net";
+ github = "majewsky";
+ githubId = 24696;
+ name = "Stefan Majewsky";
+ };
majiir = {
email = "majiir@nabaal.net";
github = "Majiir";
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 8a8df700330e..82c4d69a7880 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -620,6 +620,7 @@
./services/misc/plikd.nix
./services/misc/podgrab.nix
./services/misc/polaris.nix
+ ./services/misc/portunus.nix
./services/misc/prowlarr.nix
./services/misc/tautulli.nix
./services/misc/pinnwand.nix
diff --git a/nixos/modules/services/misc/portunus.nix b/nixos/modules/services/misc/portunus.nix
new file mode 100644
index 000000000000..a2247272fa26
--- /dev/null
+++ b/nixos/modules/services/misc/portunus.nix
@@ -0,0 +1,288 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.portunus;
+
+in
+{
+ options.services.portunus = {
+ enable = mkEnableOption "Portunus, a self-contained user/group management and authentication service for LDAP";
+
+ domain = mkOption {
+ type = types.str;
+ example = "sso.example.com";
+ description = "Subdomain which gets reverse proxied to Portunus webserver.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 8080;
+ description = ''
+ Port where the Portunus webserver should listen on.
+
+ This must be put behind a TLS-capable reverse proxy because Portunus only listens on localhost.
+ '';
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.portunus;
+ defaultText = "pkgs.portunus";
+ description = "The Portunus package to use.";
+ };
+
+ seedPath = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Path to a portunus seed file in json format.
+ See for available options.
+ '';
+ };
+
+ stateDir = mkOption {
+ type = types.path;
+ default = "/var/lib/portunus";
+ description = "Path where Portunus stores its state.";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "portunus";
+ description = "User account under which Portunus runs its webserver.";
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "portunus";
+ description = "Group account under which Portunus runs its webserver.";
+ };
+
+ dex = {
+ enable = mkEnableOption ''
+ Dex ldap connector.
+
+ To activate dex, first a search user must be created in the Portunus web ui
+ and then the password must to be set as the DEX_SEARCH_USER_PASSWORD environment variable
+ in the setting.
+ '';
+
+ oidcClients = mkOption {
+ type = types.listOf (types.submodule {
+ options = {
+ callbackURL = mkOption {
+ type = types.str;
+ description = "URL where the OIDC client should redirect";
+ };
+ id = mkOption {
+ type = types.str;
+ description = "ID of the OIDC client";
+ };
+ };
+ });
+ default = [ ];
+ example = [
+ {
+ callbackURL = "https://example.com/client/oidc/callback";
+ id = "service";
+ }
+ ];
+ description = ''
+ List of OIDC clients.
+
+ The OIDC secret must be set as the DEX_CLIENT_''${id} environment variable
+ in the setting.
+ '';
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 5556;
+ description = "Port where dex should listen on.";
+ };
+ };
+
+ ldap = {
+ package = mkOption {
+ type = types.package;
+ default = pkgs.openldap;
+ defaultText = "pkgs.openldap";
+ description = "The OpenLDAP package to use.";
+ };
+
+ searchUserName = mkOption {
+ type = types.str;
+ default = "";
+ example = "admin";
+ description = ''
+ The login name of the search user.
+ This user account must be configured in Portunus either manually or via seeding.
+ '';
+ };
+
+ suffix = mkOption {
+ type = types.str;
+ example = "dc=example,dc=org";
+ description = ''
+ The DN of the topmost entry in your LDAP directory.
+ Please refer to the Portunus documentation for more information on how this impacts the structure of the LDAP directory.
+ '';
+ };
+
+ tls = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Wether to enable LDAPS protocol.
+ This also adds two entries to the /etc/hosts file to point to localhost,
+ so that CLIs and programs can use ldaps protocol and verify the certificate without opening the firewall port for the protocol.
+
+ This requires a TLS certificate for to be configured via .
+ '';
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "openldap";
+ description = "User account under which Portunus runs its LDAP server.";
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "openldap";
+ description = "Group account under which Portunus runs its LDAP server.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = cfg.dex.enable -> cfg.ldap.searchUserName != "";
+ message = "services.portunus.dex.enable requires services.portunus.ldap.searchUserName to be set.";
+ }
+ ];
+
+ # add ldapsearch(1) etc. to interactive shells
+ environment.systemPackages = [ cfg.ldap.package ];
+
+ # allow connecting via ldaps /w certificate without opening ports
+ networking.hosts = mkIf cfg.ldap.tls {
+ "::1" = [ cfg.domain ];
+ "127.0.0.1" = [ cfg.domain ];
+ };
+
+ services.dex = mkIf cfg.dex.enable {
+ enable = true;
+ settings = {
+ issuer = "https://${cfg.domain}/dex";
+ web.http = "127.0.0.1:${toString cfg.dex.port}";
+ storage = {
+ type = "sqlite3";
+ config.file = "/var/lib/dex/dex.db";
+ };
+ enablePasswordDB = false;
+ connectors = [{
+ type = "ldap";
+ id = "ldap";
+ name = "LDAP";
+ config = {
+ host = "${cfg.domain}:636";
+ bindDN = "uid=${cfg.ldap.searchUserName},ou=users,${cfg.ldap.suffix}";
+ bindPW = "$DEX_SEARCH_USER_PASSWORD";
+ userSearch = {
+ baseDN = "ou=users,${cfg.ldap.suffix}";
+ filter = "(objectclass=person)";
+ username = "uid";
+ idAttr = "uid";
+ emailAttr = "mail";
+ nameAttr = "cn";
+ preferredUsernameAttr = "uid";
+ };
+ groupSearch = {
+ baseDN = "ou=groups,${cfg.ldap.suffix}";
+ filter = "(objectclass=groupOfNames)";
+ nameAttr = "cn";
+ userMatchers = [{ userAttr = "DN"; groupAttr = "member"; }];
+ };
+ };
+ }];
+
+ staticClients = forEach cfg.dex.oidcClients (client: {
+ inherit (client) id;
+ redirectURIs = [ client.callbackURI ];
+ name = "OIDC for ${client.id}";
+ secret = "$DEX_CLIENT_${client.id}";
+ });
+ };
+ };
+
+ systemd.services = {
+ dex.serviceConfig = mkIf cfg.dex.enable {
+ # `dex.service` is super locked down out of the box, but we need some
+ # place to write the SQLite database. This creates $STATE_DIRECTORY below
+ # /var/lib/private because DynamicUser=true, but it gets symlinked into
+ # /var/lib/dex inside the unit
+ StateDirectory = "dex";
+ };
+
+ portunus = {
+ description = "Self-contained authentication service";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+ serviceConfig.ExecStart = "${cfg.package.out}/bin/portunus-orchestrator";
+ environment = {
+ PORTUNUS_LDAP_SUFFIX = cfg.ldap.suffix;
+ PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server";
+ PORTUNUS_SERVER_GROUP = cfg.group;
+ PORTUNUS_SERVER_USER = cfg.user;
+ PORTUNUS_SERVER_HTTP_LISTEN = "[::]:${toString cfg.port}";
+ PORTUNUS_SERVER_STATE_DIR = cfg.stateDir;
+ PORTUNUS_SLAPD_BINARY = "${cfg.ldap.package}/libexec/slapd";
+ PORTUNUS_SLAPD_GROUP = cfg.ldap.group;
+ PORTUNUS_SLAPD_USER = cfg.ldap.user;
+ PORTUNUS_SLAPD_SCHEMA_DIR = "${cfg.ldap.package}/etc/schema";
+ } // (optionalAttrs (cfg.seedPath != null) ({
+ PORTUNUS_SEED_PATH = cfg.seedPath;
+ })) // (optionalAttrs cfg.ldap.tls (
+ let
+ acmeDirectory = config.security.acme.certs."${cfg.domain}".directory;
+ in
+ {
+ PORTUNUS_SLAPD_TLS_CA_CERTIFICATE = "/etc/ssl/certs/ca-certificates.crt";
+ PORTUNUS_SLAPD_TLS_CERTIFICATE = "${acmeDirectory}/cert.pem";
+ PORTUNUS_SLAPD_TLS_DOMAIN_NAME = cfg.domain;
+ PORTUNUS_SLAPD_TLS_PRIVATE_KEY = "${acmeDirectory}/key.pem";
+ }));
+ };
+ };
+
+ users.users = mkMerge [
+ (mkIf (cfg.ldap.user == "openldap") {
+ openldap = {
+ group = cfg.ldap.group;
+ isSystemUser = true;
+ };
+ })
+ (mkIf (cfg.user == "portunus") {
+ portunus = {
+ group = cfg.group;
+ isSystemUser = true;
+ };
+ })
+ ];
+
+ users.groups = mkMerge [
+ (mkIf (cfg.ldap.user == "openldap") {
+ openldap = { };
+ })
+ (mkIf (cfg.user == "portunus") {
+ portunus = { };
+ })
+ ];
+ };
+
+ meta.maintainers = [ majewsky ] ++ teams.c3d2.members;
+}
diff --git a/nixos/modules/services/web-apps/dex.nix b/nixos/modules/services/web-apps/dex.nix
index eebf4b740c77..82fdcd212f96 100644
--- a/nixos/modules/services/web-apps/dex.nix
+++ b/nixos/modules/services/web-apps/dex.nix
@@ -11,15 +11,26 @@ let
settingsFormat = pkgs.formats.yaml {};
configFile = settingsFormat.generate "config.yaml" filteredSettings;
- startPreScript = pkgs.writeShellScript "dex-start-pre" (''
- '' + (concatStringsSep "\n" (builtins.map (file: ''
- ${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' /run/dex/config.yaml
- '') secretFiles)));
+ startPreScript = pkgs.writeShellScript "dex-start-pre"
+ (concatStringsSep "\n" (map (file: ''
+ replace-secret '${file}' '${file}' /run/dex/config.yaml
+ '')
+ secretFiles));
in
{
options.services.dex = {
enable = mkEnableOption "the OpenID Connect and OAuth2 identity provider";
+ environmentFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Environment file (see systemd.exec(5)
+ "EnvironmentFile=" section for the syntax) to define variables for dex.
+ This option can be used to safely include secret keys into the dex configuration.
+ '';
+ };
+
settings = mkOption {
type = settingsFormat.type;
default = {};
@@ -48,6 +59,9 @@ in
description = lib.mdDoc ''
The available options can be found in
[the example configuration](https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist).
+
+ It's also possible to refer to environment variables (defined in [services.dex.environmentFile](#opt-services.dex.environmentFile))
+ using the syntax `$VARIABLE_NAME`.
'';
};
};
@@ -57,15 +71,15 @@ in
description = "dex identity provider";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ] ++ (optional (cfg.settings.storage.type == "postgres") "postgresql.service");
-
+ path = with pkgs; [ replace-secret ];
serviceConfig = {
ExecStart = "${pkgs.dex-oidc}/bin/dex serve /run/dex/config.yaml";
ExecStartPre = [
"${pkgs.coreutils}/bin/install -m 600 ${configFile} /run/dex/config.yaml"
"+${startPreScript}"
];
- RuntimeDirectory = "dex";
+ RuntimeDirectory = "dex";
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
BindReadOnlyPaths = [
"/nix/store"
@@ -109,6 +123,8 @@ in
TemporaryFileSystem = "/:ro";
# Does not work well with the temporary root
#UMask = "0066";
+ } // optionalAttrs (cfg.environmentFile != null) {
+ EnvironmentFile = cfg.environmentFile;
};
};
};
diff --git a/pkgs/servers/portunus/default.nix b/pkgs/servers/portunus/default.nix
new file mode 100644
index 000000000000..c0ee915c7bbe
--- /dev/null
+++ b/pkgs/servers/portunus/default.nix
@@ -0,0 +1,31 @@
+{ lib
+, buildGoModule
+, fetchFromGitHub
+}:
+
+buildGoModule rec {
+ pname = "portunus";
+ version = "1.1.0-beta.2";
+
+ src = fetchFromGitHub {
+ owner = "majewsky";
+ repo = "portunus";
+ rev = "v${version}";
+ sha256 = "sha256-hGOMbaEWecgQvpk/2E8mcJZ9QMjllIhS3RBr7PKnbjQ=";
+ };
+
+ vendorSha256 = null;
+
+ postInstall = ''
+ mv $out/bin/{,portunus-}orchestrator
+ mv $out/bin/{,portunus-}server
+ '';
+
+ meta = with lib; {
+ description = "Self-contained user/group management and authentication service";
+ homepage = "https://github.com/majewsky/portunus";
+ license = licenses.gpl3Plus;
+ platforms = platforms.linux;
+ maintainers = with maintainers; [ majewsky ] ++ teams.c3d2.members;
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index fda2ba998a15..c8851d98c396 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -22437,6 +22437,8 @@ with pkgs;
podgrab = callPackage ../servers/misc/podgrab { };
+ portunus = callPackage ../servers/portunus { };
+
prosody = callPackage ../servers/xmpp/prosody {
withExtraLibs = [];
withExtraLuaPackages = _: [];