Skip to content
Open
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
49 changes: 42 additions & 7 deletions docs/amd-sev-snp.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,55 @@ preflight does not replace firmware, kernel, QEMU, or OVMF validation.

## Install dstack on an SNP host

Pull a current unified image and select the platform explicitly:
Use a release containing the `sev-snp` key provider in the guest OS and VMM. The
KMS container and client attestation verifiers must recognize the new provider
metadata. Set `VERSION` to the OS release version and `KMS_IMAGE` to the KMS
container reference, then install:

```bash
VERSION=0.6.0
sudo dstackup image pull --version "$VERSION"
sudo dstackup install --platform amd-sev-snp --image "dstack-$VERSION"
sudo dstackup install --platform amd-sev-snp \
--image "dstack-$VERSION" --kms-image "$KMS_IMAGE"
sudo dstackup status
```

The default `--platform auto` mode also selects SEV-SNP when the host CPU flags
advertise `sev_snp`. Explicit selection is preferable while commissioning a
host because it fails immediately when the required SNP device is absent.
Confirm that installation reports `booted` for the KMS CVM. `dstackup` selects
the `sev-snp` provider for the KMS CVM on AMD hosts. The default
`--platform auto` also detects SEV-SNP from the host CPU flags. Explicit
selection checks for the required SNP device before installation. Review the
[KMS key-release policy](#kms-key-release-policy) before serving applications.

Unlike the TDX path, SEV-SNP does not use the local SGX key provider.
## Run KMS with persistent local keys

The `sev-snp` provider lets a KMS CVM recover its encrypted disk and stored KMS
roots after restart on the same AMD hardware. For a manually managed KMS CVM,
set this field in its app-compose document:

```json
{"key_provider": "sev-snp"}
```

The deployment CLI also accepts `dstack deploy --key-provider sev-snp`. Application
CVMs continue using `key_provider: kms` and the existing KMS API.

The guest derives a seed through `SNP_GET_DERIVED_KEY` using legacy VCEK at
VMPL0. The seed binds to launch measurement, guest policy, HOST_DATA, launch
identity, and a pinned TCB. The guest requires debugging disabled and encrypted
storage, and verifies the configuration binding before deriving the seed.

Before initializing a new disk, the guest persists public derivation parameters
in `.instance_info` through the host API. Preserve this file, the encrypted
disk, and the original launch configuration together. Missing parameters for
an existing disk, unknown versions, or derivation/unlock failures stop startup.

Recovery requires the original hardware and bound launch inputs. For OS or
compose changes, or hardware replacement, preserve KMS roots through the
existing onboarding/replication flow. Firmware updates must support the stored
derivation TCB and satisfy attestation policy.

The entire KMS CVM, including its OS, is trusted. Protect KMS root and sealing
keys by keeping the KMS OS and workload under operator control. Application
key release continues to follow the existing KMS authorization policy.

## Attestation and image identity

Expand Down
8 changes: 8 additions & 0 deletions dstack/Cargo.lock

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

1 change: 1 addition & 0 deletions dstack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ schnorrkel = "0.11.4"
sha2 = { version = "0.10.8", default-features = false }
sha3 = "0.10.8"
subtle = "2"
zeroize = "1.8"
blake2 = "0.10.6"
tokio-rustls = { version = "0.26.2", features = ["ring"] }
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
Expand Down
1 change: 1 addition & 0 deletions dstack/cert-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ impl CertRequestClient {
match &keys.key_provider {
KeyProvider::None { key }
| KeyProvider::Local { key, .. }
| KeyProvider::SevSnp { key }
| KeyProvider::Tpm { key, .. } => {
let ca = CaCert::new(keys.ca_cert.clone(), key.clone())
.context("Failed to create CA")?;
Expand Down
1 change: 1 addition & 0 deletions dstack/crates/dstack-cli-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ rustix = { version = "0.38", features = ["fs"] }

[dev-dependencies]
toml.workspace = true
yaml-rust2.workspace = true
52 changes: 49 additions & 3 deletions dstack/crates/dstack-cli-core/src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! build the app-compose manifest — the JSON document the VMM hashes (to derive
//! the app id) and deploys. The raw docker-compose YAML is embedded as a string.

use dstack_types::KeyProviderKind;
use serde_json::json;

/// build a minimal app-compose manifest from a docker-compose YAML body
Expand Down Expand Up @@ -34,7 +35,11 @@ pub fn build_app_compose_with_runtime(
build_app_compose_with_runtime_and_volumes(
name,
docker_compose_yaml,
kms_enabled,
if kms_enabled {
KeyProviderKind::Kms
} else {
KeyProviderKind::None
},
runner,
snapshotter,
&[],
Expand All @@ -45,7 +50,7 @@ pub fn build_app_compose_with_runtime(
pub fn build_app_compose_with_runtime_and_volumes(
name: &str,
docker_compose_yaml: &str,
kms_enabled: bool,
key_provider: KeyProviderKind,
runner: &str,
snapshotter: Option<&str>,
verity_volumes: &[dstack_types::VerityVolume],
Expand All @@ -55,7 +60,7 @@ pub fn build_app_compose_with_runtime_and_volumes(
"name": name,
"runner": runner,
"docker_compose_file": docker_compose_yaml,
"kms_enabled": kms_enabled,
"kms_enabled": key_provider.is_kms(),
"gateway_enabled": false,
"local_key_provider_enabled": false,
"public_logs": true,
Expand All @@ -74,7 +79,48 @@ pub fn build_app_compose_with_runtime_and_volumes(
if !verity_volumes.is_empty() {
manifest["verity_volumes"] = json!(verity_volumes);
}
// Preserve legacy KMS/None manifest bytes, which determine app identity.
if !matches!(key_provider, KeyProviderKind::Kms | KeyProviderKind::None) {
manifest["key_provider"] = json!(key_provider);
}
// 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::*;

#[test]
fn typed_provider_matches_previous_cli_manifest_encoding() {
let volumes = [dstack_types::VerityVolume {
source: "test.img".into(),
verity_root: [1; 32],
target: "/test".into(),
}];
for volumes in [&[][..], &volumes[..]] {
let build = |provider| {
build_app_compose_with_runtime_and_volumes(
"test",
"",
provider,
"nerdctl-compose",
Some("soci"),
volumes,
)
};
for provider in [
KeyProviderKind::SevSnp,
KeyProviderKind::Local,
KeyProviderKind::Tpm,
] {
// The old CLI serialized a legacy manifest, then inserted the provider.
let mut legacy: serde_json::Value =
serde_json::from_str(&build(KeyProviderKind::None)).unwrap();
legacy["key_provider"] = json!(provider);
assert_eq!(build(provider), format!("{legacy:#}"));
}
}
}
}
Loading