Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ Use the narrowest owning directory. Fixture explanations and component
READMEs should stay with their fixtures/components; general guides should be
linked from the root README and live under `docs/`.

## Test without TEE

Maintainers can run a development image without TDX to test VMM and guest changes.

> [!WARNING]
> A no-TEE VM has no hardware isolation or attestation. Its disk is unencrypted, and its temporary app keys are not stable across boots. Do not use this mode for production workloads or secrets.

Deploy with KMS and TEE disabled:

```bash
dstack deploy ./docker-compose.yml \
--name no-tee-dev \
--image "YOUR_IMAGE" \
--no-kms \
--no-tee
```

Attestation and certificate APIs return errors in this mode. TEE mode cannot change after VM creation; recreate the VM to switch modes.

## Commit Convention

This project uses [Conventional Commits](https://www.conventionalcommits.org/). Please format your commit messages as:
Expand Down
3 changes: 2 additions & 1 deletion docs/security/cvm-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ This is the main configuration file for the application in JSON format:
| public_tcbinfo | 0.5.1 | boolean | Whether TCB info is public |
| allowed_envs | 0.4.2 | array of string | List of allowed environment variable names |
| no_instance_id | 0.4.2 | boolean | Disable instance ID generation |
| no_tee | 0.6.0 | boolean | Development only. Disable TEE and storage encryption. |
| secure_time | 0.5.0 | boolean | Whether secure time is enabled |
| pre_launch_script | 0.4.0 | string | Prelaunch bash script that runs before execute `docker compose up` |
| init_script | 0.5.5 | string | Bash script that executed prior to dockerd startup |
Expand All @@ -47,7 +48,7 @@ This is the main configuration file for the application in JSON format:
| key_provider | 0.5.6 | string | Key provider type. Supported values: "none", "kms", "local", "tpm". |


The hash of this file content is extended to RTMR3 as event name `compose-hash`. Remote verifier can extract the compose-hash during remote attestation.
In TEE mode, the hash of this file is extended to RTMR3 as `compose-hash` for remote verification.


### .instance-info
Expand Down
1 change: 1 addition & 0 deletions dstack/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 29 additions & 4 deletions dstack/crates/dstack-cli-core/src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ use serde_json::json;
/// build a minimal app-compose manifest from a docker-compose YAML body
/// (single-node, no gateway).
///
/// `kms_enabled` selects KMS mode (deterministic, upgradeable per-app keys);
/// gateway and local-key-provider are off for the direct-port single-node flow.
pub fn build_app_compose(name: &str, docker_compose_yaml: &str, kms_enabled: bool) -> String {
let manifest = json!({
/// `kms_enabled` selects KMS mode (deterministic, upgradeable per-app keys).
/// `no_tee` is development-only and requires KMS to be disabled by the caller.
pub fn build_app_compose(
name: &str,
docker_compose_yaml: &str,
kms_enabled: bool,
no_tee: bool,
) -> String {
let mut manifest = json!({
"manifest_version": 2,
"name": name,
"runner": "docker-compose",
Expand All @@ -31,7 +36,27 @@ pub fn build_app_compose(name: &str, docker_compose_yaml: &str, kms_enabled: boo
// (NTS is also currently broken in guest images — see dstack#745.)
"secure_time": false,
});
if no_tee {
manifest["no_tee"] = true.into();
}
// pretty-print via Value's Display (`{:#}`) — infallible, and byte-identical
// to serde_json::to_string_pretty (avoids an expect on an unfailable Result).
format!("{manifest:#}")
}

#[cfg(test)]
mod tests {
use super::build_app_compose;

#[test]
fn no_tee_is_emitted_only_when_enabled() {
let tee: serde_json::Value =
serde_json::from_str(&build_app_compose("app", "services: {}", true, false)).unwrap();
assert!(tee.get("no_tee").is_none());

let no_tee: serde_json::Value =
serde_json::from_str(&build_app_compose("app", "services: {}", false, true)).unwrap();
assert_eq!(no_tee["no_tee"], true);
assert_eq!(no_tee["kms_enabled"], false);
}
}
24 changes: 23 additions & 1 deletion dstack/crates/dstack-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ enum Command {
/// deploy in non-KMS mode (ephemeral keys; no KMS required).
#[arg(long)]
no_kms: bool,
/// disable TEE and storage encryption for development.
#[arg(long, requires = "no_kms")]
no_tee: bool,
/// register the app's compose hash in this auth-allowlist.json. Defaults
/// to the local allowlist from `dstackup install`.
#[arg(long, value_name = "PATH")]
Expand Down Expand Up @@ -129,6 +132,7 @@ async fn main() -> Result<()> {
disk,
ports,
no_kms,
no_tee,
allowlist,
dry_run,
} => {
Expand Down Expand Up @@ -157,6 +161,7 @@ async fn main() -> Result<()> {
disk,
&ports,
no_kms,
no_tee,
allowlist.as_deref(),
dry_run,
json,
Expand Down Expand Up @@ -302,6 +307,21 @@ mod tests {
_ => panic!("expected deploy command"),
}
}

#[test]
fn no_tee_requires_no_kms() {
assert!(Cli::try_parse_from(["dstack", "deploy", "compose.yaml", "--no-tee"]).is_err());

let cli = Cli::parse_from(["dstack", "deploy", "compose.yaml", "--no-kms", "--no-tee"]);
assert!(matches!(
cli.command,
Command::Deploy {
no_kms: true,
no_tee: true,
..
}
));
}
}

#[allow(clippy::too_many_arguments)]
Expand All @@ -315,13 +335,14 @@ async fn cmd_deploy(
disk: u32,
port_specs: &[String],
no_kms: bool,
no_tee: bool,
allowlist: Option<&str>,
dry_run: bool,
json: bool,
) -> Result<()> {
let yaml = std::fs::read_to_string(compose_path)
.with_context(|| format!("reading compose file '{compose_path}'"))?;
let app_compose = compose::build_app_compose(name, &yaml, !no_kms);
let app_compose = compose::build_app_compose(name, &yaml, !no_kms, no_tee);

let mut port_maps = Vec::new();
for spec in port_specs {
Expand All @@ -336,6 +357,7 @@ async fn cmd_deploy(
memory,
disk_size: disk,
ports: port_maps.clone(),
no_tee,
..Default::default()
};

Expand Down
3 changes: 3 additions & 0 deletions dstack/dstack-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ pub struct AppCompose {
pub allowed_envs: Vec<String>,
#[serde(default)]
pub no_instance_id: bool,
/// Development only. Disables TEE and storage encryption.
#[serde(default)]
pub no_tee: bool,
#[serde(default = "default_true")]
pub secure_time: bool,
#[serde(default)]
Expand Down
44 changes: 37 additions & 7 deletions dstack/dstack-util/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,14 @@ fn cmd_gen_app_keys(args: GenAppKeysArgs) -> Result<()> {
let key_provider = KeyProvider::None {
key: key.serialize_pem(),
};
let app_keys = make_app_keys(&key, &disk_key, &k256_key, args.ca_level, key_provider)?;
let app_keys = make_app_keys(
&key,
&disk_key,
&k256_key,
args.ca_level,
key_provider,
true,
)?;
let app_keys = serde_json::to_string(&app_keys).context("Failed to serialize app keys")?;
fs::write(&args.output, app_keys).context("Failed to write app keys")?;
Ok(())
Expand All @@ -839,6 +846,7 @@ fn gen_app_keys_from_seed(
seed: &[u8],
provider: KeyProviderKind,
mr: Option<Vec<u8>>,
with_attestation: bool,
) -> Result<AppKeys> {
let key = derive_p256_key_pair_from_bytes(seed, &["app-key".as_bytes()])?;
let disk_key = derive_p256_key_pair_from_bytes(seed, &["app-disk-key".as_bytes()])?;
Expand All @@ -860,7 +868,14 @@ fn gen_app_keys_from_seed(
anyhow::bail!("KMS keys must be fetched from the KMS server")
}
};
make_app_keys(&key, &disk_key, &k256_key, 1, key_provider)
make_app_keys(
&key,
&disk_key,
&k256_key,
1,
key_provider,
with_attestation,
)
}

fn make_app_keys(
Expand All @@ -869,16 +884,23 @@ fn make_app_keys(
k256_key: &SigningKey,
ca_level: u8,
key_provider: KeyProvider,
with_attestation: bool,
) -> Result<AppKeys> {
use ra_tls::cert::CertRequest;
let pubkey = app_key.public_key_der();
let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey);
let attestation = Attestation::quote(&report_data)
.context("Failed to get attestation")?
.into_versioned();
let attestation = if with_attestation {
Some(
Attestation::quote(&report_data)
.context("failed to get attestation")?
.into_versioned(),
)
} else {
None
};
let req = CertRequest::builder()
.subject("App Root Cert")
.attestation(&attestation)
.maybe_attestation(attestation.as_ref())
.key(app_key)
.ca_level(ca_level)
.build();
Expand All @@ -891,12 +913,20 @@ fn make_app_keys(
env_crypt_key: vec![],
k256_key: k256_key.to_bytes().to_vec(),
k256_signature: vec![],
gateway_app_id: "".to_string(),
gateway_app_id: String::new(),
ca_cert: cert.pem(),
key_provider,
})
}

#[cfg(test)]
#[test]
fn generates_unattested_app_keys_without_tee() {
let keys = gen_app_keys_from_seed(&[0x42; 32], KeyProviderKind::None, None, false).unwrap();
assert!(!keys.k256_key.is_empty());
assert!(!keys.ca_cert.is_empty());
}

async fn cmd_notify_host(args: HostNotifyArgs) -> Result<()> {
let client = HostApi::load_or_default(args.url)?;
client.notify(&args.event, &args.payload).await?;
Expand Down
Loading
Loading