diff --git a/docs/guest-api-auth.md b/docs/guest-api-auth.md index 3a665f5b4..f3b4cabc9 100644 --- a/docs/guest-api-auth.md +++ b/docs/guest-api-auth.md @@ -116,7 +116,9 @@ it over on registration, bound to the app id and short-lived: the agent, which are upgraded together but may briefly disagree, so it is never versioned again. 2. `dstack-util` signs a token with `iss = "dstack:guest-api:"`, - `aud = `, a 10-minute lifetime, and only the `guest:health` scope. + `aud = `, only the `guest:health` scope, `iat` rounded down to the + half hour and `exp` one hour later. Every registration within a half hour + carries the same token, and it never has less than 30 minutes left. 3. Registration carries it to the gateway, which presents it on the v1 `Health` poll. diff --git a/dstack/crates/api-auth/src/jwt.rs b/dstack/crates/api-auth/src/jwt.rs index 855af3150..33eb66747 100644 --- a/dstack/crates/api-auth/src/jwt.rs +++ b/dstack/crates/api-auth/src/jwt.rs @@ -21,9 +21,9 @@ use serde::{Deserialize, Serialize}; /// Longest lifetime a token may claim between `iat` and `exp`. /// /// There is no revocation list: a token is trusted until it expires, so the -/// lifetime is the whole of the exposure control. The gateway re-registers -/// every three minutes and mints a fresh token each time, so this costs it -/// nothing. +/// lifetime is the whole of the exposure control. The CVM re-registers with +/// the gateway every three minutes and mints a fresh token every half hour, so +/// this costs it nothing. pub const MAX_TOKEN_LIFETIME_SECS: u64 = 3600; /// Tolerance for clock skew in either direction. diff --git a/dstack/dstack-util/src/system_setup.rs b/dstack/dstack-util/src/system_setup.rs index 3e46ae25c..80326ee93 100644 --- a/dstack/dstack-util/src/system_setup.rs +++ b/dstack/dstack-util/src/system_setup.rs @@ -537,10 +537,12 @@ struct GatewayContext<'a> { /// it never needs `Info` from a CVM that can mint this token. const GATEWAY_AGENT_SCOPES: &[&str] = &[jwt::scope::HEALTH]; -/// Lifetime of the gateway's token. Registration refreshes every three -/// minutes; this survives a few failed refreshes so a gateway outage does not -/// also lock the gateway out of the agent. -const GATEWAY_AGENT_TOKEN_LIFETIME_SECS: u64 = 600; +/// How often the gateway's token changes. `iat` is rounded down to this, and +/// Ed25519 signatures are deterministic, so every registration within one +/// period carries the same token: the stored record stays unchanged instead of +/// changing every three minutes. With `exp` an hour after `iat`, a token always +/// has at least half an hour left, which outlasts many failed refreshes. +const GATEWAY_AGENT_TOKEN_PERIOD_SECS: u64 = 1800; /// Mint the token the gateway presents to this CVM's guest agent, signed with /// the key the agent derives the same way from the app root key. @@ -554,11 +556,12 @@ fn gateway_agent_token(app_id: &[u8], app_root_key: &[u8]) -> Result { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs(); + let iat = now - now % GATEWAY_AGENT_TOKEN_PERIOD_SECS; let claims = jwt::Claims { iss: jwt::internal_issuer(&app_id), aud: app_id, - iat: now, - exp: now + GATEWAY_AGENT_TOKEN_LIFETIME_SECS, + iat, + exp: iat + jwt::MAX_TOKEN_LIFETIME_SECS, scope: GATEWAY_AGENT_SCOPES.join(" "), }; jwt::sign(&secret, &claims)