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
78 changes: 71 additions & 7 deletions devolutions-gateway/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -95,7 +96,7 @@ pub struct Conf {
pub job_queue_database: Utf8PathBuf,
pub traffic_audit_database: Utf8PathBuf,
pub tls: Option<Tls>,
pub credssp_tls: Option<Tls>,
pub credssp_tls: Tls,
pub provisioner_public_key: PublicKey,
pub provisioner_private_key: Option<PrivateKey>,
pub sub_provisioner_public_key: Option<Subkey>,
Expand Down Expand Up @@ -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") => {
Expand All @@ -730,13 +730,34 @@ 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 => {
// 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 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)
.context("generate self-signed CredSSP certificate")?;
Comment on lines +742 to +746
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

A self-signed RSA keypair/certificate is generated during config loading when no TLS cert is configured. Since Conf is constructed on every startup (even if CredSSP injection is never used), this adds avoidable startup CPU/entropy cost and could slow/block startup on low-entropy systems. Consider lazy/on-demand generation (only when credential injection is invoked).

Copilot uses AI. Check for mistakes.

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
Expand Down Expand Up @@ -1106,6 +1127,49 @@ fn default_hostname() -> Option<String> {
hostname::get().ok()?.into_string().ok()
}

fn generate_self_signed_certificate(
hostname: &str,
) -> anyhow::Result<(
Vec<pki_types::CertificateDer<'static>>,
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(),
Comment on lines +1149 to +1152
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Building not_after with (now.year() + 2, now.month(), now.day()) can fail on dates like Feb 29 (non-leap year two years later), causing config load/startup to error on those days. Consider computing the expiration by adding a duration to now (or clamping to a valid day) to avoid invalid calendar dates.

Suggested change
let not_after = UtcDate::ymd(
(now.year() + 2).try_into().expect("valid year"),
now.month().into(),
now.day(),
let then = now + time::Duration::days(365_i64 * 2);
let not_after = UtcDate::ymd(
then.year().try_into().expect("valid year"),
then.month().into(),
then.day(),

Copilot uses AI. Check for mistakes.
)
.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>,
Expand Down
6 changes: 1 addition & 5 deletions devolutions-gateway/src/rd_clean_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,7 @@ async fn handle_with_credential_injection(
cleanpath_pdu: RDCleanPathPdu,
credential_entry: Arc<CredentialEntry>,
) -> 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();

Expand Down
6 changes: 1 addition & 5 deletions devolutions-gateway/src/rdp_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")?;
Expand Down
Loading