Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6ec14c2
migration with config options
j-chmielewski Feb 27, 2026
d7b81f9
Settings::update_from_config
j-chmielewski Feb 27, 2026
cbf4440
fix Settings::save function
j-chmielewski Feb 27, 2026
6317617
replace config variables occurences with settings, part 1
j-chmielewski Mar 2, 2026
87f35a5
replace config variables occurences with settings, part 2
j-chmielewski Mar 2, 2026
1cf605f
implement Settings time variables accessors with unit conversion
j-chmielewski Mar 2, 2026
caef131
make old DefguardConfig fields optional and deprecated
j-chmielewski Mar 2, 2026
6339f75
make secret_key optional at the DB level, ensure presence during startup
j-chmielewski Mar 2, 2026
9944096
validate secret for proxy during startup
j-chmielewski Mar 2, 2026
acbbcc2
remove cert options from DefguardConfig
j-chmielewski Mar 2, 2026
8991b80
restore grpc_cert, grpc_key config - workers still use this
j-chmielewski Mar 2, 2026
878401c
remove proxy_grpc_ca config field
j-chmielewski Mar 2, 2026
8a8a8ca
migrate DefguardConfig::enrollment_url
j-chmielewski Mar 2, 2026
a3585fa
migrate mfa_code_timeout
j-chmielewski Mar 2, 2026
03de230
migrate session_timeout
j-chmielewski Mar 2, 2026
df30caf
generate secret key during startup if not present
j-chmielewski Mar 2, 2026
7555d69
remove unused imports, allow deprecated where necessary
j-chmielewski Mar 2, 2026
b416531
remove unused methods
j-chmielewski Mar 2, 2026
42769ee
config secret validation
j-chmielewski Mar 2, 2026
b6468eb
Merge branch 'migration' into migration-config-to-db
j-chmielewski Mar 2, 2026
38e7b24
cargo fmt
j-chmielewski Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions crates/defguard/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,16 @@ 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) =
run_setup_web_server(pool.clone(), config.http_bind_address, config.http_port).await
{
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();
Expand All @@ -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 {
Expand Down Expand Up @@ -153,7 +163,11 @@ async fn main() -> Result<(), anyhow::Error> {

let incompatible_components: Arc<RwLock<IncompatibleComponents>> = 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()
Expand Down Expand Up @@ -184,11 +198,13 @@ async fn main() -> Result<(), anyhow::Error> {
}

let (proxy_control_tx, proxy_control_rx) = channel::<ProxyControlMessage>(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(
Expand Down Expand Up @@ -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:?}"),
Expand Down
198 changes: 89 additions & 109 deletions crates/defguard_common/src/config.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,7 +13,6 @@ use rsa::{
};
use secrecy::{ExposeSecret, SecretString};
use serde::Serialize;
use tonic::transport::{Certificate, ClientTlsConfig, Identity};

use crate::db::models::Settings;

Expand All @@ -38,13 +37,15 @@ pub struct DefGuardConfig {
#[arg(long, env = "DEFGUARD_LOG_FILE")]
pub log_file: Option<String>,

#[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<Duration>,

#[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<SecretString>,

#[arg(long, env = "DEFGUARD_DB_HOST", default_value = "localhost")]
pub database_host: String,
Expand All @@ -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<String>,
// 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<String>,
#[arg(long, env = "DEFGUARD_GRPC_KEY")]
Expand All @@ -92,80 +92,88 @@ 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<String>,
#[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<Url>,

#[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<bool>,

#[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<Duration>,

#[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<Duration>,

#[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<Url>,

#[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<Duration>,

#[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<Duration>,

#[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<Duration>,

#[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<Duration>,

#[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<Duration>,

#[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<Duration>,

#[arg(long, env = "DEFGUARD_COOKIE_DOMAIN")]
pub cookie_domain: Option<String>,

#[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<String>,

#[command(subcommand)]
#[serde(skip_serializing)]
pub cmd: Option<Command>,
Expand Down Expand Up @@ -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
}

Expand All @@ -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() {
Expand All @@ -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<RsaPrivateKey, rsa::pkcs8::Error> {
if let Ok(key) = RsaPrivateKey::read_pkcs1_pem_file(path) {
Expand All @@ -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<Option<ClientTlsConfig>, 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 {
Expand All @@ -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() {
Expand Down
Loading