From 42b6d626997c4ebe420da7ccd70352cc08681238 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Wed, 16 Jul 2025 03:04:44 +0200 Subject: [PATCH] lldap: add options to set important secrets --- nixos/modules/services/databases/lldap.nix | 71 +++++++++++++++++++++ nixos/tests/lldap.nix | 72 +++++++++++++++++++--- 2 files changed, 134 insertions(+), 9 deletions(-) diff --git a/nixos/modules/services/databases/lldap.nix b/nixos/modules/services/databases/lldap.nix index a9fbe8f7e11a..1607754d7d9c 100644 --- a/nixos/modules/services/databases/lldap.nix +++ b/nixos/modules/services/databases/lldap.nix @@ -102,12 +102,83 @@ in default = "sqlite://./users.db?mode=rwc"; example = "postgres://postgres-user:password@postgres-server/my-database"; }; + + ldap_user_pass_file = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Path to a file containing the default admin password. + + If you want to update the default admin password through this setting, + you must set `force_ldap_user_pass_reset` to `true`. + Otherwise changing this setting will have no effect + unless this is the very first time LLDAP is started and its database is still empty. + ''; + }; + + force_ldap_user_pass_reset = mkOption { + type = types.oneOf [ + types.bool + (types.enum [ "always" ]) + ]; + default = false; + description = '' + Force reset of the admin password. + + Set this setting to `"always"` to update the admin password when `ldap_user_pass_file` changes. + Setting to `"always"` also means any password update in the UI will be overwritten next time the service restarts. + + The difference between `true` and `"always"` is the former is intended for a one time fix + while the latter is intended for a declarative workflow. In practice, the result + is the same: the password gets reset. The only practical difference is the former + outputs a warning message while the latter outputs an info message. + ''; + }; + + jwt_secret_file = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Path to a file containing the JWT secret. + ''; + }; }; }; + + # TOML does not allow null values, so we use null to omit those fields + apply = lib.filterAttrsRecursive (_: v: v != null); + }; + + silenceForceUserPassResetWarning = mkOption { + type = types.bool; + default = false; + description = '' + Disable warning when the admin password is set declaratively with the `ldap_user_pass_file` setting + but the `force_ldap_user_pass_reset` is set to `false`. + + This can lead to the admin password to drift from the one given declaratively. + If that is okay for you and you want to silence the warning, set this option to `true`. + ''; }; }; config = lib.mkIf cfg.enable { + warnings = + lib.optionals + ( + (cfg.settings.ldap_user_pass_file or null) != null + && cfg.settings.force_ldap_user_pass_reset == false + && cfg.silenceForceUserPassResetWarning == false + ) + [ + '' + lldap: The default admin password is declared with the setting `ldap_user_pass_file`, but `force_ldap_user_pass_reset` is set to `false`. + This means the admin password can be changed through the UI and will drift from the one defined in your nix config. + It also means changing the setting `ldap_user_pass_file` will have no effect on the admin password. + Either set `force_ldap_user_pass_reset` to `"always"` or silence this warning by setting the option `services.lldap.silenceForceUserPassResetWarning` to `true`. + '' + ]; + systemd.services.lldap = { description = "Lightweight LDAP server (lldap)"; wants = [ "network-online.target" ]; diff --git a/nixos/tests/lldap.nix b/nixos/tests/lldap.nix index c2e48525a5f3..aea6b2058727 100644 --- a/nixos/tests/lldap.nix +++ b/nixos/tests/lldap.nix @@ -1,4 +1,7 @@ { ... }: +let + adminPassword = "mySecretPassword"; +in { name = "lldap"; @@ -7,23 +10,74 @@ { services.lldap = { enable = true; + settings = { verbose = true; ldap_base_dn = "dc=example,dc=com"; }; }; environment.systemPackages = [ pkgs.openldap ]; + + specialisation = { + differentAdminPassword.configuration = + { ... }: + { + services.lldap.settings = { + ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" adminPassword); + force_ldap_user_pass_reset = "always"; + }; + }; + + changeAdminPassword.configuration = + { ... }: + { + services.lldap.settings = { + ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" "password"); + force_ldap_user_pass_reset = false; + }; + }; + }; }; - testScript = '' - machine.wait_for_unit("lldap.service") - machine.wait_for_open_port(3890) - machine.wait_for_open_port(17170) + testScript = + { nodes, ... }: + let + specializations = "${nodes.machine.system.build.toplevel}/specialisation"; + in + '' + machine.wait_for_unit("lldap.service") + machine.wait_for_open_port(3890) + machine.wait_for_open_port(17170) - machine.succeed("curl --location --fail http://localhost:17170/") + machine.succeed("curl --location --fail http://localhost:17170/") - print( - machine.succeed('ldapsearch -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w password') - ) - ''; + adminPassword="${adminPassword}" + + def try_login(user, password, expect_success=True): + cmd = f'ldapsearch -H ldap://localhost:3890 -D uid={user},ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w {password}' + code, response = machine.execute(cmd) + print(cmd) + print(response) + if expect_success: + if code != 0: + raise Exception(f"Expected success, had failure {code}") + else: + if code == 0: + raise Exception("Expected failure, had success") + return response + + with subtest("default admin password"): + try_login("admin", "password", expect_success=True) + try_login("admin", adminPassword, expect_success=False) + + with subtest("different admin password"): + machine.succeed('${specializations}/differentAdminPassword/bin/switch-to-configuration test') + try_login("admin", "password", expect_success=False) + try_login("admin", adminPassword, expect_success=True) + + with subtest("change admin password has no effect"): + machine.succeed('${specializations}/differentAdminPassword/bin/switch-to-configuration test') + try_login("admin", "password", expect_success=False) + try_login("admin", adminPassword, expect_success=True) + ''; }