From cc93843b1b79ad43e0b7ba89b4bde1834e1d1e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Sat, 14 Feb 2026 02:12:11 +0900 Subject: [PATCH 1/2] feat(dgw): add CredSSP certificate configuration keys Add optional `CredSspCertificateFile` and `CredSspPrivateKeyFile` configuration keys allowing usage of a different certificate for CredSSP credential injection instead of the main TLS certificate. When unset, the existing TLS certificate is used (no behavior change). --- README.md | 7 ++++ config_schema.json | 8 +++++ devolutions-gateway/src/config.rs | 46 ++++++++++++++++++++++++ devolutions-gateway/src/rd_clean_path.rs | 3 +- devolutions-gateway/src/rdp_proxy.rs | 3 +- devolutions-gateway/tests/config.rs | 12 +++++++ docs/COOKBOOK.md | 1 + 7 files changed, 78 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62eb05d2e..8894b823e 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,13 @@ Stable options are: (e.g., Chrome, macOS). Therefore, we strongly recommend using certificates that comply with these standards. +- **CredSspCertificateFile** (_FilePath_): Path to the certificate to use for CredSSP credential injection. + When set, this certificate is presented to the client during proxy-based credential injection instead + of the main TLS certificate. If unset, the TLS certificate is used. + +- **CredSspPrivateKeyFile** (_FilePath_): Path to the private key to use for CredSSP credential injection. + Required when **CredSspCertificateFile** is set (unless using a PFX/PKCS12 file which bundles the private key). + - **Listeners** (_Array_): Array of listener URLs. Each element has the following schema: diff --git a/config_schema.json b/config_schema.json index a669f66eb..b460cab18 100644 --- a/config_schema.json +++ b/config_schema.json @@ -65,6 +65,14 @@ "$ref": "#/definitions/CertStoreLocation", "description": "Location of the System Certificate Store to use for TLS." }, + "CredSspCertificateFile": { + "type": "string", + "description": "Path to the certificate to use for CredSSP credential injection (overrides TLS certificate)." + }, + "CredSspPrivateKeyFile": { + "type": "string", + "description": "Path to the private key to use for CredSSP credential injection (overrides TLS private key)." + }, "Listeners": { "type": "array", "items": { diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index 2df6ed073..1ea9b78cf 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -95,6 +95,7 @@ pub struct Conf { pub job_queue_database: Utf8PathBuf, pub traffic_audit_database: Utf8PathBuf, pub tls: Option, + pub credssp_tls: Option, pub provisioner_public_key: PublicKey, pub provisioner_private_key: Option, pub sub_provisioner_public_key: Option, @@ -701,6 +702,40 @@ impl Conf { anyhow::bail!("TLS usage implied but TLS configuration is missing (certificate or/and private key)"); } + 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") => { + read_pfx_file(certificate_path, None).context("read CredSSP PFX/PKCS12 file")? + } + None | Some(_) => { + let certificates = + read_rustls_certificate_file(certificate_path).context("read CredSSP certificate")?; + + let private_key = conf_file + .credssp_private_key_file + .as_ref() + .context("CredSSP private key file is missing")? + .pipe_deref(read_rustls_priv_key_file) + .context("read CredSSP private key")?; + + (certificates, private_key) + } + }; + + let cert_source = crate::tls::CertificateSource::External { + certificates, + private_key, + }; + + let credssp_tls = Tls::init(cert_source, strict_checks) + .context("failed to initialize CredSSP TLS configuration")?; + + Some(credssp_tls) + } + }; + let data_dir = get_data_dir(); let log_file = conf_file @@ -780,6 +815,7 @@ impl Conf { job_queue_database, traffic_audit_database, tls, + credssp_tls, provisioner_public_key, provisioner_private_key, sub_provisioner_public_key, @@ -1480,6 +1516,14 @@ pub mod dto { #[serde(skip_serializing_if = "Option::is_none")] pub tls_verify_strict: Option, + /// Certificate to use for CredSSP credential injection (overrides TLS certificate) + #[serde(skip_serializing_if = "Option::is_none")] + pub credssp_certificate_file: Option, + + /// Private key to use for CredSSP credential injection (overrides TLS private key) + #[serde(skip_serializing_if = "Option::is_none")] + pub credssp_private_key_file: Option, + /// Listeners to launch at startup #[serde(default, skip_serializing_if = "Vec::is_empty")] pub listeners: Vec, @@ -1563,6 +1607,8 @@ pub mod dto { tls_certificate_store_name: None, tls_certificate_store_location: None, tls_verify_strict: Some(true), + credssp_certificate_file: None, + credssp_private_key_file: None, listeners: vec![ ListenerConf { internal_url: "tcp://*:8181".to_owned(), diff --git a/devolutions-gateway/src/rd_clean_path.rs b/devolutions-gateway/src/rd_clean_path.rs index 78b19f4ed..5e9ae7524 100644 --- a/devolutions-gateway/src/rd_clean_path.rs +++ b/devolutions-gateway/src/rd_clean_path.rs @@ -288,8 +288,9 @@ async fn handle_with_credential_injection( credential_entry: Arc, ) -> anyhow::Result<()> { let tls_conf = conf - .tls + .credssp_tls .as_ref() + .or(conf.tls.as_ref()) .context("TLS configuration required for credential injection feature")?; let gateway_hostname = conf.hostname.clone(); diff --git a/devolutions-gateway/src/rdp_proxy.rs b/devolutions-gateway/src/rdp_proxy.rs index 0ff02eb1f..bab282e9f 100644 --- a/devolutions-gateway/src/rdp_proxy.rs +++ b/devolutions-gateway/src/rdp_proxy.rs @@ -66,8 +66,9 @@ where } = proxy; let tls_conf = conf - .tls + .credssp_tls .as_ref() + .or(conf.tls.as_ref()) .context("TLS configuration required for credential injection feature")?; let gateway_hostname = conf.hostname.clone(); diff --git a/devolutions-gateway/tests/config.rs b/devolutions-gateway/tests/config.rs index 67bbcca78..8fb97033d 100644 --- a/devolutions-gateway/tests/config.rs +++ b/devolutions-gateway/tests/config.rs @@ -73,6 +73,8 @@ fn hub_sample() -> Sample { tls_certificate_store_location: None, tls_certificate_store_name: None, tls_verify_strict: Some(true), + credssp_certificate_file: None, + credssp_private_key_file: None, listeners: vec![ ListenerConf { internal_url: "tcp://*:8080".to_owned(), @@ -128,6 +130,8 @@ fn legacy_sample() -> Sample { tls_certificate_store_location: None, tls_certificate_store_name: None, tls_verify_strict: None, + credssp_certificate_file: None, + credssp_private_key_file: None, listeners: vec![], subscriber: None, log_file: Some("/path/to/log/file.log".into()), @@ -173,6 +177,8 @@ fn system_store_sample() -> Sample { tls_certificate_store_location: Some(CertStoreLocation::LocalMachine), tls_certificate_store_name: Some("My".to_owned()), tls_verify_strict: None, + credssp_certificate_file: None, + credssp_private_key_file: None, listeners: vec![], subscriber: None, log_file: None, @@ -234,6 +240,8 @@ fn standalone_custom_auth_sample() -> Sample { tls_certificate_store_location: None, tls_certificate_store_name: None, tls_verify_strict: None, + credssp_certificate_file: None, + credssp_private_key_file: None, listeners: vec![ ListenerConf { internal_url: "tcp://*:8080".to_owned(), @@ -311,6 +319,8 @@ fn standalone_no_auth_sample() -> Sample { tls_certificate_store_location: None, tls_certificate_store_name: None, tls_verify_strict: None, + credssp_certificate_file: None, + credssp_private_key_file: None, listeners: vec![ ListenerConf { internal_url: "tcp://*:8080".to_owned(), @@ -388,6 +398,8 @@ fn proxy_sample() -> Sample { tls_certificate_store_location: None, tls_certificate_store_name: None, tls_verify_strict: None, + credssp_certificate_file: None, + credssp_private_key_file: None, listeners: vec![ ListenerConf { internal_url: "tcp://*:8080".to_owned(), diff --git a/docs/COOKBOOK.md b/docs/COOKBOOK.md index 324cbc467..b3e7b8bfe 100644 --- a/docs/COOKBOOK.md +++ b/docs/COOKBOOK.md @@ -504,6 +504,7 @@ tail -f /var/log/devolutions-agent/agent.log | grep -i "download\|proxy" - Generate a session token for the RDP session. - Generate a scope token for the preflight API. - Configure the TLS certificate and private key. + Optionally, configure `CredSspCertificateFile` and `CredSspPrivateKeyFile` to use a different certificate for CredSSP. - Run the Devolutions Gateway. - We’ll assume it runs on localhost, and it listens for HTTP on 7171 and TCP on 8181. - Adjust to your needs. From 482a72fed5f7eda460836e363668bc3e0af8a907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Sat, 14 Feb 2026 02:17:12 +0900 Subject: [PATCH 2/2] . --- devolutions-gateway/src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index 1ea9b78cf..b467f799e 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -729,8 +729,8 @@ impl Conf { private_key, }; - let credssp_tls = Tls::init(cert_source, strict_checks) - .context("failed to initialize CredSSP TLS configuration")?; + let credssp_tls = + Tls::init(cert_source, strict_checks).context("failed to initialize CredSSP TLS configuration")?; Some(credssp_tls) }