Repository navigation
Description
The HKDF key derivation in dstack/kms/src/main_service.rs and dstack/dstack-util/src/system_setup.rs concatenates variable-length info slices without length prefixes, enabling context collisions between different (app_id, instance_id) pairs. Combined with no_instance_id=true, all instances of the same app derive identical disk encryption keys.
Root Cause
Two independent issues combine:
-
When
no_instance_id=true, the instance_id is empty, so all instances of the same app derive the same disk encryption key. This means every CVM running the same app-compose shares identical disk encryption keys. -
The HKDF
expand()call concatenates info slices without length prefixes. For example,expand(&[app_id.as_bytes(), instance_id.as_bytes()])concatenates the raw bytes, so(app_id="abc", instance_id="def")produces the same HKDF context as(app_id="abcdef", instance_id="").
Attack Path
- MPC uses
no_instance_id=truein its compose configuration - All nodes for the same app derive the same disk encryption key
- If one node's disk is compromised, the same key decrypts all other nodes' disks
- Additionally, the HKDF ambiguity allows crafting a different (app_id, instance_id) pair that produces the same disk key as a target app
Impact
All nodes running the same app-compose configuration share identical disk encryption keys. Compromising one node's disk key compromises all nodes. The HKDF ambiguity additionally allows cross-app key collisions through careful choice of app_id and instance_id values.
Suggested Fix
- For the key collision: generate a random per-instance salt even when
no_instance_id=true, or use the VM's unique identifier as part of the key derivation context - For the HKDF ambiguity: use a structured encoding (e.g., CBOR or length-prefixed fields) for the HKDF info parameter to ensure unambiguous separation of variable-length inputs:
// Option A: structured encoding (preferred)
#[derive(serde::Serialize)]
struct DiskKeyContext<'a> {
app_id: &'a str,
instance_id: &'a str,
}
let info = serde_cbor::to_vec(&DiskKeyContext { app_id, instance_id })?;
hkdf.expand(&[&info], &mut okm)?;
// Option B: manual length-prefixing
let mut info = Vec::new();
info.extend_from_slice(&(app_id.len() as u32).to_be_bytes());
info.extend_from_slice(app_id.as_bytes());
info.extend_from_slice(&(instance_id.len() as u32).to_be_bytes());
info.extend_from_slice(instance_id.as_bytes());
hkdf.expand(&[&info], &mut okm)?;Note: changing the KDF derivation will invalidate all existing derived keys, requiring a migration strategy for deployed instances.
Note
Reported to dstack as GHSA-whv5-vxvv-q856. Pending triage.
Note: This issue was created automatically. The vulnerability report was generated by Claude and has not been verified by a human.
Activity
kvinwang commentedon Mar 26, 2026
Thanks for the report. A few clarifications:
This is intentional behavior. The default is no_instance_id=false (instance ID enabled). When a user explicitly sets no_instance_id=true, they are deliberately opting out of per-instance uniqueness — this is a known trade-off for specific use cases.
All three segments in the HKDF info parameter are fixed-length:
app_id: hash output (fixed length)
instance_id: either empty or a 20-byte truncated hash
"app-disk-crypt-key": a constant string literal
Since none of these are variable-length user-controlled strings, the concatenation ambiguity described in the report cannot occur. That said, we could pad instance_id with all-zeros when no_instance_id=true to maintain consistent total length across all cases — this would be a minor hardening improvement rather than a fix for an actual vulnerability.
h4x3rotab commentedon Jun 30, 2026
Maintainer triage update: this is now documented in the public security report table: https://github.com/Dstack-TEE/dstack/blob/master/docs/security/public-security-reports.md#public-reports-and-findings
Disposition: valid report, documented.
no_instance_id=trueintentionally shares disk keys across instances, and the HKDF inputs have fixed lengths. No code fix has been applied; zero-padding for an unset instance ID remains optional hardening.Closing this issue as answered/documented. New exploitable vulnerabilities should use the private reporting path in SECURITY.md: https://github.com/Dstack-TEE/dstack/blob/master/SECURITY.md