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