diff --git a/docs/security/cvm-boundaries.md b/docs/security/cvm-boundaries.md index b0a106eef..f02be61fc 100644 --- a/docs/security/cvm-boundaries.md +++ b/docs/security/cvm-boundaries.md @@ -126,7 +126,7 @@ dstack uses encrypted environment variables to allow app developers to securely - CVM decrypts the ciphertext using AESGCM with the derived shared secret - CVM parses the JSON and only stores variables listed in allowed_envs from app-compose.json - CVM performs basic regex validation on values - - Final result is stored as /dstack/.hostshared/.decrypted-env and loaded system-wide via app-compose.service + - Final result is stored as /dstack/.host-shared/.decrypted-env.json and passed to app-compose.service through `dstack-util exec-with-env` This file is not measured to RTMRs. But it is highly recommended to add application-specific integrity checks on encrypted environment variables at the application layer. See [security-best-practices.md](./security-best-practices.md) for more details. diff --git a/dstack/dstack-util/src/main.rs b/dstack/dstack-util/src/main.rs index 10373f94d..c85953e5e 100644 --- a/dstack/dstack-util/src/main.rs +++ b/dstack/dstack-util/src/main.rs @@ -104,6 +104,18 @@ enum Commands { Encrypt(EncryptArgs), /// Sample NVIDIA GPU telemetry through NVML and print it as JSON GpuInfo, + /// Exec a command with extra environment variables from a JSON object file + ExecWithEnv(ExecWithEnvArgs), +} + +#[derive(Parser)] +struct ExecWithEnvArgs { + /// JSON object mapping variable names to values + #[arg(long)] + env_file: PathBuf, + /// Command to exec, with its arguments + #[arg(last = true, required = true)] + command: Vec, } #[derive(Parser)] @@ -721,6 +733,20 @@ async fn cmd_get_keys(args: GetKeysArgs) -> Result<()> { Ok(()) } +fn cmd_exec_with_env(args: ExecWithEnvArgs) -> Result<()> { + use std::os::unix::process::CommandExt; + + let envs: std::collections::BTreeMap = + utils::deserialize_json_file(&args.env_file) + .with_context(|| format!("failed to load env from {}", args.env_file.display()))?; + let (program, program_args) = args.command.split_first().context("no command given")?; + let err = std::process::Command::new(program) + .args(program_args) + .envs(envs) + .exec(); + Err(err).with_context(|| format!("failed to exec {program}")) +} + fn cmd_decrypt(args: DecryptArgs) -> Result<()> { use dstack_types::shared_filenames::{host_shared_dir, APP_KEYS}; @@ -1667,6 +1693,7 @@ async fn main() -> Result<()> { Commands::GpuInfo => { gpu_info::cmd_gpu_info()?; } + Commands::ExecWithEnv(args) => cmd_exec_with_env(args)?, } Ok(()) diff --git a/os/common/rootfs/app-compose.service b/os/common/rootfs/app-compose.service index 5fa2ae8fd..e7d4b0bb2 100644 --- a/os/common/rootfs/app-compose.service +++ b/os/common/rootfs/app-compose.service @@ -9,10 +9,9 @@ OnFailure=dstack-boot-error@%n.service [Service] Type=oneshot RemainAfterExit=true -EnvironmentFile=-/dstack/.host-shared/.decrypted-env WorkingDirectory=/dstack -ExecStart=/bin/app-compose.sh -ExecStop=/bin/app-compose.sh stop +ExecStart=/bin/dstack-util exec-with-env --env-file /dstack/.host-shared/.decrypted-env.json -- /bin/app-compose.sh +ExecStop=/bin/dstack-util exec-with-env --env-file /dstack/.host-shared/.decrypted-env.json -- /bin/app-compose.sh stop StandardOutput=journal+console StandardError=journal+console