at 23.05-pre 9.3 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 = "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 default = pkgs.openldap; 111 defaultText = "pkgs.openldap"; 112 description = lib.mdDoc "The OpenLDAP package to use."; 113 }; 114 115 searchUserName = mkOption { 116 type = types.str; 117 default = ""; 118 example = "admin"; 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. 122 ''; 123 }; 124 125 suffix = mkOption { 126 type = types.str; 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. 131 ''; 132 }; 133 134 tls = mkOption { 135 type = types.bool; 136 default = false; 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. 141 142 This requires a TLS certificate for [](#opt-services.portunus.domain) to be configured via [](#opt-security.acme.certs). 143 ''; 144 }; 145 146 user = mkOption { 147 type = types.str; 148 default = "openldap"; 149 description = lib.mdDoc "User account under which Portunus runs its LDAP server."; 150 }; 151 152 group = mkOption { 153 type = types.str; 154 default = "openldap"; 155 description = lib.mdDoc "Group account under which Portunus runs its LDAP server."; 156 }; 157 }; 158 }; 159 160 config = mkIf cfg.enable { 161 assertions = [ 162 { 163 assertion = cfg.dex.enable -> cfg.ldap.searchUserName != ""; 164 message = "services.portunus.dex.enable requires services.portunus.ldap.searchUserName to be set."; 165 } 166 ]; 167 168 # add ldapsearch(1) etc. to interactive shells 169 environment.systemPackages = [ cfg.ldap.package ]; 170 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 ]; 175 }; 176 177 services.dex = mkIf cfg.dex.enable { 178 enable = true; 179 settings = { 180 issuer = "https://${cfg.domain}/dex"; 181 web.http = "127.0.0.1:${toString cfg.dex.port}"; 182 storage = { 183 type = "sqlite3"; 184 config.file = "/var/lib/dex/dex.db"; 185 }; 186 enablePasswordDB = false; 187 connectors = [{ 188 type = "ldap"; 189 id = "ldap"; 190 name = "LDAP"; 191 config = { 192 host = "${cfg.domain}:636"; 193 bindDN = "uid=${cfg.ldap.searchUserName},ou=users,${cfg.ldap.suffix}"; 194 bindPW = "$DEX_SEARCH_USER_PASSWORD"; 195 userSearch = { 196 baseDN = "ou=users,${cfg.ldap.suffix}"; 197 filter = "(objectclass=person)"; 198 username = "uid"; 199 idAttr = "uid"; 200 emailAttr = "mail"; 201 nameAttr = "cn"; 202 preferredUsernameAttr = "uid"; 203 }; 204 groupSearch = { 205 baseDN = "ou=groups,${cfg.ldap.suffix}"; 206 filter = "(objectclass=groupOfNames)"; 207 nameAttr = "cn"; 208 userMatchers = [{ userAttr = "DN"; groupAttr = "member"; }]; 209 }; 210 }; 211 }]; 212 213 staticClients = forEach cfg.dex.oidcClients (client: { 214 inherit (client) id; 215 redirectURIs = [ client.callbackURL ]; 216 name = "OIDC for ${client.id}"; 217 secretEnv = "DEX_CLIENT_${client.id}"; 218 }); 219 }; 220 }; 221 222 systemd.services = { 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"; 229 }; 230 231 portunus = { 232 description = "Self-contained authentication service"; 233 wantedBy = [ "multi-user.target" ]; 234 after = [ "network.target" ]; 235 serviceConfig.ExecStart = "${cfg.package.out}/bin/portunus-orchestrator"; 236 environment = { 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 ( 250 let 251 acmeDirectory = config.security.acme.certs."${cfg.domain}".directory; 252 in 253 { 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"; 258 })); 259 }; 260 }; 261 262 users.users = mkMerge [ 263 (mkIf (cfg.ldap.user == "openldap") { 264 openldap = { 265 group = cfg.ldap.group; 266 isSystemUser = true; 267 }; 268 }) 269 (mkIf (cfg.user == "portunus") { 270 portunus = { 271 group = cfg.group; 272 isSystemUser = true; 273 }; 274 }) 275 ]; 276 277 users.groups = mkMerge [ 278 (mkIf (cfg.ldap.user == "openldap") { 279 openldap = { }; 280 }) 281 (mkIf (cfg.user == "portunus") { 282 portunus = { }; 283 }) 284 ]; 285 }; 286 287 meta.maintainers = [ maintainers.majewsky ] ++ teams.c3d2.members; 288}