From de9d1738ac2af5ef6a762da61c350dbfef6de178 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:56:54 +0100 Subject: [PATCH 1/7] issue certificates for gateway --- Cargo.lock | 2 + crates/defguard_certs/Cargo.toml | 2 + crates/defguard_certs/src/lib.rs | 73 +++- .../defguard_common/src/db/models/gateway.rs | 4 + .../defguard_core/src/grpc/gateway/handler.rs | 326 ++++++++++++------ crates/defguard_core/src/grpc/gateway/mod.rs | 79 +++-- ...4_gateway_certificates_management.down.sql | 2 + ...304_gateway_certificates_management.up.sql | 2 + 8 files changed, 358 insertions(+), 132 deletions(-) create mode 100644 migrations/20260113094304_gateway_certificates_management.down.sql create mode 100644 migrations/20260113094304_gateway_certificates_management.up.sql diff --git a/Cargo.lock b/Cargo.lock index ce1a729fa3..eea5159b42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,6 +1148,8 @@ dependencies = [ "serde", "sqlx", "thiserror 2.0.17", + "time", + "x509-parser 0.18.0", ] [[package]] diff --git a/crates/defguard_certs/Cargo.toml b/crates/defguard_certs/Cargo.toml index 8ea34cbeb3..b769b0f9ca 100644 --- a/crates/defguard_certs/Cargo.toml +++ b/crates/defguard_certs/Cargo.toml @@ -14,3 +14,5 @@ serde.workspace = true sqlx.workspace = true thiserror.workspace = true rustls-pki-types.workspace = true +time = "0.3" +x509-parser = "0.18" diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 574505b213..444e2dcf0f 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -1,10 +1,12 @@ use base64::{Engine, prelude::BASE64_STANDARD}; use rcgen::{ - BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, IsCa, - Issuer, KeyPair, SigningKey, + BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, + ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SigningKey, }; use rustls_pki_types::{CertificateDer, CertificateSigningRequestDer, pem::PemObject}; use thiserror::Error; +use time::{Duration, OffsetDateTime}; +use x509_parser::parse_x509_certificate; const CA_NAME: &str = "Defguard CA"; const CA_ORG: &str = "Defguard"; @@ -59,7 +61,8 @@ impl CertificateAuthority<'_> { pub fn new() -> Result { let mut ca_params = CertificateParams::new(vec![CA_NAME.to_string()])?; - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + // path length 0 to avoid issuing further CAs + ca_params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0)); ca_params .distinguished_name .push(rcgen::DnType::OrganizationName, CA_ORG); @@ -73,8 +76,35 @@ impl CertificateAuthority<'_> { } pub fn sign_csr(&self, csr: &Csr) -> Result { - let csr = csr.params()?; - let cert = csr.signed_by(&self.issuer)?; + // TODO: make validity configurable? + self.sign_csr_with_validity(csr, 360) + } + + /// Sign CSR with explicit validity in days. + pub fn sign_csr_with_validity( + &self, + csr: &Csr, + days_valid: i64, + ) -> Result { + let mut csr_params = csr.params()?; + + let now = OffsetDateTime::now_utc(); + let not_before = now - Duration::minutes(5); + let not_after = now + Duration::days(days_valid); + + csr_params.params.not_before = not_before; + csr_params.params.not_after = not_after; + + csr_params.params.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::KeyEncipherment, + ]; + csr_params.params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ]; + + let cert = csr_params.signed_by(&self.issuer)?; Ok(cert) } @@ -93,6 +123,14 @@ impl CertificateAuthority<'_> { } } +/// Extract the expiry date (not_after) from a certificate. +pub fn get_certificate_expiry(cert: &Certificate) -> Result { + let (_, parsed) = parse_x509_certificate(cert.der()) + .map_err(|e| CertificateError::ParsingError(format!("Failed to parse certificate: {e}")))?; + + Ok(parsed.tbs_certificate.validity.not_after.to_datetime()) +} + pub struct Csr<'a> { csr: CertificateSigningRequestDer<'a>, } @@ -207,7 +245,7 @@ mod tests { #[test] fn test_sign_csr() { let ca = CertificateAuthority::new().unwrap(); - let cert_key_pair = KeyPair::generate().unwrap(); + let cert_key_pair = generate_key_pair().unwrap(); let csr = Csr::new( &cert_key_pair, &["example.com".to_string(), "www.example.com".to_string()], @@ -221,6 +259,29 @@ mod tests { assert!(signed_cert.pem().contains("BEGIN CERTIFICATE")); } + #[test] + fn test_sign_csr_with_validity() { + use x509_parser::parse_x509_certificate; + + let ca = CertificateAuthority::new().unwrap(); + let cert_key_pair = generate_key_pair().unwrap(); + let csr = Csr::new( + &cert_key_pair, + &["example.com".to_string()], + vec![(rcgen::DnType::CommonName, "example.com")], + ) + .unwrap(); + let signed_cert: Certificate = ca.sign_csr_with_validity(&csr, 90).unwrap(); + let der = signed_cert.der(); + let (_rem, parsed) = parse_x509_certificate(&der).unwrap(); + let validity = parsed.tbs_certificate.validity; + let not_before = validity.not_before.to_datetime(); + let not_after = validity.not_after.to_datetime(); + let days = (not_after - not_before).whole_days(); + assert!(days >= 89 && days <= 91, "expected 89-91 days, got {days}"); + assert!(not_after > not_before); + } + #[test] fn test_der_to_pem() { assert_eq!(PemLabel::Certificate.as_str(), "CERTIFICATE"); diff --git a/crates/defguard_common/src/db/models/gateway.rs b/crates/defguard_common/src/db/models/gateway.rs index bab5cff05f..f7f25189d7 100644 --- a/crates/defguard_common/src/db/models/gateway.rs +++ b/crates/defguard_common/src/db/models/gateway.rs @@ -15,6 +15,8 @@ pub struct Gateway { pub hostname: Option, pub connected_at: Option, pub disconnected_at: Option, + pub has_certificate: bool, + pub certificate_expiry: Option, } impl Gateway { @@ -39,6 +41,8 @@ impl Gateway { hostname: None, connected_at: None, disconnected_at: None, + has_certificate: false, + certificate_expiry: None, } } } diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 51995484f5..747b161e03 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -8,22 +8,25 @@ use std::{ }; use chrono::{DateTime, TimeDelta, Utc}; +use defguard_certs::{Csr, der_to_pem}; use defguard_common::{ VERSION, auth::claims::Claims, db::{ Id, NoId, models::{ - Device, User, WireguardNetwork, gateway::Gateway, + Device, Settings, User, WireguardNetwork, gateway::Gateway, wireguard_peer_stats::WireguardPeerStats, }, }, }; use defguard_mail::Mail; use defguard_proto::gateway::{ - CoreResponse, PeerStats, core_request, core_response, gateway_client, + CoreResponse, DerPayload, InitialSetupInfo, PeerStats, core_request, core_response, + gateway_client, gateway_setup_client, }; use defguard_version::client::ClientVersionInterceptor; +use reqwest::Url; use semver::Version; use sqlx::PgPool; use tokio::{ @@ -36,7 +39,7 @@ use tokio::{ use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{ Code, Status, - transport::{ClientTlsConfig, Endpoint}, + transport::{Certificate, ClientTlsConfig, Endpoint}, }; use crate::{ @@ -44,11 +47,27 @@ use crate::{ enterprise::firewall::try_get_location_firewall_config, grpc::{ ClientMap, GrpcEvent, TEN_SECS, - gateway::{GrpcRequestContext, events::GatewayEvent, get_peers}, + gateway::{GatewayError, GrpcRequestContext, events::GatewayEvent, get_peers}, }, handlers::mail::send_gateway_disconnected_email, }; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Scheme { + Http, + Https, +} + +impl Scheme { + #[must_use] + pub const fn as_str(&self) -> &str { + match self { + Self::Http => "http", + Self::Https => "https", + } + } +} + fn peer_stats_from_proto(stats: PeerStats, network_id: Id, device_id: Id) -> WireguardPeerStats { let endpoint = match stats.endpoint { endpoint if endpoint.is_empty() => None, @@ -71,7 +90,8 @@ fn peer_stats_from_proto(stats: PeerStats, network_id: Id, device_id: Id) -> Wir /// One instance per connected Gateway. pub(crate) struct GatewayHandler { - endpoint: Endpoint, + // Gateway server endpoint URL. + url: Url, gateway: Gateway, message_id: AtomicU64, pool: PgPool, @@ -84,25 +104,21 @@ pub(crate) struct GatewayHandler { impl GatewayHandler { pub(crate) fn new( gateway: Gateway, - tls_config: Option, pool: PgPool, client_state: Arc>, events_tx: Sender, mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, - ) -> Result { - let endpoint = Endpoint::from_shared(gateway.url.clone())? - .http2_keep_alive_interval(TEN_SECS) - .tcp_keepalive(Some(TEN_SECS)) - .keep_alive_while_idle(true); - let endpoint = if let Some(tls) = tls_config { - endpoint.tls_config(tls)? - } else { - endpoint - }; + ) -> Result { + let url = Url::from_str(&gateway.url).map_err(|err| { + GatewayError::EndpointError(format!( + "Failed to parse Gateway URL {}: {}", + &gateway.url, err + )) + })?; Ok(Self { - endpoint, + url, gateway, message_id: AtomicU64::new(0), pool, @@ -113,33 +129,74 @@ impl GatewayHandler { }) } + pub const fn has_certificate(&self) -> bool { + self.gateway.has_certificate + } + + fn endpoint(&self, scheme: Scheme) -> Result { + let mut url = self.url.clone(); + + if let Err(err) = url.set_scheme(scheme.as_str()) { + return Err(GatewayError::EndpointError(format!( + "Failed to set scheme {} for Gateway URL {:?}", + scheme.as_str(), + self.url + ))); + } + + let endpoint = Endpoint::from_shared(url.to_string()) + .map_err(|err| { + GatewayError::EndpointError(format!( + "Failed to create endpoint for Gateway URL {:?}: {}", + url, err + )) + })? + .http2_keep_alive_interval(TEN_SECS) + .tcp_keepalive(Some(TEN_SECS)) + .keep_alive_while_idle(true); + + if scheme == Scheme::Https { + let settings = Settings::get_current_settings(); + let Some(ca_cert_der) = settings.ca_cert_der else { + return Err(GatewayError::EndpointError( + "Core CA is not setup, can't create a Gateway endpoint.".to_string(), + )); + }; + + let cert_pem = der_to_pem(&ca_cert_der, defguard_certs::PemLabel::Certificate) + .map_err(|err| { + GatewayError::EndpointError(format!( + "Failed to convert CA certificate DER to PEM for Gateway URL {:?}: {}", + url, err + )) + })?; + let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(&cert_pem)); + + Ok(endpoint.tls_config(tls).map_err(|err| { + GatewayError::EndpointError(format!( + "Failed to set TLS config for Gateway URL {:?}: {}", + url, err + )) + })?) + } else { + Ok(endpoint) + } + } + /// Send network and VPN configuration to Gateway. async fn send_configuration( &self, tx: &UnboundedSender, - ) -> Result, Status> { + ) -> Result, GatewayError> { debug!("Sending configuration to Gateway"); let network_id = self.gateway.network_id; - let mut conn = self.pool.acquire().await.map_err(|err| { - error!("Failed to acquire DB connection: {err}"); - Status::new( - Code::Internal, - "Failed to acquire database connection".to_string(), - ) - })?; + let mut conn = self.pool.acquire().await?; let mut network = WireguardNetwork::find_by_id(&mut *conn, network_id) - .await - .map_err(|err| { - error!("Network {network_id} not found"); - Status::new(Code::Internal, format!("Failed to retrieve network: {err}")) - })? + .await? .ok_or_else(|| { - Status::new( - Code::Internal, - format!("Network with id {network_id} not found"), - ) + GatewayError::NotFound(format!("Network with id {network_id} not found")) })?; debug!( @@ -153,23 +210,9 @@ impl GatewayHandler { ); } - let peers = get_peers(&network, &self.pool).await.map_err(|error| { - error!("Failed to fetch peers from the database for network {network_id}: {error}",); - Status::new( - Code::Internal, - format!("Failed to retrieve peers from the database for network: {network_id}"), - ) - })?; + let peers = get_peers(&network, &self.pool).await?; - let maybe_firewall_config = try_get_location_firewall_config(&network, &mut conn) - .await - .map_err(|err| { - error!("Failed to generate firewall config for network {network_id}: {err}"); - Status::new( - Code::Internal, - format!("Failed to generate firewall config for network: {network_id}"), - ) - })?; + let maybe_firewall_config = try_get_location_firewall_config(&network, &mut conn).await?; let payload = Some(core_response::Payload::Config(super::gen_config( &network, peers, @@ -184,10 +227,10 @@ impl GatewayHandler { } Err(err) => { error!("Failed to send configuration sent to {}", self.gateway); - Err(Status::new( - Code::Internal, - format!("Configuration not sent to {}, error {err}", self.gateway), - )) + Err(GatewayError::MessageChannelError(format!( + "Configuration not sent to {}, error {err}", + self.gateway + ))) } } } @@ -241,17 +284,11 @@ impl GatewayHandler { } /// Helper method to fetch `Device` info from DB by pubkey and return appropriate errors - async fn fetch_device_from_db(&self, public_key: &str) -> Result>, Status> { - let device = Device::find_by_pubkey(&self.pool, public_key) - .await - .map_err(|err| { - error!("Failed to retrieve device with public key {public_key}: {err}",); - Status::new( - Code::Internal, - format!("Failed to retrieve device with public key {public_key}: {err}",), - ) - })?; - + async fn fetch_device_from_db( + &self, + public_key: &str, + ) -> Result>, GatewayError> { + let device = Device::find_by_pubkey(&self.pool, public_key).await?; Ok(device) } @@ -259,48 +296,32 @@ impl GatewayHandler { async fn fetch_location_from_db( &self, location_id: Id, - ) -> Result, Status> { - let location = match WireguardNetwork::find_by_id(&self.pool, location_id).await { - Ok(Some(location)) => location, - Ok(None) => { + ) -> Result, GatewayError> { + let location = match WireguardNetwork::find_by_id(&self.pool, location_id).await? { + Some(location) => location, + None => { error!("Location {location_id} not found"); - return Err(Status::new( - Code::Internal, - format!("Location {location_id} not found"), - )); - } - Err(err) => { - error!("Failed to retrieve location {location_id}: {err}",); - return Err(Status::new( - Code::Internal, - format!("Failed to retrieve location {location_id}: {err}",), - )); + return Err(GatewayError::NotFound(format!( + "Location {location_id} not found" + ))); } }; Ok(location) } /// Helper method to fetch `User` info from DB and return appropriate errors - async fn fetch_user_from_db(&self, user_id: Id, public_key: &str) -> Result, Status> { - let user = match User::find_by_id(&self.pool, user_id).await { - Ok(Some(user)) => user, - Ok(None) => { + async fn fetch_user_from_db( + &self, + user_id: Id, + public_key: &str, + ) -> Result, GatewayError> { + let user = match User::find_by_id(&self.pool, user_id).await? { + Some(user) => user, + None => { error!("User {user_id} assigned to device with public key {public_key} not found"); - return Err(Status::new( - Code::Internal, - format!("User assigned to device with public key {public_key} not found"), - )); - } - Err(err) => { - error!( - "Failed to retrieve user {user_id} for device with public key {public_key}: {err}", - ); - return Err(Status::new( - Code::Internal, - format!( - "Failed to retrieve user for device with public key {public_key}: {err}", - ), - )); + return Err(GatewayError::NotFound(format!( + "User assigned to device with public key {public_key} not found" + ))); } }; @@ -313,14 +334,113 @@ impl GatewayHandler { } } + pub(crate) async fn handle_setup(&mut self) -> Result<(), GatewayError> { + debug!("Handling initial setup for Gateway {}", self.gateway); + let endpoint = self.endpoint(Scheme::Http)?; + let uri = endpoint.uri().to_string(); + + let hostname = self + .url + .host_str() + .ok_or_else(|| { + error!("Failed to get hostname from Gateway URL {}", self.url); + GatewayError::EndpointError(format!( + "Failed to get hostname from Gateway URL {}", + self.url + )) + })? + .to_string(); + + #[cfg(not(test))] + let channel = endpoint.connect_lazy(); + #[cfg(test)] + let channel = endpoint.connect_with_connector_lazy(tower::service_fn( + |_: tonic::transport::Uri| async { + Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new( + tokio::net::UnixStream::connect(super::TONIC_SOCKET).await?, + )) + }, + )); + + debug!("Connecting to Gateway {uri}"); + let interceptor = ClientVersionInterceptor::new( + Version::parse(VERSION).expect("failed to parse self version"), + ); + let mut client = + gateway_setup_client::GatewaySetupClient::with_interceptor(channel, interceptor); + + let request = InitialSetupInfo { + cert_hostname: hostname, + }; + + let response = client.start(request).await?; + let response = response.into_inner(); + + let csr = Csr::from_der(&response.der_data)?; + + let settings = Settings::get_current_settings(); + + let ca_cert_der = settings.ca_cert_der.ok_or_else(|| { + GatewayError::ConfigurationError( + "CA certificate DER not found in settings for Gateway setup".to_string(), + ) + })?; + let ca_key_pair = settings.ca_key_der.ok_or_else(|| { + GatewayError::ConfigurationError( + "CA key pairs DER not found in settings for Gateway setup".to_string(), + ) + })?; + + let ca = defguard_certs::CertificateAuthority::from_cert_der_key_pair( + &ca_cert_der, + &ca_key_pair, + )?; + + match ca.sign_csr(&csr) { + Ok(cert) => { + let req = DerPayload { + der_data: cert.der().to_vec(), + }; + + client.send_cert(req).await?; + + let expiry = defguard_certs::get_certificate_expiry(&cert)?; + + self.gateway.has_certificate = true; + self.gateway.certificate_expiry = Some( + chrono::DateTime::from_timestamp(expiry.unix_timestamp(), 0) + .ok_or_else(|| { + GatewayError::ConversionError(format!( + "Failed to convert certificate expiry timestamp {} to DateTime", + expiry.unix_timestamp() + )) + })? + .naive_utc(), + ); + self.gateway.save(&self.pool).await?; + } + Err(err) => { + error!("Failed to sign CSR: {err}"); + } + } + + debug!( + "Saving information about issued certificate to the database for Gateway {}", + self.gateway + ); + + Ok(()) + } + /// Connect to Gateway and handle its messages through gRPC. - pub(crate) async fn handle_connection(&mut self) -> ! { - let uri = self.endpoint.uri(); + pub(crate) async fn handle_connection(&mut self) -> Result<(), GatewayError> { + let endpoint = self.endpoint(Scheme::Https)?; + let uri = endpoint.uri().to_string(); loop { #[cfg(not(test))] - let channel = self.endpoint.connect_lazy(); + let channel = endpoint.connect_lazy(); #[cfg(test)] - let channel = self.endpoint.connect_with_connector_lazy(tower::service_fn( + let channel = endpoint.connect_with_connector_lazy(tower::service_fn( |_: tonic::transport::Uri| async { Ok::<_, std::io::Error>(hyper_util::rt::TokioIo::new( tokio::net::UnixStream::connect(super::TONIC_SOCKET).await?, diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index e9963f89dd..670d1a2d37 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, net::IpAddr, sync::{Arc, Mutex}, + time::Duration, }; use defguard_common::{ @@ -28,7 +29,7 @@ use tokio::{ use tonic::{Code, Status}; use crate::{ - enterprise::is_enterprise_license_active, + enterprise::{firewall::FirewallError, is_enterprise_license_active}, events::{GrpcEvent, GrpcRequestContext}, grpc::gateway::{client_state::ClientMap, events::GatewayEvent, handler::GatewayHandler}, }; @@ -90,15 +91,34 @@ pub fn send_multiple_wireguard_events(events: Vec, wg_tx: &Sender< #[allow(clippy::large_enum_variant)] #[derive(Debug, Error)] -pub enum GatewayServerError { +pub enum GatewayError { #[error("Failed to acquire lock on VPN client state map")] ClientStateMutexError, #[error("gRPC event channel error: {0}")] GrpcEventChannelError(#[from] SendError), + #[error("Endpoint error: {0}")] + EndpointError(String), + #[error("gRPC communication error: {0}")] + GrpcCommunicationError(#[from] tonic::Status), + #[error(transparent)] + CertificateError(#[from] defguard_certs::CertificateError), + #[error("Configuration error: {0}")] + ConfigurationError(String), + #[error("Conversion error: {0}")] + ConversionError(String), + #[error(transparent)] + SqlxError(#[from] sqlx::Error), + #[error("Not found: {0}")] + NotFound(String), + // mpsc channel send/receive error + #[error("Message channel error: {0}")] + MessageChannelError(String), + #[error(transparent)] + FirewallError(#[from] FirewallError), } -impl From for Status { - fn from(value: GatewayServerError) -> Self { +impl From for Status { + fn from(value: GatewayError) -> Self { Self::new(Code::Internal, value.to_string()) } } @@ -198,8 +218,10 @@ fn gen_config( } const GATEWAY_TABLE_TRIGGER: &str = "gateway_change"; +const GATEWAY_SETUP_DELAY: Duration = Duration::from_secs(1); +const GATEWAY_RECONNECT_DELAY: Duration = Duration::from_secs(5); -/// Bi-directional gRPC stream for comminication with Defguard Gateway. +/// Bi-directional gRPC stream for communication with Defguard Gateway. pub async fn run_grpc_gateway_stream( pool: PgPool, client_state: Arc>, @@ -208,28 +230,39 @@ pub async fn run_grpc_gateway_stream( grpc_event_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { let config = server_config(); - let tls_config = config.grpc_client_tls_config()?; - let mut abort_handles = HashMap::new(); let mut tasks = JoinSet::new(); // Helper closure to launch `GatewayHandler`. - let mut launch_gateway_handler = - |gateway: Gateway| -> Result { - let mut gateway_handler = GatewayHandler::new( - gateway, - tls_config.clone(), - pool.clone(), - Arc::clone(&client_state), - events_tx.clone(), - mail_tx.clone(), - grpc_event_tx.clone(), - )?; - let abort_handle = tasks.spawn(async move { - gateway_handler.handle_connection().await; - }); - Ok(abort_handle) - }; + let mut launch_gateway_handler = |gateway: Gateway| -> Result { + let mut gateway_handler = GatewayHandler::new( + gateway, + pool.clone(), + Arc::clone(&client_state), + events_tx.clone(), + mail_tx.clone(), + grpc_event_tx.clone(), + )?; + let abort_handle = tasks.spawn(async move { + loop { + if gateway_handler.has_certificate() { + info!("Gateway has a valid certificate, proceeding to connection"); + } else { + info!("Gateway does not have a valid certificate, proceeding to setup"); + if let Err(err) = gateway_handler.handle_setup().await { + warn!("Gateway setup failed: {err}, will try to connect anyway..."); + } else { + tokio::time::sleep(GATEWAY_SETUP_DELAY).await; + } + } + if let Err(err) = gateway_handler.handle_connection().await { + error!("Gateway connection error: {err}, retrying in 5 seconds..."); + tokio::time::sleep(GATEWAY_RECONNECT_DELAY).await; + } + } + }); + Ok(abort_handle) + }; for gateway in Gateway::all(&pool).await? { let id = gateway.id; diff --git a/migrations/20260113094304_gateway_certificates_management.down.sql b/migrations/20260113094304_gateway_certificates_management.down.sql new file mode 100644 index 0000000000..fa9a529419 --- /dev/null +++ b/migrations/20260113094304_gateway_certificates_management.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE gateway DROP COLUMN has_certificate; +ALTER TABLE gateway DROP COLUMN certificate_expiry; diff --git a/migrations/20260113094304_gateway_certificates_management.up.sql b/migrations/20260113094304_gateway_certificates_management.up.sql new file mode 100644 index 0000000000..aa5825457d --- /dev/null +++ b/migrations/20260113094304_gateway_certificates_management.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE gateway ADD COLUMN has_certificate boolean NOT NULL DEFAULT false; +ALTER TABLE gateway ADD COLUMN certificate_expiry timestamp without time zone NULL; From 3382c67240e85a7b8987cd580aea96ce700ddb68 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:32:05 +0100 Subject: [PATCH 2/7] remove gw tokens, cleanup --- .../defguard_common/src/db/models/device.rs | 32 ++++++------- crates/defguard_common/src/db/models/group.rs | 2 +- .../defguard_common/src/db/models/mfa_info.rs | 5 +- .../src/db/models/oauth2authorizedapp.rs | 3 +- .../src/db/models/oauth2token.rs | 3 +- .../src/db/models/polling_token.rs | 7 +-- .../defguard_common/src/db/models/session.rs | 3 +- crates/defguard_common/src/db/models/user.rs | 24 +++++----- .../defguard_common/src/db/models/webauthn.rs | 3 +- .../src/db/models/wireguard.rs | 2 +- .../src/db/models/wireguard_peer_stats.rs | 3 +- .../defguard_common/src/db/models/yubikey.rs | 3 +- crates/defguard_common/src/types/user_info.rs | 9 ++-- .../src/enterprise/handlers/mod.rs | 1 - .../defguard_core/src/grpc/gateway/handler.rs | 47 ++----------------- crates/defguard_core/src/grpc/gateway/mod.rs | 1 - crates/defguard_core/src/wg_config.rs | 7 ++- .../defguard_proxy_manager/src/enrollment.rs | 27 +++++------ .../src/password_reset.rs | 17 ++++--- 19 files changed, 83 insertions(+), 116 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 2153bea867..a7dad283ba 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -1,19 +1,5 @@ use std::{fmt, net::IpAddr}; -use crate::{ - KEY_LENGTH, - csv::AsCsv, - db::{ - Id, NoId, - models::{ - ModelError, WireguardNetwork, - user::User, - wireguard::{ - LocationMfaMode, NetworkAddressError, ServiceLocationMode, WIREGUARD_MAX_HANDSHAKE, - }, - }, - }, -}; use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::{NaiveDate, NaiveDateTime, Timelike, Utc}; use ipnetwork::IpNetwork; @@ -32,6 +18,21 @@ use thiserror::Error; use tracing::{debug, error, info}; use utoipa::ToSchema; +use crate::{ + KEY_LENGTH, + csv::AsCsv, + db::{ + Id, NoId, + models::{ + ModelError, WireguardNetwork, + user::User, + wireguard::{ + LocationMfaMode, NetworkAddressError, ServiceLocationMode, WIREGUARD_MAX_HANDSHAKE, + }, + }, + }, +}; + #[derive(Serialize, ToSchema)] pub struct DeviceConfig { pub network_id: Id, @@ -1005,9 +1006,8 @@ mod test { use claims::{assert_err, assert_ok}; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - use crate::db::setup_pool; - use super::*; + use crate::db::setup_pool; impl Device { /// Create new device and assign IP in a given network diff --git a/crates/defguard_common/src/db/models/group.rs b/crates/defguard_common/src/db/models/group.rs index 8acd6cab65..e6f65e19f3 100644 --- a/crates/defguard_common/src/db/models/group.rs +++ b/crates/defguard_common/src/db/models/group.rs @@ -160,10 +160,10 @@ impl Group { #[cfg(test)] mod test { - use crate::db::setup_pool; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; + use crate::db::setup_pool; #[sqlx::test] async fn test_group(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_common/src/db/models/mfa_info.rs b/crates/defguard_common/src/db/models/mfa_info.rs index b7925ce97a..07eda69483 100644 --- a/crates/defguard_common/src/db/models/mfa_info.rs +++ b/crates/defguard_common/src/db/models/mfa_info.rs @@ -1,9 +1,10 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{Error as SqlxError, PgPool, query_as}; + use crate::db::{ Id, models::{MFAMethod, user::User}, }; -use serde::{Deserialize, Serialize}; -use sqlx::{Error as SqlxError, PgPool, query_as}; #[derive(Deserialize, Serialize)] pub struct MFAInfo { diff --git a/crates/defguard_common/src/db/models/oauth2authorizedapp.rs b/crates/defguard_common/src/db/models/oauth2authorizedapp.rs index e6f5119abd..421a93437b 100644 --- a/crates/defguard_common/src/db/models/oauth2authorizedapp.rs +++ b/crates/defguard_common/src/db/models/oauth2authorizedapp.rs @@ -1,7 +1,8 @@ -use crate::db::{Id, NoId}; use model_derive::Model; use sqlx::{Error as SqlxError, PgPool, query_as}; +use crate::db::{Id, NoId}; + #[derive(Model)] pub struct OAuth2AuthorizedApp { pub id: I, diff --git a/crates/defguard_common/src/db/models/oauth2token.rs b/crates/defguard_common/src/db/models/oauth2token.rs index 468e83f64e..c7bc50e521 100644 --- a/crates/defguard_common/src/db/models/oauth2token.rs +++ b/crates/defguard_common/src/db/models/oauth2token.rs @@ -1,7 +1,8 @@ -use crate::{config::server_config, db::Id, random::gen_alphanumeric}; use chrono::{TimeDelta, Utc}; use sqlx::{Error as SqlxError, PgPool, query, query_as}; +use crate::{config::server_config, db::Id, random::gen_alphanumeric}; + pub struct OAuth2Token { pub oauth2authorizedapp_id: Id, pub access_token: String, diff --git a/crates/defguard_common/src/db/models/polling_token.rs b/crates/defguard_common/src/db/models/polling_token.rs index f834402ff2..750ec80a80 100644 --- a/crates/defguard_common/src/db/models/polling_token.rs +++ b/crates/defguard_common/src/db/models/polling_token.rs @@ -1,10 +1,11 @@ +use chrono::{NaiveDateTime, Utc}; +use model_derive::Model; +use sqlx::{PgExecutor, query_as}; + use crate::{ db::{Id, NoId}, random::gen_alphanumeric, }; -use chrono::{NaiveDateTime, Utc}; -use model_derive::Model; -use sqlx::{PgExecutor, query_as}; // Token used for polling requests. #[derive(Clone, Debug, Model)] diff --git a/crates/defguard_common/src/db/models/session.rs b/crates/defguard_common/src/db/models/session.rs index e1859844e8..6a8de55ee7 100644 --- a/crates/defguard_common/src/db/models/session.rs +++ b/crates/defguard_common/src/db/models/session.rs @@ -1,8 +1,9 @@ -use crate::{config::server_config, db::Id, random::gen_alphanumeric}; use chrono::{NaiveDateTime, TimeDelta, Utc}; use sqlx::{Error as SqlxError, PgExecutor, PgPool, Type, query, query_as}; use webauthn_rs::prelude::{PasskeyAuthentication, PasskeyRegistration}; +use crate::{config::server_config, db::Id, random::gen_alphanumeric}; + #[derive(Clone, PartialEq, Type)] #[repr(i16)] pub enum SessionState { diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs index 96c266dfef..7557aedd6e 100644 --- a/crates/defguard_common/src/db/models/user.rs +++ b/crates/defguard_common/src/db/models/user.rs @@ -1,14 +1,5 @@ use std::{fmt, time::SystemTime}; -use crate::{ - config::server_config, - db::{ - Id, NoId, - models::{MFAInfo, Session, WebAuthn}, - }, - random::{gen_alphanumeric, gen_totp_secret}, - types::user_info::OAuth2AuthorizedAppInfo, -}; use argon2::{ Argon2, password_hash::{ @@ -36,6 +27,15 @@ use super::{ device::{Device, DeviceType, UserDevice}, group::{Group, Permission}, }; +use crate::{ + config::server_config, + db::{ + Id, NoId, + models::{MFAInfo, Session, WebAuthn}, + }, + random::{gen_alphanumeric, gen_totp_secret}, + types::user_info::OAuth2AuthorizedAppInfo, +}; const RECOVERY_CODES_COUNT: usize = 8; pub const TOTP_CODE_VALIDITY_PERIOD: u64 = 30; @@ -1221,13 +1221,13 @@ impl Distribution> for Standard { #[cfg(test)] mod test { + use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + + use super::*; use crate::{ config::{DefGuardConfig, SERVER_CONFIG}, db::{models::settings::initialize_current_settings, setup_pool}, }; - use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - - use super::*; #[sqlx::test] async fn test_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_common/src/db/models/webauthn.rs b/crates/defguard_common/src/db/models/webauthn.rs index 2861a13b10..2fc9730f6a 100644 --- a/crates/defguard_common/src/db/models/webauthn.rs +++ b/crates/defguard_common/src/db/models/webauthn.rs @@ -1,8 +1,9 @@ -use crate::db::{Id, NoId, models::ModelError}; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query, query_as, query_scalar}; use webauthn_rs::prelude::Passkey; +use crate::db::{Id, NoId, models::ModelError}; + #[derive(Model, Clone, Debug, PartialEq)] pub struct WebAuthn { pub id: I, diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index ee6dd32924..d64c777d8a 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -1182,12 +1182,12 @@ pub async fn networks_stats( mod test { use std::str::FromStr; - use crate::db::setup_pool; use chrono::{SubsecRound, TimeDelta, Utc}; use matches::assert_matches; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; + use crate::db::setup_pool; #[sqlx::test] async fn test_connected_at_reconnection(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_common/src/db/models/wireguard_peer_stats.rs b/crates/defguard_common/src/db/models/wireguard_peer_stats.rs index 902f20028a..099f89229f 100644 --- a/crates/defguard_common/src/db/models/wireguard_peer_stats.rs +++ b/crates/defguard_common/src/db/models/wireguard_peer_stats.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use crate::db::{Id, NoId}; use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc}; use humantime::format_duration; use ipnetwork::IpNetwork; @@ -9,6 +8,8 @@ use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, query, query_as, query_scalar}; use tracing::{debug, info}; +use crate::db::{Id, NoId}; + #[derive(Debug, Deserialize, Model, Serialize)] #[table(wireguard_peer_stats)] pub struct WireguardPeerStats { diff --git a/crates/defguard_common/src/db/models/yubikey.rs b/crates/defguard_common/src/db/models/yubikey.rs index 5eec85d52a..171de03d81 100644 --- a/crates/defguard_common/src/db/models/yubikey.rs +++ b/crates/defguard_common/src/db/models/yubikey.rs @@ -1,8 +1,9 @@ -use crate::db::{Id, NoId}; use model_derive::Model; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, query, query_as}; +use crate::db::{Id, NoId}; + #[derive(Deserialize, Model, Serialize)] pub struct YubiKey { pub id: I, diff --git a/crates/defguard_common/src/types/user_info.rs b/crates/defguard_common/src/types/user_info.rs index 9609d5d005..6716d877a0 100644 --- a/crates/defguard_common/src/types/user_info.rs +++ b/crates/defguard_common/src/types/user_info.rs @@ -1,3 +1,7 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{Error as SqlxError, PgConnection, PgPool}; +use utoipa::ToSchema; + use crate::{ db::{ Id, @@ -5,9 +9,6 @@ use crate::{ }, types::group_diff::GroupDiff, }; -use serde::{Deserialize, Serialize}; -use sqlx::{Error as SqlxError, PgConnection, PgPool}; -use utoipa::ToSchema; #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct OAuth2AuthorizedAppInfo { @@ -146,10 +147,10 @@ impl UserInfo { #[cfg(test)] mod test { - use crate::db::setup_pool; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; + use crate::db::setup_pool; #[sqlx::test] async fn test_user_info(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/enterprise/handlers/mod.rs b/crates/defguard_core/src/enterprise/handlers/mod.rs index 084b1a117f..781eded1b3 100644 --- a/crates/defguard_core/src/enterprise/handlers/mod.rs +++ b/crates/defguard_core/src/enterprise/handlers/mod.rs @@ -15,7 +15,6 @@ use axum::{ extract::{FromRef, FromRequestParts}, http::{StatusCode, request::Parts}, }; - use serde::Serialize; use super::{ diff --git a/crates/defguard_core/src/grpc/gateway/handler.rs b/crates/defguard_core/src/grpc/gateway/handler.rs index 747b161e03..0afcfca7bd 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -11,7 +11,6 @@ use chrono::{DateTime, TimeDelta, Utc}; use defguard_certs::{Csr, der_to_pem}; use defguard_common::{ VERSION, - auth::claims::Claims, db::{ Id, NoId, models::{ @@ -37,13 +36,9 @@ use tokio::{ time::sleep, }; use tokio_stream::wrappers::UnboundedReceiverStream; -use tonic::{ - Code, Status, - transport::{Certificate, ClientTlsConfig, Endpoint}, -}; +use tonic::transport::{Certificate, ClientTlsConfig, Endpoint}; use crate::{ - ClaimsType, enterprise::firewall::try_get_location_firewall_config, grpc::{ ClientMap, GrpcEvent, TEN_SECS, @@ -136,7 +131,7 @@ impl GatewayHandler { fn endpoint(&self, scheme: Scheme) -> Result { let mut url = self.url.clone(); - if let Err(err) = url.set_scheme(scheme.as_str()) { + if let Err(()) = url.set_scheme(scheme.as_str()) { return Err(GatewayError::EndpointError(format!( "Failed to set scheme {} for Gateway URL {:?}", scheme.as_str(), @@ -147,8 +142,7 @@ impl GatewayHandler { let endpoint = Endpoint::from_shared(url.to_string()) .map_err(|err| { GatewayError::EndpointError(format!( - "Failed to create endpoint for Gateway URL {:?}: {}", - url, err + "Failed to create endpoint for Gateway URL {url:?}: {err}", )) })? .http2_keep_alive_interval(TEN_SECS) @@ -166,16 +160,14 @@ impl GatewayHandler { let cert_pem = der_to_pem(&ca_cert_der, defguard_certs::PemLabel::Certificate) .map_err(|err| { GatewayError::EndpointError(format!( - "Failed to convert CA certificate DER to PEM for Gateway URL {:?}: {}", - url, err + "Failed to convert CA certificate DER to PEM for Gateway URL {url:?}: {err}", )) })?; let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(&cert_pem)); Ok(endpoint.tls_config(tls).map_err(|err| { GatewayError::EndpointError(format!( - "Failed to set TLS config for Gateway URL {:?}: {}", - url, err + "Failed to set TLS config for Gateway URL {url:?}: {err}", )) })?) } else { @@ -486,35 +478,6 @@ impl GatewayHandler { ); continue; } - // Validate authorization token. - if let Ok(claims) = Claims::from_jwt( - ClaimsType::Gateway, - &config_request.auth_token, - ) { - if let Ok(client_id) = Id::from_str(&claims.client_id) { - if client_id == self.gateway.network_id { - debug!( - "Authorization token is correct for {}", - self.gateway - ); - } else { - warn!( - "Authorization token received from {uri} has \ - `client_id` for a different network" - ); - continue; - } - } else { - warn!( - "Authorization token received from {uri} has incorrect \ - `client_id`" - ); - continue; - } - } else { - warn!("Invalid authorization token received from {uri}"); - continue; - } // Send network configuration to Gateway. match self.send_configuration(&tx).await { diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 670d1a2d37..d734941b8d 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -229,7 +229,6 @@ pub async fn run_grpc_gateway_stream( mail_tx: UnboundedSender, grpc_event_tx: UnboundedSender, ) -> Result<(), anyhow::Error> { - let config = server_config(); let mut abort_handles = HashMap::new(); let mut tasks = JoinSet::new(); diff --git a/crates/defguard_core/src/wg_config.rs b/crates/defguard_core/src/wg_config.rs index 33de00fc8f..429d841d53 100644 --- a/crates/defguard_core/src/wg_config.rs +++ b/crates/defguard_core/src/wg_config.rs @@ -1,10 +1,6 @@ use std::net::IpAddr; use base64::{DecodeError, Engine, prelude::BASE64_STANDARD}; -use ipnetwork::{IpNetwork, IpNetworkError}; -use thiserror::Error; -use x25519_dalek::{PublicKey, StaticSecret}; - use defguard_common::{ KEY_LENGTH, db::models::{ @@ -15,6 +11,9 @@ use defguard_common::{ }, }, }; +use ipnetwork::{IpNetwork, IpNetworkError}; +use thiserror::Error; +use x25519_dalek::{PublicKey, StaticSecret}; #[derive(Clone, Deserialize, Serialize)] pub struct ImportedDevice { diff --git a/crates/defguard_proxy_manager/src/enrollment.rs b/crates/defguard_proxy_manager/src/enrollment.rs index e0358ce9cf..16aea97128 100644 --- a/crates/defguard_proxy_manager/src/enrollment.rs +++ b/crates/defguard_proxy_manager/src/enrollment.rs @@ -12,20 +12,6 @@ use defguard_common::{ }, }, }; -use defguard_mail::{Mail, templates::TemplateLocation}; -use defguard_proto::proxy::{ - ActivateUserRequest, AdminInfo, CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, - CodeMfaSetupStartRequest, CodeMfaSetupStartResponse, DeviceConfigResponse, - EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, InitialUserInfo, MfaMethod, - NewDevice, RegisterMobileAuthRequest, -}; -use sqlx::{PgPool, query_scalar}; -use tokio::sync::{ - broadcast::Sender, - mpsc::{UnboundedSender, error::SendError}, -}; -use tonic::Status; - use defguard_core::{ db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}, enterprise::{ @@ -50,6 +36,19 @@ use defguard_core::{ headers::get_device_info, is_valid_phone_number, }; +use defguard_mail::{Mail, templates::TemplateLocation}; +use defguard_proto::proxy::{ + ActivateUserRequest, AdminInfo, CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, + CodeMfaSetupStartRequest, CodeMfaSetupStartResponse, DeviceConfigResponse, + EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, InitialUserInfo, MfaMethod, + NewDevice, RegisterMobileAuthRequest, +}; +use sqlx::{PgPool, query_scalar}; +use tokio::sync::{ + broadcast::Sender, + mpsc::{UnboundedSender, error::SendError}, +}; +use tonic::Status; pub(super) struct EnrollmentServer { pool: PgPool, diff --git a/crates/defguard_proxy_manager/src/password_reset.rs b/crates/defguard_proxy_manager/src/password_reset.rs index 3c27dbd384..208b3e5260 100644 --- a/crates/defguard_proxy_manager/src/password_reset.rs +++ b/crates/defguard_proxy_manager/src/password_reset.rs @@ -1,13 +1,4 @@ use defguard_common::{config::server_config, db::models::User}; -use defguard_mail::Mail; -use defguard_proto::proxy::{ - DeviceInfo, PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, - PasswordResetStartResponse, -}; -use sqlx::PgPool; -use tokio::sync::mpsc::{UnboundedSender, error::SendError}; -use tonic::Status; - use defguard_core::{ db::models::enrollment::{PASSWORD_RESET_TOKEN_TYPE, Token}, enterprise::ldap::utils::ldap_change_password, @@ -19,6 +10,14 @@ use defguard_core::{ }, headers::get_device_info, }; +use defguard_mail::Mail; +use defguard_proto::proxy::{ + DeviceInfo, PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, + PasswordResetStartResponse, +}; +use sqlx::PgPool; +use tokio::sync::mpsc::{UnboundedSender, error::SendError}; +use tonic::Status; pub(super) struct PasswordResetServer { pool: PgPool, From 5f51eb685cc46cab021fbfae1d82e6797a545a4c Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:53:49 +0100 Subject: [PATCH 3/7] cleanup --- crates/defguard_core/src/grpc/gateway/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index d734941b8d..45bbe078e0 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -5,12 +5,9 @@ use std::{ time::Duration, }; -use defguard_common::{ - config::server_config, - db::{ - ChangeNotification, Id, TriggerOperation, - models::{WireguardNetwork, gateway::Gateway, wireguard::ServiceLocationMode}, - }, +use defguard_common::db::{ + ChangeNotification, Id, TriggerOperation, + models::{WireguardNetwork, gateway::Gateway, wireguard::ServiceLocationMode}, }; use defguard_mail::Mail; use defguard_proto::{ From 2f8a147e27c52d04daab18c09dea9cfc02e1cd63 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:02:32 +0100 Subject: [PATCH 4/7] clippy --- crates/defguard_certs/src/lib.rs | 4 ++-- crates/defguard_core/src/grpc/gateway/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 444e2dcf0f..bf0b009043 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -273,12 +273,12 @@ mod tests { .unwrap(); let signed_cert: Certificate = ca.sign_csr_with_validity(&csr, 90).unwrap(); let der = signed_cert.der(); - let (_rem, parsed) = parse_x509_certificate(&der).unwrap(); + let (_rem, parsed) = parse_x509_certificate(der).unwrap(); let validity = parsed.tbs_certificate.validity; let not_before = validity.not_before.to_datetime(); let not_after = validity.not_after.to_datetime(); let days = (not_after - not_before).whole_days(); - assert!(days >= 89 && days <= 91, "expected 89-91 days, got {days}"); + assert!((89..=91).contains(&days), "expected 89-91 days, got {days}"); assert!(not_after > not_before); } diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 45bbe078e0..0f4f317536 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -242,7 +242,7 @@ pub async fn run_grpc_gateway_stream( let abort_handle = tasks.spawn(async move { loop { if gateway_handler.has_certificate() { - info!("Gateway has a valid certificate, proceeding to connection"); + info!("A certificate was already issued for Gateway, proceeding to connection"); } else { info!("Gateway does not have a valid certificate, proceeding to setup"); if let Err(err) = gateway_handler.handle_setup().await { From 85ab322bc76fbad59b9cd68c14a326f97d1a9774 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:07:25 +0100 Subject: [PATCH 5/7] sqlx prepare --- ...6f56f89123c84fc2351ca92ab1e17525c1097ef.json} | 6 ++++-- ...73b1033ff78a115ae0a3f4882c28b3becb3d0a5.json} | 6 ++++-- ...94ce3d23b6d3b80d820d022502916dd8adc0262.json} | 16 ++++++++++++++-- ...fa8c23bff66bc40eafba7400a7d7db49ab36e2e.json} | 16 ++++++++++++++-- ...bddf997807b66e0b532da747b146513c34e15c5c.json | 12 ++++++++++++ 5 files changed, 48 insertions(+), 8 deletions(-) rename .sqlx/{query-dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4.json => query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json} (66%) rename .sqlx/{query-5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e.json => query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json} (61%) rename .sqlx/{query-0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554.json => query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json} (67%) rename .sqlx/{query-0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d.json => query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json} (67%) diff --git a/.sqlx/query-dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4.json b/.sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json similarity index 66% rename from .sqlx/query-dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4.json rename to .sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json index f5f9307d7d..b52e770941 100644 --- a/.sqlx/query-dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4.json +++ b/.sqlx/query-5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"gateway\" SET \"network_id\" = $2,\"url\" = $3,\"hostname\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6 WHERE id = $1", + "query": "UPDATE \"gateway\" SET \"network_id\" = $2,\"url\" = $3,\"hostname\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6,\"has_certificate\" = $7,\"certificate_expiry\" = $8 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -10,10 +10,12 @@ "Text", "Text", "Timestamp", + "Timestamp", + "Bool", "Timestamp" ] }, "nullable": [] }, - "hash": "dbcf7be91c6a4c92e865e35d565cf3da8e12f7179da8cb42fa0446b224f9dec4" + "hash": "5acdf80bf0c44933e3d74e2c86f56f89123c84fc2351ca92ab1e17525c1097ef" } diff --git a/.sqlx/query-5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e.json b/.sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json similarity index 61% rename from .sqlx/query-5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e.json rename to .sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json index 58a9bd507f..b45febc008 100644 --- a/.sqlx/query-5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e.json +++ b/.sqlx/query-95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\") VALUES ($1,$2,$3,$4,$5) RETURNING id", + "query": "INSERT INTO \"gateway\" (\"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id", "describe": { "columns": [ { @@ -15,6 +15,8 @@ "Text", "Text", "Timestamp", + "Timestamp", + "Bool", "Timestamp" ] }, @@ -22,5 +24,5 @@ false ] }, - "hash": "5d00c03eccbe17efc023bd0c6006e24f24239df0ba778666ca1751c9436aec8e" + "hash": "95cc72beeb57a84ed791fcc6873b1033ff78a115ae0a3f4882c28b3becb3d0a5" } diff --git a/.sqlx/query-0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554.json b/.sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json similarity index 67% rename from .sqlx/query-0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554.json rename to .sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json index 8afd59ce08..ed24b8d62a 100644 --- a/.sqlx/query-0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554.json +++ b/.sqlx/query-9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\" FROM \"gateway\" WHERE id = $1", + "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\" FROM \"gateway\" WHERE id = $1", "describe": { "columns": [ { @@ -32,6 +32,16 @@ "ordinal": 5, "name": "disconnected_at", "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "certificate_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -45,8 +55,10 @@ false, true, true, + true, + false, true ] }, - "hash": "0648b5c0e4d4a4cd922ccaef80e06ff43f8ff063a6772586b79f1c524a848554" + "hash": "9c70a4374828099fa19032f4394ce3d23b6d3b80d820d022502916dd8adc0262" } diff --git a/.sqlx/query-0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d.json b/.sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json similarity index 67% rename from .sqlx/query-0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d.json rename to .sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json index 9476d09d17..c321d2e9e7 100644 --- a/.sqlx/query-0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d.json +++ b/.sqlx/query-a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\" FROM \"gateway\"", + "query": "SELECT id, \"network_id\",\"url\",\"hostname\",\"connected_at\",\"disconnected_at\",\"has_certificate\",\"certificate_expiry\" FROM \"gateway\"", "describe": { "columns": [ { @@ -32,6 +32,16 @@ "ordinal": 5, "name": "disconnected_at", "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "certificate_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -43,8 +53,10 @@ false, true, true, + true, + false, true ] }, - "hash": "0cd356bb88839bcc76d1fe3ed26719811a7c80e0954d6024f00b5ef883aeab3d" + "hash": "a0818064c80739debc8a8788dfa8c23bff66bc40eafba7400a7d7db49ab36e2e" } diff --git a/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json b/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json index 6fa0952987..6b597e4488 100644 --- a/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json +++ b/.sqlx/query-d10c9a7b0b391aeb8b4869f6bddf997807b66e0b532da747b146513c34e15c5c.json @@ -32,6 +32,16 @@ "ordinal": 5, "name": "disconnected_at", "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "has_certificate", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "certificate_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -45,6 +55,8 @@ false, true, true, + true, + false, true ] }, From e5e73ee31e71b9c8bce2de5c380952d0e7cfc772 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:16:56 +0100 Subject: [PATCH 6/7] consts --- crates/defguard_certs/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index bf0b009043..09ca0ea91a 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -10,6 +10,8 @@ use x509_parser::parse_x509_certificate; const CA_NAME: &str = "Defguard CA"; const CA_ORG: &str = "Defguard"; +const NOT_BEFORE_OFFSET_SECS: Duration = Duration::minutes(5); +const DEFAULT_CERT_VALIDITY_DAYS: i64 = 365; #[derive(Debug, Error)] pub enum CertificateError { @@ -77,7 +79,7 @@ impl CertificateAuthority<'_> { pub fn sign_csr(&self, csr: &Csr) -> Result { // TODO: make validity configurable? - self.sign_csr_with_validity(csr, 360) + self.sign_csr_with_validity(csr, DEFAULT_CERT_VALIDITY_DAYS) } /// Sign CSR with explicit validity in days. @@ -89,7 +91,7 @@ impl CertificateAuthority<'_> { let mut csr_params = csr.params()?; let now = OffsetDateTime::now_utc(); - let not_before = now - Duration::minutes(5); + let not_before = now - NOT_BEFORE_OFFSET_SECS; let not_after = now + Duration::days(days_valid); csr_params.params.not_before = not_before; From f87c198a2ec81c9e03b206a19fc3cce102869db5 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:26:03 +0100 Subject: [PATCH 7/7] update protobufs --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index c48340f72b..161c6c6776 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit c48340f72b9de3a69cf71318c75ff1361ebd7897 +Subproject commit 161c6c677662130924e8bac0c16421b8ed085d33