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

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