diff --git a/Cargo.lock b/Cargo.lock index fb3c0daa2..db954797d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2352,6 +2352,7 @@ dependencies = [ name = "meilisearch-auth" version = "1.0.0" dependencies = [ + "base64", "enum-iterator", "hmac", "meilisearch-types", diff --git a/meilisearch-auth/Cargo.toml b/meilisearch-auth/Cargo.toml index fbddc14d0..383be69cf 100644 --- a/meilisearch-auth/Cargo.toml +++ b/meilisearch-auth/Cargo.toml @@ -4,6 +4,7 @@ version = "1.0.0" edition = "2021" [dependencies] +base64 = "0.13.1" enum-iterator = "1.1.3" hmac = "0.12.1" meilisearch-types = { path = "../meilisearch-types" } diff --git a/meilisearch-auth/src/lib.rs b/meilisearch-auth/src/lib.rs index 020a2821c..659447d44 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -268,3 +268,20 @@ fn generate_default_keys(store: &HeedAuthStore) -> Result<()> { Ok(()) } + +pub const MASTER_KEY_MIN_SIZE: usize = 16; +const MASTER_KEY_GEN_SIZE: usize = 32; + +pub fn generate_master_key() -> String { + use rand::rngs::OsRng; + use rand::RngCore; + + // We need to use a cryptographically-secure source of randomness. That's why we're using the OsRng; https://crates.io/crates/getrandom + let mut csprng = OsRng; + let mut buf = vec![0; MASTER_KEY_GEN_SIZE]; + csprng.fill_bytes(&mut buf); + + // let's encode the random bytes to base64 to make them human-readable and not too long. + // We're using the URL_SAFE alphabet that will produce keys without =, / or other unusual characters. + base64::encode_config(buf, base64::URL_SAFE_NO_PAD) +} diff --git a/meilisearch/src/analytics/segment_analytics.rs b/meilisearch/src/analytics/segment_analytics.rs index 880d34c9a..8bde71688 100644 --- a/meilisearch/src/analytics/segment_analytics.rs +++ b/meilisearch/src/analytics/segment_analytics.rs @@ -277,6 +277,7 @@ impl From for Infos { indexer_options, scheduler_options, config_file_path, + generate_master_key: _, #[cfg(all(not(debug_assertions), feature = "analytics"))] no_analytics: _, } = options; diff --git a/meilisearch/src/main.rs b/meilisearch/src/main.rs index 50575cbbf..e3309dde8 100644 --- a/meilisearch/src/main.rs +++ b/meilisearch/src/main.rs @@ -8,7 +8,7 @@ use actix_web::HttpServer; use index_scheduler::IndexScheduler; use meilisearch::analytics::Analytics; use meilisearch::{analytics, create_app, setup_meilisearch, Opt}; -use meilisearch_auth::AuthController; +use meilisearch_auth::{generate_master_key, AuthController, MASTER_KEY_MIN_SIZE}; #[global_allocator] static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -33,16 +33,36 @@ async fn main() -> anyhow::Result<()> { setup(&opt)?; - match opt.env.as_ref() { - "production" => { - if opt.master_key.is_none() { - anyhow::bail!( - "In production mode, the environment variable MEILI_MASTER_KEY is mandatory" - ) - } + if opt.generate_master_key { + println!("{}", generate_master_key()); + return Ok(()); + } + + match (opt.env.as_ref(), &opt.master_key) { + ("production", Some(master_key)) if master_key.len() < MASTER_KEY_MIN_SIZE => { + anyhow::bail!( + "In production mode, the master key must be of at least {MASTER_KEY_MIN_SIZE} characters, but the provided key is only {} characters long + +We generated a secure Master Key for you (you can safely copy this token): + +>> export MEILI_MASTER_KEY={} <<", + master_key.len(), + generate_master_key(), + ) } - "development" => (), - _ => unreachable!(), + ("production", None) => { + anyhow::bail!( + "In production mode, you must provide a master key to secure your instance. It can be specified via the MEILI_MASTER_KEY environment variable or the --master-key launch option. + +We generated a secure Master Key for you (you can safely copy this token): + +>> export MEILI_MASTER_KEY={} << +", + generate_master_key() + ) + } + // No error; continue + _ => (), } let (index_scheduler, auth_controller) = setup_meilisearch(&opt)?; @@ -151,11 +171,29 @@ Anonymous telemetry:\t\"Enabled\"" eprintln!(); - if opt.master_key.is_some() { - eprintln!("A Master Key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key."); - } else { - eprintln!("No master key found; The server will accept unidentified requests. \ - If you need some protection in development mode, please export a key: export MEILI_MASTER_KEY=xxx"); + match (opt.env.as_ref(), &opt.master_key) { + ("production", Some(_)) => { + eprintln!("A Master Key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key."); + } + ("development", Some(master_key)) => { + eprintln!("A Master Key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key."); + + if master_key.len() < MASTER_KEY_MIN_SIZE { + eprintln!(); + log::warn!( + "The provided Master Key is too short (< {MASTER_KEY_MIN_SIZE} characters)" + ); + eprintln!("A Master Key of at least {MASTER_KEY_MIN_SIZE} characters will be required when switching to the production environment."); + eprintln!("Restart Meilisearch with the `--generate-master-key` flag to generate a secure Master Key you can use"); + } + } + ("development", None) => { + log::warn!("No master key found; The server will accept unidentified requests"); + eprintln!("If you need some protection in development mode, please export a key:\n\nexport MEILI_MASTER_KEY={}", generate_master_key()); + eprintln!("\nA Master Key of at least {MASTER_KEY_MIN_SIZE} characters will be required when switching to the production environment."); + } + // unreachable because Opt::try_build above would have failed already if any other value had been produced + _ => unreachable!(), } eprintln!(); diff --git a/meilisearch/src/option.rs b/meilisearch/src/option.rs index 40535305b..cc8aeaf50 100644 --- a/meilisearch/src/option.rs +++ b/meilisearch/src/option.rs @@ -49,6 +49,7 @@ const MEILI_IGNORE_MISSING_DUMP: &str = "MEILI_IGNORE_MISSING_DUMP"; const MEILI_IGNORE_DUMP_IF_DB_EXISTS: &str = "MEILI_IGNORE_DUMP_IF_DB_EXISTS"; const MEILI_DUMP_DIR: &str = "MEILI_DUMP_DIR"; const MEILI_LOG_LEVEL: &str = "MEILI_LOG_LEVEL"; +const MEILI_GENERATE_MASTER_KEY: &str = "MEILI_GENERATE_MASTER_KEY"; #[cfg(feature = "metrics")] const MEILI_ENABLE_METRICS_ROUTE: &str = "MEILI_ENABLE_METRICS_ROUTE"; @@ -230,6 +231,13 @@ pub struct Opt { #[serde(default = "default_log_level")] pub log_level: String, + /// Generates a string of characters that can be used as a master key and exits. + /// + /// Pass the generated master key using the `--master-key` argument or the `MEILI_MASTER_KEY` environment variable in a subsequent Meilisearch invocation. + #[clap(long, env = MEILI_GENERATE_MASTER_KEY)] + #[serde(default)] + pub generate_master_key: bool, + /// Enables Prometheus metrics and /metrics route. #[cfg(feature = "metrics")] #[clap(long, env = MEILI_ENABLE_METRICS_ROUTE)] @@ -328,6 +336,7 @@ impl Opt { ignore_missing_snapshot: _, ignore_snapshot_if_db_exists: _, import_dump: _, + generate_master_key: _, ignore_missing_dump: _, ignore_dump_if_db_exists: _, config_file_path: _,