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 f5f9307d7..b52e77094 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 58a9bd507..b45febc00 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 8afd59ce0..ed24b8d62 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 9476d09d1..c321d2e9e 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 6fa095298..6b597e448 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 ] }, diff --git a/Cargo.lock b/Cargo.lock index ce1a729fa..eea5159b4 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 8ea34cbeb..b769b0f9c 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 574505b21..09ca0ea91 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -1,13 +1,17 @@ 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"; +const NOT_BEFORE_OFFSET_SECS: Duration = Duration::minutes(5); +const DEFAULT_CERT_VALIDITY_DAYS: i64 = 365; #[derive(Debug, Error)] pub enum CertificateError { @@ -59,7 +63,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 +78,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, DEFAULT_CERT_VALIDITY_DAYS) + } + + /// 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 - NOT_BEFORE_OFFSET_SECS; + 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 +125,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 +247,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 +261,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!((89..=91).contains(&days), "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/device.rs b/crates/defguard_common/src/db/models/device.rs index 2153bea86..a7dad283b 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/gateway.rs b/crates/defguard_common/src/db/models/gateway.rs index bab5cff05..f7f25189d 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_common/src/db/models/group.rs b/crates/defguard_common/src/db/models/group.rs index 8acd6cab6..e6f65e19f 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 b7925ce97..07eda6948 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 e6f5119ab..421a93437 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 468e83f64..c7bc50e52 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 f834402ff..750ec80a8 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 e1859844e..6a8de55ee 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 96c266dfe..7557aedd6 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 2861a13b1..2fc9730f6 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 ee6dd3292..d64c777d8 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 902f20028..099f89229 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 5eec85d52..171de03d8 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 9609d5d00..6716d877a 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 084b1a117..781eded1b 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 51995484f..0afcfca7b 100644 --- a/crates/defguard_core/src/grpc/gateway/handler.rs +++ b/crates/defguard_core/src/grpc/gateway/handler.rs @@ -8,22 +8,24 @@ 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::{ @@ -34,21 +36,33 @@ use tokio::{ time::sleep, }; use tokio_stream::wrappers::UnboundedReceiverStream; -use tonic::{ - Code, Status, - transport::{ClientTlsConfig, Endpoint}, -}; +use tonic::transport::{Certificate, ClientTlsConfig, Endpoint}; use crate::{ - ClaimsType, 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 +85,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 +99,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 +124,71 @@ 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(()) = 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 +202,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 +219,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 +276,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 +288,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 +326,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?, @@ -366,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 e9963f89d..0f4f31753 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -2,14 +2,12 @@ use std::{ collections::HashMap, net::IpAddr, sync::{Arc, Mutex}, + 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::{ @@ -28,7 +26,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 +88,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 +215,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>, @@ -207,29 +226,39 @@ pub async fn run_grpc_gateway_stream( mail_tx: UnboundedSender, 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!("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 { + 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/crates/defguard_core/src/wg_config.rs b/crates/defguard_core/src/wg_config.rs index 33de00fc8..429d841d5 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 e0358ce9c..16aea9712 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 3c27dbd38..208b3e526 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, diff --git a/migrations/20260113094304_gateway_certificates_management.down.sql b/migrations/20260113094304_gateway_certificates_management.down.sql new file mode 100644 index 000000000..fa9a52941 --- /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 000000000..aa5825457 --- /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; diff --git a/proto b/proto index c48340f72..161c6c677 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit c48340f72b9de3a69cf71318c75ff1361ebd7897 +Subproject commit 161c6c677662130924e8bac0c16421b8ed085d33