mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
fix(drivers): narrow OCI workspace path restrictions
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
This commit is contained in:
@@ -429,8 +429,9 @@ live.
|
||||
Docker and Podman resolve OCI `Config.User` and `Config.WorkingDir` from one
|
||||
immutable image inspection. Empty, `/`, and explicit `/sandbox` values use the
|
||||
managed `/sandbox` workspace. Custom paths must be normalized absolute paths
|
||||
that do not overlap `/proc`, `/sys`, `/dev`, or OpenShell's private paths. Image
|
||||
and driver mounts cannot cover the workspace path or one of its parents.
|
||||
that do not overlap `/proc`, `/sys`, `/dev`, or private mounts still inside the
|
||||
workload container. Image and driver mounts cannot cover the workspace path or
|
||||
one of its parents. Supervisor-only paths are not reserved in the workload.
|
||||
|
||||
Podman mounts its persistent workspace volume at a custom root. When the volume
|
||||
is first created, Podman copies existing image-directory contents into it.
|
||||
|
||||
@@ -84,9 +84,17 @@ pub fn validate_mount_subpath(subpath: &str) -> Result<(), String> {
|
||||
/// Workspace collisions depend on the inspected image's resolved working
|
||||
/// directory and are checked separately by `validate_workspace_mount_target`.
|
||||
pub fn validate_container_mount_target(target: &str) -> Result<(), String> {
|
||||
validate_container_mount_target_for_workload(target, CONTROL_ROOTS)
|
||||
}
|
||||
|
||||
/// Validate a mount target against paths used by this specific workload.
|
||||
pub fn validate_container_mount_target_for_workload(
|
||||
target: &str,
|
||||
workload_reserved_paths: &[&str],
|
||||
) -> Result<(), String> {
|
||||
let normalized = normalize_absolute_container_path(target, "mount target")?;
|
||||
let path = Path::new(&normalized);
|
||||
for reserved in CONTROL_ROOTS {
|
||||
for reserved in workload_reserved_paths {
|
||||
let reserved = Path::new(reserved);
|
||||
if paths_overlap(path, reserved) {
|
||||
return Err(format!(
|
||||
@@ -106,6 +114,16 @@ pub fn validate_container_mount_target(target: &str) -> Result<(), String> {
|
||||
/// value and the path passed to the supervisor cannot be interpreted
|
||||
/// differently.
|
||||
pub fn resolve_oci_workspace_root(working_dir: &str) -> Result<String, String> {
|
||||
// The sandbox runtime checks syntax and OCI mounts again; each compute
|
||||
// driver checks its own workload mounts before admitting the workspace.
|
||||
resolve_oci_workspace_root_for_workload(working_dir, &[])
|
||||
}
|
||||
|
||||
/// Resolve a workspace against paths still mounted inside this workload.
|
||||
pub fn resolve_oci_workspace_root_for_workload(
|
||||
working_dir: &str,
|
||||
workload_reserved_paths: &[&str],
|
||||
) -> Result<String, String> {
|
||||
if working_dir.is_empty() || working_dir == "/" {
|
||||
return Ok(DEFAULT_WORKSPACE_ROOT.to_string());
|
||||
}
|
||||
@@ -113,7 +131,7 @@ pub fn resolve_oci_workspace_root(working_dir: &str) -> Result<String, String> {
|
||||
for runtime_path in OCI_RUNTIME_MOUNT_ROOTS {
|
||||
validate_workspace_reserved_path(&workspace_root, runtime_path, "OCI runtime mount")?;
|
||||
}
|
||||
for control_path in CONTROL_ROOTS {
|
||||
for control_path in workload_reserved_paths {
|
||||
validate_workspace_control_path(&workspace_root, control_path)?;
|
||||
}
|
||||
|
||||
@@ -178,23 +196,6 @@ fn validate_workspace_reserved_path(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject a mount that contains or is contained by a runtime-configured
|
||||
/// `OpenShell` control path, such as the sandbox SSH socket.
|
||||
pub fn validate_mount_control_path(target: &str, control_path: &str) -> Result<(), String> {
|
||||
let normalized_target = normalize_absolute_container_path(target, "mount target")?;
|
||||
let normalized_control =
|
||||
normalize_absolute_container_path(control_path, "OpenShell control path")?;
|
||||
if paths_overlap(
|
||||
Path::new(&normalized_target),
|
||||
Path::new(&normalized_control),
|
||||
) {
|
||||
return Err(format!(
|
||||
"mount target '{target}' conflicts with OpenShell control path '{control_path}'"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject a user-supplied mount that would replace or contain the resolved
|
||||
/// workspace root. Mounts below the workspace remain valid.
|
||||
pub fn validate_workspace_mount_target(target: &str, workspace_root: &str) -> Result<(), String> {
|
||||
@@ -278,86 +279,33 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oci_workspace_root_rejects_runtime_and_openshell_control_path_collisions() {
|
||||
fn oci_workspace_root_rejects_runtime_and_selected_workload_paths() {
|
||||
let reserved = &["/control"];
|
||||
for invalid in [
|
||||
"/proc",
|
||||
"/proc/self",
|
||||
"/sys",
|
||||
"/sys/fs/cgroup",
|
||||
"/dev",
|
||||
"/dev/shm",
|
||||
"/etc",
|
||||
"/opt",
|
||||
"/opt/openshell",
|
||||
"/opt/openshell/bin/project",
|
||||
"/etc/openshell/tls/client",
|
||||
"/etc/openshell/auth",
|
||||
"/etc/openshell/skills",
|
||||
"/etc/openshell-tls",
|
||||
"/run",
|
||||
"/run/openshell/cache",
|
||||
"/run/openshell-sidecar/control.sock",
|
||||
"/run/netns/project",
|
||||
"/var/run/netns/project",
|
||||
"/control",
|
||||
"/control/data",
|
||||
] {
|
||||
assert!(
|
||||
resolve_oci_workspace_root(invalid).is_err(),
|
||||
"expected control-path workspace '{invalid}' to be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
for valid in [
|
||||
"/app",
|
||||
"/etc/project",
|
||||
"/home/app",
|
||||
"/opt/app",
|
||||
"/usr/bin/project",
|
||||
"/usr/src/app",
|
||||
"/var/lib/app",
|
||||
"/var/app/current",
|
||||
"/var/task",
|
||||
"/var/www/app",
|
||||
"/processor",
|
||||
"/system",
|
||||
"/device",
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_oci_workspace_root(valid).unwrap(),
|
||||
valid,
|
||||
"expected application workspace '{valid}' to remain valid"
|
||||
resolve_oci_workspace_root_for_workload(invalid, reserved).is_err(),
|
||||
"expected workspace '{invalid}' to be rejected"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
resolve_oci_workspace_root_for_workload("/etc/openshell", reserved).unwrap(),
|
||||
"/etc/openshell"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn container_target_rejects_reserved_openshell_tls_legacy_path() {
|
||||
let err = validate_container_mount_target("/etc/openshell-tls/proxy/client").unwrap_err();
|
||||
|
||||
assert!(err.contains("/etc/openshell-tls"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn container_target_rejects_reserved_openshell_tree() {
|
||||
let err = validate_container_mount_target("/etc/openshell/tls/client").unwrap_err();
|
||||
|
||||
assert!(err.contains("/etc/openshell"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn container_target_does_not_prefix_match_unrelated_paths() {
|
||||
validate_container_mount_target("/etc/openshell-tools").unwrap();
|
||||
validate_container_mount_target("/run/openshell-tools").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_target_rejects_runtime_configured_control_path_overlap() {
|
||||
for target in ["/custom", "/custom/ssh.sock", "/custom/ssh.sock/cache"] {
|
||||
assert!(
|
||||
validate_mount_control_path(target, "/custom/ssh.sock").is_err(),
|
||||
"expected '{target}' to conflict with the configured control path"
|
||||
);
|
||||
}
|
||||
validate_mount_control_path("/custom-other", "/custom/ssh.sock").unwrap();
|
||||
fn container_target_uses_selected_workload_paths() {
|
||||
let reserved = &["/control"];
|
||||
assert!(validate_container_mount_target_for_workload("/control/data", reserved).is_err());
|
||||
validate_container_mount_target_for_workload("/control-tools", reserved).unwrap();
|
||||
validate_container_mount_target_for_workload("/etc/openshell", reserved).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -67,7 +67,8 @@ traverse every parent and write and enter the workdir; OpenShell does not
|
||||
change its ownership or mode.
|
||||
|
||||
Image `VOLUME` declarations and user mounts must not cover the workdir, one of
|
||||
its parents, or the reserved `/.openshell` runtime/channel tree. OpenShell asks
|
||||
its parents, the workload's `/.openshell` runtime/channel tree, or its
|
||||
`/run/openshell-supervisor-ca` mount. OpenShell asks
|
||||
the kernel to validate access under the final identity, so POSIX ACL and host
|
||||
LSM decisions remain authoritative.
|
||||
|
||||
@@ -114,7 +115,8 @@ mount types are:
|
||||
Host bind mounts are disabled by default because they expose daemon-host paths
|
||||
to sandbox requests. User bind and volume mounts are read-only by default.
|
||||
Targets must be absolute, normalized paths and cannot overlap the workspace
|
||||
root or OpenShell control paths.
|
||||
root or private mounts still used inside the workload. Supervisor-only paths
|
||||
are allowed.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -105,6 +105,10 @@ const BOUNDARY_CONFIG_MOUNT_PATH: &str = "/.openshell/channel/sandbox/bootstrap.
|
||||
const BOUNDARY_SOCKET_MOUNT_PATH: &str = "/.openshell/channel/sandbox/control.sock";
|
||||
const BOUNDARY_CERTIFICATE_MOUNT_PATH: &str = "/.openshell/channel/sandbox/server.crt";
|
||||
const BOUNDARY_PRIVATE_KEY_MOUNT_PATH: &str = "/.openshell/channel/sandbox/server.key";
|
||||
const WORKLOAD_RESERVED_PATHS: &[&str] = &[
|
||||
"/.openshell",
|
||||
openshell_sandbox_backend::SUPERVISOR_CA_RUNTIME_ROOT,
|
||||
];
|
||||
const SUPERVISOR_STATE_MOUNT_PATH: &str = "/.openshell/supervisor";
|
||||
const SUPERVISOR_PROXY_AUTH_MOUNT_PATH: &str = "/.openshell/supervisor/upstream-proxy-auth";
|
||||
const PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR: &str =
|
||||
@@ -3759,7 +3763,8 @@ fn docker_bind_string(
|
||||
"bind source path does not exist: {source}"
|
||||
)));
|
||||
}
|
||||
driver_mounts::validate_container_mount_target(target).map_err(Status::failed_precondition)?;
|
||||
driver_mounts::validate_container_mount_target_for_workload(target, WORKLOAD_RESERVED_PATHS)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
let normalized_target = driver_mounts::normalize_mount_target(target);
|
||||
|
||||
let mut opts = Vec::new();
|
||||
@@ -3897,8 +3902,11 @@ fn validate_docker_driver_mounts(
|
||||
));
|
||||
}
|
||||
};
|
||||
driver_mounts::validate_container_mount_target(target)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
driver_mounts::validate_container_mount_target_for_workload(
|
||||
target,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
let normalized_target = driver_mounts::normalize_mount_target(target);
|
||||
if !targets.insert(normalized_target.clone()) {
|
||||
return Err(Status::failed_precondition(format!(
|
||||
@@ -4524,8 +4532,11 @@ async fn prepare_docker_boundary_files(
|
||||
gpu_requested: bool,
|
||||
) -> Result<(), Status> {
|
||||
let directory = docker_boundary_state_dir(sandbox, config)?;
|
||||
let workspace_root = driver_mounts::resolve_oci_workspace_root(&image.working_dir)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
let workspace_root = driver_mounts::resolve_oci_workspace_root_for_workload(
|
||||
&image.working_dir,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
let launch_authentication = sandbox
|
||||
.spec
|
||||
.as_ref()
|
||||
@@ -5652,12 +5663,17 @@ fn build_container_create_body_for_image(
|
||||
.as_ref()
|
||||
.ok_or_else(|| Status::invalid_argument("sandbox.spec.template is required"))?;
|
||||
let resource_limits = docker_resource_limits(template)?;
|
||||
let workspace_root = driver_mounts::resolve_oci_workspace_root(&image.working_dir)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
driver_mounts::validate_workspace_control_path(&workspace_root, BOUNDARY_MOUNT_PATH)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
let workspace_root = driver_mounts::resolve_oci_workspace_root_for_workload(
|
||||
&image.working_dir,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
for volume in &image.volumes {
|
||||
driver_mounts::validate_container_mount_target(volume).map_err(|error| {
|
||||
driver_mounts::validate_container_mount_target_for_workload(
|
||||
volume,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)
|
||||
.map_err(|error| {
|
||||
Status::failed_precondition(format!(
|
||||
"invalid image-declared volume '{volume}': {error}"
|
||||
))
|
||||
@@ -5667,8 +5683,6 @@ fn build_container_create_body_for_image(
|
||||
"image-declared volume '{volume}' masks OCI WorkingDir '{workspace_root}' before workspace validation"
|
||||
))
|
||||
})?;
|
||||
driver_mounts::validate_mount_control_path(volume, BOUNDARY_MOUNT_PATH)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
}
|
||||
for mount in &driver_config.mounts {
|
||||
let target = match mount {
|
||||
@@ -5679,8 +5693,6 @@ fn build_container_create_body_for_image(
|
||||
};
|
||||
driver_mounts::validate_workspace_mount_target(target, &workspace_root)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
driver_mounts::validate_mount_control_path(target, BOUNDARY_MOUNT_PATH)
|
||||
.map_err(Status::failed_precondition)?;
|
||||
}
|
||||
let mut user_mounts = docker_driver_mounts(driver_config)?;
|
||||
user_mounts.push(Mount {
|
||||
|
||||
@@ -1551,10 +1551,10 @@ fn container_creation_rejects_invalid_oci_working_dir() {
|
||||
|
||||
#[test]
|
||||
fn container_creation_rejects_openshell_control_path_working_dir() {
|
||||
let metadata = DockerImageMetadata {
|
||||
let mut metadata = DockerImageMetadata {
|
||||
id: "sha256:immutable".to_string(),
|
||||
user: "1234:1235".to_string(),
|
||||
working_dir: "/opt/openshell/bin/project".to_string(),
|
||||
working_dir: "/.openshell/runtime/project".to_string(),
|
||||
volumes: Vec::new(),
|
||||
};
|
||||
let err = build_container_create_body_for_image(
|
||||
@@ -1569,6 +1569,17 @@ fn container_creation_rejects_openshell_control_path_working_dir() {
|
||||
|
||||
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
|
||||
assert!(err.message().contains("OpenShell control path"));
|
||||
|
||||
metadata.working_dir = "/opt/openshell/bin/project".to_string();
|
||||
build_container_create_body_for_image(
|
||||
&test_sandbox(),
|
||||
&runtime_config(),
|
||||
&DockerSandboxDriverConfig::default(),
|
||||
None,
|
||||
&metadata,
|
||||
&test_workload_identity(),
|
||||
)
|
||||
.expect("supervisor-only paths are not reserved in the workload");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2145,7 +2156,7 @@ fn driver_config_rejects_reserved_mount_targets() {
|
||||
"mounts": [{
|
||||
"type": "volume",
|
||||
"source": "work-nfs",
|
||||
"target": "/etc/openshell/auth"
|
||||
"target": "/.openshell/runtime"
|
||||
}]
|
||||
})));
|
||||
|
||||
@@ -2153,6 +2164,19 @@ fn driver_config_rejects_reserved_mount_targets() {
|
||||
|
||||
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
|
||||
assert!(err.message().contains("reserved OpenShell path"));
|
||||
|
||||
sandbox
|
||||
.spec
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.template
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.driver_config = Some(json_struct(serde_json::json!({
|
||||
"mounts": [{"type": "volume", "source": "work-nfs", "target": "/etc/openshell/auth"}]
|
||||
})));
|
||||
build_container_create_body(&sandbox, &runtime_config())
|
||||
.expect("supervisor-only paths are not reserved in the workload");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -95,9 +95,12 @@ environment belong to agent children, never the supervisor process.
|
||||
OpenShell reads `WORKDIR` from the workload image. If it is unset, `/`, or
|
||||
`/sandbox`, OpenShell uses its managed `/sandbox` workspace. A custom path must
|
||||
be absolute, with no `.` or `..` segments. It cannot overlap container system
|
||||
paths (`/proc`, `/sys`, `/dev`) or OpenShell's private paths (for example,
|
||||
`/.openshell` and `/run/openshell`). Image and driver mounts may be inside the
|
||||
workspace, but cannot replace the workspace path or one of its parents.
|
||||
paths (`/proc`, `/sys`, `/dev`) or mounts still used in the workload:
|
||||
`/.openshell` for the control channel, `/opt/openshell/bin` for the sandbox
|
||||
runtime, and `/run/openshell-supervisor-ca` for generated CA material. Paths
|
||||
used only by the separate supervisor are allowed. Image and driver mounts may
|
||||
be inside the workspace, but cannot replace the workspace path or one of its
|
||||
parents.
|
||||
|
||||
For a custom path, Podman mounts a persistent workspace volume there. When the
|
||||
volume is first created, Podman copies any files already in that image directory
|
||||
@@ -136,7 +139,7 @@ User `bind`, `volume`, `tmpfs`, and `image` mounts and CDI GPU selection remain
|
||||
native Podman features and apply only to the workload. Bind mounts require the
|
||||
operator's `enable_bind_mounts` opt-in and disabled label admission. Supplemental
|
||||
image mounts also require disabled admission. Driver JSON requires
|
||||
`allow_driver_config = true`. Reserved control paths and the workspace
|
||||
`allow_driver_config = true`. The workload's private mounts and workspace
|
||||
root cannot be replaced. User-owned volumes are never created or deleted.
|
||||
|
||||
See [gateway configuration](../../docs/how-it-works/gateways/configuration.mdx) for
|
||||
|
||||
@@ -75,6 +75,11 @@ const PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR: &str =
|
||||
const SUPERVISOR_MOUNT_DIR: &str = openshell_core::driver_utils::SUPERVISOR_CONTAINER_DIR;
|
||||
/// Full path to the supervisor binary inside sandbox containers.
|
||||
const SUPERVISOR_BINARY_PATH: &str = openshell_core::driver_utils::SUPERVISOR_CONTAINER_BINARY;
|
||||
const WORKLOAD_RESERVED_PATHS: &[&str] = &[
|
||||
"/.openshell",
|
||||
SUPERVISOR_MOUNT_DIR,
|
||||
openshell_sandbox_backend::SUPERVISOR_CA_RUNTIME_ROOT,
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
@@ -235,15 +240,18 @@ impl ResolvedPodmanImage {
|
||||
/// - an image volume covering the workspace or any of its ancestors.
|
||||
pub fn from_inspect(inspected: &ImageInspect) -> Result<Self, ComputeDriverError> {
|
||||
let image_config = inspected.config.as_ref();
|
||||
let workspace_root = driver_mounts::resolve_oci_workspace_root(
|
||||
let workspace_root = driver_mounts::resolve_oci_workspace_root_for_workload(
|
||||
image_config.map_or("", |config| config.working_dir.as_str()),
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)
|
||||
.map_err(ComputeDriverError::Precondition)?;
|
||||
driver_mounts::validate_workspace_control_path(&workspace_root, "/.openshell")
|
||||
.map_err(ComputeDriverError::Precondition)?;
|
||||
if let Some(volumes) = image_config.and_then(|config| config.volumes.as_ref()) {
|
||||
for volume in volumes.keys() {
|
||||
driver_mounts::validate_container_mount_target(volume).map_err(|error| {
|
||||
driver_mounts::validate_container_mount_target_for_workload(
|
||||
volume,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)
|
||||
.map_err(|error| {
|
||||
ComputeDriverError::Precondition(format!(
|
||||
"invalid image-declared volume '{volume}': {error}"
|
||||
))
|
||||
@@ -255,8 +263,6 @@ impl ResolvedPodmanImage {
|
||||
))
|
||||
},
|
||||
)?;
|
||||
driver_mounts::validate_mount_control_path(volume, "/.openshell")
|
||||
.map_err(ComputeDriverError::Precondition)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,7 +877,10 @@ fn podman_user_mounts(
|
||||
None => {}
|
||||
}
|
||||
driver_mounts::validate_absolute_mount_source(&source, "bind source")?;
|
||||
driver_mounts::validate_container_mount_target(&target)?;
|
||||
driver_mounts::validate_container_mount_target_for_workload(
|
||||
&target,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)?;
|
||||
result.mounts.push(Mount {
|
||||
kind: "bind".into(),
|
||||
source,
|
||||
@@ -887,7 +896,10 @@ fn podman_user_mounts(
|
||||
} => {
|
||||
reject_subpath(subpath.as_deref(), "podman volume mounts")?;
|
||||
driver_mounts::validate_mount_source(&source, "volume source")?;
|
||||
driver_mounts::validate_container_mount_target(&target)?;
|
||||
driver_mounts::validate_container_mount_target_for_workload(
|
||||
&target,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)?;
|
||||
result.volumes.push(NamedVolume {
|
||||
name: source,
|
||||
dest: target,
|
||||
@@ -913,7 +925,10 @@ fn podman_user_mounts(
|
||||
{
|
||||
options.push(format!("mode={mode:o}"));
|
||||
}
|
||||
driver_mounts::validate_container_mount_target(&target)?;
|
||||
driver_mounts::validate_container_mount_target_for_workload(
|
||||
&target,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)?;
|
||||
result.mounts.push(Mount {
|
||||
kind: "tmpfs".into(),
|
||||
source: "tmpfs".into(),
|
||||
@@ -929,7 +944,10 @@ fn podman_user_mounts(
|
||||
} => {
|
||||
reject_subpath(subpath.as_deref(), "podman image mounts")?;
|
||||
driver_mounts::validate_mount_source(&source, "image source")?;
|
||||
driver_mounts::validate_container_mount_target(&target)?;
|
||||
driver_mounts::validate_container_mount_target_for_workload(
|
||||
&target,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)?;
|
||||
result.image_volumes.push(ImageVolume {
|
||||
source,
|
||||
destination: target,
|
||||
@@ -1004,8 +1022,10 @@ fn validate_podman_driver_mounts(
|
||||
target
|
||||
}
|
||||
};
|
||||
driver_mounts::validate_container_mount_target(target)?;
|
||||
driver_mounts::validate_mount_control_path(target, "/.openshell")?;
|
||||
driver_mounts::validate_container_mount_target_for_workload(
|
||||
target,
|
||||
WORKLOAD_RESERVED_PATHS,
|
||||
)?;
|
||||
let normalized_target = driver_mounts::normalize_mount_target(target);
|
||||
if !targets.insert(normalized_target.clone()) {
|
||||
return Err(format!(
|
||||
@@ -2104,6 +2124,26 @@ mod tests {
|
||||
assert_eq!(image.workspace_root, "/workspace/project");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_image_allows_supervisor_only_workdir_but_reserves_workload_mounts() {
|
||||
let inspect = |working_dir: &str| ImageInspect {
|
||||
id: "sha256:image".into(),
|
||||
config: Some(ImageConfig {
|
||||
working_dir: working_dir.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
assert!(ResolvedPodmanImage::from_inspect(&inspect("/opt/openshell/bin/project")).is_err());
|
||||
assert!(ResolvedPodmanImage::from_inspect(&inspect("/.openshell/channel")).is_err());
|
||||
assert_eq!(
|
||||
ResolvedPodmanImage::from_inspect(&inspect("/etc/openshell/tls/client"))
|
||||
.unwrap()
|
||||
.workspace_root,
|
||||
"/etc/openshell/tls/client"
|
||||
);
|
||||
}
|
||||
|
||||
fn json_struct(value: Value) -> prost_types::Struct {
|
||||
let Value::Object(object) = value else {
|
||||
panic!("expected JSON object");
|
||||
@@ -3449,7 +3489,7 @@ mod tests {
|
||||
"mounts": [{
|
||||
"type": "volume",
|
||||
"source": "work-nfs",
|
||||
"target": "/etc/openshell/tls/client"
|
||||
"target": "/opt/openshell/bin"
|
||||
}]
|
||||
}))),
|
||||
..Default::default()
|
||||
@@ -3461,6 +3501,19 @@ mod tests {
|
||||
let err = try_build_container_spec_with_token(&sandbox, &config, None).unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("reserved OpenShell path"));
|
||||
|
||||
sandbox
|
||||
.spec
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.template
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.driver_config = Some(json_struct(serde_json::json!({
|
||||
"mounts": [{"type": "volume", "source": "work-nfs", "target": "/etc/openshell/tls/client"}]
|
||||
})));
|
||||
try_build_container_spec_with_token(&sandbox, &config, None)
|
||||
.expect("supervisor-only paths are not reserved in the workload");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -271,7 +271,8 @@ On Docker and Podman, the image's `WORKDIR` becomes the workspace. Images with
|
||||
no `WORKDIR`, `WORKDIR /`, or `WORKDIR /sandbox` use OpenShell's managed
|
||||
`/sandbox` workspace. Custom paths must be absolute, with no `.` or `..`
|
||||
segments, and cannot overlap container system paths (`/proc`, `/sys`, `/dev`)
|
||||
or OpenShell's private paths. Image and driver mounts cannot replace the
|
||||
or private mounts still used inside the workload. Paths used only by the
|
||||
separate supervisor are allowed. Image and driver mounts cannot replace the
|
||||
workspace or one of its parents.
|
||||
|
||||
Docker validates the directory in the image filesystem. Podman mounts the
|
||||
|
||||
@@ -281,7 +281,7 @@ Common findings:
|
||||
- Sandbox fails before readiness with an identity-resolution error: inspect the image's OCI `USER` and matching `/etc/passwd` and `/etc/group` entries, or explicitly set both process identity fields in policy. Numeric workload identities `1` through `4294967294` are accepted; root, the invalid identity sentinel, and missing identities are rejected.
|
||||
- Sandbox fails before readiness with an OCI workspace validation error: inspect the image's `WorkingDir` using the immutable image ID reported by the gateway. Empty, `/`, and explicit `/sandbox` use the managed `/sandbox` compatibility workspace. Any other workdir must be an absolute normalized directory with no symlink components; the final policy UID, primary GID, and supplementary groups must pass the kernel's effective traverse/write checks, including POSIX ACL and LSM decisions. OpenShell does not create, chown, or chmod a non-default Docker workdir or a copied-up custom Podman workspace.
|
||||
- Docker and Podman also reject an image `VOLUME` that covers the workdir or one of its parents because the runtime would mask the path before validation. Move the `VOLUME` below the workspace or remove the declaration.
|
||||
- A workdir rejected as a special filesystem or OpenShell control-path collision cannot be made valid with permissions. Move the image workdir away from kernel-backed mounts and the concrete supervisor, TLS, token, runtime, and socket paths named in the error.
|
||||
- A workdir rejected as a special filesystem or workload-mount collision cannot be made valid with permissions. Move it away from `/proc`, `/sys`, `/dev`, and the sandbox runtime or control-channel paths named in the error. Paths used only by the separate supervisor are allowed.
|
||||
- Local Docker gateway setup cannot copy `openshell-sandbox` after exporting a supervisor image: the sandbox runtime and supervisor are separate artifacts. The runtime image must provide `/openshell-sandbox`; the supervisor image provides `/openshell-supervisor`.
|
||||
- Docker driver cannot initialize because it cannot find `openshell-sandbox`: verify the sibling binary next to `openshell-gateway`, or that the configured `sandbox_runtime_image` contains `/openshell-sandbox`.
|
||||
- Sandbox never registers: check gateway logs and the supervisor's gateway endpoint.
|
||||
|
||||
Reference in New Issue
Block a user