1 { config, lib, pkgs, ... }:
6 cfg = config.services.portunus;
10 options.services.portunus = {
11 enable = mkEnableOption (lib.mdDoc "Portunus, a self-contained user/group management and authentication service for LDAP");
15 example = "sso.example.com";
16 description = lib.mdDoc "Subdomain which gets reverse proxied to Portunus webserver.";
22 description = lib.mdDoc ''
23 Port where the Portunus webserver should listen on.
25 This must be put behind a TLS-capable reverse proxy because Portunus only listens on localhost.
31 default = pkgs.portunus;
32 defaultText = "pkgs.portunus";
33 description = lib.mdDoc "The Portunus package to use.";
37 type = types.nullOr types.path;
39 description = lib.mdDoc ''
40 Path to a portunus seed file in json format.
41 See <https://github.com/majewsky/portunus#seeding-users-and-groups-from-static-configuration> for available options.
47 default = "/var/lib/portunus";
48 description = lib.mdDoc "Path where Portunus stores its state.";
54 description = lib.mdDoc "User account under which Portunus runs its webserver.";
60 description = lib.mdDoc "Group account under which Portunus runs its webserver.";
64 enable = mkEnableOption (lib.mdDoc ''
67 To activate dex, first a search user must be created in the Portunus web ui
68 and then the password must to be set as the `DEX_SEARCH_USER_PASSWORD` environment variable
69 in the [](#opt-services.dex.environmentFile) setting.
72 oidcClients = mkOption {
73 type = types.listOf (types.submodule {
75 callbackURL = mkOption {
77 description = lib.mdDoc "URL where the OIDC client should redirect";
81 description = lib.mdDoc "ID of the OIDC client";
88 callbackURL = "https://example.com/client/oidc/callback";
92 description = lib.mdDoc ''
95 The OIDC secret must be set as the `DEX_CLIENT_''${id}` environment variable
96 in the [](#opt-services.dex.environmentFile) setting.
103 description = lib.mdDoc "Port where dex should listen on.";
109 type = types.package;
110 default = pkgs.openldap;
111 defaultText = "pkgs.openldap";
112 description = lib.mdDoc "The OpenLDAP package to use.";
115 searchUserName = mkOption {
119 description = lib.mdDoc ''
120 The login name of the search user.
121 This user account must be configured in Portunus either manually or via seeding.
127 example = "dc=example,dc=org";
128 description = lib.mdDoc ''
129 The DN of the topmost entry in your LDAP directory.
130 Please refer to the Portunus documentation for more information on how this impacts the structure of the LDAP directory.
137 description = lib.mdDoc ''
138 Wether to enable LDAPS protocol.
139 This also adds two entries to the `/etc/hosts` file to point [](#opt-services.portunus.domain) to localhost,
140 so that CLIs and programs can use ldaps protocol and verify the certificate without opening the firewall port for the protocol.
142 This requires a TLS certificate for [](#opt-services.portunus.domain) to be configured via [](#opt-security.acme.certs).
148 default = "openldap";
149 description = lib.mdDoc "User account under which Portunus runs its LDAP server.";
154 default = "openldap";
155 description = lib.mdDoc "Group account under which Portunus runs its LDAP server.";
160 config = mkIf cfg.enable {
163 assertion = cfg.dex.enable -> cfg.ldap.searchUserName != "";
164 message = "services.portunus.dex.enable requires services.portunus.ldap.searchUserName to be set.";
168 # add ldapsearch(1) etc. to interactive shells
169 environment.systemPackages = [ cfg.ldap.package ];
171 # allow connecting via ldaps /w certificate without opening ports
172 networking.hosts = mkIf cfg.ldap.tls {
173 "::1" = [ cfg.domain ];
174 "127.0.0.1" = [ cfg.domain ];
177 services.dex = mkIf cfg.dex.enable {
180 issuer = "https://${cfg.domain}/dex";
181 web.http = "127.0.0.1:${toString cfg.dex.port}";
184 config.file = "/var/lib/dex/dex.db";
186 enablePasswordDB = false;
192 host = "${cfg.domain}:636";
193 bindDN = "uid=${cfg.ldap.searchUserName},ou=users,${cfg.ldap.suffix}";
194 bindPW = "$DEX_SEARCH_USER_PASSWORD";
196 baseDN = "ou=users,${cfg.ldap.suffix}";
197 filter = "(objectclass=person)";
202 preferredUsernameAttr = "uid";
205 baseDN = "ou=groups,${cfg.ldap.suffix}";
206 filter = "(objectclass=groupOfNames)";
208 userMatchers = [{ userAttr = "DN"; groupAttr = "member"; }];
213 staticClients = forEach cfg.dex.oidcClients (client: {
215 redirectURIs = [ client.callbackURL ];
216 name = "OIDC for ${client.id}";
217 secretEnv = "DEX_CLIENT_${client.id}";
223 dex.serviceConfig = mkIf cfg.dex.enable {
224 # `dex.service` is super locked down out of the box, but we need some
225 # place to write the SQLite database. This creates $STATE_DIRECTORY below
226 # /var/lib/private because DynamicUser=true, but it gets symlinked into
227 # /var/lib/dex inside the unit
228 StateDirectory = "dex";
232 description = "Self-contained authentication service";
233 wantedBy = [ "multi-user.target" ];
234 after = [ "network.target" ];
235 serviceConfig.ExecStart = "${cfg.package.out}/bin/portunus-orchestrator";
237 PORTUNUS_LDAP_SUFFIX = cfg.ldap.suffix;
238 PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server";
239 PORTUNUS_SERVER_GROUP = cfg.group;
240 PORTUNUS_SERVER_USER = cfg.user;
241 PORTUNUS_SERVER_HTTP_LISTEN = "[::]:${toString cfg.port}";
242 PORTUNUS_SERVER_STATE_DIR = cfg.stateDir;
243 PORTUNUS_SLAPD_BINARY = "${cfg.ldap.package}/libexec/slapd";
244 PORTUNUS_SLAPD_GROUP = cfg.ldap.group;
245 PORTUNUS_SLAPD_USER = cfg.ldap.user;
246 PORTUNUS_SLAPD_SCHEMA_DIR = "${cfg.ldap.package}/etc/schema";
247 } // (optionalAttrs (cfg.seedPath != null) ({
248 PORTUNUS_SEED_PATH = cfg.seedPath;
249 })) // (optionalAttrs cfg.ldap.tls (
251 acmeDirectory = config.security.acme.certs."${cfg.domain}".directory;
254 PORTUNUS_SLAPD_TLS_CA_CERTIFICATE = "/etc/ssl/certs/ca-certificates.crt";
255 PORTUNUS_SLAPD_TLS_CERTIFICATE = "${acmeDirectory}/cert.pem";
256 PORTUNUS_SLAPD_TLS_DOMAIN_NAME = cfg.domain;
257 PORTUNUS_SLAPD_TLS_PRIVATE_KEY = "${acmeDirectory}/key.pem";
262 users.users = mkMerge [
263 (mkIf (cfg.ldap.user == "openldap") {
265 group = cfg.ldap.group;
269 (mkIf (cfg.user == "portunus") {
277 users.groups = mkMerge [
278 (mkIf (cfg.ldap.user == "openldap") {
281 (mkIf (cfg.user == "portunus") {
287 meta.maintainers = [ maintainers.majewsky ] ++ teams.c3d2.members;