3274: Reject master keys that are less than 16 bytes and add `--generate-master-key` CLI option r=irevoire a=dureuill

# Pull Request

## Related issue
Fix #3272 
Fix #3287

## What does this PR do?

### User standpoint

---

- Adds a `--generate-master-key` CLI flag to generate a fresh Master Key and exit.

<img width="1351" alt="Capture d’écran 2022-12-22 à 14 18 58" src="https://user-images.githubusercontent.com/41078892/209142778-eab52eeb-eaa8-409b-897a-c0d5728c8aaa.png">

---

(relevant fragment of the `--help` message)

<img width="1351" alt="Capture d’écran 2022-12-22 à 14 19 40" src="https://user-images.githubusercontent.com/41078892/209142891-ebfa2ed6-f231-4f76-a3ae-b7542c7aef04.png">

---

- When `meilisearch` is started in the `development` environment and no Master Key has been provided, then the binary prints a warning before starting.

<img width="1351" alt="Capture d’écran 2022-12-22 à 14 14 49" src="https://user-images.githubusercontent.com/41078892/209142158-54eba3b7-bf71-4f3f-8840-0600b13a1a9f.png">

---

- When `meilisearch` is started in the `development` environment and the provided Master Key is shorter than 16 bytes, then the binary prints a warning before starting.

<img width="1351" alt="Capture d’écran 2022-12-22 à 14 15 58" src="https://user-images.githubusercontent.com/41078892/209142295-0209fe47-c03b-424f-a73f-cee9b633137a.png">

---

- When `meilisearch` is started in the `production` environment, and no Master Key is provided, the error message is altered to generate a fresh Master Key.

<img width="1351" alt="Capture d’écran 2022-12-22 à 17 29 02" src="https://user-images.githubusercontent.com/41078892/209180540-0def5798-15db-47f0-a6ec-8cfa081dea77.png">


---

- When `meilisearch` is started in the `production` environment, and the provided Master Key is shorter than 16 bytes, then the binary exits with an error.

<img width="1351" alt="Capture d’écran 2022-12-22 à 17 28 47" src="https://user-images.githubusercontent.com/41078892/209180567-fa54fe33-fbc4-4b9f-b281-7dfb7b33af85.png">


---

This implements the solution B described here: https://github.com/meilisearch/product/discussions/538#discussioncomment-4391346 

### Implementation standpoint

- Add a new `meilisearch-auth::generate_master_key` function that uses a Cryptographic Random Number Generator (CRNG) to fill a vector of 32 bytes before encoding these bytes as base64

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)?
- [x] Have you read the contributing guidelines?
- [x] Have you made sure that the title is accurate and descriptive of the changes?

Thank you so much for contributing to Meilisearch!


Co-authored-by: Louis Dureuil <louis@meilisearch.com>
Co-authored-by: Tamo <tamo@meilisearch.com>
This commit is contained in:
bors[bot] 2023-01-02 16:00:40 +00:00 committed by GitHub
commit 6425e06cf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 82 additions and 15 deletions

1
Cargo.lock generated
View File

@ -2352,6 +2352,7 @@ dependencies = [
name = "meilisearch-auth" name = "meilisearch-auth"
version = "1.0.0" version = "1.0.0"
dependencies = [ dependencies = [
"base64",
"enum-iterator", "enum-iterator",
"hmac", "hmac",
"meilisearch-types", "meilisearch-types",

View File

@ -4,6 +4,7 @@ version = "1.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
base64 = "0.13.1"
enum-iterator = "1.1.3" enum-iterator = "1.1.3"
hmac = "0.12.1" hmac = "0.12.1"
meilisearch-types = { path = "../meilisearch-types" } meilisearch-types = { path = "../meilisearch-types" }

View File

@ -268,3 +268,20 @@ fn generate_default_keys(store: &HeedAuthStore) -> Result<()> {
Ok(()) 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)
}

View File

@ -277,6 +277,7 @@ impl From<Opt> for Infos {
indexer_options, indexer_options,
scheduler_options, scheduler_options,
config_file_path, config_file_path,
generate_master_key: _,
#[cfg(all(not(debug_assertions), feature = "analytics"))] #[cfg(all(not(debug_assertions), feature = "analytics"))]
no_analytics: _, no_analytics: _,
} = options; } = options;

View File

@ -8,7 +8,7 @@ use actix_web::HttpServer;
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use meilisearch::analytics::Analytics; use meilisearch::analytics::Analytics;
use meilisearch::{analytics, create_app, setup_meilisearch, Opt}; 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] #[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
@ -33,16 +33,36 @@ async fn main() -> anyhow::Result<()> {
setup(&opt)?; setup(&opt)?;
match opt.env.as_ref() { if opt.generate_master_key {
"production" => { println!("{}", generate_master_key());
if opt.master_key.is_none() { return Ok(());
anyhow::bail!( }
"In production mode, the environment variable MEILI_MASTER_KEY is mandatory"
) 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" => (), ("production", None) => {
_ => unreachable!(), 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)?; let (index_scheduler, auth_controller) = setup_meilisearch(&opt)?;
@ -151,11 +171,29 @@ Anonymous telemetry:\t\"Enabled\""
eprintln!(); eprintln!();
if opt.master_key.is_some() { match (opt.env.as_ref(), &opt.master_key) {
eprintln!("A Master Key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key."); ("production", Some(_)) => {
} else { eprintln!("A Master Key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key.");
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"); ("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!(); eprintln!();

View File

@ -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_IGNORE_DUMP_IF_DB_EXISTS: &str = "MEILI_IGNORE_DUMP_IF_DB_EXISTS";
const MEILI_DUMP_DIR: &str = "MEILI_DUMP_DIR"; const MEILI_DUMP_DIR: &str = "MEILI_DUMP_DIR";
const MEILI_LOG_LEVEL: &str = "MEILI_LOG_LEVEL"; const MEILI_LOG_LEVEL: &str = "MEILI_LOG_LEVEL";
const MEILI_GENERATE_MASTER_KEY: &str = "MEILI_GENERATE_MASTER_KEY";
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
const MEILI_ENABLE_METRICS_ROUTE: &str = "MEILI_ENABLE_METRICS_ROUTE"; const MEILI_ENABLE_METRICS_ROUTE: &str = "MEILI_ENABLE_METRICS_ROUTE";
@ -230,6 +231,13 @@ pub struct Opt {
#[serde(default = "default_log_level")] #[serde(default = "default_log_level")]
pub log_level: String, 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. /// Enables Prometheus metrics and /metrics route.
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
#[clap(long, env = MEILI_ENABLE_METRICS_ROUTE)] #[clap(long, env = MEILI_ENABLE_METRICS_ROUTE)]
@ -328,6 +336,7 @@ impl Opt {
ignore_missing_snapshot: _, ignore_missing_snapshot: _,
ignore_snapshot_if_db_exists: _, ignore_snapshot_if_db_exists: _,
import_dump: _, import_dump: _,
generate_master_key: _,
ignore_missing_dump: _, ignore_missing_dump: _,
ignore_dump_if_db_exists: _, ignore_dump_if_db_exists: _,
config_file_path: _, config_file_path: _,