Skip to content

Commit 8443c5f

Browse files
committed
More light weight gateway registering
1 parent 92f61ec commit 8443c5f

3 files changed

Lines changed: 82 additions & 18 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dstack-util/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ scopeguard.workspace = true
5555
tempfile.workspace = true
5656
ez-hash.workspace = true
5757
cc-eventlog.workspace = true
58+
safe-write.workspace = true
5859

5960
[dev-dependencies]
6061
rand.workspace = true

dstack-util/src/system_setup.rs

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use luks2::{
3030
use ra_rpc::client::{CertInfo, RaClient, RaClientConfig};
3131
use ra_tls::cert::{generate_ra_cert, CertConfigV2};
3232
use rand::Rng as _;
33+
use safe_write::safe_write;
3334
use scopeguard::defer;
3435
use serde::{Deserialize, Serialize};
3536
use 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+
330365
struct 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

Comments
 (0)