at master 9.0 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.lldap; 10 format = pkgs.formats.toml { }; 11in 12{ 13 options.services.lldap = with lib; { 14 enable = mkEnableOption "lldap, a lightweight authentication server that provides an opinionated, simplified LDAP interface for authentication"; 15 16 package = mkPackageOption pkgs "lldap" { }; 17 18 environment = mkOption { 19 type = with types; attrsOf str; 20 default = { }; 21 example = { 22 LLDAP_JWT_SECRET_FILE = "/run/lldap/jwt_secret"; 23 LLDAP_LDAP_USER_PASS_FILE = "/run/lldap/user_password"; 24 }; 25 description = '' 26 Environment variables passed to the service. 27 Any config option name prefixed with `LLDAP_` takes priority over the one in the configuration file. 28 ''; 29 }; 30 31 environmentFile = mkOption { 32 type = types.nullOr types.path; 33 default = null; 34 description = '' 35 Environment file as defined in {manpage}`systemd.exec(5)` passed to the service. 36 ''; 37 }; 38 39 settings = mkOption { 40 description = '' 41 Free-form settings written directly to the `lldap_config.toml` file. 42 Refer to <https://github.com/lldap/lldap/blob/main/lldap_config.docker_template.toml> for supported values. 43 ''; 44 45 default = { }; 46 47 type = types.submodule { 48 freeformType = format.type; 49 options = { 50 ldap_host = mkOption { 51 type = types.str; 52 description = "The host address that the LDAP server will be bound to."; 53 default = "::"; 54 }; 55 56 ldap_port = mkOption { 57 type = types.port; 58 description = "The port on which to have the LDAP server."; 59 default = 3890; 60 }; 61 62 http_host = mkOption { 63 type = types.str; 64 description = "The host address that the HTTP server will be bound to."; 65 default = "::"; 66 }; 67 68 http_port = mkOption { 69 type = types.port; 70 description = "The port on which to have the HTTP server, for user login and administration."; 71 default = 17170; 72 }; 73 74 http_url = mkOption { 75 type = types.str; 76 description = "The public URL of the server, for password reset links."; 77 default = "http://localhost"; 78 }; 79 80 ldap_base_dn = mkOption { 81 type = types.str; 82 description = "Base DN for LDAP."; 83 example = "dc=example,dc=com"; 84 }; 85 86 ldap_user_dn = mkOption { 87 type = types.str; 88 description = "Admin username"; 89 default = "admin"; 90 }; 91 92 ldap_user_email = mkOption { 93 type = types.str; 94 description = "Admin email."; 95 default = "admin@example.com"; 96 }; 97 98 database_url = mkOption { 99 type = types.str; 100 description = "Database URL."; 101 default = "sqlite://./users.db?mode=rwc"; 102 example = "postgres://postgres-user:password@postgres-server/my-database"; 103 }; 104 105 ldap_user_pass = mkOption { 106 type = types.nullOr types.str; 107 default = null; 108 description = '' 109 Password for default admin password. 110 111 Unsecure: Use `ldap_user_pass_file` settings instead. 112 ''; 113 }; 114 115 ldap_user_pass_file = mkOption { 116 type = types.nullOr types.str; 117 default = null; 118 description = '' 119 Path to a file containing the default admin password. 120 121 If you want to update the default admin password through this setting, 122 you must set `force_ldap_user_pass_reset` to `true`. 123 Otherwise changing this setting will have no effect 124 unless this is the very first time LLDAP is started and its database is still empty. 125 ''; 126 }; 127 128 force_ldap_user_pass_reset = mkOption { 129 type = types.oneOf [ 130 types.bool 131 (types.enum [ "always" ]) 132 ]; 133 default = false; 134 description = '' 135 Force reset of the admin password. 136 137 Set this setting to `"always"` to update the admin password when `ldap_user_pass_file` changes. 138 Setting to `"always"` also means any password update in the UI will be overwritten next time the service restarts. 139 140 The difference between `true` and `"always"` is the former is intended for a one time fix 141 while the latter is intended for a declarative workflow. In practice, the result 142 is the same: the password gets reset. The only practical difference is the former 143 outputs a warning message while the latter outputs an info message. 144 ''; 145 }; 146 147 jwt_secret_file = mkOption { 148 type = types.nullOr types.str; 149 default = null; 150 description = '' 151 Path to a file containing the JWT secret. 152 ''; 153 }; 154 }; 155 }; 156 157 # TOML does not allow null values, so we use null to omit those fields 158 apply = lib.filterAttrsRecursive (_: v: v != null); 159 }; 160 161 silenceForceUserPassResetWarning = mkOption { 162 type = types.bool; 163 default = false; 164 description = '' 165 Disable warning when the admin password is set declaratively with the `ldap_user_pass_file` setting 166 but the `force_ldap_user_pass_reset` is set to `false`. 167 168 This can lead to the admin password to drift from the one given declaratively. 169 If that is okay for you and you want to silence the warning, set this option to `true`. 170 ''; 171 }; 172 }; 173 174 config = lib.mkIf cfg.enable { 175 assertions = [ 176 { 177 assertion = 178 (cfg.settings.ldap_user_pass_file or null) != null 179 || (cfg.settings.ldap_user_pass or null) != null 180 || (cfg.environment.LLDAP_LDAP_USER_PASS_FILE or null) != null; 181 message = "lldap: Default admin user password must be set. Please set the `ldap_user_pass` or better the `ldap_user_pass_file` setting. Alternatively, you can set the `LLDAP_LDAP_USER_PASS_FILE` environment variable."; 182 } 183 { 184 assertion = 185 (cfg.settings.ldap_user_pass_file or null) == null || (cfg.settings.ldap_user_pass or null) == null; 186 message = "lldap: Both `ldap_user_pass` and `ldap_user_pass_file` settings should not be set at the same time. Set one to `null`."; 187 } 188 ]; 189 190 warnings = 191 lib.optionals (cfg.settings.ldap_user_pass or null != null) [ 192 '' 193 lldap: Unsecure `ldap_user_pass` setting is used. Prefer `ldap_user_pass_file` instead. 194 '' 195 ] 196 ++ 197 lib.optionals 198 (cfg.settings.force_ldap_user_pass_reset == false && cfg.silenceForceUserPassResetWarning == false) 199 [ 200 '' 201 lldap: The `force_ldap_user_pass_reset` setting is set to `false` which means 202 the admin password can be changed through the UI and will drift from the one defined in your nix config. 203 It also means changing the setting `ldap_user_pass` or `ldap_user_pass_file` will have no effect on the admin password. 204 Either set `force_ldap_user_pass_reset` to `"always"` or silence this warning by setting the option `services.lldap.silenceForceUserPassResetWarning` to `true`. 205 '' 206 ]; 207 208 systemd.services.lldap = { 209 description = "Lightweight LDAP server (lldap)"; 210 wants = [ "network-online.target" ]; 211 after = [ "network-online.target" ]; 212 wantedBy = [ "multi-user.target" ]; 213 # lldap defaults to a hardcoded `jwt_secret` value if none is provided, which is bad, because 214 # an attacker could create a valid admin jwt access token fairly trivially. 215 # Because there are 3 different ways `jwt_secret` can be provided, we check if any one of them is present, 216 # and if not, bootstrap a secret in `/var/lib/lldap/jwt_secret_file` and give that to lldap. 217 script = 218 lib.optionalString (!cfg.settings ? jwt_secret) '' 219 if [[ -z "$LLDAP_JWT_SECRET_FILE" ]] && [[ -z "$LLDAP_JWT_SECRET" ]]; then 220 if [[ ! -e "./jwt_secret_file" ]]; then 221 ${lib.getExe pkgs.openssl} rand -base64 -out ./jwt_secret_file 32 222 fi 223 export LLDAP_JWT_SECRET_FILE="./jwt_secret_file" 224 fi 225 '' 226 + '' 227 ${lib.getExe cfg.package} run --config-file ${format.generate "lldap_config.toml" cfg.settings} 228 ''; 229 serviceConfig = { 230 StateDirectory = "lldap"; 231 StateDirectoryMode = "0750"; 232 WorkingDirectory = "%S/lldap"; 233 UMask = "0027"; 234 User = "lldap"; 235 Group = "lldap"; 236 DynamicUser = true; 237 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; 238 }; 239 inherit (cfg) environment; 240 }; 241 }; 242}