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