mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-21 06:11:31 +08:00
* fix(vm): enforce the configured workload identity Reject conflicting policy users and groups before VM image preparation and before guest attach or process startup changes state. Validate supervisor policy updates against the protected VM workload identity. Preserve the gateway CA transport and capability-free sandbox launcher. Signed-off-by: Shiju <shiju@nvidia.com> * test(sandbox): clarify VM identity rejection fixtures Name invalid user and group fixtures distinctly and move the final workload identity into its group mismatch test. Signed-off-by: Shiju <shiju@nvidia.com> * fix(supervisor): align VM identity startup with current APIs Pass the optional rejection-log key for VM identity failures and keep generic startup-write regressions free of VM identity constraints. Repair the call sites after the branch rebase so the identity and cleanup proposals compile against the current startup helpers. Signed-off-by: Shiju <shiju@nvidia.com> * fix(vm): restore inactive sandbox workload identity Recover the persisted overlay owner before publishing stopped and terminal sandboxes. Keep resources manageable when identity metadata is invalid. Clarify fixed MicroVM ownership in policy-generation guidance. Signed-off-by: Shiju <shiju@nvidia.com> * test(vm): flush identity fixture before restart Persist the canonical identity file before readiness and report the observed exec, canonical and file-owner identities before comparing them. Signed-off-by: Shiju <shiju@nvidia.com> --------- Signed-off-by: Shiju <shiju@nvidia.com>
215 lines
7.6 KiB
Rust
215 lines
7.6 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
//! VM-driver-specific assertions for the sandbox root filesystem.
|
|
|
|
use std::process::Stdio;
|
|
use std::time::Duration;
|
|
|
|
use openshell_e2e::harness::binary::openshell_cmd;
|
|
use openshell_e2e::harness::cli::{run_cli, wait_for_sandbox_phase};
|
|
use openshell_e2e::harness::output::strip_ansi;
|
|
use openshell_e2e::harness::sandbox::{SandboxGuard, unique_sandbox_name};
|
|
|
|
#[tokio::test]
|
|
async fn vm_overlay() {
|
|
let mut sandbox = SandboxGuard::create_keep(
|
|
&["sh", "-c", "echo vm-sandbox-ready; exec sleep infinity"],
|
|
"vm-sandbox-ready",
|
|
)
|
|
.await
|
|
.expect("sandbox create should start a durable main process");
|
|
|
|
let script = concat!(
|
|
"set -eu; ",
|
|
"test \"$(stat -f -c %T /)\" = \"overlayfs\"; ",
|
|
"printf \"overlay-write\\n\" > /sandbox/overlay-check; ",
|
|
"test \"$(cat /sandbox/overlay-check)\" = \"overlay-write\"; ",
|
|
"if [ -e /opt/openshell/tls/tls.key ]; then ",
|
|
"test \"$(stat -c %a /opt/openshell/tls/tls.key)\" = \"600\"; ",
|
|
"fi; ",
|
|
"echo vm-overlay-ok",
|
|
);
|
|
|
|
let mut exec_cmd = openshell_cmd();
|
|
exec_cmd
|
|
.args(["sandbox", "exec", "--name", &sandbox.name, "--no-tty", "--"])
|
|
.arg("sh")
|
|
.arg("-lc")
|
|
.arg(script)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let output = exec_cmd
|
|
.output()
|
|
.await
|
|
.expect("failed to run VM overlay assertion");
|
|
let combined = strip_ansi(&format!(
|
|
"{}{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr),
|
|
));
|
|
assert!(
|
|
output.status.success() && combined.contains("vm-overlay-ok"),
|
|
"VM overlay assertion failed (status {:?}):\n{combined}",
|
|
output.status.code(),
|
|
);
|
|
|
|
sandbox.cleanup().await;
|
|
}
|
|
|
|
// VM stop terminates the guest without flushing its page cache. Flush this
|
|
// fixture before readiness so restart checks durable file content and ownership.
|
|
const IDENTITY_MAIN: &str = "set -eu; if ! test -f /sandbox/canonical-identity; then printf '%s:%s\\n' \"$(id -u)\" \"$(id -g)\" > /sandbox/canonical-identity; sync; fi; echo vm-identity-ready; exec sleep infinity";
|
|
|
|
fn identity_policy(user: &str, group: &str) -> tempfile::NamedTempFile {
|
|
let file = tempfile::NamedTempFile::new().expect("temporary identity policy");
|
|
std::fs::write(
|
|
file.path(),
|
|
format!(
|
|
r#"version: 1
|
|
filesystem_policy:
|
|
include_workdir: true
|
|
read_only: [/usr, /lib, /lib64, /proc, /etc, /dev/urandom]
|
|
read_write: [/sandbox, /tmp, /dev/null]
|
|
landlock:
|
|
compatibility: hard_requirement
|
|
process:
|
|
run_as_user: "{user}"
|
|
run_as_group: "{group}"
|
|
network_policies: {{}}
|
|
"#
|
|
),
|
|
)
|
|
.expect("write identity policy");
|
|
file
|
|
}
|
|
|
|
async fn assert_workload_identity(sandbox: &SandboxGuard) -> (String, String) {
|
|
let output = sandbox.exec(&[
|
|
"sh", "-c",
|
|
"set -eu; actual=$(id -u):$(id -g); canonical=$(cat /sandbox/canonical-identity); owner=$(stat -c %u:%g /sandbox/canonical-identity); printf 'exec=%s canonical=%s owner=%s\\n' \"$actual\" \"$canonical\" \"$owner\"; test \"$canonical\" = \"$actual\"; test \"$owner\" = \"$actual\"; printf 'identity=%s\\n' \"$actual\"",
|
|
]).await.expect("canonical and exec identities and file ownership agree");
|
|
let clean = strip_ansi(&output);
|
|
let pair = clean
|
|
.lines()
|
|
.find_map(|line| line.strip_prefix("identity="))
|
|
.expect("observed workload identity");
|
|
let (uid, gid) = pair.split_once(':').expect("UID:GID pair");
|
|
let (status, code) = run_cli(&["sandbox", "get", &sandbox.name, "--output", "json"]).await;
|
|
assert_eq!(code, 0, "{status}");
|
|
let status: serde_json::Value =
|
|
serde_json::from_str(&strip_ansi(&status)).expect("sandbox status JSON");
|
|
assert!(
|
|
status["conditions"]
|
|
.as_array()
|
|
.expect("conditions")
|
|
.iter()
|
|
.any(|condition| {
|
|
condition["type"] == "WorkloadIdentity"
|
|
&& condition["message"] == format!("Resolved workload UID:GID is {pair}")
|
|
}),
|
|
"status must report observed workload identity: {status}"
|
|
);
|
|
(uid.to_string(), gid.to_string())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn vm_identity_matches_status_and_rejects_conflicts() {
|
|
// Use observed defaults so the test also works with configured driver IDs.
|
|
// The canonical process writes a file before signaling readiness; exec
|
|
// verifies its content and ownership independently of the status report.
|
|
let default_policy = identity_policy("", "");
|
|
let mut sandbox = SandboxGuard::create_keep_with_args(
|
|
&[
|
|
"--policy",
|
|
default_policy.path().to_str().unwrap(),
|
|
"--no-tty",
|
|
],
|
|
&["sh", "-c", IDENTITY_MAIN],
|
|
"vm-identity-ready",
|
|
)
|
|
.await
|
|
.expect("omitted selectors use driver identity");
|
|
let (uid, gid) = assert_workload_identity(&sandbox).await;
|
|
let original = sandbox
|
|
.exec(&["stat", "-c", "%u:%g", "/sandbox/canonical-identity"])
|
|
.await
|
|
.unwrap();
|
|
let (output, code) = run_cli(&["sandbox", "stop", &sandbox.name]).await;
|
|
assert_eq!(code, 0, "{output}");
|
|
wait_for_sandbox_phase(&sandbox.name, "Stopped", Duration::from_secs(60))
|
|
.await
|
|
.unwrap();
|
|
let (output, code) = run_cli(&["sandbox", "start", &sandbox.name]).await;
|
|
assert_eq!(code, 0, "{output}");
|
|
wait_for_sandbox_phase(&sandbox.name, "Ready", Duration::from_secs(120))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
assert_workload_identity(&sandbox).await,
|
|
(uid.clone(), gid.clone())
|
|
);
|
|
let restored = sandbox
|
|
.exec(&["stat", "-c", "%u:%g", "/sandbox/canonical-identity"])
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
strip_ansi(&restored),
|
|
strip_ansi(&original),
|
|
"restart must retain the owned overlay file"
|
|
);
|
|
sandbox.cleanup().await;
|
|
|
|
for (user, group) in [
|
|
(uid.as_str(), ""),
|
|
("", gid.as_str()),
|
|
("sandbox", "sandbox"),
|
|
] {
|
|
let policy = identity_policy(user, group);
|
|
let mut matching = SandboxGuard::create_keep_with_args(
|
|
&["--policy", policy.path().to_str().unwrap(), "--no-tty"],
|
|
&["sh", "-c", IDENTITY_MAIN],
|
|
"vm-identity-ready",
|
|
)
|
|
.await
|
|
.expect("matching numeric or symbolic selector");
|
|
assert_eq!(
|
|
assert_workload_identity(&matching).await,
|
|
(uid.clone(), gid.clone())
|
|
);
|
|
matching.cleanup().await;
|
|
}
|
|
|
|
let wrong_uid = if uid == "10000" { "10001" } else { "10000" };
|
|
let wrong_gid = if gid == "10000" { "10001" } else { "10000" };
|
|
for (user, group, field) in [
|
|
(wrong_uid, "", "run_as_user"),
|
|
("", wrong_gid, "run_as_group"),
|
|
] {
|
|
let policy = identity_policy(user, group);
|
|
let name = unique_sandbox_name();
|
|
let mut cleanup = SandboxGuard::manage_existing(name.clone());
|
|
let result = SandboxGuard::create(&[
|
|
"--name",
|
|
&name,
|
|
"--policy",
|
|
policy.path().to_str().unwrap(),
|
|
"--no-tty",
|
|
])
|
|
.await;
|
|
let error = match result {
|
|
Ok(mut launched) => {
|
|
launched.cleanup().await;
|
|
panic!("conflicting VM identity reached Ready");
|
|
}
|
|
Err(error) => error,
|
|
};
|
|
assert!(
|
|
error.contains(field) && error.contains(&format!("{uid}:{gid}")),
|
|
"rejection must name selector and resolved identity: {error}"
|
|
);
|
|
cleanup.cleanup().await;
|
|
}
|
|
}
|