11use std:: io;
2- use std:: net:: SocketAddr ;
32
43use axum:: Router ;
5- use axum:: extract:: { self , ConnectInfo , State } ;
4+ use axum:: extract:: State ;
65use axum:: http:: StatusCode ;
76use axum:: routing:: post;
8- use kdc:: handle_kdc_proxy_message;
97use picky_krb:: messages:: KdcProxyMessage ;
108use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
119use tokio:: net:: { TcpStream , UdpSocket } ;
1210
1311use crate :: DgwState ;
12+ use crate :: credential_injection_kdc:: {
13+ CredentialInjectionKdcInterception , CredentialInjectionKdcRequest , CredentialInjectionKdcResolveError ,
14+ kdc_proxy_message_realm,
15+ } ;
16+ use crate :: extract:: KdcToken ;
1417use crate :: http:: { HttpError , HttpErrorBuilder } ;
1518use crate :: target_addr:: TargetAddr ;
16- use crate :: token:: { AccessTokenClaims , KdcDestination } ;
19+ use crate :: token:: { KdcDestination , KdcTokenClaims } ;
1720
1821pub fn make_router < S > ( state : DgwState ) -> Router < S > {
1922 Router :: new ( ) . route ( "/{token}" , post ( kdc_proxy) ) . with_state ( state)
@@ -22,106 +25,144 @@ pub fn make_router<S>(state: DgwState) -> Router<S> {
2225async fn kdc_proxy (
2326 State ( DgwState {
2427 conf_handle,
25- token_cache,
26- jrl,
27- recordings,
28+ credentials,
2829 ..
2930 } ) : State < DgwState > ,
30- extract:: Path ( token) : extract:: Path < String > ,
31- ConnectInfo ( source_addr) : ConnectInfo < SocketAddr > ,
31+ KdcToken ( KdcTokenClaims { destination } ) : KdcToken ,
3232 body : axum:: body:: Bytes ,
3333) -> Result < Vec < u8 > , HttpError > {
3434 let conf = conf_handle. get_conf ( ) ;
3535
36- let claims = crate :: middleware:: auth:: authenticate (
37- source_addr,
38- & token,
39- & conf,
40- & token_cache,
41- & jrl,
42- & recordings. active_recordings ,
43- None ,
44- )
45- . map_err ( HttpError :: unauthorized ( ) . err ( ) ) ?;
46-
47- let AccessTokenClaims :: Kdc ( claims) = claims else {
48- return Err ( HttpError :: forbidden ( ) . msg ( "token not allowed (expected KDC token)" ) ) ;
49- } ;
50-
5136 let kdc_proxy_message = KdcProxyMessage :: from_raw ( & body) . map_err ( HttpError :: bad_request ( ) . err ( ) ) ?;
5237
5338 trace ! ( ?kdc_proxy_message, "Received KDC message" ) ;
54-
5539 debug ! (
5640 ?kdc_proxy_message. target_domain,
5741 ?kdc_proxy_message. dclocator_hint,
5842 "KDC message" ,
5943 ) ;
6044
61- let realm = if let Some ( realm) = & kdc_proxy_message. target_domain . 0 {
62- realm. 0 . to_string ( )
63- } else {
64- return Err ( HttpError :: bad_request ( ) . msg ( "realm is missing from KDC request" ) ) ;
65- } ;
66-
67- debug ! ( "Request is for realm (target_domain): {realm}" ) ;
45+ match destination {
46+ KdcDestination :: Inject { jti } => {
47+ enforce_credential_injection_enabled ( jti, conf. debug . enable_unstable ) ?;
6848
69- let ( claims_realm, claims_kdc) = match & claims. destination {
70- KdcDestination :: Real { krb_realm, krb_kdc } => ( krb_realm, krb_kdc) ,
71- KdcDestination :: Inject { .. } => {
72- // TODO(DGW-378): dispatch credential-injection KDC requests to the in-process
73- // sspi-rs server backed by the credentials provisioned at session establishment.
74- return Err ( HttpError :: internal ( ) . msg ( "credential injection KDC dispatch is not implemented yet" ) ) ;
75- }
76- } ;
49+ let kdc = credentials. kdc_for ( jti) . map_err ( credential_injection_resolve_error) ?;
7750
78- if !claims_realm. eq_ignore_ascii_case ( & realm) {
79- if conf. debug . disable_token_validation {
80- warn ! (
81- token_realm = %claims_realm,
82- request_realm = %realm,
83- "**DEBUG OPTION** Allowed a KDC request towards a KDC whose Kerberos realm differs from what's inside the KDC token"
51+ debug ! (
52+ jti = %kdc. jti( ) ,
53+ "Proxy-based credential injection with Kerberos. Processing KdcProxy message internally"
8454 ) ;
85- } else {
86- let error_message = format ! ( "expected: {}, got: {}" , claims_realm, realm) ;
8755
88- return Err ( HttpError :: bad_request ( )
89- . with_msg ( "requested domain is not allowed" )
90- . err ( ) ( error_message) ) ;
56+ match kdc
57+ . handle_kdc_proxy_request ( CredentialInjectionKdcRequest :: from_token ( kdc_proxy_message) )
58+ . map_err ( HttpError :: internal ( ) . err ( ) ) ?
59+ {
60+ CredentialInjectionKdcInterception :: Intercepted ( reply) => Ok ( reply) ,
61+ CredentialInjectionKdcInterception :: NotInjectionRealm ( mismatch) => {
62+ Err ( HttpError :: bad_request ( )
63+ . with_msg ( "requested domain is not allowed" )
64+ . err ( ) ( mismatch) )
65+ }
66+ CredentialInjectionKdcInterception :: NotInjectionRequest => {
67+ Err ( HttpError :: internal ( ) . msg ( "credential-injection KDC did not handle the KDC proxy request" ) )
68+ }
69+ }
70+ }
71+ KdcDestination :: Real { krb_realm, krb_kdc } => {
72+ let envelope_realm = kdc_proxy_message_realm ( & kdc_proxy_message) ;
73+ forward_to_real_kdc (
74+ kdc_proxy_message,
75+ envelope_realm,
76+ & krb_realm,
77+ & krb_kdc,
78+ conf. debug . override_kdc . as_ref ( ) ,
79+ conf. debug . disable_token_validation ,
80+ )
81+ . await
9182 }
9283 }
84+ }
9385
94- let gateway_id = conf
95- . id
96- . ok_or_else ( || HttpError :: internal ( ) . build ( "Gateway ID is missing" ) ) ?;
97- if let Some ( krb_config) = & conf. debug . kerberos
98- && realm. eq_ignore_ascii_case ( & krb_config. kerberos_server . realm ( gateway_id) )
99- && conf. debug . enable_unstable
100- {
101- debug ! ( "Proxy-based credential injection with Kerberos. Processing KdcProxy message internally..." ) ;
102-
103- let config = krb_config. kerberos_server . clone ( ) . into_kdc_kerberos_config ( gateway_id) ;
104- let kdc_reply_message = handle_kdc_proxy_message ( kdc_proxy_message, & config, & conf. hostname )
105- . map_err ( HttpError :: internal ( ) . err ( ) ) ?;
106-
107- return kdc_reply_message. to_vec ( ) . map_err ( HttpError :: internal ( ) . err ( ) ) ;
86+ fn credential_injection_resolve_error ( error : CredentialInjectionKdcResolveError ) -> HttpError {
87+ match error {
88+ CredentialInjectionKdcResolveError :: BuildKdcConfig { .. } => HttpError :: internal ( )
89+ . with_msg ( "credential-injection KDC could not be initialized" )
90+ . build ( error) ,
91+ _ => HttpError :: bad_request ( )
92+ . with_msg ( "credential-injection state is not available" )
93+ . build ( error) ,
10894 }
95+ }
10996
110- let kdc_addr = if let Some ( kdc_addr) = & conf. debug . override_kdc {
111- warn ! ( "**DEBUG OPTION** KDC address has been overridden with {kdc_addr}" ) ;
112- kdc_addr
113- } else {
114- claims_kdc
97+ // Forwards the request to the real KDC indicated by the token (or by the debug override) and
98+ // returns the response wrapped as a `KdcProxyMessage`.
99+ //
100+ // The forward path requires the envelope realm to be set: there is no fallback since this is
101+ // not a credential-injection session. After resolving, validates the realm against the
102+ // token's `krb_realm` claim before forwarding anything.
103+ async fn forward_to_real_kdc (
104+ kdc_proxy_message : KdcProxyMessage ,
105+ envelope_realm : Option < String > ,
106+ token_realm : & str ,
107+ token_kdc_addr : & TargetAddr ,
108+ override_kdc : Option < & TargetAddr > ,
109+ bypass_realm_check : bool ,
110+ ) -> Result < Vec < u8 > , HttpError > {
111+ let realm = envelope_realm. ok_or_else ( || HttpError :: bad_request ( ) . msg ( "realm is missing from KDC request" ) ) ?;
112+ debug ! ( resolved_realm = %realm, "Forward-to-real-KDC realm resolved" ) ;
113+ enforce_realm_token_match ( token_realm, & realm, bypass_realm_check) ?;
114+
115+ let kdc_addr = match override_kdc {
116+ Some ( override_addr) => {
117+ warn ! ( %override_addr, "**DEBUG OPTION** KDC address has been overridden" ) ;
118+ override_addr
119+ }
120+ None => token_kdc_addr,
115121 } ;
116122
117- let kdc_reply_message = send_krb_message ( kdc_addr, & kdc_proxy_message. kerb_message . 0 . 0 ) . await ?;
123+ let kdc_reply_bytes = send_krb_message ( kdc_addr, & kdc_proxy_message. kerb_message . 0 . 0 ) . await ?;
118124
119- let kdc_reply_message = KdcProxyMessage :: from_raw_kerb_message ( & kdc_reply_message )
125+ let reply = KdcProxyMessage :: from_raw_kerb_message ( & kdc_reply_bytes )
120126 . map_err ( HttpError :: internal ( ) . with_msg ( "couldn't create KDC proxy reply" ) . err ( ) ) ?;
121127
122- trace ! ( ?kdc_reply_message, "Sending back KDC reply" ) ;
128+ trace ! ( ?reply, "Sending back KDC reply" ) ;
129+
130+ reply. to_vec ( ) . map_err ( HttpError :: internal ( ) . err ( ) )
131+ }
132+
133+ fn enforce_credential_injection_enabled ( jet_cred_id : uuid:: Uuid , enable_unstable : bool ) -> Result < ( ) , HttpError > {
134+ if enable_unstable {
135+ return Ok ( ( ) ) ;
136+ }
137+
138+ warn ! (
139+ %jet_cred_id,
140+ "Credential-injection KDC token rejected because unstable Kerberos injection is disabled"
141+ ) ;
142+ Err ( HttpError :: bad_request ( ) . msg ( "credential-injection KDC proxy is not enabled" ) )
143+ }
144+
145+ /// Refuses to forward a KDC request whose realm disagrees with the realm the token was issued for.
146+ ///
147+ /// `bypass=true` (only when `__debug__.disable_token_validation` is on) downgrades the mismatch
148+ /// to a warning. Production never opts into this.
149+ fn enforce_realm_token_match ( token_realm : & str , request_realm : & str , bypass : bool ) -> Result < ( ) , HttpError > {
150+ if token_realm. eq_ignore_ascii_case ( request_realm) {
151+ return Ok ( ( ) ) ;
152+ }
153+
154+ if bypass {
155+ warn ! (
156+ %token_realm,
157+ %request_realm,
158+ "**DEBUG OPTION** Allowed a KDC request towards a KDC whose Kerberos realm differs from what's inside the KDC token"
159+ ) ;
160+ return Ok ( ( ) ) ;
161+ }
123162
124- kdc_reply_message. to_vec ( ) . map_err ( HttpError :: internal ( ) . err ( ) )
163+ Err ( HttpError :: bad_request ( )
164+ . with_msg ( "requested domain is not allowed" )
165+ . err ( ) ( format ! ( "expected: {token_realm}, got: {request_realm}" ) ) )
125166}
126167
127168async fn read_kdc_reply_message ( connection : & mut TcpStream ) -> io:: Result < Vec < u8 > > {
@@ -221,3 +262,37 @@ pub async fn send_krb_message(kdc_addr: &TargetAddr, message: &[u8]) -> Result<V
221262 Ok ( reply_buf)
222263 }
223264}
265+
266+ #[ cfg( test) ]
267+ mod tests {
268+ use super :: * ;
269+
270+ #[ test]
271+ fn enforce_realm_match_accepts_case_insensitive_match ( ) {
272+ assert ! ( enforce_realm_token_match( "ad.example" , "AD.EXAMPLE" , false ) . is_ok( ) ) ;
273+ }
274+
275+ #[ test]
276+ fn enforce_realm_mismatch_rejects_without_bypass ( ) {
277+ assert ! ( enforce_realm_token_match( "ad.example" , "evil.example" , false ) . is_err( ) ) ;
278+ }
279+
280+ #[ test]
281+ fn enforce_realm_mismatch_passes_under_bypass ( ) {
282+ // `bypass=true` is the `__debug__.disable_token_validation` downgrade. CBenoit asked
283+ // for explicit coverage of this branch because it is the only place the realm
284+ // authorization is intentionally weakened, and slipping the gate (e.g. by inverting the
285+ // condition) would only surface in production.
286+ assert ! ( enforce_realm_token_match( "ad.example" , "evil.example" , true ) . is_ok( ) ) ;
287+ }
288+
289+ #[ test]
290+ fn credential_injection_gate_allows_jet_cred_id_when_enabled ( ) {
291+ assert ! ( enforce_credential_injection_enabled( uuid:: Uuid :: new_v4( ) , true ) . is_ok( ) ) ;
292+ }
293+
294+ #[ test]
295+ fn credential_injection_gate_rejects_jet_cred_id_when_disabled ( ) {
296+ assert ! ( enforce_credential_injection_enabled( uuid:: Uuid :: new_v4( ) , false ) . is_err( ) ) ;
297+ }
298+ }
0 commit comments