From 61f5a8ec424bd0a860c8552449ddb911dd65e0f4 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:53:39 +0100 Subject: [PATCH 01/17] ca generating testing --- Cargo.lock | 121 ++++++++++++++++++++-- Cargo.toml | 3 + crates/defguard_certs/Cargo.toml | 15 +++ crates/defguard_certs/src/main.rs | 165 ++++++++++++++++++++++++++++++ 4 files changed, 295 insertions(+), 9 deletions(-) create mode 100644 crates/defguard_certs/Cargo.toml create mode 100644 crates/defguard_certs/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index a0722d94d..b0e89d242 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,7 +195,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ - "asn1-rs-derive", + "asn1-rs-derive 0.5.1", "asn1-rs-impl", "displaydoc", "nom 7.1.3", @@ -205,6 +205,22 @@ dependencies = [ "time", ] +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive 0.6.0", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + [[package]] name = "asn1-rs-derive" version = "0.5.1" @@ -217,6 +233,18 @@ dependencies = [ "synstructure", ] +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "asn1-rs-impl" version = "0.2.0" @@ -1105,6 +1133,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "defguard_certs" +version = "0.0.0" +dependencies = [ + "rcgen", + "rustls-pki-types", + "serde", + "sqlx", + "thiserror 2.0.17", +] + [[package]] name = "defguard_common" version = "1.6.0" @@ -1313,7 +1352,21 @@ version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ - "asn1-rs", + "asn1-rs 0.6.2", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs 0.7.1", "displaydoc", "nom 7.1.3", "num-bigint", @@ -2591,7 +2644,7 @@ dependencies = [ "serde", "serde_json", "thiserror 1.0.69", - "yasna", + "yasna 0.4.0", "zeroize", ] @@ -3326,7 +3379,16 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" dependencies = [ - "asn1-rs", + "asn1-rs 0.6.2", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs 0.7.1", ] [[package]] @@ -4152,6 +4214,20 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rcgen" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser 0.18.0", + "yasna 0.5.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -6349,7 +6425,7 @@ checksum = "19f1d80f3146382529fe70a3ab5d0feb2413a015204ed7843f9377cd39357fc4" dependencies = [ "base64 0.21.7", "base64urlsafedata", - "der-parser", + "der-parser 9.0.0", "hex", "nom 7.1.3", "openssl", @@ -6365,7 +6441,7 @@ dependencies = [ "uuid", "webauthn-attestation-ca", "webauthn-rs-proto", - "x509-parser", + "x509-parser 0.16.0", ] [[package]] @@ -6764,17 +6840,35 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" dependencies = [ - "asn1-rs", + "asn1-rs 0.6.2", "data-encoding", - "der-parser", + "der-parser 9.0.0", "lazy_static", "nom 7.1.3", - "oid-registry", + "oid-registry 0.7.1", "rusticata-macros", "thiserror 1.0.69", "time", ] +[[package]] +name = "x509-parser" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" +dependencies = [ + "asn1-rs 0.7.1", + "data-encoding", + "der-parser 10.0.0", + "lazy_static", + "nom 7.1.3", + "oid-registry 0.8.1", + "ring", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + [[package]] name = "yasna" version = "0.4.0" @@ -6784,6 +6878,15 @@ dependencies = [ "num-bigint", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 673c673cc..e64a51ee7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ defguard_mail = { path = "./crates/defguard_mail", version = "0.0.0" } defguard_proto = { path = "./crates/defguard_proto", version = "0.0.0" } defguard_version = { path = "./crates/defguard_version", version = "0.0.0" } defguard_web_ui = { path = "./crates/defguard_web_ui", version = "0.0.0" } +defguard_certs = { path = "./crates/defguard_certs", version = "0.0.0" } model_derive = { path = "./crates/model_derive", version = "0.0.0" } # external dependencies @@ -120,6 +121,8 @@ webauthn-rs = { version = "0.5", features = [ ] } webauthn-rs-proto = "0.5" x25519-dalek = { version = "2.0", features = ["static_secrets"] } +rcgen = { version = "0.14", features = ["x509-parser", "pem"] } +rustls-pki-types = "1.13" [profile.release] codegen-units = 1 diff --git a/crates/defguard_certs/Cargo.toml b/crates/defguard_certs/Cargo.toml new file mode 100644 index 000000000..072e8a4f1 --- /dev/null +++ b/crates/defguard_certs/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "defguard_certs" +version = "0.0.0" +edition.workspace = true +license-file.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +rcgen.workspace = true +serde.workspace = true +sqlx.workspace = true +thiserror.workspace = true +rustls-pki-types.workspace = true diff --git a/crates/defguard_certs/src/main.rs b/crates/defguard_certs/src/main.rs new file mode 100644 index 000000000..b05377b6b --- /dev/null +++ b/crates/defguard_certs/src/main.rs @@ -0,0 +1,165 @@ +use rcgen::{ + BasicConstraints, Certificate, CertificateParams, CertificateSigningRequest, + CertificateSigningRequestParams, IsCa, Issuer, KeyPair, PublicKey, RcgenError, +}; +use rustls_pki_types::{CertificateDer, pem::PemObject}; +use thiserror::Error; + +const CA_NAME: &str = "DefGuard CA"; +const CA_ORG: &str = "DefGuard"; + +#[derive(Debug, Error)] +pub enum CAError { + #[error("Certificate generation error: {0}")] + RCGenError(#[from] rcgen::Error), + #[error("PEM error: {0}")] + PemParsingError(String), +} + +pub struct CertificateAuthority<'a> { + issuer: Issuer<'a, KeyPair>, + cert_pem: String, +} + +impl CertificateAuthority<'_> { + pub fn from_ca_cert_pem(ca_cert_pem: &str, ca_key_pair: &str) -> Result { + let key_pair = KeyPair::from_pem(ca_key_pair)?; + let cert_der = CertificateDer::from_pem_slice(ca_cert_pem.as_bytes()) + .map_err(|e| CAError::PemParsingError(e.to_string()))?; + let issuer = Issuer::from_ca_cert_der(&cert_der, key_pair)?; + Ok(CertificateAuthority { + issuer, + cert_pem: ca_cert_pem.to_string(), + }) + } + + pub fn from_key_cert_params( + key_pair: KeyPair, + ca_cert_params: CertificateParams, + ) -> Result { + let cert = ca_cert_params.self_signed(&key_pair)?; + let issuer = Issuer::new(ca_cert_params, key_pair); + Ok(CertificateAuthority { + issuer, + cert_pem: cert.pem(), + }) + } + + pub fn new() -> Result { + let mut ca_params = CertificateParams::new(vec![CA_NAME.to_string()])?; + + ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params + .distinguished_name + .push(rcgen::DnType::OrganizationName, CA_ORG); + ca_params + .distinguished_name + .push(rcgen::DnType::CommonName, CA_NAME); + + let ca_key_pair = KeyPair::generate()?; + + CertificateAuthority::from_key_cert_params(ca_key_pair, ca_params) + } + + pub fn sign_csr(&self, csr: &CertificateSigningRequestParams) -> Result { + let cert = csr.signed_by(&self.issuer)?; + Ok(cert) + } + + pub fn save(&self) {} +} + +fn main() { + let ca = CertificateAuthority::new().unwrap(); + + let key = ca.issuer.key(); + + let pem = ca.cert_pem; + + let ca = CertificateAuthority::from_ca_cert_pem(&pem, &key.serialize_pem()).unwrap(); + + println!("Loaded CA: {:?}", ca.cert_pem); + // let der_key = CertificateDer::from_slice(&pem); + + // let mut cert_params = CertificateParams::new(vec![ + // "example.com".to_string(), + // "www.example.com".to_string(), + // ]) + // .unwrap(); + + // cert_params + // .distinguished_name + // .push(rcgen::DnType::CommonName, "example.com"); + // cert_params + // .distinguished_name + // .push(rcgen::DnType::OrganizationName, "Example Org"); + + // let cert_key_pair = KeyPair::generate().unwrap(); + + // let cert = cert_params.serialize_request(&cert_key_pair).unwrap(); + + // let pem = cert.pem().unwrap(); + + // let csr = CertificateSigningRequestParams::from_pem(&pem).unwrap(); + + // let cert = ca.sign_csr(&csr).unwrap(); + + // println!("CSR PEM:\n{:?}", cert); + + // let mut ca_params = CertificateParams::new(vec!["My CA".to_string()]).unwrap(); + + // // Configure as a Certificate Authority + // ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + // ca_params + // .distinguished_name + // .push(rcgen::DnType::OrganizationName, "My Organization"); + // ca_params + // .distinguished_name + // .push(rcgen::DnType::CommonName, "My Root CA"); + + // // Generate key pair for CA + // let ca_key_pair = KeyPair::generate().unwrap(); + + // // Create the CA certificate + // let ca_cert = ca_params.self_signed(&ca_key_pair).unwrap(); + + // println!("=== CA Certificate ==="); + // println!("{}", ca_cert.pem()); + // println!("\n=== CA Private Key ==="); + // println!("{}", ca_key_pair.serialize_pem()); + + // let test_ca_string = ca_cert.pem(); + + // let ca = Issuer::from_ca_cert_pem(&test_ca_string, &ca_key_pair).unwrap(); + // println!("Loaded CA: {:?}", ca); + + // let ca_cert_pem = ca_cert.pem(); + // std::fs::write("ca_certificate.pem", &ca_cert_pem) + // .expect("Failed to write CA certificate to file"); + + // let mut cert_params = CertificateParams::new(vec![ + // "example.com".to_string(), + // "www.example.com".to_string(), + // ]) + // .unwrap(); + + // cert_params + // .distinguished_name + // .push(rcgen::DnType::CommonName, "example.com"); + // cert_params + // .distinguished_name + // .push(rcgen::DnType::OrganizationName, "Example Org"); + + // let cert_key_pair = KeyPair::generate().unwrap(); + + // let issuer = Issuer::from_params(&ca_params, &ca_key_pair); + + // // Sign the certificate with the CA + // // let signed_cert = cert_params.signed_by(&cert_key_pair, &issuer).unwrap(); + // let signed_cert = cert_params.self_signed(&cert_key_pair).unwrap(); + + // println!("\n=== Signed Certificate ==="); + // println!("{}", signed_cert.pem()); + // println!("\n=== Certificate Private Key ==="); + // println!("{}", cert_key_pair.serialize_pem()); +} From 08a51556b8a8979bbb67952bcd347f248360f946 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 19 Dec 2025 10:07:55 +0100 Subject: [PATCH 02/17] certificate handshake --- Cargo.lock | 3 + crates/defguard/Cargo.toml | 1 + crates/defguard/src/main.rs | 18 +- crates/defguard_certs/Cargo.toml | 1 + crates/defguard_certs/src/lib.rs | 261 ++++++++++++++++++ crates/defguard_certs/src/main.rs | 165 ----------- .../defguard_common/src/db/models/settings.rs | 11 +- crates/defguard_core/Cargo.toml | 1 + crates/defguard_core/src/grpc/mod.rs | 165 +++++++++-- migrations/20251218140442_core_ca.down.sql | 2 + migrations/20251218140442_core_ca.up.sql | 2 + 11 files changed, 444 insertions(+), 186 deletions(-) create mode 100644 crates/defguard_certs/src/lib.rs delete mode 100644 crates/defguard_certs/src/main.rs create mode 100644 migrations/20251218140442_core_ca.down.sql create mode 100644 migrations/20251218140442_core_ca.up.sql diff --git a/Cargo.lock b/Cargo.lock index fc70460ca..82140ccdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,6 +1121,7 @@ version = "0.0.0" dependencies = [ "anyhow", "bytes", + "defguard_certs", "defguard_common", "defguard_core", "defguard_event_logger", @@ -1138,6 +1139,7 @@ dependencies = [ name = "defguard_certs" version = "0.0.0" dependencies = [ + "base64 0.22.1", "rcgen", "rustls-pki-types", "serde", @@ -1197,6 +1199,7 @@ dependencies = [ "bytes", "chrono", "claims", + "defguard_certs", "defguard_common", "defguard_mail", "defguard_proto", diff --git a/crates/defguard/Cargo.toml b/crates/defguard/Cargo.toml index 0dc170ff5..964fb2651 100644 --- a/crates/defguard/Cargo.toml +++ b/crates/defguard/Cargo.toml @@ -16,6 +16,7 @@ defguard_event_logger = { workspace = true } defguard_mail = { workspace = true } defguard_session_manager = { workspace = true } defguard_version = { workspace = true } +defguard_certs = { workspace = true } # external dependencies anyhow = { workspace = true } diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 087ac62bd..3ab187b55 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -12,7 +12,7 @@ use defguard_common::{ models::{ Settings, User, - settings::initialize_current_settings, + settings::{initialize_current_settings, update_current_settings}, // wireguard_peer_stats::WireguardPeerStats, }, }, @@ -128,6 +128,22 @@ async fn main() -> Result<(), anyhow::Error> { // initialize global settings struct initialize_current_settings(&pool).await?; + let mut settings = Settings::get_current_settings(); + if settings.ca_cert_der.is_none() || settings.ca_key_der.is_none() { + warn!( + "No gRPC TLS certificate or key found in settings, generating self-signed certificate for gRPC server." + ); + + let ca = defguard_certs::CertificateAuthority::new()?; + + let (cert_der, key_der) = (ca.cert_der().to_vec(), ca.key_pair_der().to_vec()); + + settings.ca_cert_der = Some(cert_der); + settings.ca_key_der = Some(key_der); + + update_current_settings(&pool, settings).await?; + } + // read grpc TLS cert and key let grpc_cert = config .grpc_cert diff --git a/crates/defguard_certs/Cargo.toml b/crates/defguard_certs/Cargo.toml index 072e8a4f1..8ea34cbeb 100644 --- a/crates/defguard_certs/Cargo.toml +++ b/crates/defguard_certs/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] +base64.workspace = true rcgen.workspace = true serde.workspace = true sqlx.workspace = true diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs new file mode 100644 index 000000000..07dc7bd22 --- /dev/null +++ b/crates/defguard_certs/src/lib.rs @@ -0,0 +1,261 @@ +use std::path::Path; + +use base64::{Engine, prelude::BASE64_STANDARD}; +use rcgen::{ + BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, IsCa, + Issuer, KeyPair, SigningKey, +}; +use rustls_pki_types::{CertificateDer, CertificateSigningRequestDer, pem::PemObject}; +use thiserror::Error; + +const CA_NAME: &str = "Defguard CA"; +const CA_ORG: &str = "Defguard"; + +#[derive(Debug, Error)] +pub enum CertificateError { + #[error("Certificate generation error: {0}")] + RCGenError(#[from] rcgen::Error), + #[error("Failed to parse: {0}")] + ParsingError(String), + #[error(transparent)] + IoError(#[from] std::io::Error), +} + +pub struct CertificateAuthority<'a> { + issuer: Issuer<'a, KeyPair>, + cert_der: CertificateDer<'a>, +} + +impl CertificateAuthority<'_> { + pub fn from_ca_cert_pem( + ca_cert_pem: &str, + ca_key_pair: &str, + ) -> Result { + let key_pair = KeyPair::from_pem(ca_key_pair)?; + let cert_der = CertificateDer::from_pem_slice(ca_cert_pem.as_bytes()) + .map_err(|e| CertificateError::ParsingError(e.to_string()))?; + let issuer = Issuer::from_ca_cert_der(&cert_der, key_pair)?; + Ok(CertificateAuthority { issuer, cert_der }) + } + + pub fn from_cert_der_key_pair( + ca_cert_der: &[u8], + ca_key_pair: &[u8], + ) -> Result { + let key_pair = KeyPair::try_from(ca_key_pair)?; + let cert_der = CertificateDer::from(ca_cert_der.to_vec()); + let issuer = Issuer::from_ca_cert_der(&cert_der, key_pair)?; + Ok(CertificateAuthority { issuer, cert_der }) + } + + pub fn from_key_cert_params( + key_pair: KeyPair, + ca_cert_params: CertificateParams, + ) -> Result { + let cert = ca_cert_params.self_signed(&key_pair)?; + let issuer = Issuer::new(ca_cert_params, key_pair); + let cert_der = cert.der().clone(); + Ok(CertificateAuthority { issuer, cert_der }) + } + + pub fn new() -> Result { + let mut ca_params = CertificateParams::new(vec![CA_NAME.to_string()])?; + + ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params + .distinguished_name + .push(rcgen::DnType::OrganizationName, CA_ORG); + ca_params + .distinguished_name + .push(rcgen::DnType::CommonName, CA_NAME); + + let ca_key_pair = KeyPair::generate()?; + + CertificateAuthority::from_key_cert_params(ca_key_pair, ca_params) + } + + pub fn sign_csr(&self, csr: &Csr) -> Result { + let csr = csr.params()?; + let cert = csr.signed_by(&self.issuer)?; + Ok(cert) + } + + pub fn cert_pem(&self) -> Result { + der_to_pem(self.cert_der.as_ref(), &PemLabel::Certificate) + } + + #[must_use] + pub fn cert_der(&self) -> &[u8] { + self.cert_der.as_ref() + } + + #[must_use] + pub fn key_pair_der(&self) -> &[u8] { + self.issuer.key().serialized_der() + } +} + +pub struct Csr<'a> { + csr: CertificateSigningRequestDer<'a>, +} + +impl Csr<'_> { + pub fn new( + key_pair: &impl SigningKey, + subject_alt_names: &[String], + dinstinguished_name: Vec<(rcgen::DnType, &str)>, + ) -> Result { + let mut csr_params = CertificateParams::new(subject_alt_names.to_vec())?; + for (dn_type, value) in dinstinguished_name { + csr_params.distinguished_name.push(dn_type, value); + } + let request = csr_params.serialize_request(key_pair)?; + let csr = request.der().clone(); + Ok(Self { csr }) + } + + pub fn from_der(csr_der: &[u8]) -> Result { + let csr = CertificateSigningRequestDer::from(csr_der.to_vec()); + Ok(Self { csr }) + } + + pub fn params(&self) -> Result { + let params = CertificateSigningRequestParams::from_der(&self.csr) + .map_err(|e| CertificateError::ParsingError(e.to_string()))?; + Ok(params) + } + + #[must_use] + pub fn to_der(&self) -> &[u8] { + self.csr.as_ref() + } +} + +pub enum PemLabel { + Certificate, + PrivateKey, + PublicKey, +} + +impl PemLabel { + #[must_use] + pub const fn as_str(&self) -> &str { + match self { + Self::Certificate => "CERTIFICATE", + Self::PrivateKey => "PRIVATE KEY", + Self::PublicKey => "PUBLIC KEY", + } + } +} + +pub fn der_to_pem(der: &[u8], label: &PemLabel) -> Result { + let b64 = BASE64_STANDARD.encode(der); + let pem_string = format!( + "-----BEGIN {}-----\n{}\n-----END {}-----", + label.as_str(), + b64.as_bytes() + .chunks(64) + .map(|chunk| std::str::from_utf8(chunk) + .map_err(|e| CertificateError::ParsingError(e.to_string()))) + .collect::, _>>()? + .join("\n"), + label.as_str(), + ); + Ok(pem_string) +} + +pub fn cert_der_to_pem(cert_der: &[u8]) -> Result { + der_to_pem(cert_der, &PemLabel::Certificate) +} + +pub fn generate_key_pair() -> Result { + let key_pair = KeyPair::generate()?; + Ok(key_pair) +} + +pub fn save_key_pair_to_pem_file(key_pair: &KeyPair, path: &Path) -> Result<(), CertificateError> { + let pem_string = key_pair.serialize_pem(); + std::fs::write(path, pem_string)?; + Ok(()) +} + +pub fn save_cert_to_pem_file(cert: &Certificate, path: &Path) -> Result<(), CertificateError> { + let pem_string = cert.pem(); + std::fs::write(path, pem_string)?; + Ok(()) +} + +pub type DnType = rcgen::DnType; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_from_der() { + let key_pair = KeyPair::generate().unwrap(); + let csr = Csr::new( + &key_pair, + &["example.com".to_string()], + vec![(rcgen::DnType::CommonName, "example.com")], + ) + .unwrap(); + let der = csr.to_der(); + let csr_loaded = Csr::from_der(der).unwrap(); + assert_eq!(csr.to_der(), csr_loaded.to_der()); + } + + #[test] + fn test_ca_creation() { + let ca = CertificateAuthority::new().unwrap(); + let key = ca.issuer.key(); + let der = &ca.cert_der; + let pem_string = cert_der_to_pem(der.as_ref()).unwrap(); + let ca_loaded = + CertificateAuthority::from_ca_cert_pem(&pem_string, &key.serialize_pem()).unwrap(); + assert_eq!(ca.cert_der, ca_loaded.cert_der); + } + + #[test] + fn test_sign_csr() { + let ca = CertificateAuthority::new().unwrap(); + let cert_key_pair = KeyPair::generate().unwrap(); + let csr = Csr::new( + &cert_key_pair, + &["example.com".to_string(), "www.example.com".to_string()], + vec![ + (rcgen::DnType::CommonName, "example.com"), + (rcgen::DnType::OrganizationName, "Example Org"), + ], + ) + .unwrap(); + let signed_cert: Certificate = ca.sign_csr(&csr).unwrap(); + assert!(signed_cert.pem().contains("BEGIN CERTIFICATE")); + } + + #[test] + fn test_der_to_pem() { + assert_eq!(PemLabel::Certificate.as_str(), "CERTIFICATE"); + assert_eq!(PemLabel::PrivateKey.as_str(), "PRIVATE KEY"); + assert_eq!(PemLabel::PublicKey.as_str(), "PUBLIC KEY"); + + // chunking: make sure lines are 64 chars except last + let data = vec![0u8; 200]; + let pem = der_to_pem(&data, &PemLabel::PublicKey).unwrap(); + assert!(pem.starts_with("-----BEGIN PUBLIC KEY-----")); + assert!(pem.ends_with("-----END PUBLIC KEY-----")); + let inner_lines: Vec<&str> = pem + .lines() + .skip(1) + .take_while(|l| !l.starts_with("-----END")) + .collect(); + assert!(inner_lines.len() >= 2); + for (i, line) in inner_lines.iter().enumerate() { + if i + 1 < inner_lines.len() { + assert_eq!(line.len(), 64); + } else { + assert!(line.len() <= 64); + } + } + } +} diff --git a/crates/defguard_certs/src/main.rs b/crates/defguard_certs/src/main.rs deleted file mode 100644 index b05377b6b..000000000 --- a/crates/defguard_certs/src/main.rs +++ /dev/null @@ -1,165 +0,0 @@ -use rcgen::{ - BasicConstraints, Certificate, CertificateParams, CertificateSigningRequest, - CertificateSigningRequestParams, IsCa, Issuer, KeyPair, PublicKey, RcgenError, -}; -use rustls_pki_types::{CertificateDer, pem::PemObject}; -use thiserror::Error; - -const CA_NAME: &str = "DefGuard CA"; -const CA_ORG: &str = "DefGuard"; - -#[derive(Debug, Error)] -pub enum CAError { - #[error("Certificate generation error: {0}")] - RCGenError(#[from] rcgen::Error), - #[error("PEM error: {0}")] - PemParsingError(String), -} - -pub struct CertificateAuthority<'a> { - issuer: Issuer<'a, KeyPair>, - cert_pem: String, -} - -impl CertificateAuthority<'_> { - pub fn from_ca_cert_pem(ca_cert_pem: &str, ca_key_pair: &str) -> Result { - let key_pair = KeyPair::from_pem(ca_key_pair)?; - let cert_der = CertificateDer::from_pem_slice(ca_cert_pem.as_bytes()) - .map_err(|e| CAError::PemParsingError(e.to_string()))?; - let issuer = Issuer::from_ca_cert_der(&cert_der, key_pair)?; - Ok(CertificateAuthority { - issuer, - cert_pem: ca_cert_pem.to_string(), - }) - } - - pub fn from_key_cert_params( - key_pair: KeyPair, - ca_cert_params: CertificateParams, - ) -> Result { - let cert = ca_cert_params.self_signed(&key_pair)?; - let issuer = Issuer::new(ca_cert_params, key_pair); - Ok(CertificateAuthority { - issuer, - cert_pem: cert.pem(), - }) - } - - pub fn new() -> Result { - let mut ca_params = CertificateParams::new(vec![CA_NAME.to_string()])?; - - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - ca_params - .distinguished_name - .push(rcgen::DnType::OrganizationName, CA_ORG); - ca_params - .distinguished_name - .push(rcgen::DnType::CommonName, CA_NAME); - - let ca_key_pair = KeyPair::generate()?; - - CertificateAuthority::from_key_cert_params(ca_key_pair, ca_params) - } - - pub fn sign_csr(&self, csr: &CertificateSigningRequestParams) -> Result { - let cert = csr.signed_by(&self.issuer)?; - Ok(cert) - } - - pub fn save(&self) {} -} - -fn main() { - let ca = CertificateAuthority::new().unwrap(); - - let key = ca.issuer.key(); - - let pem = ca.cert_pem; - - let ca = CertificateAuthority::from_ca_cert_pem(&pem, &key.serialize_pem()).unwrap(); - - println!("Loaded CA: {:?}", ca.cert_pem); - // let der_key = CertificateDer::from_slice(&pem); - - // let mut cert_params = CertificateParams::new(vec![ - // "example.com".to_string(), - // "www.example.com".to_string(), - // ]) - // .unwrap(); - - // cert_params - // .distinguished_name - // .push(rcgen::DnType::CommonName, "example.com"); - // cert_params - // .distinguished_name - // .push(rcgen::DnType::OrganizationName, "Example Org"); - - // let cert_key_pair = KeyPair::generate().unwrap(); - - // let cert = cert_params.serialize_request(&cert_key_pair).unwrap(); - - // let pem = cert.pem().unwrap(); - - // let csr = CertificateSigningRequestParams::from_pem(&pem).unwrap(); - - // let cert = ca.sign_csr(&csr).unwrap(); - - // println!("CSR PEM:\n{:?}", cert); - - // let mut ca_params = CertificateParams::new(vec!["My CA".to_string()]).unwrap(); - - // // Configure as a Certificate Authority - // ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - // ca_params - // .distinguished_name - // .push(rcgen::DnType::OrganizationName, "My Organization"); - // ca_params - // .distinguished_name - // .push(rcgen::DnType::CommonName, "My Root CA"); - - // // Generate key pair for CA - // let ca_key_pair = KeyPair::generate().unwrap(); - - // // Create the CA certificate - // let ca_cert = ca_params.self_signed(&ca_key_pair).unwrap(); - - // println!("=== CA Certificate ==="); - // println!("{}", ca_cert.pem()); - // println!("\n=== CA Private Key ==="); - // println!("{}", ca_key_pair.serialize_pem()); - - // let test_ca_string = ca_cert.pem(); - - // let ca = Issuer::from_ca_cert_pem(&test_ca_string, &ca_key_pair).unwrap(); - // println!("Loaded CA: {:?}", ca); - - // let ca_cert_pem = ca_cert.pem(); - // std::fs::write("ca_certificate.pem", &ca_cert_pem) - // .expect("Failed to write CA certificate to file"); - - // let mut cert_params = CertificateParams::new(vec![ - // "example.com".to_string(), - // "www.example.com".to_string(), - // ]) - // .unwrap(); - - // cert_params - // .distinguished_name - // .push(rcgen::DnType::CommonName, "example.com"); - // cert_params - // .distinguished_name - // .push(rcgen::DnType::OrganizationName, "Example Org"); - - // let cert_key_pair = KeyPair::generate().unwrap(); - - // let issuer = Issuer::from_params(&ca_params, &ca_key_pair); - - // // Sign the certificate with the CA - // // let signed_cert = cert_params.signed_by(&cert_key_pair, &issuer).unwrap(); - // let signed_cert = cert_params.self_signed(&cert_key_pair).unwrap(); - - // println!("\n=== Signed Certificate ==="); - // println!("{}", signed_cert.pem()); - // println!("\n=== Certificate Private Key ==="); - // println!("{}", cert_key_pair.serialize_pem()); -} diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index 018aa0b91..ca20e0e2e 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -143,6 +143,8 @@ pub struct Settings { pub gateway_disconnect_notifications_enabled: bool, pub gateway_disconnect_notifications_inactivity_threshold: i32, pub gateway_disconnect_notifications_reconnect_notification_enabled: bool, + pub ca_key_der: Option>, + pub ca_cert_der: Option>, } // Implement manually to avoid exposing the license key. @@ -249,7 +251,8 @@ impl Settings { ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, \ ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, \ ldap_user_rdn_attr, ldap_sync_groups, \ - openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" \ + openid_username_handling \"openid_username_handling: OpenidUsernameHandling\", \ + ca_key_der, ca_cert_der \ FROM \"settings\" WHERE id = 1", ) .fetch_optional(executor) @@ -326,7 +329,9 @@ impl Settings { ldap_uses_ad = $45, \ ldap_user_rdn_attr = $46, \ ldap_sync_groups = $47, \ - openid_username_handling = $48 \ + openid_username_handling = $48, \ + ca_key_der = $49, \ + ca_cert_der = $50 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -376,6 +381,8 @@ impl Settings { self.ldap_user_rdn_attr, &self.ldap_sync_groups as &Vec, &self.openid_username_handling as &OpenidUsernameHandling, + &self.ca_key_der as &Option>, + &self.ca_cert_der as &Option>, ) .execute(executor) .await?; diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index 2d57b1318..7bcc5100c 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -15,6 +15,7 @@ defguard_proto = { workspace = true } defguard_web_ui = { workspace = true } defguard_version = { workspace = true } model_derive = { workspace = true } +defguard_certs = { workspace = true } # external dependencies anyhow = { workspace = true } diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index 90ef3c681..a459dad8b 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -90,8 +90,9 @@ use defguard_proto::{ auth::auth_service_server::AuthServiceServer, gateway::gateway_service_server::GatewayServiceServer, proxy::{ - AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, core_request, - core_response, proxy_client::ProxyClient, + AuthCallbackResponse, AuthInfoResponse, CertResponse, CoreError, CoreRequest, CoreResponse, + CsrRequest, Done, ProxySetupRequest, ProxySetupResponse, core_request, core_response, + proxy_client::ProxyClient, proxy_setup_request, }, worker::worker_service_server::WorkerServiceServer, }; @@ -545,6 +546,105 @@ async fn handle_proxy_message_loop( Ok(()) } +pub async fn perform_initial_proxy_setup( + ca: &defguard_certs::CertificateAuthority<'_>, +) -> Result<(), anyhow::Error> { + let config = server_config(); + + let mut url = Url::parse(config.proxy_url.as_deref().unwrap())?; + + if url.scheme() != "http" { + url.set_scheme("http").unwrap(); + } + + println!("Connecting to proxy at {}", url); + + let hostname = url.host_str().unwrap_or("localhost"); + + let endpoint = Endpoint::from_shared(config.proxy_url.as_deref().unwrap())?; + let endpoint = endpoint + .http2_keep_alive_interval(TEN_SECS) + .tcp_keepalive(Some(TEN_SECS)) + .keep_alive_while_idle(true); + let mut client = ProxyClient::new(endpoint.connect_lazy()); + + 'connection: loop { + let (tx, rx) = mpsc::unbounded_channel(); + let mut stream = match client.proxy_setup(UnboundedReceiverStream::new(rx)).await { + Ok(response) => response.into_inner(), + Err(err) => { + error!( + "Failed to connect to proxy @ {}, retrying in 10s: {}", + endpoint.uri(), + err + ); + sleep(TEN_SECS).await; + continue 'connection; + } + }; + tx.send(ProxySetupResponse { + payload: Some( + defguard_proto::proxy::proxy_setup_response::Payload::InitialSetupInfo( + defguard_proto::proxy::InitialSetupInfo { + cert_hostname: hostname.to_string(), + }, + ), + ), + })?; + + 'message: loop { + match stream.message().await { + Ok(Some(req)) => match req.payload { + Some(proxy_setup_request::Payload::CsrRequest(CsrRequest { csr_der })) => { + match defguard_certs::Csr::from_der(&csr_der) { + Ok(csr) => match ca.sign_csr(&csr) { + Ok(cert) => { + let response = CertResponse { + cert_der: cert.der().to_vec(), + }; + tx.send(ProxySetupResponse { payload: Some( + defguard_proto::proxy::proxy_setup_response::Payload::CertResponse(response) + ) })?; + info!("Signed CSR and sent certificate to proxy"); + } + Err(err) => { + error!("Failed to sign CSR: {err}"); + } + }, + Err(err) => { + error!("Failed to parse CSR: {err}"); + } + } + } + Some(proxy_setup_request::Payload::Done(Done {})) => { + info!("Proxy setup completed"); + tx.send(ProxySetupResponse { + payload: Some( + defguard_proto::proxy::proxy_setup_response::Payload::Done(Done {}), + ), + })?; + break 'connection; + } + _ => { + error!("Expected CertRequest from proxy during setup"); + continue; + } + }, + Ok(None) => { + error!("Proxy setup stream closed unexpectedly"); + break; + } + Err(err) => { + error!("Failed to receive CSR request from proxy: {err}"); + break; + } + } + } + } + + Ok(()) +} + /// Bi-directional gRPC stream for communication with Defguard Proxy. #[instrument(skip_all)] pub async fn run_grpc_bidi_stream( @@ -568,21 +668,49 @@ pub async fn run_grpc_bidi_stream( let mut client_mfa_server = ClientMfaServer::new(pool.clone(), mail_tx, wireguard_tx.clone(), bidi_event_tx); let mut polling_server = PollingServer::new(pool.clone()); - - let endpoint = Endpoint::from_shared(config.proxy_url.as_deref().unwrap())?; - let endpoint = endpoint - .http2_keep_alive_interval(TEN_SECS) - .tcp_keepalive(Some(TEN_SECS)) - .keep_alive_while_idle(true); - let endpoint = if let Some(ca) = &config.proxy_grpc_ca { - let ca = read_to_string(ca)?; - let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(ca)); - endpoint.tls_config(tls)? - } else { - endpoint.tls_config(ClientTlsConfig::new().with_enabled_roots())? - }; + let settings = Settings::get_current_settings(); + let ca_cert_der = settings.ca_cert_der.ok_or_else(|| { + anyhow::anyhow!("CA certificate DER not found in settings for proxy gRPC bidi stream") + })?; + let ca_key_pair = settings.ca_key_der.ok_or_else(|| { + anyhow::anyhow!("CA key pair DER not found in settings for proxy gRPC bidi stream") + })?; + + let ca = + defguard_certs::CertificateAuthority::from_cert_der_key_pair(&ca_cert_der, &ca_key_pair)?; + let cert_pem = ca.cert_pem()?; loop { + if let Err(err) = perform_initial_proxy_setup(&ca).await { + error!("Failed to perform initial proxy setup: {err}"); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + + let mut url = Url::parse(config.proxy_url.as_deref().unwrap())?; + if url.scheme() != "https" { + url.set_scheme("https").unwrap(); + } + + println!("Connecting to proxy at {}", url); + + let endpoint = Endpoint::from_shared(url.to_string())?; + + let endpoint = endpoint + .http2_keep_alive_interval(TEN_SECS) + .tcp_keepalive(Some(TEN_SECS)) + .keep_alive_while_idle(true); + // let endpoint = if let Some(ca) = &config.proxy_grpc_ca { + // let ca = read_to_string(ca)?; + // let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(ca)); + // endpoint.tls_config(tls)? + // } else { + // endpoint.tls_config(ClientTlsConfig::new().with_enabled_roots())? + // }; + + let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(&cert_pem)); + let endpoint = endpoint.tls_config(tls)?; + debug!("Connecting to proxy at {}", endpoint.uri()); let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); let mut client = ProxyClient::with_interceptor(endpoint.connect_lazy(), interceptor); @@ -637,7 +765,8 @@ pub async fn run_grpc_bidi_stream( info!("Connected to proxy at {}", endpoint.uri()); let mut resp_stream = response.into_inner(); - handle_proxy_message_loop(ProxyMessageLoopContext { + + let context = ProxyMessageLoopContext { pool: pool.clone(), tx, wireguard_tx: wireguard_tx.clone(), @@ -647,8 +776,8 @@ pub async fn run_grpc_bidi_stream( client_mfa_server: &mut client_mfa_server, polling_server: &mut polling_server, endpoint_uri: endpoint.uri(), - }) - .await?; + }; + handle_proxy_message_loop(context).await?; } } diff --git a/migrations/20251218140442_core_ca.down.sql b/migrations/20251218140442_core_ca.down.sql new file mode 100644 index 000000000..070c45ebb --- /dev/null +++ b/migrations/20251218140442_core_ca.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE settings DROP COLUMN ca_key_der; +ALTER TABLE settings DROP COLUMN ca_cert_der; diff --git a/migrations/20251218140442_core_ca.up.sql b/migrations/20251218140442_core_ca.up.sql new file mode 100644 index 000000000..eb1daffd0 --- /dev/null +++ b/migrations/20251218140442_core_ca.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE settings ADD COLUMN ca_key_der BYTEA DEFAULT NULL; +ALTER TABLE settings ADD COLUMN ca_cert_der BYTEA DEFAULT NULL; From 40c086ddfb160d3d1f1eb805af757972d5b05bba Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 19 Dec 2025 13:54:07 +0100 Subject: [PATCH 03/17] post merge changes --- Cargo.lock | 1 + crates/defguard_certs/src/lib.rs | 9 +- crates/defguard_proxy_manager/Cargo.toml | 1 + crates/defguard_proxy_manager/src/lib.rs | 259 +++++++++++++++++------ 4 files changed, 205 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b366127d..d6467a44c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1330,6 +1330,7 @@ dependencies = [ "anyhow", "axum", "chrono", + "defguard_certs", "defguard_common", "defguard_core", "defguard_mail", diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 07dc7bd22..901cdd719 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -81,7 +81,7 @@ impl CertificateAuthority<'_> { } pub fn cert_pem(&self) -> Result { - der_to_pem(self.cert_der.as_ref(), &PemLabel::Certificate) + der_to_pem(self.cert_der.as_ref(), PemLabel::Certificate) } #[must_use] @@ -131,6 +131,7 @@ impl Csr<'_> { } } +#[derive(Debug, Copy, Clone)] pub enum PemLabel { Certificate, PrivateKey, @@ -148,7 +149,7 @@ impl PemLabel { } } -pub fn der_to_pem(der: &[u8], label: &PemLabel) -> Result { +pub fn der_to_pem(der: &[u8], label: PemLabel) -> Result { let b64 = BASE64_STANDARD.encode(der); let pem_string = format!( "-----BEGIN {}-----\n{}\n-----END {}-----", @@ -165,7 +166,7 @@ pub fn der_to_pem(der: &[u8], label: &PemLabel) -> Result Result { - der_to_pem(cert_der, &PemLabel::Certificate) + der_to_pem(cert_der, PemLabel::Certificate) } pub fn generate_key_pair() -> Result { @@ -241,7 +242,7 @@ mod tests { // chunking: make sure lines are 64 chars except last let data = vec![0u8; 200]; - let pem = der_to_pem(&data, &PemLabel::PublicKey).unwrap(); + let pem = der_to_pem(&data, PemLabel::PublicKey).unwrap(); assert!(pem.starts_with("-----BEGIN PUBLIC KEY-----")); assert!(pem.ends_with("-----END PUBLIC KEY-----")); let inner_lines: Vec<&str> = pem diff --git a/crates/defguard_proxy_manager/Cargo.toml b/crates/defguard_proxy_manager/Cargo.toml index 63580a5b1..e6865ba2a 100644 --- a/crates/defguard_proxy_manager/Cargo.toml +++ b/crates/defguard_proxy_manager/Cargo.toml @@ -14,6 +14,7 @@ defguard_core.workspace = true defguard_mail.workspace = true defguard_proto.workspace = true defguard_version.workspace = true +defguard_certs.workspace = true openidconnect.workspace = true reqwest.workspace = true semver.workspace = true diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index f950214de..0eef67f7a 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -7,26 +7,8 @@ use std::{ }; use axum::http::Uri; -use openidconnect::{AuthorizationCode, Nonce, Scope, core::CoreAuthenticationFlow}; -use reqwest::Url; -use semver::Version; -use sqlx::PgPool; -use thiserror::Error; -use tokio::{ - sync::{ - broadcast::Sender, - mpsc::{self, UnboundedSender}, - }, - task::JoinSet, - time::sleep, -}; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tonic::{ - Code, Streaming, - transport::{Certificate, ClientTlsConfig, Endpoint}, -}; - -use defguard_common::{VERSION, config::server_config}; +use defguard_certs::der_to_pem; +use defguard_common::{VERSION, config::server_config, db::models::Settings}; use defguard_core::{ db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError}, enrollment_management::clear_unused_enrollment_tokens, @@ -46,12 +28,31 @@ use defguard_core::{ }; use defguard_mail::Mail; use defguard_proto::proxy::{ - AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, core_request, - core_response, proxy_client::ProxyClient, + AuthCallbackResponse, AuthInfoResponse, CertResponse, CoreError, CoreRequest, CoreResponse, + CsrRequest, Done, ProxySetupResponse, core_request, core_response, proxy_client::ProxyClient, + proxy_setup_request, }; use defguard_version::{ ComponentInfo, DefguardComponent, client::ClientVersionInterceptor, get_tracing_variables, }; +use openidconnect::{AuthorizationCode, Nonce, Scope, core::CoreAuthenticationFlow, url}; +use reqwest::Url; +use semver::Version; +use sqlx::PgPool; +use thiserror::Error; +use tokio::{ + sync::{ + broadcast::Sender, + mpsc::{self, UnboundedSender}, + }, + task::JoinSet, + time::sleep, +}; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tonic::{ + Code, Streaming, + transport::{Certificate, ClientTlsConfig, Endpoint}, +}; use crate::{enrollment::EnrollmentServer, password_reset::PasswordResetServer}; @@ -78,6 +79,16 @@ pub enum ProxyError { SqlxError(#[from] sqlx::Error), #[error(transparent)] TokenError(#[from] TokenError), + #[error(transparent)] + CertificateError(#[from] defguard_certs::CertificateError), + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + #[error("Missing proxy configuration: {0}")] + MissingConfiguration(String), + #[error("URL error: {0}")] + UrlError(String), + #[error("Proxy setup error: {0}")] + SetupError(#[from] tokio::sync::mpsc::error::SendError), } /// Maintains routing state for proxy-specific responses by associating @@ -157,7 +168,7 @@ impl ProxyOrchestrator { pool, tx, incompatible_components, - router: Default::default(), + router: Arc::default(), } } @@ -172,10 +183,10 @@ impl ProxyOrchestrator { }; let proxies = vec![Proxy::new( self.pool.clone(), - Uri::from_str(url)?, + Url::from_str(url)?, self.tx.clone(), Arc::clone(&self.router), - )?]; + )]; let mut tasks = JoinSet::>::new(); for proxy in proxies { tasks.spawn(proxy.run(self.tx.clone(), self.incompatible_components.clone())); @@ -201,7 +212,8 @@ pub struct ProxyTxSet { } impl ProxyTxSet { - pub fn new( + #[must_use] + pub const fn new( wireguard: Sender, mail: UnboundedSender, bidi_events: UnboundedSender, @@ -223,51 +235,57 @@ impl ProxyTxSet { /// `ProxyOrchestrator`. struct Proxy { pool: PgPool, - /// Proxy server gRPC URI - endpoint: Endpoint, /// gRPC servers services: ProxyServices, /// Router shared between proxies and the orchestrator router: Arc>, + /// Proxy server gRPC URL + url: Url, } impl Proxy { - pub fn new( - pool: PgPool, - uri: Uri, - tx: ProxyTxSet, - router: Arc>, - ) -> Result { - let endpoint = Endpoint::from(uri); + pub fn new(pool: PgPool, url: Url, tx: ProxyTxSet, router: Arc>) -> Self { + // Instantiate gRPC servers. + let services = ProxyServices::new(pool.clone(), tx); + + Self { + pool, + services, + router, + url, + } + } + + fn endpoint(&self, with_tls: bool) -> Result { + let mut url = self.url.clone(); - // Set endpoint keep-alive to avoid connectivity issues in proxied deployments. + let scheme = if with_tls { "https" } else { "http" }; + url.set_scheme(scheme).map_err(|()| { + ProxyError::UrlError(format!("Failed to set {scheme} scheme on URL {url}")) + })?; + let endpoint = Endpoint::from_shared(url.to_string())?; let endpoint = endpoint .http2_keep_alive_interval(TEN_SECS) .tcp_keepalive(Some(TEN_SECS)) .keep_alive_while_idle(true); - // Setup certs. - let config = server_config(); - let endpoint = if let Some(ca) = &config.proxy_grpc_ca { - let ca = read_to_string(ca).map_err(|err| { - error!("Failed to read CA certificate: {err:?}"); - ProxyError::CaCertReadError(err) - })?; - let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(ca)); + let endpoint = if with_tls { + let settings = Settings::get_current_settings(); + let Some(ca_cert_der) = settings.ca_cert_der else { + return Err(ProxyError::MissingConfiguration( + "Core CA is not setup, can't create a Proxy endpoint.".to_string(), + )); + }; + + let cert_pem = der_to_pem(&ca_cert_der, defguard_certs::PemLabel::Certificate)?; + let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(&cert_pem)); + endpoint.tls_config(tls)? } else { - endpoint.tls_config(ClientTlsConfig::new().with_enabled_roots())? + endpoint }; - // Instantiate gRPC servers. - let services = ProxyServices::new(pool.clone(), tx); - - Ok(Self { - pool, - endpoint, - router, - services, - }) + Ok(endpoint) } /// Establishes and maintains a gRPC bidirectional stream to the proxy. @@ -281,10 +299,19 @@ impl Proxy { incompatible_components: Arc>, ) -> Result<(), ProxyError> { loop { - debug!("Connecting to proxy at {}", self.endpoint.uri()); + if let Err(err) = self.perform_initial_setup().await { + error!("Failed to perform initial proxy setup: {err}"); + } + + // Wait a bit before reconnecting, reconnecting too fast will often result in an + // error since proxy may have not restarted the server yet. + tokio::time::sleep(Duration::from_secs(1)).await; + + let endpoint = self.endpoint(true)?; + + debug!("Connecting to proxy at {}", endpoint.uri()); let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); - let mut client = - ProxyClient::with_interceptor(self.endpoint.connect_lazy(), interceptor); + let mut client = ProxyClient::with_interceptor(endpoint.connect_lazy(), interceptor); let (tx, rx) = mpsc::unbounded_channel(); let response = match client.bidi(UnboundedReceiverStream::new(rx)).await { Ok(response) => response, @@ -294,14 +321,14 @@ impl Proxy { error!( "Failed to connect to proxy @ {}, version check failed, retrying in \ 10s: {err}", - self.endpoint.uri() + endpoint.uri() ); // TODO push event } err => { error!( "Failed to connect to proxy @ {}, retrying in 10s: {err}", - self.endpoint.uri() + endpoint.uri() ); } } @@ -334,13 +361,123 @@ impl Proxy { } IncompatibleComponents::remove_proxy(&incompatible_components); - info!("Connected to proxy at {}", self.endpoint.uri()); + info!("Connected to proxy at {}", endpoint.uri()); let mut resp_stream = response.into_inner(); self.message_loop(tx, tx_set.wireguard.clone(), &mut resp_stream) .await?; } } + /// Attempt to perform an initial setup of the target proxy. + /// If the proxy doesn't have signed gRPC certificates by Core yet, + /// this step will perform the signing. Otherwise, the step will be skipped + /// by instantly sending the "Done" message by both parties. + pub async fn perform_initial_setup(&self) -> Result<(), ProxyError> { + let endpoint = self.endpoint(false)?; + + let mut client = ProxyClient::new(endpoint.connect_lazy()); + let hostname = self.url.host_str().unwrap_or("localhost"); + + 'connection: loop { + let (tx, rx) = mpsc::unbounded_channel(); + let mut stream = match client.proxy_setup(UnboundedReceiverStream::new(rx)).await { + Ok(response) => response.into_inner(), + Err(err) => { + error!( + "Failed to connect to proxy @ {}, retrying in 10s: {}", + endpoint.uri(), + err + ); + sleep(TEN_SECS).await; + continue 'connection; + } + }; + + tx.send(ProxySetupResponse { + payload: Some( + defguard_proto::proxy::proxy_setup_response::Payload::InitialSetupInfo( + defguard_proto::proxy::InitialSetupInfo { + cert_hostname: hostname.to_string(), + }, + ), + ), + })?; + + loop { + match stream.message().await { + Ok(Some(req)) => match req.payload { + Some(proxy_setup_request::Payload::CsrRequest(CsrRequest { csr_der })) => { + match defguard_certs::Csr::from_der(&csr_der) { + Ok(csr) => { + let settings = Settings::get_current_settings(); + + let ca_cert_der = settings.ca_cert_der.ok_or_else(|| { + ProxyError::MissingConfiguration( + "CA certificate DER not found in settings for proxy gRPC bidi stream".to_string(), + ) + })?; + let ca_key_pair = settings.ca_key_der.ok_or_else(|| { + ProxyError::MissingConfiguration( + "CA key pairs DER not found in settings for proxy gRPC bidi stream".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 response = CertResponse { + cert_der: cert.der().to_vec(), + }; + tx.send(ProxySetupResponse { payload: Some( + defguard_proto::proxy::proxy_setup_response::Payload::CertResponse(response) + ) })?; + info!("Signed CSR and sent certificate to proxy"); + } + Err(err) => { + error!("Failed to sign CSR: {err}"); + } + } + } + Err(err) => { + error!("Failed to parse CSR: {err}"); + } + } + } + Some(proxy_setup_request::Payload::Done(Done {})) => { + info!("Proxy setup completed"); + tx.send(ProxySetupResponse { + payload: Some( + defguard_proto::proxy::proxy_setup_response::Payload::Done( + Done {}, + ), + ), + })?; + break 'connection; + } + _ => { + error!("Expected CertRequest from proxy during setup"); + continue; + } + }, + Ok(None) => { + error!("Proxy setup stream closed unexpectedly"); + break; + } + Err(err) => { + error!("Failed to receive CSR request from proxy: {err}"); + break; + } + } + } + } + + Ok(()) + } + /// Processes incoming requests from the proxy over an active gRPC stream. /// /// This loop receives `CoreRequest` messages from the proxy, dispatches @@ -792,7 +929,7 @@ impl Proxy { }; } Err(err) => { - error!("Disconnected from proxy at {}: {err}", self.endpoint.uri()); + error!("Disconnected from proxy at {}: {err}", self.url); debug!("waiting 10s to re-establish the connection"); sleep(TEN_SECS).await; break 'message; From 3fb1b9596b31b9b504dd1f5727b5e998c96efbf4 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:50:17 +0100 Subject: [PATCH 04/17] versions --- crates/defguard_proxy_manager/src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 0eef67f7a..3f8797dc2 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -1,12 +1,10 @@ use std::{ collections::HashMap, - fs::read_to_string, str::FromStr, sync::{Arc, RwLock}, time::Duration, }; -use axum::http::Uri; use defguard_certs::der_to_pem; use defguard_common::{VERSION, config::server_config, db::models::Settings}; use defguard_core::{ @@ -375,7 +373,8 @@ impl Proxy { pub async fn perform_initial_setup(&self) -> Result<(), ProxyError> { let endpoint = self.endpoint(false)?; - let mut client = ProxyClient::new(endpoint.connect_lazy()); + let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); + let mut client = ProxyClient::with_interceptor(endpoint.connect_lazy(), interceptor); let hostname = self.url.host_str().unwrap_or("localhost"); 'connection: loop { From 35dde3753a5c903b50b801f52a30edf2e6f9bdcd Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:42:53 +0100 Subject: [PATCH 05/17] detect if endpoint is https or http --- crates/defguard_proxy_manager/src/lib.rs | 92 ++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index f55090558..43d60c508 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -87,6 +87,10 @@ pub enum ProxyError { UrlError(String), #[error("Proxy setup error: {0}")] SetupError(#[from] tokio::sync::mpsc::error::SendError), + #[error(transparent)] + Transport(#[from] tonic::Status), + #[error("Connection timeout: {0}")] + ConnectionTimeout(String), } /// Maintains routing state for proxy-specific responses by associating @@ -298,14 +302,27 @@ impl Proxy { incompatible_components: Arc>, ) -> Result<(), ProxyError> { loop { - if let Err(err) = self.perform_initial_setup().await { - error!("Failed to perform initial proxy setup: {err}"); + // Probe endpoint for HTTPS availability before performing initial setup + match self.is_https_configured().await { + Ok(true) => { + // HTTPS already present - skip initial setup + } + Ok(false) => { + // HTTPS not configured - try to perform initial setup + if let Err(err) = self.perform_initial_setup().await { + error!("Failed to perform initial proxy setup: {err}"); + } + // Wait a bit before establishing proper connection, reconnecting too fast will often result in an + // error since proxy may have not restarted the server yet. + tokio::time::sleep(Duration::from_secs(1)).await; + } + Err(err) => { + error!("Failed to probe proxy endpoint: {err}, retrying in 10s"); + sleep(TEN_SECS).await; + continue; + } } - // Wait a bit before reconnecting, reconnecting too fast will often result in an - // error since proxy may have not restarted the server yet. - tokio::time::sleep(Duration::from_secs(1)).await; - let endpoint = self.endpoint(true)?; debug!("Connecting to proxy at {}", endpoint.uri()); @@ -367,6 +384,69 @@ impl Proxy { } } + /// Probe the endpoint to check if HTTPS is available. + /// Returns Ok(true) if HTTPS is configured and working. + /// Returns Ok(false) if there's a protocol/TLS error (HTTPS not configured). + /// Returns Err for other errors like timeouts or network issues. + async fn is_https_configured(&self) -> Result { + let endpoint = self.endpoint(true)?; + let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); + let (tx, rx) = mpsc::unbounded_channel(); + let mut client = ProxyClient::with_interceptor(endpoint.connect_lazy(), interceptor); + + match tokio::time::timeout( + Duration::from_secs(10), + client.proxy_setup(UnboundedReceiverStream::new(rx)), + ) + .await + { + Ok(Ok(_)) => { + info!("Proxy endpoint is already using HTTPS, skipping initial setup"); + let _ = tx.send(ProxySetupResponse { + payload: Some(defguard_proto::proxy::proxy_setup_response::Payload::Done( + Done {}, + )), + }); + Ok(true) + } + Ok(Err(err)) => { + let http_endpoint = self.endpoint(false)?; + let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); + let (tx, rx) = mpsc::unbounded_channel(); + let mut client = + ProxyClient::with_interceptor(http_endpoint.connect_lazy(), interceptor); + + match tokio::time::timeout( + Duration::from_secs(10), + client.proxy_setup(UnboundedReceiverStream::new(rx)), + ) + .await + { + // HTTP works = HTTPS not configured + Ok(Ok(_)) => { + info!("Proxy endpoint is available via HTTP, HTTPS not configured"); + let _ = tx.send(ProxySetupResponse { + payload: Some( + defguard_proto::proxy::proxy_setup_response::Payload::Done(Done {}), + ), + }); + Ok(false) + } + Ok(Err(_)) => { + // gRPC errors should be propagated + Err(ProxyError::Transport(err)) + } + Err(_) => Err(ProxyError::ConnectionTimeout( + "Timeout while probing HTTP endpoint".to_string(), + )), + } + } + Err(_) => Err(ProxyError::ConnectionTimeout( + "Timeout while probing HTTPS endpoint".to_string(), + )), + } + } + /// Attempt to perform an initial setup of the target proxy. /// If the proxy doesn't have signed gRPC certificates by Core yet, /// this step will perform the signing. Otherwise, the step will be skipped From 660a0c0269c5ab72f2af3beb70d299b765b2b5f8 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 2 Jan 2026 14:11:52 +0100 Subject: [PATCH 06/17] wait a bit before reconnecting --- crates/defguard_proxy_manager/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 43d60c508..32577a148 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -553,6 +553,8 @@ impl Proxy { } } } + + sleep(Duration::from_secs(1)).await; } Ok(()) From ed27480de36ba5a62cc55ed1dccceea65e888d5e Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:26:24 +0100 Subject: [PATCH 07/17] small cleanup --- crates/defguard_proxy_manager/src/lib.rs | 32 ++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 32577a148..043205307 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -61,6 +61,8 @@ pub(crate) mod password_reset; extern crate tracing; const TEN_SECS: Duration = Duration::from_secs(10); +const PROXY_AFTER_SETUP_CONNECT_DELAY: Duration = Duration::from_secs(1); +const PROXY_SETUP_RESTART_DELAY: Duration = Duration::from_secs(5); static VERSION_ZERO: Version = Version::new(0, 0, 0); #[derive(Error, Debug)] @@ -310,14 +312,18 @@ impl Proxy { Ok(false) => { // HTTPS not configured - try to perform initial setup if let Err(err) = self.perform_initial_setup().await { - error!("Failed to perform initial proxy setup: {err}"); + error!("Failed to perform initial Proxy setup: {err}"); } // Wait a bit before establishing proper connection, reconnecting too fast will often result in an // error since proxy may have not restarted the server yet. - tokio::time::sleep(Duration::from_secs(1)).await; + tokio::time::sleep(PROXY_AFTER_SETUP_CONNECT_DELAY).await; } Err(err) => { - error!("Failed to probe proxy endpoint: {err}, retrying in 10s"); + error!( + "Failed to check if Proxy gRPC server is running on {} or {}, retrying in 10s: {err}.", + self.endpoint(false)?.uri(), + self.endpoint(true)?.uri() + ); sleep(TEN_SECS).await; continue; } @@ -396,7 +402,7 @@ impl Proxy { match tokio::time::timeout( Duration::from_secs(10), - client.proxy_setup(UnboundedReceiverStream::new(rx)), + client.setup(UnboundedReceiverStream::new(rx)), ) .await { @@ -418,7 +424,7 @@ impl Proxy { match tokio::time::timeout( Duration::from_secs(10), - client.proxy_setup(UnboundedReceiverStream::new(rx)), + client.setup(UnboundedReceiverStream::new(rx)), ) .await { @@ -456,11 +462,15 @@ impl Proxy { let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); let mut client = ProxyClient::with_interceptor(endpoint.connect_lazy(), interceptor); - let hostname = self.url.host_str().unwrap_or("localhost"); + let Some(hostname) = self.url.host_str() else { + return Err(ProxyError::UrlError( + "Proxy URL missing hostname".to_string(), + )); + }; 'connection: loop { let (tx, rx) = mpsc::unbounded_channel(); - let mut stream = match client.proxy_setup(UnboundedReceiverStream::new(rx)).await { + let mut stream = match client.setup(UnboundedReceiverStream::new(rx)).await { Ok(response) => response.into_inner(), Err(err) => { error!( @@ -487,6 +497,7 @@ impl Proxy { match stream.message().await { Ok(Some(req)) => match req.payload { Some(proxy_setup_request::Payload::CsrRequest(CsrRequest { csr_der })) => { + debug!("Received CSR from proxy during initial setup"); match defguard_certs::Csr::from_der(&csr_der) { Ok(csr) => { let settings = Settings::get_current_settings(); @@ -515,7 +526,9 @@ impl Proxy { tx.send(ProxySetupResponse { payload: Some( defguard_proto::proxy::proxy_setup_response::Payload::CertResponse(response) ) })?; - info!("Signed CSR and sent certificate to proxy"); + info!( + "Signed CSR received from proxy during initial setup and sent back the certificate" + ); } Err(err) => { error!("Failed to sign CSR: {err}"); @@ -540,7 +553,6 @@ impl Proxy { } _ => { error!("Expected CertRequest from proxy during setup"); - continue; } }, Ok(None) => { @@ -554,7 +566,7 @@ impl Proxy { } } - sleep(Duration::from_secs(1)).await; + sleep(PROXY_SETUP_RESTART_DELAY).await; } Ok(()) From 97b415dcbddbe8c917d5eb352f60dcade63d2f70 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:18:43 +0100 Subject: [PATCH 08/17] fix cargo lock --- Cargo.lock | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f6563e639..ce1a729fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1328,6 +1328,7 @@ name = "defguard_proxy_manager" version = "0.0.0" dependencies = [ "axum", + "defguard_certs", "defguard_common", "defguard_core", "defguard_mail", @@ -4258,16 +4259,16 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" +checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f" dependencies = [ "pem", "ring", "rustls-pki-types", "time", "x509-parser 0.18.0", - "yasna 0.5.2", + "yasna", ] [[package]] @@ -6905,14 +6906,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ "num-bigint", -] - -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ "time", ] From 04ccd290d69e4a8422f7c74249812433e25c8357 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:47:22 +0100 Subject: [PATCH 09/17] fix cloning --- crates/defguard_proxy_manager/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 4ddf82f44..3cd1ca541 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -251,7 +251,7 @@ struct Proxy { impl Proxy { pub fn new(pool: PgPool, url: Url, tx: &ProxyTxSet, router: Arc>) -> Self { // Instantiate gRPC servers. - let services = ProxyServices::new(pool.clone(), tx); + let services = ProxyServices::new(&pool, tx); Self { pool, From 7532e01c50b6f17a9ac022600c557e447457ce52 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:48:44 +0100 Subject: [PATCH 10/17] cleanup --- crates/defguard_certs/src/lib.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 901cdd719..3f6b0b26a 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -174,18 +174,6 @@ pub fn generate_key_pair() -> Result { Ok(key_pair) } -pub fn save_key_pair_to_pem_file(key_pair: &KeyPair, path: &Path) -> Result<(), CertificateError> { - let pem_string = key_pair.serialize_pem(); - std::fs::write(path, pem_string)?; - Ok(()) -} - -pub fn save_cert_to_pem_file(cert: &Certificate, path: &Path) -> Result<(), CertificateError> { - let pem_string = cert.pem(); - std::fs::write(path, pem_string)?; - Ok(()) -} - pub type DnType = rcgen::DnType; #[cfg(test)] From 923b11fec28ddb27cb5cf955e84c279938329517 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:07:48 +0100 Subject: [PATCH 11/17] clippy --- crates/defguard_certs/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 3f6b0b26a..c5564ba0a 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use base64::{Engine, prelude::BASE64_STANDARD}; use rcgen::{ BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, IsCa, From 87eef2022baf3868ca5c3851b49c0d3946b297bb Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:19:38 +0100 Subject: [PATCH 12/17] sqlx prepare --- ...a37735aec6f1bac187cc4a331207154996346.json} | 18 +++++++++++++++--- ...66a2ea59a46b7aec66b86522bb0251be3b07a.json} | 8 +++++--- 2 files changed, 20 insertions(+), 6 deletions(-) rename .sqlx/{query-4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2.json => query-217e11c616a96fdd002fbf31bb0a37735aec6f1bac187cc4a331207154996346.json} (95%) rename .sqlx/{query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json => query-bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a.json} (94%) diff --git a/.sqlx/query-4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2.json b/.sqlx/query-217e11c616a96fdd002fbf31bb0a37735aec6f1bac187cc4a331207154996346.json similarity index 95% rename from .sqlx/query-4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2.json rename to .sqlx/query-217e11c616a96fdd002fbf31bb0a37735aec6f1bac187cc4a331207154996346.json index fa1068e52..f52051d44 100644 --- a/.sqlx/query-4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2.json +++ b/.sqlx/query-217e11c616a96fdd002fbf31bb0a37735aec6f1bac187cc4a331207154996346.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" FROM \"settings\" WHERE id = 1", + "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: LdapSyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\", ca_key_der, ca_cert_der FROM \"settings\" WHERE id = 1", "describe": { "columns": [ { @@ -274,6 +274,16 @@ } } } + }, + { + "ordinal": 48, + "name": "ca_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 49, + "name": "ca_cert_der", + "type_info": "Bytea" } ], "parameters": { @@ -327,8 +337,10 @@ false, true, false, - false + false, + true, + true ] }, - "hash": "4a1fba6c990265bc278d4e1534f06a96461ecb5edf023ab88d71f9887fc0f2f2" + "hash": "217e11c616a96fdd002fbf31bb0a37735aec6f1bac187cc4a331207154996346" } diff --git a/.sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json b/.sqlx/query-bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a.json similarity index 94% rename from .sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json rename to .sqlx/query-bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a.json index beabc1823..4a1c1ceec 100644 --- a/.sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json +++ b/.sqlx/query-bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48 WHERE id = 1", + "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48, ca_key_der = $49, ca_cert_der = $50 WHERE id = 1", "describe": { "columns": [], "parameters": { @@ -84,10 +84,12 @@ ] } } - } + }, + "Bytea", + "Bytea" ] }, "nullable": [] }, - "hash": "3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f" + "hash": "bc64228b7c366e1a12db1397d0f66a2ea59a46b7aec66b86522bb0251be3b07a" } From 534c2b2e7f4843e532e6fd4b32d8f3a9e4666027 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:15:09 +0100 Subject: [PATCH 13/17] use derpayload proto --- crates/defguard_proxy_manager/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 3cd1ca541..892c1312d 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -26,8 +26,8 @@ use defguard_core::{ }; use defguard_mail::Mail; use defguard_proto::proxy::{ - AuthCallbackResponse, AuthInfoResponse, CertResponse, CoreError, CoreRequest, CoreResponse, - CsrRequest, Done, ProxySetupResponse, core_request, core_response, proxy_client::ProxyClient, + AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, DerPayload, Done, + ProxySetupResponse, core_request, core_response, proxy_client::ProxyClient, proxy_setup_request, }; use defguard_version::{ @@ -496,9 +496,9 @@ impl Proxy { loop { match stream.message().await { Ok(Some(req)) => match req.payload { - Some(proxy_setup_request::Payload::CsrRequest(CsrRequest { csr_der })) => { + Some(proxy_setup_request::Payload::CsrRequest(DerPayload { der_data })) => { debug!("Received CSR from proxy during initial setup"); - match defguard_certs::Csr::from_der(&csr_der) { + match defguard_certs::Csr::from_der(&der_data) { Ok(csr) => { let settings = Settings::get_current_settings(); @@ -520,8 +520,8 @@ impl Proxy { match ca.sign_csr(&csr) { Ok(cert) => { - let response = CertResponse { - cert_der: cert.der().to_vec(), + let response = DerPayload { + der_data: cert.der().to_vec(), }; tx.send(ProxySetupResponse { payload: Some( defguard_proto::proxy::proxy_setup_response::Payload::CertResponse(response) From 2c2db1112c39cebfa3ae731b882fc59b6b246f7b Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:33:03 +0100 Subject: [PATCH 14/17] simplify the setup --- crates/defguard_certs/src/lib.rs | 1 + crates/defguard_proxy_manager/src/lib.rs | 232 +++++------------------ 2 files changed, 47 insertions(+), 186 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index c5564ba0a..574505b21 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -173,6 +173,7 @@ pub fn generate_key_pair() -> Result { } pub type DnType = rcgen::DnType; +pub type RcGenKeyPair = rcgen::KeyPair; #[cfg(test)] mod tests { diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index 892c1312d..ce8619ee1 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -26,9 +26,9 @@ use defguard_core::{ }; use defguard_mail::Mail; use defguard_proto::proxy::{ - AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, DerPayload, Done, - ProxySetupResponse, core_request, core_response, proxy_client::ProxyClient, - proxy_setup_request, + AuthCallbackResponse, AuthInfoResponse, CoreError, CoreRequest, CoreResponse, DerPayload, + InitialSetupInfo, core_request, core_response, proxy_client::ProxyClient, + proxy_setup_client::ProxySetupClient, }; use defguard_version::{ ComponentInfo, DefguardComponent, client::ClientVersionInterceptor, get_tracing_variables, @@ -62,7 +62,6 @@ extern crate tracing; const TEN_SECS: Duration = Duration::from_secs(10); const PROXY_AFTER_SETUP_CONNECT_DELAY: Duration = Duration::from_secs(1); -const PROXY_SETUP_RESTART_DELAY: Duration = Duration::from_secs(5); static VERSION_ZERO: Version = Version::new(0, 0, 0); #[derive(Error, Debug)] @@ -87,8 +86,6 @@ pub enum ProxyError { MissingConfiguration(String), #[error("URL error: {0}")] UrlError(String), - #[error("Proxy setup error: {0}")] - SetupError(#[from] tokio::sync::mpsc::error::SendError), #[error(transparent)] Transport(#[from] tonic::Status), #[error("Connection timeout: {0}")] @@ -304,29 +301,14 @@ impl Proxy { incompatible_components: Arc>, ) -> Result<(), ProxyError> { loop { - // Probe endpoint for HTTPS availability before performing initial setup - match self.is_https_configured().await { - Ok(true) => { - // HTTPS already present - skip initial setup - } - Ok(false) => { - // HTTPS not configured - try to perform initial setup - if let Err(err) = self.perform_initial_setup().await { - error!("Failed to perform initial Proxy setup: {err}"); - } - // Wait a bit before establishing proper connection, reconnecting too fast will often result in an - // error since proxy may have not restarted the server yet. - tokio::time::sleep(PROXY_AFTER_SETUP_CONNECT_DELAY).await; - } - Err(err) => { - error!( - "Failed to check if Proxy gRPC server is running on {} or {}, retrying in 10s: {err}.", - self.endpoint(false)?.uri(), - self.endpoint(true)?.uri() - ); - sleep(TEN_SECS).await; - continue; - } + // TODO: When we will have proxy table, we should first check in DB if we already configured + // this proxy, and only perform initial setup if not. + if let Err(err) = self.perform_initial_setup().await { + warn!( + "Failed to perform initial Proxy setup: {err}. Will try to connect anyway as proxy may be already setup." + ); + } else { + sleep(PROXY_AFTER_SETUP_CONNECT_DELAY).await; } let endpoint = self.endpoint(true)?; @@ -390,69 +372,6 @@ impl Proxy { } } - /// Probe the endpoint to check if HTTPS is available. - /// Returns Ok(true) if HTTPS is configured and working. - /// Returns Ok(false) if there's a protocol/TLS error (HTTPS not configured). - /// Returns Err for other errors like timeouts or network issues. - async fn is_https_configured(&self) -> Result { - let endpoint = self.endpoint(true)?; - let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); - let (tx, rx) = mpsc::unbounded_channel(); - let mut client = ProxyClient::with_interceptor(endpoint.connect_lazy(), interceptor); - - match tokio::time::timeout( - Duration::from_secs(10), - client.setup(UnboundedReceiverStream::new(rx)), - ) - .await - { - Ok(Ok(_)) => { - info!("Proxy endpoint is already using HTTPS, skipping initial setup"); - let _ = tx.send(ProxySetupResponse { - payload: Some(defguard_proto::proxy::proxy_setup_response::Payload::Done( - Done {}, - )), - }); - Ok(true) - } - Ok(Err(err)) => { - let http_endpoint = self.endpoint(false)?; - let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); - let (tx, rx) = mpsc::unbounded_channel(); - let mut client = - ProxyClient::with_interceptor(http_endpoint.connect_lazy(), interceptor); - - match tokio::time::timeout( - Duration::from_secs(10), - client.setup(UnboundedReceiverStream::new(rx)), - ) - .await - { - // HTTP works = HTTPS not configured - Ok(Ok(_)) => { - info!("Proxy endpoint is available via HTTP, HTTPS not configured"); - let _ = tx.send(ProxySetupResponse { - payload: Some( - defguard_proto::proxy::proxy_setup_response::Payload::Done(Done {}), - ), - }); - Ok(false) - } - Ok(Err(_)) => { - // gRPC errors should be propagated - Err(ProxyError::Transport(err)) - } - Err(_) => Err(ProxyError::ConnectionTimeout( - "Timeout while probing HTTP endpoint".to_string(), - )), - } - } - Err(_) => Err(ProxyError::ConnectionTimeout( - "Timeout while probing HTTPS endpoint".to_string(), - )), - } - } - /// Attempt to perform an initial setup of the target proxy. /// If the proxy doesn't have signed gRPC certificates by Core yet, /// this step will perform the signing. Otherwise, the step will be skipped @@ -461,112 +380,53 @@ impl Proxy { let endpoint = self.endpoint(false)?; let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); - let mut client = ProxyClient::with_interceptor(endpoint.connect_lazy(), interceptor); + let mut client = ProxySetupClient::with_interceptor(endpoint.connect_lazy(), interceptor); let Some(hostname) = self.url.host_str() else { return Err(ProxyError::UrlError( "Proxy URL missing hostname".to_string(), )); }; - 'connection: loop { - let (tx, rx) = mpsc::unbounded_channel(); - let mut stream = match client.setup(UnboundedReceiverStream::new(rx)).await { - Ok(response) => response.into_inner(), - Err(err) => { - error!( - "Failed to connect to proxy @ {}, retrying in 10s: {}", - endpoint.uri(), - err - ); - sleep(TEN_SECS).await; - continue 'connection; - } - }; + let csr = client + .start(InitialSetupInfo { + cert_hostname: hostname.to_string(), + }) + .await? + .into_inner(); - tx.send(ProxySetupResponse { - payload: Some( - defguard_proto::proxy::proxy_setup_response::Payload::InitialSetupInfo( - defguard_proto::proxy::InitialSetupInfo { - cert_hostname: hostname.to_string(), - }, - ), - ), - })?; + let csr = defguard_certs::Csr::from_der(&csr.der_data)?; - loop { - match stream.message().await { - Ok(Some(req)) => match req.payload { - Some(proxy_setup_request::Payload::CsrRequest(DerPayload { der_data })) => { - debug!("Received CSR from proxy during initial setup"); - match defguard_certs::Csr::from_der(&der_data) { - Ok(csr) => { - let settings = Settings::get_current_settings(); + let settings = Settings::get_current_settings(); - let ca_cert_der = settings.ca_cert_der.ok_or_else(|| { - ProxyError::MissingConfiguration( - "CA certificate DER not found in settings for proxy gRPC bidi stream".to_string(), - ) - })?; - let ca_key_pair = settings.ca_key_der.ok_or_else(|| { - ProxyError::MissingConfiguration( - "CA key pairs DER not found in settings for proxy gRPC bidi stream".to_string(), - ) - })?; + let ca_cert_der = settings.ca_cert_der.ok_or_else(|| { + ProxyError::MissingConfiguration( + "CA certificate DER not found in settings for proxy gRPC bidi stream".to_string(), + ) + })?; + let ca_key_pair = settings.ca_key_der.ok_or_else(|| { + ProxyError::MissingConfiguration( + "CA key pairs DER not found in settings for proxy gRPC bidi stream".to_string(), + ) + })?; - let ca = defguard_certs::CertificateAuthority::from_cert_der_key_pair( - &ca_cert_der, - &ca_key_pair, - )?; + let ca = defguard_certs::CertificateAuthority::from_cert_der_key_pair( + &ca_cert_der, + &ca_key_pair, + )?; - match ca.sign_csr(&csr) { - Ok(cert) => { - let response = DerPayload { - der_data: cert.der().to_vec(), - }; - tx.send(ProxySetupResponse { payload: Some( - defguard_proto::proxy::proxy_setup_response::Payload::CertResponse(response) - ) })?; - info!( - "Signed CSR received from proxy during initial setup and sent back the certificate" - ); - } - Err(err) => { - error!("Failed to sign CSR: {err}"); - } - } - } - Err(err) => { - error!("Failed to parse CSR: {err}"); - } - } - } - Some(proxy_setup_request::Payload::Done(Done {})) => { - info!("Proxy setup completed"); - tx.send(ProxySetupResponse { - payload: Some( - defguard_proto::proxy::proxy_setup_response::Payload::Done( - Done {}, - ), - ), - })?; - break 'connection; - } - _ => { - error!("Expected CertRequest from proxy during setup"); - } - }, - Ok(None) => { - error!("Proxy setup stream closed unexpectedly"); - break; - } - Err(err) => { - error!("Failed to receive CSR request from proxy: {err}"); - break; - } - } + match ca.sign_csr(&csr) { + Ok(cert) => { + let response = DerPayload { + der_data: cert.der().to_vec(), + }; + client.send_cert(response).await?; + info!( + "Signed CSR received from proxy during initial setup and sent back the certificate" + ); + } + Err(err) => { + error!("Failed to sign CSR: {err}"); } - - sleep(PROXY_SETUP_RESTART_DELAY).await; } Ok(()) From c36f22a88c7e7bb8cdc219f11d602903ed4494e4 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:30:16 +0100 Subject: [PATCH 15/17] change flag to enum --- crates/defguard/src/main.rs | 2 +- crates/defguard_proxy_manager/src/lib.rs | 29 ++++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/defguard/src/main.rs b/crates/defguard/src/main.rs index 4d7f607d7..4f3b12a63 100644 --- a/crates/defguard/src/main.rs +++ b/crates/defguard/src/main.rs @@ -130,7 +130,7 @@ async fn main() -> Result<(), anyhow::Error> { let mut settings = Settings::get_current_settings(); if settings.ca_cert_der.is_none() || settings.ca_key_der.is_none() { - warn!( + info!( "No gRPC TLS certificate or key found in settings, generating self-signed certificate for gRPC server." ); diff --git a/crates/defguard_proxy_manager/src/lib.rs b/crates/defguard_proxy_manager/src/lib.rs index ce8619ee1..1ca2649c3 100644 --- a/crates/defguard_proxy_manager/src/lib.rs +++ b/crates/defguard_proxy_manager/src/lib.rs @@ -64,6 +64,22 @@ const TEN_SECS: Duration = Duration::from_secs(10); const PROXY_AFTER_SETUP_CONNECT_DELAY: Duration = Duration::from_secs(1); static VERSION_ZERO: Version = Version::new(0, 0, 0); +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) enum Scheme { + Http, + Https, +} + +impl Scheme { + #[must_use] + pub const fn as_str(&self) -> &str { + match self { + Self::Http => "http", + Self::Https => "https", + } + } +} + #[derive(Error, Debug)] pub enum ProxyError { #[error(transparent)] @@ -258,12 +274,11 @@ impl Proxy { } } - fn endpoint(&self, with_tls: bool) -> Result { + fn endpoint(&self, scheme: Scheme) -> Result { let mut url = self.url.clone(); - let scheme = if with_tls { "https" } else { "http" }; - url.set_scheme(scheme).map_err(|()| { - ProxyError::UrlError(format!("Failed to set {scheme} scheme on URL {url}")) + url.set_scheme(scheme.as_str()).map_err(|()| { + ProxyError::UrlError(format!("Failed to set {scheme:?} scheme on URL {url}")) })?; let endpoint = Endpoint::from_shared(url.to_string())?; let endpoint = endpoint @@ -271,7 +286,7 @@ impl Proxy { .tcp_keepalive(Some(TEN_SECS)) .keep_alive_while_idle(true); - let endpoint = if with_tls { + let endpoint = if scheme == Scheme::Https { let settings = Settings::get_current_settings(); let Some(ca_cert_der) = settings.ca_cert_der else { return Err(ProxyError::MissingConfiguration( @@ -311,7 +326,7 @@ impl Proxy { sleep(PROXY_AFTER_SETUP_CONNECT_DELAY).await; } - let endpoint = self.endpoint(true)?; + let endpoint = self.endpoint(Scheme::Https)?; debug!("Connecting to proxy at {}", endpoint.uri()); let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); @@ -377,7 +392,7 @@ impl Proxy { /// this step will perform the signing. Otherwise, the step will be skipped /// by instantly sending the "Done" message by both parties. pub async fn perform_initial_setup(&self) -> Result<(), ProxyError> { - let endpoint = self.endpoint(false)?; + let endpoint = self.endpoint(Scheme::Http)?; let interceptor = ClientVersionInterceptor::new(Version::parse(VERSION)?); let mut client = ProxySetupClient::with_interceptor(endpoint.connect_lazy(), interceptor); From 1d7de25ed179d7973aae3a8ef7184ca5e7553797 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:37:09 +0100 Subject: [PATCH 16/17] proto --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index d8a8d1b27..c48340f72 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit d8a8d1b27fe38f1bd71241971c90ed3852f06d5b +Subproject commit c48340f72b9de3a69cf71318c75ff1361ebd7897 From 69b5f7672464c49c7f4b29b8056623806d69bc95 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:56:07 +0100 Subject: [PATCH 17/17] whitelist defguard certs --- deny.toml | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/deny.toml b/deny.toml index b6b55bf1b..4097d67ed 100644 --- a/deny.toml +++ b/deny.toml @@ -110,41 +110,57 @@ confidence-threshold = 0.8 # aren't accepted for every possible crate as with the normal allow list exceptions = [ { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_common" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_core" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_mail" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_proto" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_web_ui" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_event_router" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_event_logger" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_proxy_manager" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_session_manager" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "defguard_version" }, { allow = [ - "AGPL-3.0-only", "AGPL-3.0-or-later", + "AGPL-3.0-only", + "AGPL-3.0-or-later", ], crate = "model_derive" }, + { allow = [ + "AGPL-3.0-only", + "AGPL-3.0-or-later", + ], crate = "defguard_certs" }, ] # Some crates don't have (easily) machine readable licensing information,