diff --git a/config_schema.json b/config_schema.json
index dadad26e1..0ac6594c2 100644
--- a/config_schema.json
+++ b/config_schema.json
@@ -707,6 +707,11 @@
"type": "string",
"description": "Path to lib XMF files."
},
+ "kerberos_credential_injection": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether to enable proxy-based RDP credential injection against Kerberos-enforced targets."
+ },
"enable_unstable": {
"type": "boolean",
"default": false,
diff --git a/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md b/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md
index 926754235..36aa61953 100644
--- a/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md
+++ b/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md
@@ -7,6 +7,7 @@ Name | Type | Description | Notes
**HostToResolve** | **string** | The hostname to perform DNS resolution on. Required for \"resolve-host\" kind. | [optional]
**Id** | **Guid** | Unique ID identifying the preflight operation. |
**Kind** | **PreflightOperationKind** | |
+**KrbKdc** | **string** | Real KDC address (e.g. \"tcp://dc.example.com:88\") for Kerberos-enforced credential injection. Optional for \"provision-credentials\" kind; omit for NTLM targets. | [optional]
**ProxyCredential** | [**AppCredential**](AppCredential.md) | | [optional]
**TargetCredential** | [**AppCredential**](AppCredential.md) | | [optional]
**TimeToLive** | **int?** | Minimum persistance duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\" and \"provision-credentials\" kinds. | [optional]
diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperation.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperation.cs
index f2d1e9b56..b9ab5ac90 100644
--- a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperation.cs
+++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/PreflightOperation.cs
@@ -54,7 +54,8 @@ protected PreflightOperation() { }
/// targetCredential.
/// Minimum persistance duration in seconds for the data provisioned via this operation. Optional parameter for \"provision-token\" and \"provision-credentials\" kinds..
/// The token to be stored on the proxy-side. Required for \"provision-token\" and \"provision-credentials\" kinds..
- public PreflightOperation(string hostToResolve = default(string), Guid id = default(Guid), PreflightOperationKind kind = default(PreflightOperationKind), AppCredential proxyCredential = default(AppCredential), AppCredential targetCredential = default(AppCredential), int? timeToLive = default(int?), string token = default(string))
+ /// Real KDC address (e.g. \"tcp://dc.example.com:88\") for Kerberos-enforced credential injection. Optional for \"provision-credentials\" kind; omit for NTLM targets..
+ public PreflightOperation(string hostToResolve = default(string), Guid id = default(Guid), PreflightOperationKind kind = default(PreflightOperationKind), AppCredential proxyCredential = default(AppCredential), AppCredential targetCredential = default(AppCredential), int? timeToLive = default(int?), string token = default(string), string krbKdc = default(string))
{
this.Id = id;
this.Kind = kind;
@@ -63,6 +64,7 @@ protected PreflightOperation() { }
this.TargetCredential = targetCredential;
this.TimeToLive = timeToLive;
this.Token = token;
+ this.KrbKdc = krbKdc;
}
///
@@ -79,6 +81,13 @@ protected PreflightOperation() { }
[DataMember(Name = "id", IsRequired = true, EmitDefaultValue = true)]
public Guid Id { get; set; }
+ ///
+ /// Real KDC address (e.g. \"tcp://dc.example.com:88\") for Kerberos-enforced credential injection. Optional for \"provision-credentials\" kind; omit for NTLM targets.
+ ///
+ /// Real KDC address (e.g. \"tcp://dc.example.com:88\") for Kerberos-enforced credential injection. Optional for \"provision-credentials\" kind; omit for NTLM targets.
+ [DataMember(Name = "krb_kdc", EmitDefaultValue = true)]
+ public string KrbKdc { get; set; }
+
///
/// Gets or Sets ProxyCredential
///
@@ -116,6 +125,7 @@ public override string ToString()
sb.Append(" HostToResolve: ").Append(HostToResolve).Append("\n");
sb.Append(" Id: ").Append(Id).Append("\n");
sb.Append(" Kind: ").Append(Kind).Append("\n");
+ sb.Append(" KrbKdc: ").Append(KrbKdc).Append("\n");
sb.Append(" ProxyCredential: ").Append(ProxyCredential).Append("\n");
sb.Append(" TargetCredential: ").Append(TargetCredential).Append("\n");
sb.Append(" TimeToLive: ").Append(TimeToLive).Append("\n");
diff --git a/devolutions-gateway/openapi/gateway-api.yaml b/devolutions-gateway/openapi/gateway-api.yaml
index 8fabd66fe..8325a0636 100644
--- a/devolutions-gateway/openapi/gateway-api.yaml
+++ b/devolutions-gateway/openapi/gateway-api.yaml
@@ -7,7 +7,7 @@ info:
email: infos@devolutions.net
license:
name: MIT/Apache-2.0
- version: 2026.1.2
+ version: 2026.2.3
paths:
/jet/config:
patch:
@@ -1785,6 +1785,13 @@ components:
description: Unique ID identifying the preflight operation.
kind:
$ref: '#/components/schemas/PreflightOperationKind'
+ krb_kdc:
+ type: string
+ description: |-
+ Real KDC address (e.g. "tcp://dc.example.com:88") for Kerberos-enforced credential injection.
+
+ Optional for "provision-credentials" kind; omit for NTLM targets.
+ nullable: true
proxy_credential:
allOf:
- $ref: '#/components/schemas/AppCredential'
diff --git a/devolutions-gateway/openapi/ts-angular-client/model/preflightOperation.ts b/devolutions-gateway/openapi/ts-angular-client/model/preflightOperation.ts
index 440e61c4a..488fd2d6a 100644
--- a/devolutions-gateway/openapi/ts-angular-client/model/preflightOperation.ts
+++ b/devolutions-gateway/openapi/ts-angular-client/model/preflightOperation.ts
@@ -21,6 +21,10 @@ export interface PreflightOperation {
*/
id: string;
kind: PreflightOperationKind;
+ /**
+ * Real KDC address (e.g. \"tcp://dc.example.com:88\") for Kerberos-enforced credential injection. Optional for \"provision-credentials\" kind; omit for NTLM targets.
+ */
+ krb_kdc?: string | null;
proxy_credential?: AppCredential | null;
target_credential?: AppCredential | null;
/**
diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs
index 893263bdc..db89d7366 100644
--- a/devolutions-gateway/src/config.rs
+++ b/devolutions-gateway/src/config.rs
@@ -1837,101 +1837,6 @@ pub mod dto {
}
}
- /// Domain user credentials.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct DomainUser {
- /// Username in FQDN format (e.g. "pw13@example.com").
- ///
- /// **Note**: the user's domain part must match the internal KDC realm.
- /// The KDC realm is derived from the gateway ID using the [KerberosServer::realm] method.
- pub fqdn: String,
- /// User password.
- #[serde(serialize_with = "serialize_secret_string")]
- pub password: SecretString,
- /// Salt for generating the user's key.
- ///
- /// Usually, it is equal to `{REALM}{username}` (e.g. "EXAMPLEpw13").
- pub salt: String,
- }
-
- impl From for kdc::config::DomainUser {
- fn from(user: DomainUser) -> Self {
- let DomainUser { fqdn, password, salt } = user;
-
- Self {
- username: fqdn,
- password: password.expose_secret().to_owned(),
- salt,
- }
- }
- }
-
- /// Kerberos server config
- ///
- /// This config is used to configure the Kerberos server during RDP proxying.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct KerberosServer {
- /// Users credentials inside fake KDC.
- pub users: Vec,
- /// The maximum allowed time difference between client and proxy clocks.
- ///
- /// The value must be in seconds. [RFC 4120 8.2. Recommended KDC Values](https://www.rfc-editor.org/rfc/rfc4120#section-8.2):
- /// > Acceptable clock skew 5 minutes
- pub max_time_skew: u64,
- /// `krbtgt` service key.
- ///
- /// This key is used to encrypt/decrypt TGT tickets.
- pub krbtgt_key: Vec,
- /// Ticket decryption key.
- ///
- /// This key is used to decrypt the TGS ticket sent by the client. If you do not plan
- /// to use Kerberos U2U authentication, then the `ticket_decryption_key` is required.
- pub ticket_decryption_key: Option>,
- /// The domain user credentials for the Kerberos U2U authentication.
- ///
- /// This field is needed only for Kerberos User-to-User authentication. If you do not plan
- /// to use Kerberos U2U, do not specify it.
- pub service_user: Option,
- }
-
- impl KerberosServer {
- /// Returns the internal KDC realm for the given gateway ID.
- pub fn realm(&self, gateway_id: Uuid) -> String {
- format!("{gateway_id}.jet")
- }
-
- /// Converts the [KerberosServer] into a [kdc::config::KerberosServer] for the given gateway ID.
- pub fn into_kdc_kerberos_config(self, gateway_id: Uuid) -> kdc::config::KerberosServer {
- let realm = self.realm(gateway_id);
-
- let KerberosServer {
- users,
- max_time_skew,
- krbtgt_key,
- ticket_decryption_key,
- service_user,
- } = self;
-
- kdc::config::KerberosServer {
- realm,
- users: users.into_iter().map(Into::into).collect(),
- max_time_skew,
- krbtgt_key,
- ticket_decryption_key,
- service_user: service_user.map(Into::into),
- }
- }
- }
-
- /// The Kerberos credentials-injection configuration.
- #[derive(Debug, Clone, Serialize, Deserialize)]
- pub struct KerberosConfig {
- /// Kerberos server and KDC configuration.
- pub kerberos_server: KerberosServer,
- /// Real KDC address for the Kerberos proxy client.
- pub kdc_url: Option,
- }
-
/// (Unstable) QUIC-based agent tunnel configuration
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
@@ -1999,10 +1904,14 @@ pub mod dto {
#[serde(default = "ws_keep_alive_interval_default_value")]
pub ws_keep_alive_interval: u64,
- /// Kerberos application server configuration
+ /// Enable proxy-based RDP credential injection against Kerberos-enforced targets
///
- /// It is used only during RDP proxying.
- pub kerberos: Option,
+ /// Turns on the in-process KDC acceptor the Gateway presents to the client when injecting
+ /// credentials for accounts that can't fall back to NTLM (e.g. AD Protected Users). The real
+ /// KDC used for the target leg comes from the provisioned credentials, not from here. Off by
+ /// default; still requires `enable_unstable`.
+ #[serde(default)]
+ pub kerberos_credential_injection: bool,
/// Enable unstable features which may break at any point
#[serde(default)]
@@ -2021,7 +1930,7 @@ pub mod dto {
capture_path: None,
lib_xmf_path: None,
enable_unstable: false,
- kerberos: None,
+ kerberos_credential_injection: false,
ws_keep_alive_interval: ws_keep_alive_interval_default_value(),
}
}
@@ -2036,7 +1945,7 @@ pub mod dto {
&& self.capture_path.is_none()
&& self.lib_xmf_path.is_none()
&& !self.enable_unstable
- && self.kerberos.is_none()
+ && !self.kerberos_credential_injection
&& self.ws_keep_alive_interval == ws_keep_alive_interval_default_value()
}
}
@@ -2418,13 +2327,6 @@ pub mod dto {
}
}
- fn serialize_secret_string(value: &SecretString, serializer: S) -> Result
- where
- S: serde::Serializer,
- {
- serializer.serialize_str(value.expose_secret())
- }
-
fn serialize_opt_secret_string(value: &Option, serializer: S) -> Result
where
S: serde::Serializer,
diff --git a/devolutions-gateway/src/credential/mod.rs b/devolutions-gateway/src/credential/mod.rs
index 166be9412..ef9e668db 100644
--- a/devolutions-gateway/src/credential/mod.rs
+++ b/devolutions-gateway/src/credential/mod.rs
@@ -12,6 +12,7 @@ use async_trait::async_trait;
use devolutions_gateway_task::{ShutdownSignal, Task};
use parking_lot::Mutex;
use secrecy::ExposeSecret as _;
+use serde::{Deserialize as _, de};
use uuid::Uuid;
use self::crypto::MASTER_KEY;
@@ -66,6 +67,11 @@ impl AppCredential {
pub struct AppCredentialMapping {
pub proxy: AppCredential,
pub target: AppCredential,
+ /// Real KDC for Kerberos-enforced injection, provisioned alongside the credentials.
+ ///
+ /// The Gateway's target-side CredSSP leg uses this to obtain a real Kerberos ticket; realm is
+ /// derived from the target credential, so only the address is stored. `None` for NTLM targets.
+ pub krb_kdc: Option,
}
/// Cleartext credential received from the API, used for deserialization only.
@@ -105,6 +111,26 @@ pub struct CleartextAppCredentialMapping {
pub proxy: CleartextAppCredential,
#[serde(rename = "target_credential")]
pub target: CleartextAppCredential,
+ /// Real KDC for Kerberos-enforced injection, provisioned in the same call as the credentials.
+ /// Optional: absent for NTLM targets.
+ #[serde(default, deserialize_with = "deserialize_optional_kdc_addr")]
+ pub krb_kdc: Option,
+}
+
+fn deserialize_optional_kdc_addr<'de, D>(deserializer: D) -> Result