diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index c602ea242e..270db7d1f9 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -101,6 +101,8 @@ async fn main() -> Result<(), anyhow::Error> { Settings::init_defaults(&pool).await?; // initialize global settings struct initialize_current_settings(&pool).await?; + Settings::ensure_secret_key(&pool, &config).await?; + let mut settings = Settings::get_current_settings(); if wizard_flags.initial_wizard_in_progress && !wizard_flags.initial_wizard_completed { if let Err(err) = @@ -108,6 +110,7 @@ async fn main() -> Result<(), anyhow::Error> { { anyhow::bail!("Setup web server exited with error: {err}"); } + settings = Settings::get_current_settings(); } else if wizard_flags.migration_wizard_in_progress && !wizard_flags.migration_wizard_completed { config.initialize_post_settings(); @@ -122,6 +125,13 @@ async fn main() -> Result<(), anyhow::Error> { { anyhow::bail!("Migration web server exited with error: {err}"); } + settings = Settings::get_current_settings(); + } + + if wizard_flags.migration_wizard_needed { + info!("Migration from 1.6: copying configuration options to DB"); + settings.update_from_config(&pool, &config).await?; + info!("Migration from 1.6: copied configuration options to DB"); } if ini_server_config { @@ -153,7 +163,11 @@ async fn main() -> Result<(), anyhow::Error> { let incompatible_components: Arc> = Arc::default(); - // read grpc TLS cert and key + if settings.ca_cert_der.is_none() || settings.ca_key_der.is_none() { + anyhow::bail!("CA certificate or key were not found in settings, despite completing setup.") + } + + // read grpc TLS cert and key from legacy config values let grpc_cert = config .grpc_cert .as_ref() @@ -184,11 +198,13 @@ async fn main() -> Result<(), anyhow::Error> { } let (proxy_control_tx, proxy_control_rx) = channel::(100); + let proxy_secret_key = settings.secret_key_required()?.to_string(); let proxy_manager = ProxyManager::new( pool.clone(), ProxyTxSet::new(gateway_tx.clone(), bidi_event_tx.clone()), Arc::clone(&incompatible_components), proxy_control_rx, + proxy_secret_key, ); let mut gateway_manager = GatewayManager::new( @@ -220,9 +236,9 @@ async fn main() -> Result<(), anyhow::Error> { ) => error!("Web server returned early: {res:?}"), res = run_periodic_stats_purge( pool.clone(), - config.stats_purge_frequency.into(), - config.stats_purge_threshold.into() - ), if !config.disable_stats_purge => + settings.stats_purge_frequency(), + settings.stats_purge_threshold() + ), if !settings.disable_stats_purge => error!("Periodic stats purge task returned early: {res:?}"), res = run_periodic_license_check(&pool) => error!("Periodic license check task returned early: {res:?}"), diff --git a/crates/defguard_common/src/config.rs b/crates/defguard_common/src/config.rs index 6db70311aa..b05f859d5d 100644 --- a/crates/defguard_common/src/config.rs +++ b/crates/defguard_common/src/config.rs @@ -1,4 +1,4 @@ -use std::{fs::read_to_string, io, net::IpAddr, sync::OnceLock}; +use std::{net::IpAddr, sync::OnceLock}; use clap::{Args, Parser, Subcommand}; use humantime::Duration; @@ -13,7 +13,6 @@ use rsa::{ }; use secrecy::{ExposeSecret, SecretString}; use serde::Serialize; -use tonic::transport::{Certificate, ClientTlsConfig, Identity}; use crate::db::models::Settings; @@ -38,13 +37,15 @@ pub struct DefGuardConfig { #[arg(long, env = "DEFGUARD_LOG_FILE")] pub log_file: Option, - #[arg(long, env = "DEFGUARD_AUTH_COOKIE_TIMEOUT", default_value = "7d")] + #[arg(long, env = "DEFGUARD_AUTH_COOKIE_TIMEOUT")] #[serde(skip_serializing)] - pub auth_cookie_timeout: Duration, + #[deprecated(since = "2.0.0", note = "Use Settings.auth_cookie_timeout instead")] + pub auth_cookie_timeout: Option, #[arg(long, env = "DEFGUARD_SECRET_KEY")] #[serde(skip_serializing)] - pub secret_key: SecretString, + #[deprecated(since = "2.0.0", note = "Use Settings.secret_key instead")] + pub secret_key: Option, #[arg(long, env = "DEFGUARD_DB_HOST", default_value = "localhost")] pub database_host: String, @@ -68,9 +69,8 @@ pub struct DefGuardConfig { #[arg(long, env = "DEFGUARD_GRPC_PORT", default_value_t = 50055)] pub grpc_port: u16, - // Certificate authority (CA), certificate, and key for gRPC communication over HTTPS. - #[arg(long, env = "DEFGUARD_GRPC_CA")] - pub grpc_ca: Option, + // Certificate and key for gRPC communication over HTTPS. + // Kept in runtime config for backwards compatibility - workers still use this. #[arg(long, env = "DEFGUARD_GRPC_CERT")] pub grpc_cert: Option, #[arg(long, env = "DEFGUARD_GRPC_KEY")] @@ -92,69 +92,81 @@ pub struct DefGuardConfig { // relying party id and relying party origin for WebAuthn #[arg(long, env = "DEFGUARD_WEBAUTHN_RP_ID")] + #[deprecated(since = "2.0.0", note = "Use Settings.webauthn_rp_id instead")] pub webauthn_rp_id: Option, #[arg(long, env = "DEFGUARD_URL", value_parser = Url::parse, default_value = "http://localhost:8000")] #[deprecated(since = "2.0.0", note = "Use Settings.defguard_url instead")] pub url: Url, - #[arg(long, env = "DEFGUARD_GRPC_URL", value_parser = Url::parse, default_value = "http://localhost:50055")] - pub grpc_url: Url, + #[arg(long, env = "DEFGUARD_GRPC_URL", value_parser = Url::parse)] + #[deprecated(since = "2.0.0", note = "Use Settings.grpc_url instead")] + pub grpc_url: Option, #[arg(long, env = "DEFGUARD_DISABLE_STATS_PURGE")] - pub disable_stats_purge: bool, + #[deprecated(since = "2.0.0", note = "Use Settings.disable_stats_purge instead")] + pub disable_stats_purge: Option, - #[arg(long, env = "DEFGUARD_STATS_PURGE_FREQUENCY", default_value = "24h")] + #[arg(long, env = "DEFGUARD_STATS_PURGE_FREQUENCY")] #[serde(skip_serializing)] - pub stats_purge_frequency: Duration, + #[deprecated(since = "2.0.0", note = "Use Settings.stats_purge_frequency instead")] + pub stats_purge_frequency: Option, - #[arg(long, env = "DEFGUARD_STATS_PURGE_THRESHOLD", default_value = "30d")] + #[arg(long, env = "DEFGUARD_STATS_PURGE_THRESHOLD")] #[serde(skip_serializing)] - pub stats_purge_threshold: Duration, + #[deprecated(since = "2.0.0", note = "Use Settings.stats_purge_threshold instead")] + pub stats_purge_threshold: Option, - #[arg(long, env = "DEFGUARD_ENROLLMENT_URL", value_parser = Url::parse, default_value = "http://localhost:8080")] + #[arg(long, env = "DEFGUARD_ENROLLMENT_URL", value_parser = Url::parse)] #[deprecated(since = "2.0.0", note = "Use Settings.public_proxy_url instead")] - pub enrollment_url: Url, + pub enrollment_url: Option, - #[arg(long, env = "DEFGUARD_ENROLLMENT_TOKEN_TIMEOUT", default_value = "24h")] + #[arg(long, env = "DEFGUARD_ENROLLMENT_TOKEN_TIMEOUT")] #[serde(skip_serializing)] - pub enrollment_token_timeout: Duration, + #[deprecated( + since = "2.0.0", + note = "Use Settings.enrollment_token_timeout instead" + )] + pub enrollment_token_timeout: Option, - #[arg(long, env = "DEFGUARD_MFA_CODE_TIMEOUT", default_value = "60s")] + #[arg(long, env = "DEFGUARD_MFA_CODE_TIMEOUT")] #[serde(skip_serializing)] #[deprecated( since = "2.0.0", - note = "Use Settings.default_mfa_code_lifetime instead" + note = "Use Settings.mfa_code_timeout_seconds instead" )] - pub mfa_code_timeout: Duration, + pub mfa_code_timeout: Option, - #[arg(long, env = "DEFGUARD_SESSION_TIMEOUT", default_value = "7d")] + #[arg(long, env = "DEFGUARD_SESSION_TIMEOUT")] #[serde(skip_serializing)] - #[deprecated(since = "2.0.0", note = "Use Settings.default_authentication instead")] - pub session_timeout: Duration, - - #[arg( - long, - env = "DEFGUARD_PASSWORD_RESET_TOKEN_TIMEOUT", - default_value = "24h" + #[deprecated( + since = "2.0.0", + note = "Use Settings.authentication_period_days instead" )] - #[serde(skip_serializing)] - pub password_reset_token_timeout: Duration, + pub session_timeout: Option, - #[arg( - long, - env = "DEFGUARD_ENROLLMENT_SESSION_TIMEOUT", - default_value = "10m" - )] + #[arg(long, env = "DEFGUARD_PASSWORD_RESET_TOKEN_TIMEOUT")] #[serde(skip_serializing)] - pub enrollment_session_timeout: Duration, + #[deprecated( + since = "2.0.0", + note = "Use Settings.password_reset_token_timeout instead" + )] + pub password_reset_token_timeout: Option, - #[arg( - long, - env = "DEFGUARD_PASSWORD_RESET_SESSION_TIMEOUT", - default_value = "10m" + #[arg(long, env = "DEFGUARD_ENROLLMENT_SESSION_TIMEOUT")] + #[serde(skip_serializing)] + #[deprecated( + since = "2.0.0", + note = "Use Settings.enrollment_session_timeout instead" )] + pub enrollment_session_timeout: Option, + + #[arg(long, env = "DEFGUARD_PASSWORD_RESET_SESSION_TIMEOUT")] #[serde(skip_serializing)] - pub password_reset_session_timeout: Duration, + #[deprecated( + since = "2.0.0", + note = "Use Settings.password_reset_session_timeout instead" + )] + pub password_reset_session_timeout: Option, #[arg(long, env = "DEFGUARD_COOKIE_DOMAIN")] pub cookie_domain: Option, @@ -162,10 +174,6 @@ pub struct DefGuardConfig { #[arg(long, env = "DEFGUARD_COOKIE_INSECURE")] pub cookie_insecure: bool, - // path to certificate `.pem` file used if connecting to proxy over HTTPS - #[arg(long, env = "DEFGUARD_PROXY_GRPC_CA")] - pub proxy_grpc_ca: Option, - #[command(subcommand)] #[serde(skip_serializing)] pub cmd: Option, @@ -227,7 +235,11 @@ impl DefGuardConfig { #[must_use] pub fn new() -> Self { let config = Self::parse(); - config.validate_secret_key(); + #[allow(deprecated)] + if let Some(secret_key) = &config.secret_key { + Settings::validate_secret_key(secret_key.expose_secret()) + .expect("Invalid DEFGUARD_SECRET_KEY"); + } config } @@ -240,19 +252,20 @@ impl DefGuardConfig { /// Initialize values that depend on Settings. pub fn initialize_post_settings(&mut self) { let url = Settings::url().expect("Unable to parse Defguard URL."); - self.initialize_rp_id(&url); + // TODO(jck) + // self.initialize_rp_id(&url); self.initialize_cookie_domain(&url); } - fn initialize_rp_id(&mut self, url: &Url) { - if self.webauthn_rp_id.is_none() { - self.webauthn_rp_id = Some( - url.domain() - .expect("Unable to get domain for server URL.") - .to_string(), - ); - } - } + // fn initialize_rp_id(&mut self, url: &Url) { + // if self.webauthn_rp_id.is_none() { + // self.webauthn_rp_id = Some( + // url.domain() + // .expect("Unable to get domain for server URL.") + // .to_string(), + // ); + // } + // } fn initialize_cookie_domain(&mut self, url: &Url) { if self.cookie_domain.is_none() { @@ -264,20 +277,6 @@ impl DefGuardConfig { } } - fn validate_secret_key(&self) { - let secret_key = self.secret_key.expose_secret(); - assert!( - secret_key.trim().len() == secret_key.len(), - "SECRET_KEY cannot have leading and trailing space", - ); - - assert!( - secret_key.len() >= 64, - "SECRET_KEY must be at least 64 characters long, provided value has {} characters", - secret_key.len() - ); - } - /// Try PKCS#1 and PKCS#8 PEM formats. fn parse_openid_key(path: &str) -> Result { if let Ok(key) = RsaPrivateKey::read_pkcs1_pem_file(path) { @@ -297,25 +296,6 @@ impl DefGuardConfig { None } } - - /// Provide [`ClientTlsConfig`] from paths to cerfiticate, key, and cerfiticate authority (CA). - pub fn grpc_client_tls_config(&self) -> Result, io::Error> { - if self.grpc_ca.is_none() && (self.grpc_cert.is_none() || self.grpc_key.is_none()) { - return Ok(None); - } - let mut tls = ClientTlsConfig::new(); - if let (Some(cert_path), Some(key_path)) = (&self.grpc_cert, &self.grpc_key) { - let cert = read_to_string(cert_path)?; - let key = read_to_string(key_path)?; - tls = tls.identity(Identity::from_pem(cert, key)); - } - if let Some(ca_path) = &self.grpc_ca { - let ca = read_to_string(ca_path)?; - tls = tls.ca_certificate(Certificate::from_pem(ca)); - } - - Ok(Some(tls)) - } } impl Default for DefGuardConfig { @@ -336,29 +316,29 @@ mod tests { DefGuardConfig::command().debug_assert(); } - #[test] - fn test_generate_rp_id() { - unsafe { - env::remove_var("DEFGUARD_WEBAUTHN_RP_ID"); - } + // #[test] + // fn test_generate_rp_id() { + // unsafe { + // env::remove_var("DEFGUARD_WEBAUTHN_RP_ID"); + // } - let url = Url::parse("https://defguard.example.com").unwrap(); - let mut config = DefGuardConfig::new(); - config.initialize_rp_id(&url); + // let url = Url::parse("https://defguard.example.com").unwrap(); + // let mut config = DefGuardConfig::new(); + // config.initialize_rp_id(&url); - assert_eq!( - config.webauthn_rp_id, - Some("defguard.example.com".to_string()) - ); + // assert_eq!( + // config.webauthn_rp_id, + // Some("defguard.example.com".to_string()) + // ); - unsafe { - env::set_var("DEFGUARD_WEBAUTHN_RP_ID", "example.com"); - } + // unsafe { + // env::set_var("DEFGUARD_WEBAUTHN_RP_ID", "example.com"); + // } - let config = DefGuardConfig::new(); + // let config = DefGuardConfig::new(); - assert_eq!(config.webauthn_rp_id, Some("example.com".to_string())); - } + // assert_eq!(config.webauthn_rp_id, Some("example.com".to_string())); + // } #[test] fn test_generate_cookie_domain() { diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 8daaf158a3..616cef6c39 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -1,6 +1,8 @@ use std::{collections::HashMap, fmt, time::Duration}; use chrono::NaiveDateTime; +use rand::{RngCore, rngs::OsRng}; +use secrecy::ExposeSecret; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, Type, query, query_as}; use struct_patch::Patch; @@ -10,7 +12,7 @@ use url::Url; use utoipa::ToSchema; use uuid::Uuid; -use crate::{db::Id, global_value, secret::SecretStringWrapper}; +use crate::{config::DefGuardConfig, db::Id, global_value, secret::SecretStringWrapper}; global_value!(SETTINGS, Option, None, set_settings, get_settings); @@ -46,6 +48,14 @@ pub enum SettingsValidationError { CannotEnableGatewayNotifications, } +#[derive(Error, Debug)] +pub enum SettingsRequiredValueError { + #[error("Missing required setting: {0}")] + Missing(&'static str), + #[error("Invalid required setting `{0}`: {1}")] + Invalid(&'static str, &'static str), +} + #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Type, Debug, Default)] #[sqlx(type_name = "smtp_encryption", rename_all = "lowercase")] pub enum SmtpEncryption { @@ -175,6 +185,18 @@ pub struct Settings { pub public_proxy_url: String, pub initial_setup_step: InitialSetupStep, pub default_admin_id: Option, + // 1.6 config options + pub secret_key: Option, + pub webauthn_rp_id: Option, + pub grpc_url: String, + pub disable_stats_purge: bool, + auth_cookie_timeout_days: i32, + stats_purge_frequency_hours: i32, + stats_purge_threshold_days: i32, + enrollment_token_timeout_hours: i32, + password_reset_token_timeout_hours: i32, + enrollment_session_timeout_minutes: i32, + password_reset_session_timeout_minutes: i32, } // Implement manually to avoid exposing the license key. @@ -269,6 +291,63 @@ impl fmt::Debug for Settings { } impl Settings { + pub(crate) fn validate_secret_key(secret_key: &str) -> Result<(), SettingsRequiredValueError> { + if secret_key.trim().len() != secret_key.len() { + return Err(SettingsRequiredValueError::Invalid( + "secret_key", + "cannot have leading or trailing whitespace", + )); + } + + if secret_key.len() < 64 { + return Err(SettingsRequiredValueError::Invalid( + "secret_key", + "must be at least 64 characters long", + )); + } + + Ok(()) + } + + fn generate_secret_key() -> String { + let mut bytes = [0_u8; 32]; + OsRng.fill_bytes(&mut bytes); + let mut secret_key = String::with_capacity(64); + for byte in bytes { + use std::fmt::Write as _; + let _ = write!(secret_key, "{byte:02x}"); + } + secret_key + } + + pub async fn ensure_secret_key( + pool: &PgPool, + config: &DefGuardConfig, + ) -> Result<(), anyhow::Error> { + let mut settings = Settings::get_current_settings(); + + #[allow(deprecated)] + if let Some(secret_key) = &config.secret_key { + let secret_key = secret_key.expose_secret(); + Settings::validate_secret_key(secret_key)?; + if settings.secret_key.as_deref() != Some(secret_key) { + settings.secret_key = Some(secret_key.to_string()); + update_current_settings(pool, settings).await?; + } + return Ok(()); + } + + if let Some(secret_key) = settings.secret_key.as_deref() { + Settings::validate_secret_key(secret_key)?; + return Ok(()); + } + + settings.secret_key = Some(Settings::generate_secret_key()); + update_current_settings(pool, settings).await?; + + Ok(()) + } + pub async fn get<'e, E>(executor: E) -> Result, sqlx::Error> where E: PgExecutor<'e>, @@ -297,7 +376,10 @@ impl Settings { ca_key_der, ca_cert_der, ca_expiry, initial_setup_completed, defguard_url, \ default_admin_group_name, authentication_period_days, mfa_code_timeout_seconds, \ public_proxy_url, initial_setup_step \"initial_setup_step: InitialSetupStep\", \ - default_admin_id \ + default_admin_id, auth_cookie_timeout_days, secret_key, webauthn_rp_id, grpc_url, disable_stats_purge, \ + stats_purge_frequency_hours, stats_purge_threshold_days, \ + enrollment_token_timeout_hours, password_reset_token_timeout_hours, \ + enrollment_session_timeout_minutes, password_reset_session_timeout_minutes \ FROM \"settings\" WHERE id = 1", ) .fetch_optional(executor) @@ -385,7 +467,18 @@ impl Settings { mfa_code_timeout_seconds = $56, \ public_proxy_url = $57, \ initial_setup_step = $58, \ - default_admin_id = $59 \ + default_admin_id = $59, \ + auth_cookie_timeout_days = $60, \ + secret_key = $61, \ + webauthn_rp_id = $62, \ + grpc_url = $63, \ + disable_stats_purge = $64, \ + stats_purge_frequency_hours = $65, \ + stats_purge_threshold_days = $66, \ + enrollment_token_timeout_hours = $67, \ + password_reset_token_timeout_hours = $68, \ + enrollment_session_timeout_minutes = $69, \ + password_reset_session_timeout_minutes = $70 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -446,6 +539,17 @@ impl Settings { self.public_proxy_url, &self.initial_setup_step as &InitialSetupStep, self.default_admin_id, + self.auth_cookie_timeout_days, + self.secret_key, + self.webauthn_rp_id, + self.grpc_url, + self.disable_stats_purge, + self.stats_purge_frequency_hours, + self.stats_purge_threshold_days, + self.enrollment_token_timeout_hours, + self.password_reset_token_timeout_hours, + self.enrollment_session_timeout_minutes, + self.password_reset_session_timeout_minutes, ) .execute(executor) .await?; @@ -526,9 +630,116 @@ impl Settings { Duration::from_secs(self.authentication_period_days as u64 * 24 * 3600) } + #[must_use] + pub fn auth_cookie_timeout(&self) -> Duration { + Duration::from_secs(self.auth_cookie_timeout_days as u64 * 24 * 3600) + } + + #[must_use] + pub fn stats_purge_frequency(&self) -> Duration { + Duration::from_secs(self.stats_purge_frequency_hours as u64 * 3600) + } + + #[must_use] + pub fn stats_purge_threshold(&self) -> Duration { + Duration::from_secs(self.stats_purge_threshold_days as u64 * 24 * 3600) + } + + #[must_use] + pub fn enrollment_token_timeout(&self) -> Duration { + Duration::from_secs(self.enrollment_token_timeout_hours as u64 * 3600) + } + + #[must_use] + pub fn password_reset_token_timeout(&self) -> Duration { + Duration::from_secs(self.password_reset_token_timeout_hours as u64 * 3600) + } + + #[must_use] + pub fn enrollment_session_timeout(&self) -> Duration { + Duration::from_secs(self.enrollment_session_timeout_minutes as u64 * 60) + } + + #[must_use] + pub fn password_reset_session_timeout(&self) -> Duration { + Duration::from_secs(self.password_reset_session_timeout_minutes as u64 * 60) + } + + pub fn secret_key_required(&self) -> Result<&str, SettingsRequiredValueError> { + let secret_key = self + .secret_key + .as_deref() + .ok_or(SettingsRequiredValueError::Missing("secret_key"))?; + + Settings::validate_secret_key(secret_key)?; + + Ok(secret_key) + } + pub fn proxy_public_url(&self) -> Result { Url::parse(&self.public_proxy_url) } + + #[allow(deprecated)] + pub async fn update_from_config<'e, E>( + &mut self, + executor: E, + config: &DefGuardConfig, + ) -> Result<(), sqlx::Error> + where + E: PgExecutor<'e>, + { + let minute = 60; + let hour = minute * 60; + let day = hour * 24; + if let Some(auth_cookie_timeout) = config.auth_cookie_timeout { + self.auth_cookie_timeout_days = (auth_cookie_timeout.as_secs() / day) as i32; + } + if let Some(secret_key) = &config.secret_key { + self.secret_key = Some(secret_key.expose_secret().to_string()); + } + if let Some(webauthn_rp_id) = &config.webauthn_rp_id { + self.webauthn_rp_id = Some(webauthn_rp_id.clone()); + } + if let Some(grpc_url) = &config.grpc_url { + self.grpc_url = grpc_url.to_string(); + } + if let Some(enrollment_url) = &config.enrollment_url { + self.public_proxy_url = enrollment_url.to_string(); + } + if let Some(mfa_code_timeout) = config.mfa_code_timeout { + self.mfa_code_timeout_seconds = mfa_code_timeout.as_secs() as i32; + } + if let Some(session_timeout) = config.session_timeout { + self.authentication_period_days = (session_timeout.as_secs() / day) as i32; + } + if let Some(disable_stats_purge) = config.disable_stats_purge { + self.disable_stats_purge = disable_stats_purge; + } + if let Some(stats_purge_frequency) = config.stats_purge_frequency { + self.stats_purge_frequency_hours = (stats_purge_frequency.as_secs() / hour) as i32; + } + if let Some(stats_purge_threshold) = config.stats_purge_threshold { + self.stats_purge_threshold_days = (stats_purge_threshold.as_secs() / day) as i32; + } + if let Some(enrollment_token_timeout) = config.enrollment_token_timeout { + self.enrollment_token_timeout_hours = + (enrollment_token_timeout.as_secs() / hour) as i32; + } + if let Some(password_reset_token_timeout) = config.password_reset_token_timeout { + self.password_reset_token_timeout_hours = + (password_reset_token_timeout.as_secs() / hour) as i32; + } + if let Some(enrollment_session_timeout) = config.enrollment_session_timeout { + self.enrollment_session_timeout_minutes = + (enrollment_session_timeout.as_secs() / minute) as i32; + } + if let Some(password_reset_session_timeout) = config.password_reset_session_timeout { + self.password_reset_session_timeout_minutes = + (password_reset_session_timeout.as_secs() / minute) as i32; + } + update_current_settings(executor, self.clone()).await + } } #[derive(Serialize)] diff --git a/crates/defguard_core/src/appstate.rs b/crates/defguard_core/src/appstate.rs index 2db868f0af..6e06111b70 100644 --- a/crates/defguard_core/src/appstate.rs +++ b/crates/defguard_core/src/appstate.rs @@ -2,11 +2,8 @@ use std::sync::{Arc, Mutex, RwLock}; use axum::extract::FromRef; use axum_extra::extract::cookie::Key; -use defguard_common::{ - config::server_config, db::models::Settings, types::proxy::ProxyControlMessage, -}; +use defguard_common::{db::models::Settings, types::proxy::ProxyControlMessage}; use reqwest::Client; -use secrecy::ExposeSecret; use serde_json::json; use sqlx::PgPool; use tokio::{ @@ -113,6 +110,7 @@ impl AppState { tx: UnboundedSender, rx: UnboundedReceiver, wireguard_tx: Sender, + key: Key, failed_logins: Arc>, event_tx: UnboundedSender, incompatible_components: Arc>, @@ -120,10 +118,10 @@ impl AppState { ) -> Self { spawn(Self::handle_triggers(pool.clone(), rx)); - let config = server_config(); let url = Settings::url().expect("Invalid Defguard URL configuration"); + let settings = Settings::get_current_settings(); let webauthn_builder = WebauthnBuilder::new( - config + settings .webauthn_rp_id .as_ref() .expect("Webauth RP ID configuration is required"), @@ -136,8 +134,6 @@ impl AppState { .expect("Invalid WebAuthn configuration"), ); - let key = Key::from(config.secret_key.expose_secret().as_bytes()); - Self { pool, tx, diff --git a/crates/defguard_core/src/enterprise/handlers/openid_login.rs b/crates/defguard_core/src/enterprise/handlers/openid_login.rs index 29569b1c10..8217caa295 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_login.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_login.rs @@ -605,7 +605,7 @@ pub async fn auth_callback( let (session, user_info, mfa_info) = create_session(&appstate.pool, insecure_ip, user_agent.as_str(), &mut user).await?; - let max_age = Duration::seconds(config.auth_cookie_timeout.as_secs() as i64); + let max_age = Duration::seconds(settings.auth_cookie_timeout().as_secs() as i64); let cookie_domain = config .cookie_domain .as_ref() diff --git a/crates/defguard_core/src/handlers/auth.rs b/crates/defguard_core/src/handlers/auth.rs index b0bb095fa8..1481d9e42d 100644 --- a/crates/defguard_core/src/handlers/auth.rs +++ b/crates/defguard_core/src/handlers/auth.rs @@ -237,7 +237,7 @@ pub async fn authenticate( let (session, user_info, mfa_info) = create_session(&appstate.pool, insecure_ip, user_agent.as_str(), &mut user).await?; - let max_age = Duration::seconds(server_config().auth_cookie_timeout.as_secs() as i64); + let max_age = Duration::seconds(settings.auth_cookie_timeout().as_secs() as i64); let config = server_config(); let cookie_domain = config .cookie_domain diff --git a/crates/defguard_core/src/handlers/network_devices.rs b/crates/defguard_core/src/handlers/network_devices.rs index 10a3fa94e8..8e2c78ee4e 100644 --- a/crates/defguard_core/src/handlers/network_devices.rs +++ b/crates/defguard_core/src/handlers/network_devices.rs @@ -32,7 +32,6 @@ use crate::{ enterprise::{firewall::try_get_location_firewall_config, limits::update_counts}, events::{ApiEvent, ApiEventType, ApiRequestContext}, grpc::GatewayEvent, - server_config, }; #[derive(Serialize)] @@ -448,14 +447,13 @@ pub(crate) async fn start_network_device_setup( config, device: NetworkDeviceInfo::from_device(device, &mut transaction).await?, }; - let config = server_config(); let settings = Settings::get_current_settings(); let configuration_token = start_desktop_configuration( &user, &mut transaction, &user, None, - config.enrollment_token_timeout.as_secs(), + settings.enrollment_token_timeout().as_secs(), settings.proxy_public_url()?.clone(), false, Some(result.device.id), @@ -514,14 +512,13 @@ pub(crate) async fn start_network_device_setup_for_device( user which added the device not found" )) })?; - let config = server_config(); let settings = Settings::get_current_settings(); let configuration_token = start_desktop_configuration( &user, &mut transaction, &user, None, - config.enrollment_token_timeout.as_secs(), + settings.enrollment_token_timeout().as_secs(), settings.proxy_public_url()?, false, Some(device.id), diff --git a/crates/defguard_core/src/handlers/user.rs b/crates/defguard_core/src/handlers/user.rs index bbbb8bfb93..884eed08f7 100644 --- a/crates/defguard_core/src/handlers/user.rs +++ b/crates/defguard_core/src/handlers/user.rs @@ -48,7 +48,7 @@ use crate::{ }, error::WebError, events::{ApiEvent, ApiEventType, ApiRequestContext}, - is_valid_phone_number, server_config, + is_valid_phone_number, user_management::{delete_user_and_cleanup_devices, sync_allowed_user_devices}, }; @@ -465,7 +465,7 @@ pub async fn start_enrollment( let mut transaction = appstate.pool.begin().await?; // try to parse token expiration time if provided - let config = server_config(); + let settings = Settings::get_current_settings(); let token_expiration_time_seconds = match data.token_expiration_time { Some(time) => parse_duration(&time) .map_err(|err| { @@ -473,10 +473,9 @@ pub async fn start_enrollment( WebError::BadRequest("Failed to parse token expiration time".to_owned()) })? .as_secs(), - None => config.enrollment_token_timeout.as_secs(), + None => settings.enrollment_token_timeout().as_secs(), }; - let settings: Settings = Settings::get_current_settings(); let public_proxy_url = settings.proxy_public_url()?; let enrollment_token = start_user_enrollment( @@ -580,7 +579,6 @@ pub async fn start_remote_desktop_configuration( "Generating a new desktop activation token by {}.", session.user.username ); - let config = server_config(); let settings = Settings::get_current_settings(); let public_proxy_url = settings.proxy_public_url()?; let desktop_configuration_token = start_desktop_configuration( @@ -588,7 +586,7 @@ pub async fn start_remote_desktop_configuration( &mut transaction, &session.user, Some(email), - config.enrollment_token_timeout.as_secs(), + settings.enrollment_token_timeout().as_secs(), public_proxy_url.clone(), data.send_enrollment_notification, None, @@ -1105,16 +1103,15 @@ pub async fn reset_password( Token::delete_unused_user_password_reset_tokens(&mut transaction, user.id).await?; - let config = server_config(); + let settings = Settings::get_current_settings(); let enrollment = Token::new( user.id, Some(session.user.id), Some(user.email.clone()), - config.password_reset_token_timeout.as_secs(), + settings.password_reset_token_timeout().as_secs(), Some(PASSWORD_RESET_TOKEN_TYPE.to_string()), ); enrollment.save(&mut *transaction).await?; - let settings = Settings::get_current_settings(); let public_proxy_url = settings.proxy_public_url()?; let result = Mail::new( diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 79a2499641..baf11401f5 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -11,6 +11,7 @@ use axum::{ routing::{delete, get, post, put}, serve, }; +use axum_extra::extract::cookie::Key; use defguard_certs::CertificateAuthority; use defguard_common::{ VERSION, @@ -219,6 +220,7 @@ pub fn build_webapp( wireguard_tx: Sender, worker_state: Arc>, pool: PgPool, + key: Key, failed_logins: Arc>, event_tx: UnboundedSender, version: Version, @@ -604,6 +606,7 @@ pub fn build_webapp( webhook_tx, webhook_rx, wireguard_tx, + key, failed_logins, event_tx, incompatible_components, @@ -638,12 +641,16 @@ pub async fn run_web_server( incompatible_components: Arc>, proxy_control_tx: tokio::sync::mpsc::Sender, ) -> Result<(), anyhow::Error> { + let settings = Settings::get_current_settings(); + let key = Key::from(settings.secret_key_required()?.as_bytes()); + let webapp = build_webapp( webhook_tx, webhook_rx, wireguard_tx, worker_state, pool, + key, failed_logins, event_tx, Version::parse(VERSION)?, @@ -702,8 +709,6 @@ pub async fn init_dev_env(config: &DefGuardConfig) { settings.ca_key_der = Some(ca.key_pair_der().to_vec()); settings.ca_expiry = Some(ca.expiry().expect("Failed to get CA expiry")); settings.initial_setup_completed = true; - // This should possibly be initialized somehow differently in the future since we are deprecating the enrollment URL env var. - settings.public_proxy_url = config.enrollment_url.to_string(); settings.defguard_url = config.url.to_string(); update_current_settings(&pool, settings) .await diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index 6d9ad02340..372ff5d72c 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -5,13 +5,14 @@ use std::{ sync::{Arc, Mutex}, }; +use axum_extra::extract::cookie::Key; pub use defguard_common::db::setup_pool; use defguard_common::{ VERSION, config::DefGuardConfig, db::{ Id, - models::{Device, User, WireguardNetwork, settings::initialize_current_settings}, + models::{Device, Settings, User, WireguardNetwork, settings::initialize_current_settings}, }, }; use defguard_core::{ @@ -123,12 +124,20 @@ pub(crate) async fn make_base_client( // .with(tracing_subscriber::fmt::layer()) // .init(); + let key = Key::from( + Settings::get_current_settings() + .secret_key_required() + .unwrap() + .as_bytes(), + ); + let webapp = build_webapp( tx, rx, wg_tx, worker_state, pool, + key, failed_logins, api_event_tx, Version::parse(VERSION).unwrap(), diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index 06b24aba06..02111cbcf8 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -7,7 +7,6 @@ use std::{ use axum_extra::extract::cookie::Key; use defguard_common::{ VERSION, - config::server_config, db::{ Id, models::{Settings, proxy::Proxy}, @@ -43,7 +42,6 @@ use defguard_version::{ use hyper_rustls::HttpsConnectorBuilder; use openidconnect::{AuthorizationCode, Nonce, Scope, core::CoreAuthenticationFlow}; use reqwest::Url; -use secrecy::ExposeSecret; use semver::Version; use sqlx::PgPool; use tokio::{ @@ -87,10 +85,12 @@ pub(super) struct ProxyHandler { pub(super) url: Url, shutdown_signal: Arc>, proxy_id: Id, + proxy_cookie_key: Key, client: Option>>, } impl ProxyHandler { + #[allow(clippy::too_many_arguments)] pub(super) fn new( pool: PgPool, url: Url, @@ -99,6 +99,7 @@ impl ProxyHandler { sessions: Arc>>, shutdown_signal: Arc>, proxy_id: Id, + proxy_cookie_key: Key, ) -> Self { // Instantiate gRPC servers. let services = ProxyServices::new(&pool, tx, remote_mfa_responses, sessions); @@ -109,6 +110,7 @@ impl ProxyHandler { url, shutdown_signal, proxy_id, + proxy_cookie_key, client: None, } } @@ -120,6 +122,7 @@ impl ProxyHandler { remote_mfa_responses: Arc>>>, sessions: Arc>>, shutdown_signal: Arc>, + proxy_cookie_key: Key, ) -> Result { let url = Url::from_str(&format!("http://{}:{}", proxy.address, proxy.port))?; let proxy_id = proxy.id; @@ -131,6 +134,7 @@ impl ProxyHandler { sessions, shutdown_signal, proxy_id, + proxy_cookie_key, )) } @@ -196,14 +200,13 @@ impl ProxyHandler { loop { let endpoint = self.endpoint()?; let settings = Settings::get_current_settings(); - let Some(ca_cert_der) = settings.ca_cert_der else { + let Some(ref ca_cert_der) = settings.ca_cert_der else { return Err(ProxyError::MissingConfiguration( "Core CA is not setup, can't create a Proxy endpoint.".to_string(), )); }; - let tls_config = - tls_certs::client_config(&ca_cert_der, certs_rx.clone(), self.proxy_id) - .map_err(|err| ProxyError::TlsConfigError(err.to_string()))?; + let tls_config = tls_certs::client_config(ca_cert_der, certs_rx.clone(), self.proxy_id) + .map_err(|err| ProxyError::TlsConfigError(err.to_string()))?; let connector = HttpsConnectorBuilder::new() .with_tls_config(tls_config) .https_only() @@ -270,13 +273,9 @@ impl ProxyHandler { info!("Connected to proxy at {}", endpoint.uri()); let mut resp_stream = response.into_inner(); - // Derive proxy cookie key from core secret to avoid transmitting it over gRPC. - let config = server_config(); - let proxy_cookie_key = Key::derive_from(config.secret_key.expose_secret().as_bytes()); - // Send initial info with private cookies key. let initial_info = InitialInfo { - private_cookies_key: proxy_cookie_key.master().to_vec(), + private_cookies_key: self.proxy_cookie_key.master().to_vec(), }; let _ = tx.send(CoreResponse { id: 0, @@ -722,12 +721,12 @@ impl ProxyHandler { as a result of proxy OpenID auth callback.", user.username ); - let config = server_config(); + let settings = Settings::get_current_settings(); let desktop_configuration = Token::new( user.id, Some(user.id), Some(user.email), - config.enrollment_token_timeout.as_secs(), + settings.enrollment_token_timeout().as_secs(), Some(ENROLLMENT_TOKEN_TYPE.to_string()), ); debug!("Saving a new desktop configuration token..."); @@ -736,7 +735,6 @@ impl ProxyHandler { "Saved desktop configuration token. Responding to \ proxy with the token." ); - let settings = Settings::get_current_settings(); let public_proxy_url = settings.proxy_public_url()?; Some(core_response::Payload::AuthCallback( diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index b4bd75255b..fad1fe58ca 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -4,6 +4,7 @@ use std::{ time::Duration, }; +use axum_extra::extract::cookie::Key; use defguard_common::{db::models::proxy::Proxy, types::proxy::ProxyControlMessage}; use defguard_core::{events::BidiStreamEvent, grpc::GatewayEvent, version::IncompatibleComponents}; use sqlx::PgPool; @@ -40,6 +41,7 @@ pub struct ProxyManager { tx: ProxyTxSet, incompatible_components: Arc>, proxy_control: Receiver, + proxy_cookie_key: Key, } impl ProxyManager { @@ -48,12 +50,14 @@ impl ProxyManager { tx: ProxyTxSet, incompatible_components: Arc>, proxy_control_rx: Receiver, + core_secret_key: String, ) -> Self { Self { pool, tx, incompatible_components, proxy_control: proxy_control_rx, + proxy_cookie_key: Key::derive_from(core_secret_key.as_bytes()), } } @@ -89,6 +93,7 @@ impl ProxyManager { Arc::clone(&remote_mfa_responses), Arc::clone(&sessions), Arc::new(Mutex::new(shutdown_rx)), + self.proxy_cookie_key.clone(), ) }) .collect::, _>>()?; @@ -131,6 +136,7 @@ impl ProxyManager { Arc::clone(&remote_mfa_responses), Arc::clone(&sessions), Arc::new(Mutex::new(shutdown_rx)), + self.proxy_cookie_key.clone(), ) { Ok(proxy) => { debug!("Spawning proxy task for proxy {}", proxy.url); diff --git a/crates/defguard_proxy_manager/src/servers/enrollment.rs b/crates/defguard_proxy_manager/src/servers/enrollment.rs index bd1bbef18e..1c84423dc8 100644 --- a/crates/defguard_proxy_manager/src/servers/enrollment.rs +++ b/crates/defguard_proxy_manager/src/servers/enrollment.rs @@ -1,7 +1,6 @@ use std::collections::HashSet; use defguard_common::{ - config::server_config, csv::AsCsv, db::{ Id, @@ -87,7 +86,8 @@ impl EnrollmentServer { ); return Err(Status::permission_denied("invalid token")); } - if enrollment.is_session_valid(server_config().enrollment_session_timeout.as_secs()) { + let settings = Settings::get_current_settings(); + if enrollment.is_session_valid(settings.enrollment_session_timeout().as_secs()) { info!("Enrollment session validated: {enrollment:?}"); Ok(enrollment) } else { @@ -164,10 +164,11 @@ impl EnrollmentServer { "Validating enrollment token and starting session for user {}({:?})", user.username, user.id, ); + let settings = Settings::get_current_settings(); let session_deadline = enrollment .start_session( &mut transaction, - server_config().enrollment_session_timeout.as_secs(), + settings.enrollment_session_timeout().as_secs(), ) .await?; info!( @@ -179,7 +180,6 @@ impl EnrollmentServer { "Retrieving settings for enrollment of user {}({:?}).", user.username, user.id ); - let settings = Settings::get_current_settings(); debug!("Settings: {settings:?}"); debug!( diff --git a/crates/defguard_proxy_manager/src/servers/password_reset.rs b/crates/defguard_proxy_manager/src/servers/password_reset.rs index b6d94f2532..84ad4a81a6 100644 --- a/crates/defguard_proxy_manager/src/servers/password_reset.rs +++ b/crates/defguard_proxy_manager/src/servers/password_reset.rs @@ -1,7 +1,4 @@ -use defguard_common::{ - config::server_config, - db::models::{Settings, User}, -}; +use defguard_common::db::models::{Settings, User}; use defguard_core::{ db::models::enrollment::{PASSWORD_RESET_TOKEN_TYPE, Token}, enterprise::ldap::utils::ldap_change_password, @@ -59,7 +56,8 @@ impl PasswordResetServer { return Err(Status::permission_denied("invalid token")); } - if enrollment.is_session_valid(server_config().enrollment_session_timeout.as_secs()) { + let settings = Settings::get_current_settings(); + if enrollment.is_session_valid(settings.enrollment_session_timeout().as_secs()) { info!("Password reset session validated: {enrollment:?}.",); Ok(enrollment) } else { @@ -88,7 +86,6 @@ impl PasswordResetServer { request: PasswordResetInitializeRequest, req_device_info: Option, ) -> Result<(), Status> { - let config = server_config(); debug!("Starting password reset request"); let ip_address; @@ -133,11 +130,12 @@ impl PasswordResetServer { Token::delete_unused_user_password_reset_tokens(&mut transaction, user.id).await?; + let settings = Settings::get_current_settings(); let enrollment = Token::new( user.id, None, Some(email.clone()), - config.password_reset_token_timeout.as_secs(), + settings.password_reset_token_timeout().as_secs(), Some(PASSWORD_RESET_TOKEN_TYPE.to_string()), ); enrollment.save(&mut *transaction).await?; @@ -147,7 +145,6 @@ impl PasswordResetServer { Status::internal("unexpected error") })?; - let settings = Settings::get_current_settings(); let public_proxy_url = settings.proxy_public_url().map_err(|err| { error!("Failed to get public proxy URL: {err}"); Status::internal("unexpected error") @@ -212,10 +209,11 @@ impl PasswordResetServer { Status::internal("unexpected error") })?; + let settings = Settings::get_current_settings(); let session_deadline = enrollment .start_session( &mut transaction, - server_config().password_reset_session_timeout.as_secs(), + settings.password_reset_session_timeout().as_secs(), ) .await?; diff --git a/crates/defguard_setup/src/migration.rs b/crates/defguard_setup/src/migration.rs index 4154ab84b7..bebeba9e69 100644 --- a/crates/defguard_setup/src/migration.rs +++ b/crates/defguard_setup/src/migration.rs @@ -9,7 +9,8 @@ use axum::{ routing::{get, post, put}, serve, }; -use defguard_common::VERSION; +use axum_extra::extract::cookie::Key; +use defguard_common::{VERSION, db::models::Settings}; use defguard_core::{ auth::failed_login::FailedLoginMap, handle_404, @@ -58,11 +59,18 @@ pub fn build_migration_webapp( let (wireguard_tx, _wireguard_rx) = broadcast::channel::(64); let (proxy_control_tx, _proxy_control_rx) = mpsc::channel(32); let incompatible_components = Arc::new(RwLock::new(IncompatibleComponents::default())); + let key = Key::from( + Settings::get_current_settings() + .secret_key_required() + .expect("Missing required secret key in settings") + .as_bytes(), + ); let app_state = AppState::new( pool.clone(), webhook_tx, webhook_rx, wireguard_tx, + key, failed_logins.clone(), event_tx, incompatible_components, diff --git a/migrations/20260227091211_[2.0.0]_settings_in_db.down.sql b/migrations/20260227091211_[2.0.0]_settings_in_db.down.sql new file mode 100644 index 0000000000..5e68c99968 --- /dev/null +++ b/migrations/20260227091211_[2.0.0]_settings_in_db.down.sql @@ -0,0 +1,13 @@ +ALTER TABLE settings + DROP COLUMN auth_cookie_timeout_days, + DROP COLUMN secret_key, + DROP COLUMN openid_signing_key, + DROP COLUMN webauthn_rp_id, + DROP COLUMN grpc_url, + DROP COLUMN disable_stats_purge, + DROP COLUMN stats_purge_frequency_hours, + DROP COLUMN stats_purge_threshold_days, + DROP COLUMN enrollment_token_timeout_hours, + DROP COLUMN password_reset_token_timeout_hours, + DROP COLUMN enrollment_session_timeout_minutes, + DROP COLUMN password_reset_session_timeout_minutes; diff --git a/migrations/20260227091211_[2.0.0]_settings_in_db.up.sql b/migrations/20260227091211_[2.0.0]_settings_in_db.up.sql new file mode 100644 index 0000000000..b414c04c1c --- /dev/null +++ b/migrations/20260227091211_[2.0.0]_settings_in_db.up.sql @@ -0,0 +1,13 @@ +ALTER TABLE settings + ADD COLUMN auth_cookie_timeout_days int4 NOT NULL DEFAULT 7, + ADD COLUMN secret_key text, + ADD COLUMN openid_signing_key text, + ADD COLUMN webauthn_rp_id text, + ADD COLUMN grpc_url text NOT NULL DEFAULT 'http://localhost:50055', + ADD COLUMN disable_stats_purge boolean NOT NULL DEFAULT false, + ADD COLUMN stats_purge_frequency_hours int4 NOT NULL DEFAULT 24, + ADD COLUMN stats_purge_threshold_days int4 NOT NULL DEFAULT 30, + ADD COLUMN enrollment_token_timeout_hours int4 NOT NULL DEFAULT 24, + ADD COLUMN password_reset_token_timeout_hours int4 NOT NULL DEFAULT 24, + ADD COLUMN enrollment_session_timeout_minutes int4 NOT NULL DEFAULT 10, + ADD COLUMN password_reset_session_timeout_minutes int4 NOT NULL DEFAULT 10;