Skip to content

Disk encryption key collision when no_instance_id=true + HKDF context ambiguity #549

Closed

Description

@pbeza

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:

  1. 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.

  2. 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

  1. MPC uses no_instance_id=true in its compose configuration
  2. All nodes for the same app derive the same disk encryption key
  3. If one node's disk is compromised, the same key decrypts all other nodes' disks
  4. 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

  1. 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
  2. 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

  1. kvinwang commented on Mar 26, 2026

    @kvinwang
    Collaborator

    Thanks for the report. A few clarifications:

    1. no_instance_id disk key sharing — By Design

    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.

    1. HKDF context ambiguity — Not applicable in practice

    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.

  2. h4x3rotab commented on Jun 30, 2026

    @h4x3rotab
    Contributor

    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=true intentionally 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

  3. added
    securitySecurity-related issue, report, or hardening work
    security: reportPublic security report or already-public security finding
    security: documentedSecurity report answered by documentation or threat-model clarification
    on Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    securitySecurity-related issue, report, or hardening worksecurity: documentedSecurity report answered by documentation or threat-model clarificationsecurity: reportPublic security report or already-public security finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions