Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/security-guide/cvm-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ This is the main configuration file for the application in JSON format:
| secure_time | boolean | Whether secure time is enabled |
| pre_launch_script | string | Prelaunch bash script that runs before execute `docker compose up` |
| init_script | string | Bash script that executed prior to dockerd startup |
| storage_fs | string | Filesystem type for the data disk of the CVM. Supported values: "zfs", "ext4". default to "zfs". **ZFS:** Ensures filesystem integrity with built-in data protection features. **ext4:** Provides better performance for database applications with lower overhead and faster I/O operations, but no strong integrity protection. |


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.
Expand Down
2 changes: 2 additions & 0 deletions dstack-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub struct AppCompose {
pub no_instance_id: bool,
#[serde(default = "default_true")]
pub secure_time: bool,
#[serde(default)]
pub storage_fs: Option<String>,
}

fn default_true() -> bool {
Expand Down
200 changes: 180 additions & 20 deletions dstack-util/src/system_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

use std::{
collections::{BTreeMap, BTreeSet},
fmt::Display,
ops::Deref,
path::{Path, PathBuf},
process::Command,
str::FromStr,
};

use anyhow::{anyhow, bail, Context, Result};
Expand Down Expand Up @@ -77,6 +80,67 @@ struct InstanceInfo {
app_id: Vec<u8>,
}

#[derive(Debug, Clone, Copy, PartialEq, Default)]
enum FsType {
#[default]
Zfs,
Ext4,
}

impl Display for FsType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FsType::Zfs => write!(f, "zfs"),
FsType::Ext4 => write!(f, "ext4"),
}
}
}

impl FromStr for FsType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"zfs" => Ok(FsType::Zfs),
"ext4" => Ok(FsType::Ext4),
_ => bail!("Invalid filesystem type: {s}, supported types: zfs, ext4"),
}
}
}

#[derive(Debug, Clone, Default)]
struct DstackOptions {
storage_encrypted: bool,
storage_fs: FsType,
}

fn parse_dstack_options(shared: &HostShared) -> Result<DstackOptions> {
let cmdline = fs::read_to_string("/proc/cmdline").context("Failed to read /proc/cmdline")?;

let mut options = DstackOptions {
storage_encrypted: true, // Default to encryption enabled
storage_fs: FsType::Zfs, // Default to ZFS
};

for param in cmdline.split_whitespace() {
if let Some(value) = param.strip_prefix("dstack.storage_encrypted=") {
match value {
"0" | "false" | "no" | "off" => options.storage_encrypted = false,
"1" | "true" | "yes" | "on" => options.storage_encrypted = true,
_ => {
bail!("Invalid value for dstack.storage_encrypted: {value}");
}
}
} else if let Some(value) = param.strip_prefix("dstack.storage_fs=") {
options.storage_fs = value.parse().context("Failed to parse dstack.storage_fs")?;
}
}

if let Some(fs) = &shared.app_compose.storage_fs {
options.storage_fs = fs.parse().context("Failed to parse storage_fs")?;
}
Ok(options)
}

impl InstanceInfo {
fn is_initialized(&self) -> bool {
!self.instance_id_seed.is_empty()
Expand Down Expand Up @@ -433,38 +497,122 @@ impl<'a> Stage0<'a> {
}
}

async fn mount_data_disk(&self, initialized: bool, disk_crypt_key: &str) -> Result<()> {
async fn mount_data_disk(
&self,
initialized: bool,
disk_crypt_key: &str,
opts: &DstackOptions,
) -> Result<()> {
let name = "dstack_data_disk";
let fs_dev = "/dev/mapper/".to_string() + name;
let mount_point = &self.args.mount_point;

// Determine the device to use based on encryption settings
let fs_dev = if opts.storage_encrypted {
format!("/dev/mapper/{name}")
} else {
self.args.device.to_string_lossy().to_string()
};

cmd!(mkdir -p $mount_point).context("Failed to create mount point")?;

if !initialized {
self.vmm
.notify_q("boot.progress", "initializing data disk")
.await;
info!("Setting up disk encryption");
self.luks_setup(disk_crypt_key, name)?;
cmd! {
mkdir -p $mount_point;
zpool create -o autoexpand=on dstack $fs_dev;
zfs create -o mountpoint=$mount_point -o atime=off -o checksum=blake3 dstack/data;

if opts.storage_encrypted {
info!("Setting up disk encryption");
self.luks_setup(disk_crypt_key, name)?;
} else {
info!("Skipping disk encryption as requested by kernel cmdline");
}

match opts.storage_fs {
FsType::Zfs => {
info!("Creating ZFS filesystem");
cmd! {
zpool create -o autoexpand=on dstack $fs_dev;
zfs create -o mountpoint=$mount_point -o atime=off -o checksum=blake3 dstack/data;
}
.context("Failed to create zpool")?;
}
FsType::Ext4 => {
info!("Creating ext4 filesystem");
cmd! {
mkfs.ext4 -F $fs_dev;
mount $fs_dev $mount_point;
}
.context("Failed to create ext4 filesystem")?;
}
}
.context("Failed to create zpool")?;
} else {
self.vmm
.notify_q("boot.progress", "mounting data disk")
.await;
info!("Mounting encrypted data disk");
self.open_encrypted_volume(disk_crypt_key, name)?;
cmd! {
zpool import dstack;
zpool status dstack;
zpool online -e dstack $fs_dev; // triggers autoexpand

if opts.storage_encrypted {
info!("Mounting encrypted data disk");
self.open_encrypted_volume(disk_crypt_key, name)?;
} else {
info!("Mounting unencrypted data disk");
}

match opts.storage_fs {
FsType::Zfs => {
cmd! {
zpool import dstack;
zpool status dstack;
zpool online -e dstack $fs_dev; // triggers autoexpand
}
.context("Failed to import zpool")?;
if cmd!(mountpoint -q $mount_point).is_err() {
cmd!(zfs mount dstack/data).context("Failed to mount zpool")?;
}
}
FsType::Ext4 => {
Self::mount_e2fs(&fs_dev, mount_point)
.context("Failed to mount ext4 filesystem")?;
}
}
}
Ok(())
}

fn mount_e2fs(dev: &impl AsRef<Path>, mount_point: &impl AsRef<Path>) -> Result<()> {
let dev = dev.as_ref();
let mount_point = mount_point.as_ref();
info!("Checking filesystem");

let e2fsck_status = Command::new("e2fsck")
.arg("-f")
.arg("-p")
.arg(dev)
.status()
.with_context(|| format!("Failed to run e2fsck on {}", dev.display()))?;

match e2fsck_status.code() {
Some(0 | 1) => {}
Some(code) => {
bail!(
"e2fsck exited with status {code} while checking {}",
dev.display()
);
}
.context("Failed to import zpool")?;
if cmd!(mountpoint -q $mount_point).is_err() {
cmd!(zfs mount dstack/data).context("Failed to mount zpool")?;
None => {
bail!(
"e2fsck terminated by signal while checking {}",
dev.display()
);
}
}

cmd! {
info "Trying to resize filesystem if needed";
resize2fs $dev;
info "Mounting filesystem";
mount $dev $mount_point;
}
.context("Failed to prepare ext4 filesystem")?;
Ok(())
}

Expand Down Expand Up @@ -614,9 +762,21 @@ impl<'a> Stage0<'a> {
let keys_json = serde_json::to_string(&app_keys).context("Failed to serialize app keys")?;
fs::write(self.app_keys_file(), keys_json).context("Failed to write app keys")?;

// Parse kernel command line options
let opts = parse_dstack_options(&self.shared).context("Failed to parse kernel cmdline")?;
extend_rtmr3("storage-fs", opts.storage_fs.to_string().as_bytes())?;
info!(
"Filesystem options: encryption={}, filesystem={:?}",
opts.storage_encrypted, opts.storage_fs
);

self.vmm.notify_q("boot.progress", "unsealing env").await;
self.mount_data_disk(is_initialized, &hex::encode(&app_keys.disk_crypt_key))
.await?;
self.mount_data_disk(
is_initialized,
&hex::encode(&app_keys.disk_crypt_key),
&opts,
)
.await?;
self.vmm
.notify_q(
"instance.info",
Expand Down
85 changes: 82 additions & 3 deletions vmm/src/console.html
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,56 @@
font-family: monospace;
}
</style>
<style>
/* Help icon tooltip styles */
.help-icon {
display: inline-block;
margin-left: 5px;
color: #666;
cursor: help;
position: relative;
}

.help-icon:hover {
color: #4285F4;
}

.help-icon .tooltip {
visibility: hidden;
width: 300px;
background-color: #333;
color: #fff;
text-align: left;
border-radius: 6px;
padding: 10px;
position: absolute;
z-index: 1000;
bottom: 125%;
left: 50%;
margin-left: -150px;
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
line-height: 1.4;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}

.help-icon .tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}

.help-icon:hover .tooltip {
visibility: visible;
opacity: 1;
}
</style>
<style>
.ml-text-ro {
white-space: pre-wrap;
Expand Down Expand Up @@ -662,9 +712,27 @@ <h2>Deploy a new instance</h2>
</div>

<div class="form-group">
<label for="diskSize">Disk Size (GB)</label>
<label for="diskSize">Storage (GB)</label>
<input id="diskSize" v-model.number="vmForm.disk_size" type="number"
placeholder="Disk size in GB" required>
placeholder="Storage size in GB" required>
</div>

<div class="form-group">
<label for="storageFs">Storage Filesystem
<span class="help-icon">
<i class="fas fa-question-circle"></i>
<span class="tooltip">
<strong>ZFS:</strong> Ensures filesystem integrity with built-in data protection features.<br><br>
<strong>ext4:</strong> Provides better performance for database applications
with lower overhead and faster I/O operations.
</span>
</span>
</label>
<select id="storageFs" v-model="vmForm.storage_fs">
<option value="">Default (ZFS)</option>
<option value="zfs">ZFS</option>
<option value="ext4">ext4</option>
</select>
</div>

<div class="form-group full-width">
Expand Down Expand Up @@ -1000,6 +1068,11 @@ <h4 style="margin: 0 0 12px 0;">App Information</h4>
<span class="detail-label">Features:</span>
<span class="detail-value">{{ getFlags(vm) || 'None' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Storage FS:</span>
<span class="detail-value">{{ vm.appCompose?.storage_fs ?
vm.appCompose.storage_fs.toUpperCase() : 'ZFS (default)' }}</span>
</div>
</div>

<h4 style="margin: 0 0 12px 0;">Docker Compose File</h4>
Expand Down Expand Up @@ -1464,6 +1537,7 @@ <h3>Derive VM</h3>
username: '',
token_key: ''
},
storage_fs: '',
app_id: '',
kms_enabled: true,
local_key_provider_enabled: false,
Expand Down Expand Up @@ -1642,6 +1716,10 @@ <h3>Derive VM</h3>
"secure_time": false,
};

if (vmForm.value.storage_fs) {
app_compose.storage_fs = vmForm.value.storage_fs;
}

if (vmForm.value.preLaunchScript?.trim()) {
app_compose.pre_launch_script = vmForm.value.preLaunchScript;
}
Expand Down Expand Up @@ -2336,7 +2414,8 @@ <h3>Derive VM</h3>
() => vmForm.value.docker_config.enabled,
() => vmForm.value.docker_config.username,
() => vmForm.value.docker_config.token_key,
() => vmForm.value.encryptedEnvs
() => vmForm.value.encryptedEnvs,
() => vmForm.value.storage_fs
], async () => {
try {
const appCompose = await makeAppComposeFile();
Expand Down
Loading