grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / misc / portunus.nix
blob20486f0212f7b082fc77e3b9c59d42275d6037c3
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.portunus;
5 in
7   options.services.portunus = {
8     enable = lib.mkEnableOption "Portunus, a self-contained user/group management and authentication service for LDAP";
10     domain = lib.mkOption {
11       type = lib.types.str;
12       example = "sso.example.com";
13       description = "Subdomain which gets reverse proxied to Portunus webserver.";
14     };
16     port = lib.mkOption {
17       type = lib.types.port;
18       default = 8080;
19       description = ''
20         Port where the Portunus webserver should listen on.
22         This must be put behind a TLS-capable reverse proxy because Portunus only listens on localhost.
23       '';
24     };
26     package = lib.mkPackageOption pkgs "portunus" { };
28     seedPath = lib.mkOption {
29       type = lib.types.nullOr lib.types.path;
30       default = null;
31       description = ''
32         Path to a portunus seed file in json format.
33         See <https://github.com/majewsky/portunus#seeding-users-and-groups-from-static-configuration> for available options.
34       '';
35     };
37     seedSettings = lib.mkOption {
38       type = with lib.types; nullOr (attrsOf (listOf (attrsOf anything)));
39       default = null;
40       description = ''
41         Seed settings for users and groups.
42         See upstream for format <https://github.com/majewsky/portunus#seeding-users-and-groups-from-static-configuration>
43       '';
44     };
46     stateDir = lib.mkOption {
47       type = lib.types.path;
48       default = "/var/lib/portunus";
49       description = "Path where Portunus stores its state.";
50     };
52     user = lib.mkOption {
53       type = lib.types.str;
54       default = "portunus";
55       description = "User account under which Portunus runs its webserver.";
56     };
58     group = lib.mkOption {
59       type = lib.types.str;
60       default = "portunus";
61       description = "Group account under which Portunus runs its webserver.";
62     };
64     dex = {
65       enable = lib.mkEnableOption ''
66         Dex ldap connector.
68         To activate dex, first a search user must be created in the Portunus web ui
69         and then the password must to be set as the `DEX_SEARCH_USER_PASSWORD` environment variable
70         in the [](#opt-services.dex.environmentFile) setting
71       '';
73       oidcClients = lib.mkOption {
74         type = lib.types.listOf (lib.types.submodule {
75           options = {
76             callbackURL = lib.mkOption {
77               type = lib.types.str;
78               description = "URL where the OIDC client should redirect";
79             };
80             id = lib.mkOption {
81               type = lib.types.str;
82               description = "ID of the OIDC client";
83             };
84           };
85         });
86         default = [ ];
87         example = [
88           {
89             callbackURL = "https://example.com/client/oidc/callback";
90             id = "service";
91           }
92         ];
93         description = ''
94           List of OIDC clients.
96           The OIDC secret must be set as the `DEX_CLIENT_''${id}` environment variable
97           in the [](#opt-services.dex.environmentFile) setting.
99           ::: {.note}
100           Make sure the id only contains characters that are allowed in an environment variable name, e.g. no -.
101           :::
102         '';
103       };
105       port = lib.mkOption {
106         type = lib.types.port;
107         default = 5556;
108         description = "Port where dex should listen on.";
109       };
110     };
112     ldap = {
113       package = lib.mkOption {
114         type = lib.types.package;
115         default = pkgs.openldap;
116         defaultText = lib.literalExpression "pkgs.openldap.override { libxcrypt = pkgs.libxcrypt-legacy; }";
117         description = "The OpenLDAP package to use.";
118       };
120       searchUserName = lib.mkOption {
121         type = lib.types.str;
122         default = "";
123         example = "admin";
124         description = ''
125           The login name of the search user.
126           This user account must be configured in Portunus either manually or via seeding.
127         '';
128       };
130       suffix = lib.mkOption {
131         type = lib.types.str;
132         example = "dc=example,dc=org";
133         description = ''
134           The DN of the topmost entry in your LDAP directory.
135           Please refer to the Portunus documentation for more information on how this impacts the structure of the LDAP directory.
136         '';
137       };
139       tls = lib.mkOption {
140         type = lib.types.bool;
141         default = false;
142         description = ''
143           Whether to enable LDAPS protocol.
144           This also adds two entries to the `/etc/hosts` file to point [](#opt-services.portunus.domain) to localhost,
145           so that CLIs and programs can use ldaps protocol and verify the certificate without opening the firewall port for the protocol.
147           This requires a TLS certificate for [](#opt-services.portunus.domain) to be configured via [](#opt-security.acme.certs).
148         '';
149       };
151       user = lib.mkOption {
152         type = lib.types.str;
153         default = "openldap";
154         description = "User account under which Portunus runs its LDAP server.";
155       };
157       group = lib.mkOption {
158         type = lib.types.str;
159         default = "openldap";
160         description = "Group account under which Portunus runs its LDAP server.";
161       };
162     };
163   };
165   config = lib.mkIf cfg.enable {
166     assertions = [
167       {
168         assertion = cfg.dex.enable -> cfg.ldap.searchUserName != "";
169         message = "services.portunus.dex.enable requires services.portunus.ldap.searchUserName to be set.";
170       }
171     ];
173     # add ldapsearch(1) etc. to interactive shells
174     environment.systemPackages = [ cfg.ldap.package ];
176     # allow connecting via ldaps /w certificate without opening ports
177     networking.hosts = lib.mkIf cfg.ldap.tls {
178       "::1" = [ cfg.domain ];
179       "127.0.0.1" = [ cfg.domain ];
180     };
182     services = {
183       dex = lib.mkIf cfg.dex.enable {
184         enable = true;
185         settings = {
186           issuer = "https://${cfg.domain}/dex";
187           web.http = "127.0.0.1:${toString cfg.dex.port}";
188           storage = {
189             type = "sqlite3";
190             config.file = "/var/lib/dex/dex.db";
191           };
192           enablePasswordDB = false;
193           connectors = [{
194             type = "ldap";
195             id = "ldap";
196             name = "LDAP";
197             config = {
198               host = "${cfg.domain}:636";
199               bindDN = "uid=${cfg.ldap.searchUserName},ou=users,${cfg.ldap.suffix}";
200               bindPW = "$DEX_SEARCH_USER_PASSWORD";
201               userSearch = {
202                 baseDN = "ou=users,${cfg.ldap.suffix}";
203                 filter = "(objectclass=person)";
204                 username = "uid";
205                 idAttr = "uid";
206                 emailAttr = "mail";
207                 nameAttr = "cn";
208                 preferredUsernameAttr = "uid";
209               };
210               groupSearch = {
211                 baseDN = "ou=groups,${cfg.ldap.suffix}";
212                 filter = "(objectclass=groupOfNames)";
213                 nameAttr = "cn";
214                 userMatchers = [{ userAttr = "DN"; groupAttr = "member"; }];
215               };
216             };
217           }];
219           staticClients = lib.forEach cfg.dex.oidcClients (client: {
220             inherit (client) id;
221             redirectURIs = [ client.callbackURL ];
222             name = "OIDC for ${client.id}";
223             secretEnv = "DEX_CLIENT_${client.id}";
224           });
225         };
226       };
228       portunus.seedPath = lib.mkIf (cfg.seedSettings != null) (pkgs.writeText "seed.json" (builtins.toJSON cfg.seedSettings));
229     };
231     systemd.services = {
232       dex = lib.mkIf cfg.dex.enable {
233         serviceConfig = {
234           # `dex.service` is super locked down out of the box, but we need some
235           # place to write the SQLite database. This creates $STATE_DIRECTORY below
236           # /var/lib/private because DynamicUser=true, but it gets symlinked into
237           # /var/lib/dex inside the unit
238           StateDirectory = "dex";
239         };
240       };
242       portunus = {
243         description = "Self-contained authentication service";
244         wantedBy = [ "multi-user.target" ];
245         after = [ "network.target" ];
246         serviceConfig = {
247           ExecStart = "${cfg.package}/bin/portunus-orchestrator";
248           Restart = "on-failure";
249         };
250         environment = {
251           PORTUNUS_LDAP_SUFFIX = cfg.ldap.suffix;
252           PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server";
253           PORTUNUS_SERVER_GROUP = cfg.group;
254           PORTUNUS_SERVER_USER = cfg.user;
255           PORTUNUS_SERVER_HTTP_LISTEN = "127.0.0.1:${toString cfg.port}";
256           PORTUNUS_SERVER_STATE_DIR = cfg.stateDir;
257           PORTUNUS_SLAPD_BINARY = "${cfg.ldap.package}/libexec/slapd";
258           PORTUNUS_SLAPD_GROUP = cfg.ldap.group;
259           PORTUNUS_SLAPD_USER = cfg.ldap.user;
260           PORTUNUS_SLAPD_SCHEMA_DIR = "${cfg.ldap.package}/etc/schema";
261         } // (lib.optionalAttrs (cfg.seedPath != null) ({
262           PORTUNUS_SEED_PATH = cfg.seedPath;
263         })) // (lib.optionalAttrs cfg.ldap.tls (
264           let
265             acmeDirectory = config.security.acme.certs."${cfg.domain}".directory;
266           in
267           {
268             PORTUNUS_SERVER_HTTP_SECURE = "true";
269             PORTUNUS_SLAPD_TLS_CA_CERTIFICATE = "/etc/ssl/certs/ca-certificates.crt";
270             PORTUNUS_SLAPD_TLS_CERTIFICATE = "${acmeDirectory}/cert.pem";
271             PORTUNUS_SLAPD_TLS_DOMAIN_NAME = cfg.domain;
272             PORTUNUS_SLAPD_TLS_PRIVATE_KEY = "${acmeDirectory}/key.pem";
273           }));
274       };
275     };
277     users.users = lib.mkMerge [
278       (lib.mkIf (cfg.ldap.user == "openldap") {
279         openldap = {
280           group = cfg.ldap.group;
281           isSystemUser = true;
282         };
283       })
284       (lib.mkIf (cfg.user == "portunus") {
285         portunus = {
286           group = cfg.group;
287           isSystemUser = true;
288         };
289       })
290     ];
292     users.groups = lib.mkMerge [
293       (lib.mkIf (cfg.ldap.user == "openldap") {
294         openldap = { };
295       })
296       (lib.mkIf (cfg.user == "portunus") {
297         portunus = { };
298       })
299     ];
300   };
302   meta.maintainers = [ lib.maintainers.majewsky ] ++ lib.teams.c3d2.members;