at master 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.mkPackageOption pkgs "openldap" { }; 121 122 searchUserName = lib.mkOption { 123 type = lib.types.str; 124 default = ""; 125 example = "admin"; 126 description = '' 127 The login name of the search user. 128 This user account must be configured in Portunus either manually or via seeding. 129 ''; 130 }; 131 132 suffix = lib.mkOption { 133 type = lib.types.str; 134 example = "dc=example,dc=org"; 135 description = '' 136 The DN of the topmost entry in your LDAP directory. 137 Please refer to the Portunus documentation for more information on how this impacts the structure of the LDAP directory. 138 ''; 139 }; 140 141 tls = lib.mkOption { 142 type = lib.types.bool; 143 default = false; 144 description = '' 145 Whether to enable LDAPS protocol. 146 This also adds two entries to the `/etc/hosts` file to point [](#opt-services.portunus.domain) to localhost, 147 so that CLIs and programs can use ldaps protocol and verify the certificate without opening the firewall port for the protocol. 148 149 This requires a TLS certificate for [](#opt-services.portunus.domain) to be configured via [](#opt-security.acme.certs). 150 ''; 151 }; 152 153 user = lib.mkOption { 154 type = lib.types.str; 155 default = "openldap"; 156 description = "User account under which Portunus runs its LDAP server."; 157 }; 158 159 group = lib.mkOption { 160 type = lib.types.str; 161 default = "openldap"; 162 description = "Group account under which Portunus runs its LDAP server."; 163 }; 164 }; 165 }; 166 167 config = lib.mkIf cfg.enable { 168 assertions = [ 169 { 170 assertion = cfg.dex.enable -> cfg.ldap.searchUserName != ""; 171 message = "services.portunus.dex.enable requires services.portunus.ldap.searchUserName to be set."; 172 } 173 ]; 174 175 # add ldapsearch(1) etc. to interactive shells 176 environment.systemPackages = [ cfg.ldap.package ]; 177 178 # allow connecting via ldaps /w certificate without opening ports 179 networking.hosts = lib.mkIf cfg.ldap.tls { 180 "::1" = [ cfg.domain ]; 181 "127.0.0.1" = [ cfg.domain ]; 182 }; 183 184 services = { 185 dex = lib.mkIf cfg.dex.enable { 186 enable = true; 187 settings = { 188 issuer = "https://${cfg.domain}/dex"; 189 web.http = "127.0.0.1:${toString cfg.dex.port}"; 190 storage = { 191 type = "sqlite3"; 192 config.file = "/var/lib/dex/dex.db"; 193 }; 194 enablePasswordDB = false; 195 connectors = [ 196 { 197 type = "ldap"; 198 id = "ldap"; 199 name = "LDAP"; 200 config = { 201 host = "${cfg.domain}:636"; 202 bindDN = "uid=${cfg.ldap.searchUserName},ou=users,${cfg.ldap.suffix}"; 203 bindPW = "$DEX_SEARCH_USER_PASSWORD"; 204 userSearch = { 205 baseDN = "ou=users,${cfg.ldap.suffix}"; 206 filter = "(objectclass=person)"; 207 username = "uid"; 208 idAttr = "uid"; 209 emailAttr = "mail"; 210 nameAttr = "cn"; 211 preferredUsernameAttr = "uid"; 212 }; 213 groupSearch = { 214 baseDN = "ou=groups,${cfg.ldap.suffix}"; 215 filter = "(objectclass=groupOfNames)"; 216 nameAttr = "cn"; 217 userMatchers = [ 218 { 219 userAttr = "DN"; 220 groupAttr = "member"; 221 } 222 ]; 223 }; 224 }; 225 } 226 ]; 227 228 staticClients = lib.forEach cfg.dex.oidcClients (client: { 229 inherit (client) id; 230 redirectURIs = [ client.callbackURL ]; 231 name = "OIDC for ${client.id}"; 232 secretEnv = "DEX_CLIENT_${client.id}"; 233 }); 234 }; 235 }; 236 237 portunus.seedPath = lib.mkIf (cfg.seedSettings != null) ( 238 pkgs.writeText "seed.json" (builtins.toJSON cfg.seedSettings) 239 ); 240 }; 241 242 systemd.services = { 243 dex = lib.mkIf cfg.dex.enable { 244 serviceConfig = { 245 # `dex.service` is super locked down out of the box, but we need some 246 # place to write the SQLite database. This creates $STATE_DIRECTORY below 247 # /var/lib/private because DynamicUser=true, but it gets symlinked into 248 # /var/lib/dex inside the unit 249 StateDirectory = "dex"; 250 }; 251 }; 252 253 portunus = { 254 description = "Self-contained authentication service"; 255 wantedBy = [ "multi-user.target" ]; 256 after = [ "network.target" ]; 257 serviceConfig = { 258 ExecStart = "${cfg.package}/bin/portunus-orchestrator"; 259 Restart = "on-failure"; 260 }; 261 environment = { 262 PORTUNUS_LDAP_SUFFIX = cfg.ldap.suffix; 263 PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server"; 264 PORTUNUS_SERVER_GROUP = cfg.group; 265 PORTUNUS_SERVER_USER = cfg.user; 266 PORTUNUS_SERVER_HTTP_LISTEN = "127.0.0.1:${toString cfg.port}"; 267 PORTUNUS_SERVER_STATE_DIR = cfg.stateDir; 268 PORTUNUS_SLAPD_BINARY = "${cfg.ldap.package}/libexec/slapd"; 269 PORTUNUS_SLAPD_GROUP = cfg.ldap.group; 270 PORTUNUS_SLAPD_USER = cfg.ldap.user; 271 PORTUNUS_SLAPD_SCHEMA_DIR = "${cfg.ldap.package}/etc/schema"; 272 } 273 // (lib.optionalAttrs (cfg.seedPath != null) ({ 274 PORTUNUS_SEED_PATH = cfg.seedPath; 275 })) 276 // (lib.optionalAttrs cfg.ldap.tls ( 277 let 278 acmeDirectory = config.security.acme.certs."${cfg.domain}".directory; 279 in 280 { 281 PORTUNUS_SERVER_HTTP_SECURE = "true"; 282 PORTUNUS_SLAPD_TLS_CA_CERTIFICATE = config.security.pki.caBundle; 283 PORTUNUS_SLAPD_TLS_CERTIFICATE = "${acmeDirectory}/cert.pem"; 284 PORTUNUS_SLAPD_TLS_DOMAIN_NAME = cfg.domain; 285 PORTUNUS_SLAPD_TLS_PRIVATE_KEY = "${acmeDirectory}/key.pem"; 286 } 287 )); 288 }; 289 }; 290 291 users.users = lib.mkMerge [ 292 (lib.mkIf (cfg.ldap.user == "openldap") { 293 openldap = { 294 group = cfg.ldap.group; 295 isSystemUser = true; 296 }; 297 }) 298 (lib.mkIf (cfg.user == "portunus") { 299 portunus = { 300 group = cfg.group; 301 isSystemUser = true; 302 }; 303 }) 304 ]; 305 306 users.groups = lib.mkMerge [ 307 (lib.mkIf (cfg.ldap.user == "openldap") { 308 openldap = { }; 309 }) 310 (lib.mkIf (cfg.user == "portunus") { 311 portunus = { }; 312 }) 313 ]; 314 }; 315 316 meta.maintainers = [ lib.maintainers.majewsky ] ++ lib.teams.c3d2.members; 317}