about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSandro <sandro.jaeckel@gmail.com>2022-08-17 15:20:45 +0200
committerGitHub <noreply@github.com>2022-08-17 15:20:45 +0200
commita9f3c22db5d0e73b3b3e65689bf879e8e8e46bc0 (patch)
tree9ee7040c4710c7c69ba4243b4bcc048b63be7586
parent61a3fc5f308f130f15a8eb340be1a24d79f5008c (diff)
parent49da90755b3c9d9d94246c0cabefc5d5fbac9550 (diff)
downloadnixlib-a9f3c22db5d0e73b3b3e65689bf879e8e8e46bc0.tar
nixlib-a9f3c22db5d0e73b3b3e65689bf879e8e8e46bc0.tar.gz
nixlib-a9f3c22db5d0e73b3b3e65689bf879e8e8e46bc0.tar.bz2
nixlib-a9f3c22db5d0e73b3b3e65689bf879e8e8e46bc0.tar.lz
nixlib-a9f3c22db5d0e73b3b3e65689bf879e8e8e46bc0.tar.xz
nixlib-a9f3c22db5d0e73b3b3e65689bf879e8e8e46bc0.tar.zst
nixlib-a9f3c22db5d0e73b3b3e65689bf879e8e8e46bc0.zip
Merge pull request #182382 from SuperSandro2000/portunus
-rw-r--r--maintainers/maintainer-list.nix6
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/misc/portunus.nix288
-rw-r--r--nixos/modules/services/web-apps/dex.nix28
-rw-r--r--pkgs/servers/portunus/default.nix31
-rw-r--r--pkgs/top-level/all-packages.nix2
6 files changed, 350 insertions, 6 deletions
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 <link xlink:href="https://github.com/majewsky/portunus#seeding-users-and-groups-from-static-configuration"/> 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 <literal>DEX_SEARCH_USER_PASSWORD</literal> environment variable
+        in the <xref linkend="opt-services.dex.environmentFile"/> 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 <literal>DEX_CLIENT_''${id}</literal> environment variable
+          in the <xref linkend="opt-services.dex.environmentFile"/> 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 <literal>/etc/hosts</literal> file to point <xref linkend="opt-services.portunus.domain"/> 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 <xref linkend="opt-services.portunus.domain"/> to be configured via <xref linkend="opt-security.acme.certs"/>.
+        '';
+      };
+
+      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 <literal>systemd.exec(5)</literal>
+        "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 = _: [];