diff --git a/.github/workflows/docker-build-static.yml b/.github/workflows/docker-build-static.yml index 886766e..cefab74 100644 --- a/.github/workflows/docker-build-static.yml +++ b/.github/workflows/docker-build-static.yml @@ -216,6 +216,8 @@ jobs: LLDAP_database_url: postgres://lldapuser:lldappass@localhost/lldap LLDAP_ldap_port: 3890 LLDAP_http_port: 17170 + LLDAP_JWT_SECRET: verysecret + LLDAP_LDAP_USER_PASS: password - name: Run lldap with mariadb DB (MySQL Compatible) and healthcheck @@ -227,6 +229,8 @@ jobs: LLDAP_database_url: mysql://lldapuser:lldappass@localhost/lldap LLDAP_ldap_port: 3891 LLDAP_http_port: 17171 + LLDAP_JWT_SECRET: verysecret + LLDAP_LDAP_USER_PASS: password - name: Run lldap with sqlite DB and healthcheck @@ -238,6 +242,8 @@ jobs: LLDAP_database_url: sqlite://users.db?mode=rwc LLDAP_ldap_port: 3892 LLDAP_http_port: 17172 + LLDAP_JWT_SECRET: verysecret + LLDAP_LDAP_USER_PASS: password - name: Check DB container logs run: | @@ -324,9 +330,9 @@ jobs: sleep 10s bin/lldap healthcheck env: - LLDAP_database_url: sqlite://users.db?mode=rwc - LLDAP_ldap_port: 3890 - LLDAP_http_port: 17170 + LLDAP_DATABASE_URL: sqlite://users.db?mode=rwc + LLDAP_LDAP_PORT: 3890 + LLDAP_HTTP_PORT: 17170 LLDAP_LDAP_USER_PASS: ldappass LLDAP_JWT_SECRET: somejwtsecret @@ -350,8 +356,11 @@ jobs: sed -i -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" -e ":a; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),1([^']*\);)$/\1,true\3/; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),0([^']*\);)$/\1,false\3/; ta" -e '1s/^/BEGIN;\n/' -e '$aCOMMIT;' ./dump.sql - name: Create schema on postgres + env: + LLDAP_DATABASE_URL: postgres://lldapuser:lldappass@localhost:5432/lldap + LLDAP_JWT_SECRET: somejwtsecret run: | - bin/lldap create_schema -d postgres://lldapuser:lldappass@localhost:5432/lldap + bin/lldap create_schema - name: Copy converted db to postgress and import run: | @@ -368,7 +377,10 @@ jobs: sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql - name: Create schema on mariadb - run: bin/lldap create_schema -d mysql://lldapuser:lldappass@localhost:3306/lldap + env: + LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3306/lldap + LLDAP_JWT_SECRET: somejwtsecret + run: bin/lldap create_schema - name: Copy converted db to mariadb and import run: | @@ -384,7 +396,10 @@ jobs: sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql - name: Create schema on mysql - run: bin/lldap create_schema -d mysql://lldapuser:lldappass@localhost:3307/lldap + env: + LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3307/lldap + LLDAP_JWT_SECRET: somejwtsecret + run: bin/lldap create_schema - name: Copy converted db to mysql and import run: | @@ -399,10 +414,9 @@ jobs: sleep 10s bin/lldap healthcheck env: - LLDAP_database_url: postgres://lldapuser:lldappass@localhost:5432/lldap - LLDAP_ldap_port: 3891 - LLDAP_http_port: 17171 - LLDAP_LDAP_USER_PASS: ldappass + LLDAP_DATABASE_URL: postgres://lldapuser:lldappass@localhost:5432/lldap + LLDAP_LDAP_PORT: 3891 + LLDAP_HTTP_PORT: 17171 LLDAP_JWT_SECRET: somejwtsecret - name: Run lldap with mariaDB and healthcheck again @@ -411,9 +425,9 @@ jobs: sleep 10s bin/lldap healthcheck env: - LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3306/lldap - LLDAP_ldap_port: 3892 - LLDAP_http_port: 17172 + LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3306/lldap + LLDAP_LDAP_PORT: 3892 + LLDAP_HTTP_PORT: 17172 LLDAP_JWT_SECRET: somejwtsecret - name: Run lldap with mysql and healthcheck again @@ -422,9 +436,9 @@ jobs: sleep 10s bin/lldap healthcheck env: - LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3307/lldap - LLDAP_ldap_port: 3893 - LLDAP_http_port: 17173 + LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3307/lldap + LLDAP_LDAP_PORT: 3893 + LLDAP_HTTP_PORT: 17173 LLDAP_JWT_SECRET: somejwtsecret - name: Test Dummy User Postgres diff --git a/README.md b/README.md index 75ec742..bba4f8b 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ services: - LLDAP_JWT_SECRET=REPLACE_WITH_RANDOM - LLDAP_KEY_SEED=REPLACE_WITH_RANDOM - LLDAP_LDAP_BASE_DN=dc=example,dc=com + - LLDAP_LDAP_USER_PASS=adminPas$word # If using LDAPS, set enabled true and configure cert and key path # - LLDAP_LDAPS_OPTIONS__ENABLED=true # - LLDAP_LDAPS_OPTIONS__CERT_FILE=/path/to/certfile.crt @@ -713,6 +714,9 @@ modern identity protocols, check out Kanidm. If you just set up the server, can get to the login page but the password you set isn't working, try the following: +- If you have changed the admin password in the config after the first run, it + won't be used (unless you force its use with `force_ldap_user_pass_reset`). + The config password is only for the initial admin creation. - (For docker): Make sure that the `/data` folder is persistent, either to a docker volume or mounted from the host filesystem. - Check if there is a `lldap_config.toml` file (either in `/data` for docker diff --git a/server/src/infra/access_control.rs b/server/src/infra/access_control.rs index 853e1c0..976df75 100644 --- a/server/src/infra/access_control.rs +++ b/server/src/infra/access_control.rs @@ -314,8 +314,8 @@ pub struct UserRestrictedListerBackendHandler<'a, Handler> { } #[async_trait] -impl<'a, Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler - for UserRestrictedListerBackendHandler<'a, Handler> +impl ReadSchemaBackendHandler + for UserRestrictedListerBackendHandler<'_, Handler> { async fn get_schema(&self) -> Result { let mut schema = self.handler.get_schema().await?; @@ -331,8 +331,8 @@ impl<'a, Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler } #[async_trait] -impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler - for UserRestrictedListerBackendHandler<'a, Handler> +impl UserListerBackendHandler + for UserRestrictedListerBackendHandler<'_, Handler> { async fn list_users( &self, @@ -354,8 +354,8 @@ impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler } #[async_trait] -impl<'a, Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler - for UserRestrictedListerBackendHandler<'a, Handler> +impl GroupListerBackendHandler + for UserRestrictedListerBackendHandler<'_, Handler> { async fn list_groups(&self, filters: Option) -> Result> { let group_filter = self @@ -379,7 +379,7 @@ pub trait UserAndGroupListerBackendHandler: } #[async_trait] -impl<'a, Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync> - UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'a, Handler> +impl + UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'_, Handler> { } diff --git a/server/src/infra/cli.rs b/server/src/infra/cli.rs index c7bad5f..35ba2af 100644 --- a/server/src/infra/cli.rs +++ b/server/src/infra/cli.rs @@ -37,7 +37,7 @@ impl<'de> Deserialize<'de> for TrueFalseAlways { struct Visitor; - impl<'de> serde::de::Visitor<'de> for Visitor { + impl serde::de::Visitor<'_> for Visitor { type Value = TrueFalseAlways; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/server/src/infra/configuration.rs b/server/src/infra/configuration.rs index 4c00f26..3d68d4a 100644 --- a/server/src/infra/configuration.rs +++ b/server/src/infra/configuration.rs @@ -97,16 +97,16 @@ pub struct Configuration { pub http_host: String, #[builder(default = "17170")] pub http_port: u16, - #[builder(default = r#"SecUtf8::from("secretjwtsecret")"#)] - pub jwt_secret: SecUtf8, + #[builder(default)] + pub jwt_secret: Option, #[builder(default = r#"String::from("dc=example,dc=com")"#)] pub ldap_base_dn: String, #[builder(default = r#"UserId::new("admin")"#)] pub ldap_user_dn: UserId, #[builder(default)] pub ldap_user_email: String, - #[builder(default = r#"SecUtf8::from("password")"#)] - pub ldap_user_pass: SecUtf8, + #[builder(default)] + pub ldap_user_pass: Option, #[builder(default)] pub force_ldap_user_pass_reset: TrueFalseAlways, #[builder(default = "false")] @@ -607,11 +607,24 @@ where .unwrap_or_default(), figment_config, )?); - if config.jwt_secret == SecUtf8::from("secretjwtsecret") { - println!("WARNING: Default JWT secret used! This is highly unsafe and can allow attackers to log in as admin."); - } - if config.ldap_user_pass == SecUtf8::from("password") { - println!("WARNING: Unsecure default admin password is used."); + if config.jwt_secret.is_none() { + use rand::{seq::SliceRandom, Rng}; + struct Symbols; + + impl rand::prelude::Distribution for Symbols { + fn sample(&self, rng: &mut R) -> char { + *b"01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+,-./:;<=>?_~!@#$%^&*()[]{}:;".choose(rng).unwrap() as char + } + } + bail!("The JWT secret must be initialized to a random string, preferably at least 32 characters long. \ + Either set the `jwt_secret` config value or the `LLDAP_JWT_SECRET` environment variable. \ + You can generate the value by running\n\ + LC_ALL=C tr -dc 'A-Za-z0-9!#%&'\\''()*+,-./:;<=>?@[\\]^_{{|}}~' ()); } if config.smtp_options.tls_required.is_some() { println!("DEPRECATED: smtp_options.tls_required field is deprecated, it never did anything. You can replace it with smtp_options.smtp_encryption."); @@ -669,7 +682,9 @@ mod tests { fn figment_location_extraction_key_file() { Jail::expect_with(|jail| { jail.create_file("lldap_config.toml", r#"key_file = "test""#)?; + jail.clear_env(); jail.set_env("LLDAP_KEY_SEED", "a123"); + jail.set_env("LLDAP_JWT_SECRET", "secret"); let ignore_keys = ["key_file", "cert_file"]; let figment_config = Figment::from(Serialized::defaults( ConfigurationBuilder::default().private_build().unwrap(), @@ -696,7 +711,9 @@ mod tests { fn check_server_setup_key_extraction_seed_success_with_nonexistant_file() { Jail::expect_with(|jail| { jail.create_file("lldap_config.toml", r#"key_file = "test""#)?; + jail.clear_env(); jail.set_env("LLDAP_KEY_SEED", "a123"); + jail.set_env("LLDAP_JWT_SECRET", "secret"); init(default_run_opts()).unwrap(); Ok(()) }); @@ -706,7 +723,9 @@ mod tests { fn check_server_setup_key_extraction_seed_failure_with_existing_file() { Jail::expect_with(|jail| { jail.create_file("lldap_config.toml", r#"key_file = "test""#)?; + jail.clear_env(); jail.set_env("LLDAP_KEY_SEED", "a123"); + jail.set_env("LLDAP_JWT_SECRET", "secret"); write_random_key(jail, "test"); init(default_run_opts()).unwrap_err(); Ok(()) @@ -717,6 +736,8 @@ mod tests { fn check_server_setup_key_extraction_file_success_with_existing_file() { Jail::expect_with(|jail| { jail.create_file("lldap_config.toml", r#"key_file = "test""#)?; + jail.clear_env(); + jail.set_env("LLDAP_JWT_SECRET", "secret"); write_random_key(jail, "test"); init(default_run_opts()).unwrap(); Ok(()) @@ -727,6 +748,8 @@ mod tests { fn check_server_setup_key_extraction_file_success_with_nonexistent_file() { Jail::expect_with(|jail| { jail.create_file("lldap_config.toml", r#"key_file = "test""#)?; + jail.clear_env(); + jail.set_env("LLDAP_JWT_SECRET", "secret"); init(default_run_opts()).unwrap(); Ok(()) }); @@ -736,6 +759,8 @@ mod tests { fn check_server_setup_key_extraction_file_with_previous_different_file() { Jail::expect_with(|jail| { jail.create_file("lldap_config.toml", r#"key_file = "test""#)?; + jail.clear_env(); + jail.set_env("LLDAP_JWT_SECRET", "secret"); write_random_key(jail, "test"); let config = init(default_run_opts()).unwrap(); let info = config.get_private_key_info(); @@ -766,6 +791,8 @@ mod tests { #[test] fn check_server_setup_key_extraction_file_to_seed() { Jail::expect_with(|jail| { + jail.clear_env(); + jail.set_env("LLDAP_JWT_SECRET", "secret"); jail.create_file("lldap_config.toml", "")?; write_random_key(jail, "server_key"); init(default_run_opts()).unwrap(); @@ -782,6 +809,8 @@ mod tests { #[test] fn check_server_setup_key_extraction_file_to_seed_removed_file() { Jail::expect_with(|jail| { + jail.clear_env(); + jail.set_env("LLDAP_JWT_SECRET", "secret"); jail.create_file("lldap_config.toml", "")?; write_random_key(jail, "server_key"); let config = init(default_run_opts()).unwrap(); diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index a07ff5d..d25c3ac 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -189,7 +189,7 @@ pub async fn build_tcp_server( where Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static, { - let jwt_secret = config.jwt_secret.clone(); + let jwt_secret = config.jwt_secret.clone().unwrap(); let jwt_blacklist = backend_handler .get_jwt_blacklist() .await diff --git a/server/src/main.rs b/server/src/main.rs index 6f42473..05fb397 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -33,8 +33,17 @@ use tracing::{debug, error, info, instrument, span, warn, Instrument, Level}; mod domain; mod infra; +const ADMIN_PASSWORD_MISSING_ERROR : &str = "The LDAP admin password must be initialized. \ + Either set the `ldap_user_pass` config value or the `LLDAP_LDAP_USER_PASS` environment variable. \ + A minimum of 8 characters is recommended."; + async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration) -> Result<()> { - let pass_length = config.ldap_user_pass.unsecure().len(); + let pass_length = config + .ldap_user_pass + .as_ref() + .expect(ADMIN_PASSWORD_MISSING_ERROR) + .unsecure() + .len(); assert!( pass_length >= 8, "Minimum password length is 8 characters, got {} characters", @@ -48,7 +57,11 @@ async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration) ..Default::default() }) .and_then(|_| { - register_password(handler, config.ldap_user_dn.clone(), &config.ldap_user_pass) + register_password( + handler, + config.ldap_user_dn.clone(), + config.ldap_user_pass.as_ref().unwrap(), + ) }) .await .context("Error creating admin user")?; @@ -161,7 +174,10 @@ async fn set_up_server(config: Configuration) -> Result { register_password( &backend_handler, config.ldap_user_dn.clone(), - &config.ldap_user_pass, + config + .ldap_user_pass + .as_ref() + .expect(ADMIN_PASSWORD_MISSING_ERROR), ) .instrument(span) .await diff --git a/server/tests/common/env.rs b/server/tests/common/env.rs index ffc2887..a9e0cfc 100644 --- a/server/tests/common/env.rs +++ b/server/tests/common/env.rs @@ -3,6 +3,8 @@ use std::env::var; pub const DB_KEY: &str = "LLDAP_DATABASE_URL"; pub const PRIVATE_KEY_SEED: &str = "LLDAP_KEY_SEED"; +pub const JWT_SECRET: &str = "LLDAP_JWT_SECRET"; +pub const LDAP_USER_PASSWORD: &str = "LLDAP_LDAP_USER_PASS"; pub fn database_url() -> String { let url = var(DB_KEY).ok(); diff --git a/server/tests/common/fixture.rs b/server/tests/common/fixture.rs index 4e35577..53ba5a0 100644 --- a/server/tests/common/fixture.rs +++ b/server/tests/common/fixture.rs @@ -43,14 +43,13 @@ const MAX_HEALTHCHECK_ATTEMPS: u8 = 10; impl LLDAPFixture { pub fn new() -> Self { - let mut cmd = create_lldap_command(); - cmd.arg("run"); - cmd.arg("--verbose"); - let child = cmd.spawn().expect("Unable to start server"); + let child = create_lldap_command("run") + .arg("--verbose") + .spawn() + .expect("Unable to start server"); let mut started = false; for _ in 0..MAX_HEALTHCHECK_ATTEMPS { - let status = create_lldap_command() - .arg("healthcheck") + let status = create_lldap_command("healthcheck") .status() .expect("healthcheck fail"); if status.success() { @@ -229,7 +228,7 @@ pub fn new_id(prefix: Option<&str>) -> String { } } -fn create_lldap_command() -> Command { +fn create_lldap_command(subcommand: &str) -> Command { let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).expect("cargo bin not found"); // This gives us the absolute path of the repo base instead of running it in server/ let path = canonicalize("..").expect("canonical path"); @@ -237,5 +236,10 @@ fn create_lldap_command() -> Command { cmd.current_dir(path); cmd.env(env::DB_KEY, db_url); cmd.env(env::PRIVATE_KEY_SEED, "Random value"); + cmd.env(env::JWT_SECRET, "Random value"); + cmd.env(env::LDAP_USER_PASSWORD, "password"); + cmd.arg(subcommand); + cmd.arg("--config-file=/dev/null"); + cmd.arg("--server-key-file=''"); cmd }