Skip to content

Commit 43bb030

Browse files
authored
feat(docker,podman): add SELinux label support for bind mounts (#2092)
* feat(docker,podman): add SELinux label support for bind mounts The Docker Engine structured Mount API does not support SELinux relabelling (:z / :Z). Move user-supplied bind mounts from the structured `mounts` field to the legacy string-format `binds` field, which does support these options. Add a shared `SelinuxLabel` enum (shared/private) to openshell-core so both Docker and Podman drivers accept an optional `selinux_label` field on bind mount configs. For Docker, labels are appended to the bind string; for Podman, they are pushed to the mount options vec. Signed-off-by: Florian Bergmann <fbergman@redhat.com> * fix(docker): reject missing bind source paths on legacy binds Moving user bind mounts from the structured Mount API to the legacy Binds field changed Docker's behavior for missing source directories: the legacy path silently creates them as empty root-owned dirs instead of erroring. Add an explicit Path::exists() check to preserve the fail-fast behavior operators expect. Signed-off-by: Florian Bergmann <fbergman@redhat.com> --------- Signed-off-by: Florian Bergmann <fbergman@redhat.com>
1 parent 45060f4 commit 43bb030

5 files changed

Lines changed: 377 additions & 39 deletions

File tree

crates/openshell-core/src/driver_mounts.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@
55
66
use std::path::Path;
77

8+
/// `SELinux` relabelling mode for bind mounts.
9+
///
10+
/// On hosts with `SELinux` enabled (e.g. Fedora, RHEL) a bind-mounted path
11+
/// must be relabelled so the container process can access it.
12+
///
13+
/// * `shared` (`:z`) — the label is shared across all containers that mount
14+
/// the same path. Safe when multiple sandboxes read the same data set.
15+
/// * `private` (`:Z`) — the label is private to *this* container. The host
16+
/// directory becomes inaccessible to other containers (and potentially to
17+
/// the host) until the container is removed. Use only when exclusive
18+
/// ownership is acceptable.
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
20+
#[serde(rename_all = "snake_case")]
21+
pub enum SelinuxLabel {
22+
/// Shared `SELinux` label (`:z`).
23+
Shared,
24+
/// Private `SELinux` label (`:Z`).
25+
Private,
26+
}
27+
828
const RESERVED_MOUNT_TARGETS: &[&str] = &[
929
"/opt/openshell",
1030
"/etc/openshell",

crates/openshell-driver-docker/src/lib.rs

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ impl DockerSandboxDriverConfig {
303303
}
304304
}
305305

306+
use openshell_core::driver_mounts::SelinuxLabel;
307+
306308
#[derive(Debug, Clone, serde::Deserialize)]
307309
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
308310
enum DockerDriverMountConfig {
@@ -311,6 +313,8 @@ enum DockerDriverMountConfig {
311313
target: String,
312314
#[serde(default = "default_true")]
313315
read_only: bool,
316+
#[serde(default)]
317+
selinux_label: Option<SelinuxLabel>,
314318
},
315319
Volume {
316320
source: String,
@@ -1761,29 +1765,90 @@ fn docker_driver_config(
17611765
Ok(config)
17621766
}
17631767

1768+
/// Collect user-supplied bind mounts as string-format binds.
1769+
///
1770+
/// Bind mounts use the legacy `Binds` field (`-v` syntax) rather than the
1771+
/// structured `Mount` API because the Docker Engine Mount object does not
1772+
/// support `SELinux` relabelling (`:z` / `:Z`). The string format does.
1773+
fn docker_driver_bind_strings(config: &DockerSandboxDriverConfig) -> Result<Vec<String>, Status> {
1774+
config
1775+
.mounts
1776+
.iter()
1777+
.filter_map(|m| match m {
1778+
DockerDriverMountConfig::Bind {
1779+
source,
1780+
target,
1781+
read_only,
1782+
selinux_label,
1783+
} => Some(docker_bind_string(
1784+
source,
1785+
target,
1786+
*read_only,
1787+
*selinux_label,
1788+
)),
1789+
_ => None,
1790+
})
1791+
.collect()
1792+
}
1793+
1794+
fn docker_bind_string(
1795+
source: &str,
1796+
target: &str,
1797+
read_only: bool,
1798+
selinux_label: Option<SelinuxLabel>,
1799+
) -> Result<String, Status> {
1800+
driver_mounts::validate_absolute_mount_source(source, "bind source")
1801+
.map_err(Status::failed_precondition)?;
1802+
// Legacy `-v` binds silently create missing source directories as empty,
1803+
// root-owned paths. The structured `--mount` API that was used before this
1804+
// change rejected missing sources at container-create time. Preserve that
1805+
// fail-fast behaviour with an explicit existence check.
1806+
if !Path::new(source).exists() {
1807+
return Err(Status::failed_precondition(format!(
1808+
"bind source path does not exist: {source}"
1809+
)));
1810+
}
1811+
driver_mounts::validate_container_mount_target(target).map_err(Status::failed_precondition)?;
1812+
let normalized_target = driver_mounts::normalize_mount_target(target);
1813+
1814+
let mut opts = Vec::new();
1815+
if read_only {
1816+
opts.push("ro");
1817+
}
1818+
match selinux_label {
1819+
Some(SelinuxLabel::Shared) => opts.push("z"),
1820+
Some(SelinuxLabel::Private) => opts.push("Z"),
1821+
None => {}
1822+
}
1823+
1824+
if opts.is_empty() {
1825+
Ok(format!("{source}:{normalized_target}"))
1826+
} else {
1827+
Ok(format!("{source}:{normalized_target}:{}", opts.join(",")))
1828+
}
1829+
}
1830+
1831+
/// Collect user-supplied non-bind mounts as structured `Mount` objects.
17641832
fn docker_driver_mounts(config: &DockerSandboxDriverConfig) -> Result<Vec<Mount>, Status> {
1765-
config.mounts.iter().map(docker_mount_from_config).collect()
1833+
config
1834+
.mounts
1835+
.iter()
1836+
.filter_map(|m| docker_mount_from_config(m).transpose())
1837+
.collect()
17661838
}
17671839

1768-
fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Mount, Status> {
1840+
fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Option<Mount>, Status> {
17691841
match config {
1770-
DockerDriverMountConfig::Bind {
1771-
source,
1772-
target,
1773-
read_only,
1774-
} => Ok(Mount {
1775-
typ: Some(MountTypeEnum::BIND),
1776-
source: Some(source.clone()),
1777-
target: Some(target.clone()),
1778-
read_only: Some(*read_only),
1779-
..Default::default()
1780-
}),
1842+
DockerDriverMountConfig::Bind { .. } => {
1843+
// Bind mounts are handled via docker_driver_bind_strings.
1844+
Ok(None)
1845+
}
17811846
DockerDriverMountConfig::Volume {
17821847
source,
17831848
target,
17841849
read_only,
17851850
subpath,
1786-
} => Ok(Mount {
1851+
} => Ok(Some(Mount {
17871852
typ: Some(MountTypeEnum::VOLUME),
17881853
source: Some(source.clone()),
17891854
target: Some(target.clone()),
@@ -1793,13 +1858,13 @@ fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Mount, S
17931858
..Default::default()
17941859
}),
17951860
..Default::default()
1796-
}),
1861+
})),
17971862
DockerDriverMountConfig::Tmpfs {
17981863
target,
17991864
options,
18001865
size_bytes,
18011866
mode,
1802-
} => Ok(Mount {
1867+
} => Ok(Some(Mount {
18031868
typ: Some(MountTypeEnum::TMPFS),
18041869
target: Some(target.clone()),
18051870
tmpfs_options: Some(MountTmpfsOptions {
@@ -1818,7 +1883,7 @@ fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Mount, S
18181883
.transpose()?,
18191884
}),
18201885
..Default::default()
1821-
}),
1886+
})),
18221887
DockerDriverMountConfig::Image { .. } => Err(Status::failed_precondition(
18231888
"invalid docker driver_config: docker image mounts are not supported",
18241889
)),
@@ -2261,6 +2326,7 @@ fn build_container_create_body_with_gpu_devices(
22612326
.ok_or_else(|| Status::invalid_argument("sandbox.spec.template is required"))?;
22622327
let resource_limits = docker_resource_limits(template)?;
22632328
let user_mounts = docker_driver_mounts(driver_config)?;
2329+
let user_bind_strings = docker_driver_bind_strings(driver_config)?;
22642330
let device_requests = gpu_device_ids.map(|device_ids| {
22652331
vec![DeviceRequest {
22662332
driver: Some("cdi".to_string()),
@@ -2298,7 +2364,11 @@ fn build_container_create_body_with_gpu_devices(
22982364
memory: resource_limits.memory_bytes,
22992365
pids_limit: docker_pids_limit(config.sandbox_pids_limit)?,
23002366
device_requests,
2301-
binds: Some(build_binds(sandbox, config)?),
2367+
binds: {
2368+
let mut binds = build_binds(sandbox, config)?;
2369+
binds.extend(user_bind_strings);
2370+
Some(binds)
2371+
},
23022372
mounts: Some(user_mounts),
23032373
restart_policy: Some(RestartPolicy {
23042374
name: Some(RestartPolicyNameEnum::UNLESS_STOPPED),

0 commit comments

Comments
 (0)