From 1fa0e23d0ec5c2bf169b9d2aed5aacfdc30f578f 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] 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 | 9 ++++ config_schema.json | 12 ++++++ devolutions-gateway/src/config.rs | 52 ++++++++++++++++++++++++ devolutions-gateway/src/rd_clean_path.rs | 3 +- devolutions-gateway/src/rdp_proxy.rs | 3 +- devolutions-gateway/tests/config.rs | 18 ++++++++ docs/COOKBOOK.md | 1 + 7 files changed, 96 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62eb05d2e..f61fefe19 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,15 @@ 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). + +- **CredSspPrivateKeyPassword** (_String_): Password to use for decrypting the CredSSP private key or PFX/PKCS12 file. + - **Listeners** (_Array_): Array of listener URLs. Each element has the following schema: diff --git a/config_schema.json b/config_schema.json index a669f66eb..dadad26e1 100644 --- a/config_schema.json +++ b/config_schema.json @@ -65,6 +65,18 @@ "$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)." + }, + "CredSspPrivateKeyPassword": { + "type": "string", + "description": "Password to use for decrypting the CredSSP private key or PFX/PKCS12 file." + }, "Listeners": { "type": "array", "items": { diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index 2df6ed073..b9feb53d4 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,41 @@ 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, conf_file.credssp_private_key_password.as_ref()) + .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 +816,7 @@ impl Conf { job_queue_database, traffic_audit_database, tls, + credssp_tls, provisioner_public_key, provisioner_private_key, sub_provisioner_public_key, @@ -1480,6 +1517,18 @@ 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, + + /// Password to use for decrypting the CredSSP private key + #[serde(skip_serializing_if = "Option::is_none")] + pub credssp_private_key_password: Option, + /// Listeners to launch at startup #[serde(default, skip_serializing_if = "Vec::is_empty")] pub listeners: Vec, @@ -1563,6 +1612,9 @@ 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, + credssp_private_key_password: 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..4cb015e3f 100644 --- a/devolutions-gateway/tests/config.rs +++ b/devolutions-gateway/tests/config.rs @@ -73,6 +73,9 @@ 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, + credssp_private_key_password: None, listeners: vec![ ListenerConf { internal_url: "tcp://*:8080".to_owned(), @@ -128,6 +131,9 @@ 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, + credssp_private_key_password: None, listeners: vec![], subscriber: None, log_file: Some("/path/to/log/file.log".into()), @@ -173,6 +179,9 @@ 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, + credssp_private_key_password: None, listeners: vec![], subscriber: None, log_file: None, @@ -234,6 +243,9 @@ 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, + credssp_private_key_password: None, listeners: vec![ ListenerConf { internal_url: "tcp://*:8080".to_owned(), @@ -311,6 +323,9 @@ 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, + credssp_private_key_password: None, listeners: vec![ ListenerConf { internal_url: "tcp://*:8080".to_owned(), @@ -388,6 +403,9 @@ 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, + credssp_private_key_password: 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.