@@ -30,6 +30,7 @@ use luks2::{
3030use ra_rpc:: client:: { CertInfo , RaClient , RaClientConfig } ;
3131use ra_tls:: cert:: { generate_ra_cert, CertConfigV2 } ;
3232use rand:: Rng as _;
33+ use safe_write:: safe_write;
3334use scopeguard:: defer;
3435use serde:: { Deserialize , Serialize } ;
3536use tracing:: { info, warn} ;
@@ -327,6 +328,40 @@ impl HostShared {
327328 }
328329}
329330
331+ const GATEWAY_CERT_CACHE_PATH : & str = "/run/dstack/gateway-client-cert-cache.json" ;
332+ /// Certificate validity period in seconds (10 days)
333+ const CERT_VALIDITY_SECS : u64 = 10 * 24 * 3600 ;
334+
335+ #[ derive( Serialize , Deserialize ) ]
336+ struct CachedClientCert {
337+ cert : String ,
338+ key : String ,
339+ /// Certificate expiry time as seconds since UNIX epoch
340+ not_after : u64 ,
341+ }
342+
343+ impl CachedClientCert {
344+ fn load ( ) -> Option < Self > {
345+ let content = fs:: read_to_string ( GATEWAY_CERT_CACHE_PATH ) . ok ( ) ?;
346+ serde_json:: from_str ( & content) . ok ( )
347+ }
348+
349+ fn save ( & self ) -> Result < ( ) > {
350+ let content = serde_json:: to_string ( self ) . context ( "Failed to serialize cert cache" ) ?;
351+ safe_write ( GATEWAY_CERT_CACHE_PATH , & content) . context ( "Failed to write cert cache" ) ?;
352+ Ok ( ( ) )
353+ }
354+
355+ fn is_valid ( & self ) -> bool {
356+ let now = std:: time:: SystemTime :: now ( )
357+ . duration_since ( std:: time:: UNIX_EPOCH )
358+ . map ( |d| d. as_secs ( ) )
359+ . unwrap_or ( 0 ) ;
360+ // Valid if at least 10 minutes remaining
361+ now + 600 < self . not_after
362+ }
363+ }
364+
330365struct GatewayContext < ' a > {
331366 shared : & ' a HostShared ,
332367 keys : & ' a AppKeys ,
@@ -371,20 +406,18 @@ impl<'a> GatewayContext<'a> {
371406 . context ( "Failed to register CVM" )
372407 }
373408
374- async fn setup ( & self ) -> Result < ( ) > {
375- if !self . shared . app_compose . gateway_enabled ( ) {
376- info ! ( "dstack-gateway is not enabled" ) ;
377- return Ok ( ( ) ) ;
378- }
379- if self . keys . gateway_app_id . is_empty ( ) {
380- bail ! ( "Missing allowed dstack-gateway app id" ) ;
409+ async fn get_or_request_client_cert ( & self ) -> Result < ( String , String ) > {
410+ if let Some ( cached) = CachedClientCert :: load ( ) . filter ( |c| c. is_valid ( ) ) {
411+ info ! ( "Using cached client certificate" ) ;
412+ return Ok ( ( cached. cert , cached. key ) ) ;
381413 }
382414
383- info ! ( "Setting up dstack-gateway" ) ;
384- // Generate WireGuard keys
385- let sk = cmd ! ( wg genkey) ?;
386- let pk = cmd ! ( echo $sk | wg pubkey) . or ( Err ( anyhow ! ( "Failed to generate public key" ) ) ) ?;
387-
415+ info ! ( "Requesting new client certificate" ) ;
416+ let now = std:: time:: SystemTime :: now ( )
417+ . duration_since ( std:: time:: UNIX_EPOCH )
418+ . map ( |d| d. as_secs ( ) )
419+ . unwrap_or ( 0 ) ;
420+ let not_after = now + CERT_VALIDITY_SECS ;
388421 let config = CertConfigV2 {
389422 org_name : None ,
390423 subject : "dstack-guest-agent" . to_string ( ) ,
@@ -394,7 +427,7 @@ impl<'a> GatewayContext<'a> {
394427 ext_quote : false ,
395428 ext_app_info : true ,
396429 not_before : None ,
397- not_after : None ,
430+ not_after : Some ( not_after ) ,
398431 } ;
399432 let cert_client = CertRequestClient :: create (
400433 self . keys ,
@@ -403,14 +436,43 @@ impl<'a> GatewayContext<'a> {
403436 )
404437 . await
405438 . context ( "Failed to create cert client" ) ?;
406- let client_key =
439+ let key =
407440 KeyPair :: generate_for ( & PKCS_ECDSA_P256_SHA256 ) . context ( "Failed to generate key" ) ?;
408- let client_certs = cert_client
409- . request_cert ( & client_key , config, None )
441+ let certs = cert_client
442+ . request_cert ( & key , config, None )
410443 . await
411444 . context ( "Failed to request cert" ) ?;
412- let client_cert = client_certs. join ( "\n " ) ;
413- let client_key = client_key. serialize_pem ( ) ;
445+ let cert = certs. join ( "\n " ) ;
446+ let key = key. serialize_pem ( ) ;
447+
448+ // Cache the certificate for future use
449+ let cached = CachedClientCert {
450+ cert : cert. clone ( ) ,
451+ key : key. clone ( ) ,
452+ not_after,
453+ } ;
454+ if let Err ( e) = cached. save ( ) {
455+ warn ! ( "Failed to cache client certificate: {e:?}" ) ;
456+ }
457+
458+ Ok ( ( cert, key) )
459+ }
460+
461+ async fn setup ( & self ) -> Result < ( ) > {
462+ if !self . shared . app_compose . gateway_enabled ( ) {
463+ info ! ( "dstack-gateway is not enabled" ) ;
464+ return Ok ( ( ) ) ;
465+ }
466+ if self . keys . gateway_app_id . is_empty ( ) {
467+ bail ! ( "Missing allowed dstack-gateway app id" ) ;
468+ }
469+
470+ info ! ( "Setting up dstack-gateway" ) ;
471+ // Generate WireGuard keys
472+ let sk = cmd ! ( wg genkey) ?;
473+ let pk = cmd ! ( echo $sk | wg pubkey) . or ( Err ( anyhow ! ( "Failed to generate public key" ) ) ) ?;
474+
475+ let ( client_cert, client_key) = self . get_or_request_client_cert ( ) . await ?;
414476
415477 if self . shared . sys_config . gateway_urls . is_empty ( ) {
416478 bail ! ( "Missing gateway urls" ) ;
0 commit comments