From 0e72a4ba62bd66343b69442b3bdf8a0a1bd89fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Wed, 18 Feb 2026 04:48:21 +0900 Subject: [PATCH 1/3] feat(dgw): generate self-signed certificate when no TLS cert is configured for CredSSP When neither a CredSSP-specific certificate nor a main TLS certificate is configured, automatically generate a self-signed certificate for CredSSP credential injection. --- devolutions-gateway/src/config.rs | 73 +++++++++++++++++++++--- devolutions-gateway/src/rd_clean_path.rs | 6 +- devolutions-gateway/src/rdp_proxy.rs | 6 +- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index b9feb53d4..fb61066af 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -12,6 +12,7 @@ use picky::pem::Pem; use tap::prelude::*; use tokio::sync::Notify; use tokio_rustls::rustls::pki_types; +use tracing::info; use url::Url; use uuid::Uuid; @@ -95,7 +96,7 @@ pub struct Conf { pub job_queue_database: Utf8PathBuf, pub traffic_audit_database: Utf8PathBuf, pub tls: Option, - pub credssp_tls: Option, + pub credssp_tls: Tls, pub provisioner_public_key: PublicKey, pub provisioner_private_key: Option, pub sub_provisioner_public_key: Option, @@ -703,7 +704,6 @@ impl Conf { } let credssp_tls = match conf_file.credssp_certificate_file.as_ref() { - None => None, Some(certificate_path) => { let (certificates, private_key) = match certificate_path.extension() { Some("pfx" | "p12") => { @@ -730,13 +730,29 @@ impl Conf { private_key, }; - let credssp_tls = - Tls::init(cert_source, strict_checks).context("failed to initialize CredSSP TLS configuration")?; - - Some(credssp_tls) + Tls::init(cert_source, strict_checks).context("failed to initialize CredSSP TLS configuration")? } - }; + None => match tls.clone() { + Some(tls) => tls, + None => { + info!("No TLS certificate configured; generating a self-signed certificate for CredSSP"); + + let (certificates, private_key) = generate_self_signed_certificate(&hostname) + .context("generate self-signed CredSSP certificate")?; + + let cert_source = crate::tls::CertificateSource::External { + certificates, + private_key, + }; + // Strict checks are not enforced for the auto-generated CredSSP + // self-signed certificate specifically, as it is only used for + // the CredSSP MITM with the client. + Tls::init(cert_source, false) + .context("failed to initialize self-signed CredSSP TLS configuration")? + } + }, + }; let data_dir = get_data_dir(); let log_file = conf_file @@ -1106,6 +1122,49 @@ fn default_hostname() -> Option { hostname::get().ok()?.into_string().ok() } +fn generate_self_signed_certificate( + hostname: &str, +) -> anyhow::Result<( + Vec>, + pki_types::PrivateKeyDer<'static>, +)> { + use picky::x509::certificate::CertificateBuilder; + use picky::x509::date::UtcDate; + use picky::x509::name::DirectoryName; + + let private_key = PrivateKey::generate_rsa(2048).context("generate RSA private key")?; + + let now = time::OffsetDateTime::now_utc(); + let not_before = UtcDate::ymd( + now.year().try_into().expect("valid year"), + now.month().into(), + now.day(), + ) + .context("build not_before date")?; + let not_after = UtcDate::ymd( + (now.year() + 2).try_into().expect("valid year"), + now.month().into(), + now.day(), + ) + .context("build not_after date")?; + + let name = DirectoryName::new_common_name(hostname); + + let cert = CertificateBuilder::new() + .self_signed(name, &private_key) + .validity(not_before, not_after) + .build() + .context("build self-signed certificate")?; + + let cert_der = cert.to_der().context("encode certificate to DER")?; + let key_der = private_key + .to_pkcs8() + .map(|der| pki_types::PrivateKeyDer::Pkcs8(der.into())) + .context("encode private key to PKCS8 DER")?; + + Ok((vec![pki_types::CertificateDer::from(cert_der)], key_der)) +} + fn read_pfx_file( path: &Utf8Path, password: Option<&Password>, diff --git a/devolutions-gateway/src/rd_clean_path.rs b/devolutions-gateway/src/rd_clean_path.rs index 5e9ae7524..4c391d3fb 100644 --- a/devolutions-gateway/src/rd_clean_path.rs +++ b/devolutions-gateway/src/rd_clean_path.rs @@ -287,11 +287,7 @@ async fn handle_with_credential_injection( cleanpath_pdu: RDCleanPathPdu, credential_entry: Arc, ) -> anyhow::Result<()> { - let tls_conf = conf - .credssp_tls - .as_ref() - .or(conf.tls.as_ref()) - .context("TLS configuration required for credential injection feature")?; + let tls_conf = &conf.credssp_tls; let gateway_hostname = conf.hostname.clone(); diff --git a/devolutions-gateway/src/rdp_proxy.rs b/devolutions-gateway/src/rdp_proxy.rs index bab282e9f..b38391b8c 100644 --- a/devolutions-gateway/src/rdp_proxy.rs +++ b/devolutions-gateway/src/rdp_proxy.rs @@ -65,11 +65,7 @@ where disconnect_interest, } = proxy; - let tls_conf = conf - .credssp_tls - .as_ref() - .or(conf.tls.as_ref()) - .context("TLS configuration required for credential injection feature")?; + let tls_conf = &conf.credssp_tls; let gateway_hostname = conf.hostname.clone(); let credential_mapping = credential_entry.mapping.as_ref().context("no credential mapping")?; From 427860bea7e9d61d73ab40668ba9e87bd126db6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Thu, 19 Feb 2026 20:25:02 +0900 Subject: [PATCH 2/3] . --- devolutions-gateway/src/config.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index fb61066af..48484bffc 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -735,6 +735,10 @@ impl Conf { None => match tls.clone() { Some(tls) => tls, None => { + // The self-signed certificate is intentionally not saved to disk. + // Users who need a stable certificate should configure one explicitly. + // When performing proxy-based credential injection, Devolutions Gateway + // is trusted via DVLS, and RDM should automatically ignore the warnings. info!("No TLS certificate configured; generating a self-signed certificate for CredSSP"); let (certificates, private_key) = generate_self_signed_certificate(&hostname) From 0e156025d1969aa876d0471e34b839d8a8d4385f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Thu, 19 Feb 2026 20:26:44 +0900 Subject: [PATCH 3/3] . --- devolutions-gateway/src/config.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index 48484bffc..5c0b9f436 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -738,7 +738,8 @@ impl Conf { // The self-signed certificate is intentionally not saved to disk. // Users who need a stable certificate should configure one explicitly. // When performing proxy-based credential injection, Devolutions Gateway - // is trusted via DVLS, and RDM should automatically ignore the warnings. + // is trusted via the provisioner (e.g.: Devolutions Server), + // and the client (e.g.: RDM) may automatically ignore the warnings. info!("No TLS certificate configured; generating a self-signed certificate for CredSSP"); let (certificates, private_key) = generate_self_signed_certificate(&hostname)