diff --git a/src/main.rs b/src/main.rs index 00241f6..73323b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,9 @@ async fn main() -> Result<(), error::BotError> { String::new() } }; + // The SPA admin credential is this same Secret (see below): capture it once so the + // validated value and the admin token can never diverge. + let encryption_key_raw = std::env::var("CONFIG_ENCRYPTION_KEY").unwrap_or_default(); let encryption_key = match encryption_key_from_env() { Ok(key) => key, Err(_) => { @@ -84,11 +87,11 @@ async fn main() -> Result<(), error::BotError> { let redis = RedisState::connect_with_encryption(redis_url.expose_secret(), Some(encryption_key)) .await?; - let bootstrap_token = url::Url::parse(redis_url.expose_secret()) - .ok() - .and_then(|value| value.password().map(ToOwned::to_owned)) - .map(secrecy::SecretString::new) - .unwrap_or_else(|| secrecy::SecretString::new(String::new())); + // SPA admin credential == CONFIG_ENCRYPTION_KEY (validated as a required + // startup Secret above). It must not be derived from REDIS_URL: a Redis ACL + // password only gates the database connection, and passwordless managed + // Redis would leave the admin UI permanently locked out. + let admin_token = secrecy::SecretString::new(encryption_key_raw); let worker_state: std::sync::Arc = std::sync::Arc::new( RedisState::connect_with_encryption(redis_url.expose_secret(), Some(encryption_key)) .await?, @@ -172,7 +175,7 @@ async fn main() -> Result<(), error::BotError> { allowlist, Arc::new(WorkerHandle::new(worker)), runtime, - bootstrap_token, + admin_token, ) .merge(web::router()); axum::serve(listener, app).await?; diff --git a/src/notify.rs b/src/notify.rs index 68d0210..56b5dea 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -43,7 +43,7 @@ struct AppState { allowlist: Arc>, worker: Arc, runtime: RuntimeConfigProvider, - bootstrap_token: SecretString, + admin_token: SecretString, business_config: Arc>>, business_runtime: Arc>>, business_revision: Arc>, @@ -595,12 +595,10 @@ async fn put_business_config( StatusCode::NO_CONTENT.into_response() } -/// One-shot Redis ACL bootstrap. The ACL password is only compared in constant time and is -/// never included in the response; SET-NX in ReliableState closes the initialization race. +/// One-shot business-config bootstrap. The admin credential is only compared in constant time +/// and is never included in the response; SET-NX in ReliableState closes the init race. async fn bootstrap(State(app): State, headers: HeaderMap, body: Bytes) -> Response { - if app.bootstrap_token.expose_secret().is_empty() - || !worker_authorized(&headers, app.bootstrap_token.expose_secret()) - { + if !worker_authorized(&headers, app.admin_token.expose_secret()) { return error_response(StatusCode::UNAUTHORIZED, "unauthorized", false); } let Ok(config) = serde_json::from_slice::(&body) else { @@ -759,9 +757,7 @@ async fn revoke_admin_session(State(app): State, headers: HeaderMap) - } async fn create_admin_session(State(app): State, headers: HeaderMap) -> Response { - if app.bootstrap_token.expose_secret().is_empty() - || !worker_authorized(&headers, app.bootstrap_token.expose_secret()) - { + if !worker_authorized(&headers, app.admin_token.expose_secret()) { return error_response(StatusCode::UNAUTHORIZED, "unauthorized", false); } let mut bytes = [0_u8; 32]; @@ -1229,7 +1225,7 @@ pub fn router_with_worker_state_runtime_bootstrap( allowlist: HashSet, worker_handler: Arc, runtime: RuntimeConfigProvider, - bootstrap_token: SecretString, + admin_token: SecretString, ) -> Router { router_with_worker_state_runtime_bootstrap_config( secrets, @@ -1238,7 +1234,7 @@ pub fn router_with_worker_state_runtime_bootstrap( allowlist, worker_handler, runtime, - bootstrap_token, + admin_token, Vec::new(), ) } @@ -1254,7 +1250,7 @@ fn router_with_worker_state_runtime_bootstrap_config allowlist: HashSet, worker_handler: Arc, runtime: RuntimeConfigProvider, - bootstrap_token: SecretString, + admin_token: SecretString, setup_missing: Vec, ) -> Router { let worker_handle = Arc::new(WorkerHandle::new(worker_handler)); @@ -1282,7 +1278,7 @@ fn router_with_worker_state_runtime_bootstrap_config allowlist: Arc::new(allowlist), worker: worker_handle, runtime, - bootstrap_token, + admin_token, business_config: Arc::new(RwLock::new(None)), business_runtime: Arc::new(RwLock::new(None)), business_revision: Arc::new(RwLock::new(0)), @@ -1366,7 +1362,7 @@ mod tests { } #[tokio::test] - async fn bootstrap_rejects_without_acl_bootstrap_credential() { + async fn bootstrap_rejects_missing_admin_credential() { assert_eq!( request( Request::builder()