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:
parent
1f26262e13
commit
f417427635
46
.github/workflows/docker-build-static.yml
vendored
46
.github/workflows/docker-build-static.yml
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
{
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user