From 225dccb991adb496d3ed9a5a87aacb539a36438c Mon Sep 17 00:00:00 2001 From: Pavlo Myroniuk Date: Tue, 14 Jul 2026 15:20:45 +0300 Subject: [PATCH] fix(devolutions-gateway): kdc proxy: target hostname of the fake KDC must be proxy hostname; --- .../src/credential_injection_kdc.rs | 61 ++++--------------- devolutions-gateway/src/lib.rs | 2 +- devolutions-gateway/src/service.rs | 2 +- 3 files changed, 14 insertions(+), 51 deletions(-) diff --git a/devolutions-gateway/src/credential_injection_kdc.rs b/devolutions-gateway/src/credential_injection_kdc.rs index b42a83529..bb4cbda2e 100644 --- a/devolutions-gateway/src/credential_injection_kdc.rs +++ b/devolutions-gateway/src/credential_injection_kdc.rs @@ -55,12 +55,6 @@ pub(crate) enum CredentialInjectionKdcResolveError { ExpiredCredential { jti: Uuid }, #[error("credential-injection state is not available for {jti}")] NonInjectionCredential { jti: Uuid }, - #[error("association token for {jti} is not valid for credential injection")] - InvalidAssociationToken { - jti: Uuid, - #[source] - source: anyhow::Error, - }, #[error("credential-injection KDC config could not be initialized for {jti}")] BuildKdcConfig { jti: Uuid, @@ -444,19 +438,15 @@ fn random_32_bytes() -> Vec { /// through this service, so callers see one handle instead of coordinating a store and a registry. #[derive(Debug, Clone)] pub struct CredentialService { + hostname: String, credentials: CredentialStoreHandle, sessions: Arc>>>, } -impl Default for CredentialService { - fn default() -> Self { - Self::new() - } -} - impl CredentialService { - pub fn new() -> Self { + pub fn new(hostname: String) -> Self { Self { + hostname, credentials: CredentialStoreHandle::new(), sessions: Arc::new(Mutex::new(HashMap::new())), } @@ -524,16 +514,6 @@ impl CredentialService { CredentialInjectionKdcResolveError::NonInjectionCredential { jti } })?; - let target_hostname = crate::token::extract_credential_injection_target_hostname(&credential_entry.token) - .map_err(|source| { - warn!( - %jti, - error = format!("{source:#}"), - "KDC token references invalid credential-injection association token" - ); - CredentialInjectionKdcResolveError::InvalidAssociationToken { jti, source } - })?; - let proxy_username = app_credential_username(&mapping.proxy).to_owned(); // Atomic get-or-insert: holds the lock long enough to guarantee a single Arc // wins for this JTI even under concurrent `kdc_for` calls. The derivation is fast (a few @@ -546,7 +526,7 @@ impl CredentialService { Arc::clone(session) }; - CredentialInjectionKdc::from_parts(jti, credential_entry, target_hostname, session) + CredentialInjectionKdc::from_parts(jti, credential_entry, self.hostname.clone(), session) .map_err(|source| CredentialInjectionKdcResolveError::BuildKdcConfig { jti, source }) } @@ -701,7 +681,7 @@ mod tests { #[test] fn service_kdc_for_rejects_expired_credential_entry() { - let service = CredentialService::new(); + let service = CredentialService::new("dgateway.localhost.com".to_owned()); let jti = Uuid::new_v4(); // Negative TTL: entry is born already expired. `CredentialStoreHandle::get` does not @@ -726,7 +706,7 @@ mod tests { #[test] fn service_kdc_for_returns_same_session_under_concurrent_calls() { - let service = CredentialService::new(); + let service = CredentialService::new("dgateway.localhost.com".to_owned()); let jti = Uuid::new_v4(); service @@ -753,7 +733,7 @@ mod tests { #[test] fn service_insert_drops_stale_session_even_without_credential_replacement() { - let service = CredentialService::new(); + let service = CredentialService::new("dgateway.localhost.com".to_owned()); let jti = Uuid::new_v4(); // Simulate the race called out by Codex: a previous provisioning's session is still @@ -782,7 +762,7 @@ mod tests { #[test] fn service_insert_replacement_drops_cached_kerberos_material() { - let service = CredentialService::new(); + let service = CredentialService::new("dgateway.localhost.com".to_owned()); let jti = Uuid::new_v4(); service @@ -818,7 +798,7 @@ mod tests { #[test] fn service_sweep_orphans_drops_sessions_with_no_credential_entry() { - let service = CredentialService::new(); + let service = CredentialService::new("dgateway.localhost.com".to_owned()); let jti = Uuid::new_v4(); service @@ -837,6 +817,7 @@ mod tests { // drive `credential::cleanup_task` to expire the entry, but it sleeps for 15 minutes // between ticks. Swapping the inner store is the deterministic equivalent. let orphaned_service = CredentialService { + hostname: "dgateway.localhost.com".to_owned(), credentials: CredentialStoreHandle::new(), sessions: Arc::clone(&service.sessions), }; @@ -901,7 +882,7 @@ mod tests { #[test] fn service_kdc_for_rejects_unknown_jti() { - let service = CredentialService::new(); + let service = CredentialService::new("dgateway.localhost.com".to_owned()); assert!( matches!( @@ -914,7 +895,7 @@ mod tests { #[test] fn service_kdc_for_rejects_non_injection_entry() { - let service = CredentialService::new(); + let service = CredentialService::new("dgateway.localhost.com".to_owned()); let jti = Uuid::new_v4(); service @@ -930,24 +911,6 @@ mod tests { ); } - #[test] - fn service_kdc_for_lazily_extracts_target_hostname_from_entry_token() { - let service = CredentialService::new(); - let jti = Uuid::new_v4(); - - service - .insert( - association_token(jti), - Some(cleartext_mapping_with_target_username("target")), - time::Duration::minutes(5), - ) - .expect("credential entry inserts"); - - let kdc = service.kdc_for(jti).expect("credential-injection KDC resolves"); - - assert_eq!(kdc.target_hostname, "target.example"); - } - #[test] fn intercept_ignores_non_loopback_host() { let jti = Uuid::new_v4(); diff --git a/devolutions-gateway/src/lib.rs b/devolutions-gateway/src/lib.rs index a55556b59..e27e3b371 100644 --- a/devolutions-gateway/src/lib.rs +++ b/devolutions-gateway/src/lib.rs @@ -89,7 +89,7 @@ impl DgwState { let (shutdown_handle, shutdown_signal) = devolutions_gateway_task::ShutdownHandle::new(); let (job_queue_handle, job_queue_rx) = job_queue::JobQueueHandle::new(); let (traffic_audit_handle, traffic_audit_rx) = traffic_audit::TrafficAuditHandle::new(); - let credentials = credential_injection_kdc::CredentialService::new(); + let credentials = credential_injection_kdc::CredentialService::new(conf_handle.get_conf().hostname.clone()); let monitoring_state = Arc::new(network_monitor::State::new(Arc::new(MockMonitorsCache))?); let state = Self { diff --git a/devolutions-gateway/src/service.rs b/devolutions-gateway/src/service.rs index e847e1d94..a4870f436 100644 --- a/devolutions-gateway/src/service.rs +++ b/devolutions-gateway/src/service.rs @@ -267,7 +267,7 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { .await .context("failed to initialize traffic audit manager")?; - let credentials = devolutions_gateway::credential_injection_kdc::CredentialService::new(); + let credentials = devolutions_gateway::credential_injection_kdc::CredentialService::new(conf.hostname.clone()); let filesystem_monitor_config_cache = devolutions_gateway::api::monitoring::FilesystemConfigCache::new( config::get_data_dir().join("monitors_cache.json"),