Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
46 changes: 46 additions & 0 deletions devolutions-gateway/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub struct Conf {
pub job_queue_database: Utf8PathBuf,
pub traffic_audit_database: Utf8PathBuf,
pub tls: Option<Tls>,
pub credssp_tls: Option<Tls>,
pub provisioner_public_key: PublicKey,
pub provisioner_private_key: Option<PrivateKey>,
pub sub_provisioner_public_key: Option<Subkey>,
Expand Down Expand Up @@ -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")?
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing password support for CredSSP PFX/PKCS12 files. The TLS certificate configuration supports password-protected PFX files via tls_private_key_password, but CredSSP PFX files always use None for the password parameter. This creates an inconsistency where users cannot use password-protected PFX files for CredSSP certificates. Consider adding a credssp_private_key_password configuration field and passing it here, similar to how it's done for TLS at line 641.

Suggested change
read_pfx_file(certificate_path, None).context("read CredSSP PFX/PKCS12 file")?
read_pfx_file(
certificate_path,
conf_file.credssp_private_key_password.as_deref(),
)
.context("read CredSSP PFX/PKCS12 file")?

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will address this before merging

}
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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1480,6 +1516,14 @@ pub mod dto {
#[serde(skip_serializing_if = "Option::is_none")]
pub tls_verify_strict: Option<bool>,

/// Certificate to use for CredSSP credential injection (overrides TLS certificate)
#[serde(skip_serializing_if = "Option::is_none")]
pub credssp_certificate_file: Option<Utf8PathBuf>,

/// 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<Utf8PathBuf>,

/// Listeners to launch at startup
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub listeners: Vec<ListenerConf>,
Expand Down Expand Up @@ -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(),
Expand Down
3 changes: 2 additions & 1 deletion devolutions-gateway/src/rd_clean_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,9 @@ async fn handle_with_credential_injection(
credential_entry: Arc<CredentialEntry>,
) -> 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();
Expand Down
3 changes: 2 additions & 1 deletion devolutions-gateway/src/rdp_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
12 changes: 12 additions & 0 deletions devolutions-gateway/tests/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions docs/COOKBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading