Skip to content

Commit e30602e

Browse files
refactor(dgw): rename KDC service and validate target options at construction
Rename `CredentialService` to `CredentialInjectionKdcService` (it now holds only the Kerberos session cache, no credentials) and the `DgwState` field likewise. Validate the KDC scheme when a `TargetConnectionOptions` is built, not only when deserialized, so in-crate callers can't construct an unsupported address. Document that `TokenKeyedStore::get` may return an expired-but-not-yet-swept entry.
1 parent b522611 commit e30602e

11 files changed

Lines changed: 90 additions & 66 deletions

devolutions-gateway/src/api/kdc_proxy.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub fn make_router<S>(state: DgwState) -> Router<S> {
2222
async fn kdc_proxy(
2323
State(DgwState {
2424
conf_handle,
25-
credentials,
25+
credential_injection,
2626
provisioning,
2727
agent_tunnel_handle,
2828
..
@@ -48,7 +48,7 @@ async fn kdc_proxy(
4848
KdcDestination::Inject { jti } => {
4949
enforce_credential_injection_enabled(jti, conf.debug.enable_unstable)?;
5050

51-
let kdc = credentials
51+
let kdc = credential_injection
5252
.kdc_for(&provisioning, jti)
5353
.map_err(credential_injection_resolve_error)?;
5454

devolutions-gateway/src/api/rdp.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub async fn handler(
2525
subscriber_tx,
2626
recordings,
2727
shutdown_signal,
28-
credentials,
28+
credential_injection,
2929
provisioning,
3030
agent_tunnel_handle,
3131
..
@@ -47,7 +47,7 @@ pub async fn handler(
4747
subscriber_tx,
4848
recordings.active_recordings,
4949
source_addr,
50-
credentials,
50+
credential_injection,
5151
provisioning,
5252
agent_tunnel_handle,
5353
)
@@ -68,7 +68,7 @@ async fn handle_socket(
6868
subscriber_tx: SubscriberSender,
6969
active_recordings: Arc<ActiveRecordings>,
7070
source_addr: SocketAddr,
71-
credentials: crate::credential_injection_kdc::CredentialService,
71+
credential_injection: crate::credential_injection_kdc::CredentialInjectionKdcService,
7272
provisioning: crate::provisioning::ProvisioningStore,
7373
agent_tunnel_handle: Option<Arc<agent_tunnel::AgentTunnelHandle>>,
7474
) {
@@ -87,7 +87,7 @@ async fn handle_socket(
8787
sessions,
8888
subscriber_tx,
8989
&active_recordings,
90-
&credentials,
90+
&credential_injection,
9191
&provisioning,
9292
agent_tunnel_handle,
9393
)

devolutions-gateway/src/credential_injection_kdc.rs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ impl CredentialInjectionKdc {
165165
}
166166

167167
pub(crate) fn krb_kdc(&self) -> Option<&crate::target_addr::TargetAddr> {
168-
self.connection_options.as_ref()?.krb_kdc.as_ref()
168+
self.connection_options.as_ref()?.krb_kdc()
169169
}
170170

171171
/// Selects the CredSSP acceptor backend Gateway should present to the RDP client.
@@ -445,7 +445,7 @@ fn random_32_bytes() -> Vec<u8> {
445445
/// live here, and the two are keyed by the same JTI but never reach into each other. Resolution
446446
/// reads the provisioning store passed in by the caller.
447447
#[derive(Debug, Clone)]
448-
pub struct CredentialService {
448+
pub struct CredentialInjectionKdcService {
449449
sessions: Arc<Mutex<HashMap<Uuid, CachedSession>>>,
450450
}
451451

@@ -460,13 +460,13 @@ struct CachedSession {
460460
session: Arc<CredentialInjectionKdcSession>,
461461
}
462462

463-
impl Default for CredentialService {
463+
impl Default for CredentialInjectionKdcService {
464464
fn default() -> Self {
465465
Self::new()
466466
}
467467
}
468468

469-
impl CredentialService {
469+
impl CredentialInjectionKdcService {
470470
pub fn new() -> Self {
471471
Self {
472472
sessions: Arc::new(Mutex::new(HashMap::new())),
@@ -541,11 +541,8 @@ impl CredentialService {
541541
})?;
542542

543543
let proxy_username = app_credential_username(&mapping.proxy).to_owned();
544-
// Atomic get-or-derive under the lock: guarantees a single session wins per JTI even under
545-
// concurrent calls. A cached session is reused only when it was derived for this exact
546-
// provisioning entry — a re-provisioning produces a new entry, so its stale session is
547-
// dropped and re-derived here. Derivation is fast (a few hundred bytes of OsRng), so holding
548-
// the lock across it is acceptable.
544+
// Hold the lock across derive so one session wins per JTI; reuse only if the cached session
545+
// was derived for this exact entry (re-provisioning yields a new entry, forcing a re-derive).
549546
let session = {
550547
let mut sessions = self.sessions.lock();
551548
let reuse = sessions.get(&jti).and_then(|cached| {
@@ -584,7 +581,7 @@ impl CredentialService {
584581
}
585582

586583
pub struct CleanupTask {
587-
pub service: CredentialService,
584+
pub service: CredentialInjectionKdcService,
588585
}
589586

590587
#[async_trait]
@@ -600,7 +597,7 @@ impl Task for CleanupTask {
600597
}
601598

602599
#[instrument(skip_all)]
603-
async fn cleanup_task(service: CredentialService, mut shutdown_signal: ShutdownSignal) {
600+
async fn cleanup_task(service: CredentialInjectionKdcService, mut shutdown_signal: ShutdownSignal) {
604601
use tokio::time::{Duration, sleep};
605602

606603
const TASK_INTERVAL: Duration = Duration::from_secs(60 * 15); // 15 minutes
@@ -719,7 +716,7 @@ mod tests {
719716

720717
#[test]
721718
fn service_kdc_for_rejects_expired_credential_entry() {
722-
let service = CredentialService::new();
719+
let service = CredentialInjectionKdcService::new();
723720
let jti = Uuid::new_v4();
724721

725722
// Negative TTL: entry is born already expired. `TokenKeyedStore::get` does not
@@ -738,7 +735,7 @@ mod tests {
738735

739736
#[test]
740737
fn service_kdc_for_returns_same_session_under_concurrent_calls() {
741-
let service = CredentialService::new();
738+
let service = CredentialInjectionKdcService::new();
742739
let jti = Uuid::new_v4();
743740
let provisioning = provisioned_store(jti, "target", time::Duration::minutes(5));
744741

@@ -758,7 +755,7 @@ mod tests {
758755

759756
#[test]
760757
fn service_kdc_for_ignores_cached_session_from_a_gone_entry() {
761-
let service = CredentialService::new();
758+
let service = CredentialInjectionKdcService::new();
762759
let jti = Uuid::new_v4();
763760

764761
// Simulate the race called out by Codex: a previous provisioning's session is still cached,
@@ -784,7 +781,7 @@ mod tests {
784781

785782
#[test]
786783
fn service_kdc_for_rederives_session_after_reprovisioning() {
787-
let service = CredentialService::new();
784+
let service = CredentialInjectionKdcService::new();
788785
let jti = Uuid::new_v4();
789786
let provisioning = provisioned_store(jti, "target", time::Duration::minutes(5));
790787

@@ -816,7 +813,7 @@ mod tests {
816813

817814
#[test]
818815
fn service_sweep_orphans_drops_sessions_with_no_credential_entry() {
819-
let service = CredentialService::new();
816+
let service = CredentialInjectionKdcService::new();
820817
let jti = Uuid::new_v4();
821818

822819
{
@@ -889,7 +886,7 @@ mod tests {
889886

890887
#[test]
891888
fn service_kdc_for_rejects_unknown_jti() {
892-
let service = CredentialService::new();
889+
let service = CredentialInjectionKdcService::new();
893890
let provisioning = ProvisioningStore::new();
894891

895892
assert!(
@@ -903,7 +900,7 @@ mod tests {
903900

904901
#[test]
905902
fn service_kdc_for_rejects_non_injection_entry() {
906-
let service = CredentialService::new();
903+
let service = CredentialInjectionKdcService::new();
907904
let provisioning = ProvisioningStore::new();
908905
let jti = Uuid::new_v4();
909906

@@ -922,7 +919,7 @@ mod tests {
922919

923920
#[test]
924921
fn service_kdc_for_lazily_extracts_target_hostname_from_entry_token() {
925-
let service = CredentialService::new();
922+
let service = CredentialInjectionKdcService::new();
926923
let jti = Uuid::new_v4();
927924
let provisioning = provisioned_store(jti, "target", time::Duration::minutes(5));
928925

@@ -936,7 +933,7 @@ mod tests {
936933
#[test]
937934
fn provisioning_and_credential_service_share_target_options() {
938935
let provisioning = ProvisioningStore::new();
939-
let service = CredentialService::new();
936+
let service = CredentialInjectionKdcService::new();
940937
let jti = Uuid::new_v4();
941938
let token = association_token(jti);
942939
let krb_kdc = crate::target_addr::TargetAddr::parse("tcp://kdc.example.invalid:88", Some(88))
@@ -946,9 +943,7 @@ mod tests {
946943
.insert(
947944
token.clone(),
948945
Some(cleartext_mapping_with_target_username("target@example.invalid")),
949-
Some(TargetConnectionOptions {
950-
krb_kdc: Some(krb_kdc.clone()),
951-
}),
946+
Some(TargetConnectionOptions::new(Some(krb_kdc.clone())).expect("supported KDC scheme")),
952947
time::Duration::minutes(5),
953948
)
954949
.expect("provisioning entry inserts");
@@ -959,7 +954,7 @@ mod tests {
959954
.value
960955
.connection_options
961956
.as_ref()
962-
.and_then(|options| options.krb_kdc.as_ref()),
957+
.and_then(|options| options.krb_kdc()),
963958
Some(&krb_kdc)
964959
);
965960

devolutions-gateway/src/generic_client.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tracing::field;
88
use typed_builder::TypedBuilder;
99

1010
use crate::config::Conf;
11-
use crate::credential_injection_kdc::CredentialService;
11+
use crate::credential_injection_kdc::CredentialInjectionKdcService;
1212
use crate::provisioning::ProvisioningStore;
1313
use crate::proxy::Proxy;
1414
use crate::rdp_pcb::{extract_association_claims, read_pcb};
@@ -28,7 +28,7 @@ pub struct GenericClient<S> {
2828
sessions: SessionMessageSender,
2929
subscriber_tx: SubscriberSender,
3030
active_recordings: Arc<ActiveRecordings>,
31-
credentials: CredentialService,
31+
credential_injection: CredentialInjectionKdcService,
3232
provisioning: ProvisioningStore,
3333
#[builder(default)]
3434
agent_tunnel_handle: Option<Arc<AgentTunnelHandle>>,
@@ -53,7 +53,7 @@ where
5353
sessions,
5454
subscriber_tx,
5555
active_recordings,
56-
credentials,
56+
credential_injection,
5757
provisioning,
5858
agent_tunnel_handle,
5959
} = self;
@@ -156,7 +156,7 @@ where
156156
// lookup by `claims.jti` is the primary path.
157157
if is_rdp
158158
&& let Some(credential_injection_kdc) =
159-
credentials.resolve_injection_kdc(&provisioning, claims.jti, token)?
159+
credential_injection.resolve_injection_kdc(&provisioning, claims.jti, token)?
160160
{
161161
info!(
162162
jti = %credential_injection_kdc.jti(),

devolutions-gateway/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ pub struct DgwState {
6565
pub recordings: recording::RecordingMessageSender,
6666
pub job_queue_handle: job_queue::JobQueueHandle,
6767
pub provisioning: provisioning::ProvisioningStore,
68-
pub credentials: credential_injection_kdc::CredentialService,
68+
pub credential_injection: credential_injection_kdc::CredentialInjectionKdcService,
6969
pub monitoring_state: Arc<network_monitor::State>,
7070
pub traffic_audit_handle: traffic_audit::TrafficAuditHandle,
7171
pub agent_tunnel_handle: Option<Arc<agent_tunnel::AgentTunnelHandle>>,
@@ -94,7 +94,7 @@ impl DgwState {
9494
let (job_queue_handle, job_queue_rx) = job_queue::JobQueueHandle::new();
9595
let (traffic_audit_handle, traffic_audit_rx) = traffic_audit::TrafficAuditHandle::new();
9696
let provisioning = provisioning::ProvisioningStore::new();
97-
let credentials = credential_injection_kdc::CredentialService::new();
97+
let credential_injection = credential_injection_kdc::CredentialInjectionKdcService::new();
9898
let monitoring_state = Arc::new(network_monitor::State::new(Arc::new(MockMonitorsCache))?);
9999

100100
let state = Self {
@@ -108,7 +108,7 @@ impl DgwState {
108108
job_queue_handle,
109109
traffic_audit_handle,
110110
provisioning,
111-
credentials,
111+
credential_injection,
112112
monitoring_state,
113113
agent_tunnel_handle: None,
114114
};

devolutions-gateway/src/listener.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ async fn handle_tcp_peer(stream: TcpStream, state: DgwState, peer_addr: SocketAd
158158
.sessions(state.sessions)
159159
.subscriber_tx(state.subscriber_tx)
160160
.active_recordings(state.recordings.active_recordings)
161-
.credentials(state.credentials)
161+
.credential_injection(state.credential_injection)
162162
.provisioning(state.provisioning)
163163
.agent_tunnel_handle(state.agent_tunnel_handle)
164164
.build()

devolutions-gateway/src/ngrok.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ async fn run_tcp_tunnel(mut tunnel: ngrok::tunnel::TcpTunnel, state: DgwState) {
237237
.sessions(state.sessions)
238238
.subscriber_tx(state.subscriber_tx)
239239
.active_recordings(state.recordings.active_recordings)
240-
.credentials(state.credentials)
240+
.credential_injection(state.credential_injection)
241241
.provisioning(state.provisioning)
242242
.agent_tunnel_handle(state.agent_tunnel_handle)
243243
.build()

devolutions-gateway/src/rd_clean_path.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
1111
use tracing::field;
1212

1313
use crate::config::Conf;
14-
use crate::credential_injection_kdc::{CredentialInjectionKdc, CredentialService};
14+
use crate::credential_injection_kdc::{CredentialInjectionKdc, CredentialInjectionKdcService};
1515
use crate::provisioning::ProvisioningStore;
1616
use crate::proxy::Proxy;
1717
use crate::recording::ActiveRecordings;
@@ -521,7 +521,7 @@ pub async fn handle(
521521
sessions: SessionMessageSender,
522522
subscriber_tx: SubscriberSender,
523523
active_recordings: &ActiveRecordings,
524-
credentials: &CredentialService,
524+
credential_injection: &CredentialInjectionKdcService,
525525
provisioning: &ProvisioningStore,
526526
agent_tunnel_handle: Option<Arc<agent_tunnel::AgentTunnelHandle>>,
527527
) -> anyhow::Result<()> {
@@ -543,7 +543,7 @@ pub async fn handle(
543543
// proxy-based credential injection mode. Otherwise, we continue the usual
544544
// clean path procedure. The provisioning store is keyed on the association token's JTI.
545545
if let Some(jti) = crate::token::extract_jti(token).ok()
546-
&& let Some(credential_injection_kdc) = credentials.resolve_injection_kdc(provisioning, jti, token)?
546+
&& let Some(credential_injection_kdc) = credential_injection.resolve_injection_kdc(provisioning, jti, token)?
547547
{
548548
debug!(
549549
jti = %credential_injection_kdc.jti(),

devolutions-gateway/src/service.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result<Tasks> {
268268
.context("failed to initialize traffic audit manager")?;
269269

270270
let provisioning = devolutions_gateway::provisioning::ProvisioningStore::new();
271-
let credentials = devolutions_gateway::credential_injection_kdc::CredentialService::new();
271+
let credential_injection = devolutions_gateway::credential_injection_kdc::CredentialInjectionKdcService::new();
272272

273273
let filesystem_monitor_config_cache = devolutions_gateway::api::monitoring::FilesystemConfigCache::new(
274274
config::get_data_dir().join("monitors_cache.json"),
@@ -317,7 +317,7 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result<Tasks> {
317317
recordings: recording_manager_handle.clone(),
318318
job_queue_handle: job_queue_ctx.job_queue_handle.clone(),
319319
provisioning: provisioning.clone(),
320-
credentials: credentials.clone(),
320+
credential_injection: credential_injection.clone(),
321321
monitoring_state,
322322
traffic_audit_handle: traffic_audit_task.handle(),
323323
agent_tunnel_handle,
@@ -354,7 +354,9 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result<Tasks> {
354354

355355
tasks.register(provisioning.cleanup_task());
356356

357-
tasks.register(devolutions_gateway::credential_injection_kdc::CleanupTask { service: credentials });
357+
tasks.register(devolutions_gateway::credential_injection_kdc::CleanupTask {
358+
service: credential_injection,
359+
});
358360

359361
tasks.register(devolutions_log::LogDeleterTask::<GatewayLog>::new(
360362
conf.log_file.clone(),

0 commit comments

Comments
 (0)