Prevent starting up if the JWT secret is not given

Similarly, don't create the admin if the password is not given
This commit is contained in:
Valentin Tolmer 2024-12-23 23:03:27 +01:00 committed by nitnelave
parent 1f26262e13
commit f417427635
9 changed files with 114 additions and 45 deletions

View File

@ -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

View File

@ -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

View File

@ -314,8 +314,8 @@ pub struct UserRestrictedListerBackendHandler<'a, Handler> {
}
#[async_trait]
impl<'a, Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
for UserRestrictedListerBackendHandler<'a, Handler>
impl<Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
for UserRestrictedListerBackendHandler<'_, Handler>
{
async fn get_schema(&self) -> Result<Schema> {
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<Handler: UserListerBackendHandler + Sync> 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<Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
for UserRestrictedListerBackendHandler<'_, Handler>
{
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
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<Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync>
UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'_, Handler>
{
}

View File

@ -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 {

View File

@ -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<SecUtf8>,
#[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<SecUtf8>,
#[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<char> for Symbols {
fn sample<R: Rng + ?Sized>(&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!#%&'\\''()*+,-./:;<=>?@[\\]^_{{|}}~' </dev/urandom | head -c 32; echo ''\n\
or you can use this random value: {}",
rand::thread_rng()
.sample_iter(&Symbols)
.take(32)
.collect::<String>());
}
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();

View File

@ -189,7 +189,7 @@ pub async fn build_tcp_server<Backend>(
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

View File

@ -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<ServerBuilder> {
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

View File

@ -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();

View File

@ -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
}