Skip to content

Commit 087148e

Browse files
committed
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>
1 parent 45614a3 commit 087148e

5 files changed

Lines changed: 319 additions & 32 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: 75 additions & 13 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,23 +1765,76 @@ 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+
driver_mounts::validate_container_mount_target(target).map_err(Status::failed_precondition)?;
1803+
let normalized_target = driver_mounts::normalize_mount_target(target);
1804+
1805+
let mut opts = Vec::new();
1806+
if read_only {
1807+
opts.push("ro");
1808+
}
1809+
match selinux_label {
1810+
Some(SelinuxLabel::Shared) => opts.push("z"),
1811+
Some(SelinuxLabel::Private) => opts.push("Z"),
1812+
None => {}
1813+
}
1814+
1815+
if opts.is_empty() {
1816+
Ok(format!("{source}:{normalized_target}"))
1817+
} else {
1818+
Ok(format!("{source}:{normalized_target}:{}", opts.join(",")))
1819+
}
1820+
}
1821+
1822+
/// Collect user-supplied non-bind mounts as structured `Mount` objects.
17641823
fn docker_driver_mounts(config: &DockerSandboxDriverConfig) -> Result<Vec<Mount>, Status> {
1765-
config.mounts.iter().map(docker_mount_from_config).collect()
1824+
config
1825+
.mounts
1826+
.iter()
1827+
.filter(|m| !matches!(m, DockerDriverMountConfig::Bind { .. }))
1828+
.map(docker_mount_from_config)
1829+
.collect()
17661830
}
17671831

17681832
fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Mount, Status> {
17691833
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-
}),
1834+
DockerDriverMountConfig::Bind { .. } => {
1835+
// Bind mounts are handled via docker_driver_bind_strings.
1836+
unreachable!("bind mounts are filtered out before reaching docker_mount_from_config")
1837+
}
17811838
DockerDriverMountConfig::Volume {
17821839
source,
17831840
target,
@@ -2261,6 +2318,7 @@ fn build_container_create_body_with_gpu_devices(
22612318
.ok_or_else(|| Status::invalid_argument("sandbox.spec.template is required"))?;
22622319
let resource_limits = docker_resource_limits(template)?;
22632320
let user_mounts = docker_driver_mounts(driver_config)?;
2321+
let user_bind_strings = docker_driver_bind_strings(driver_config)?;
22642322
let device_requests = gpu_device_ids.map(|device_ids| {
22652323
vec![DeviceRequest {
22662324
driver: Some("cdi".to_string()),
@@ -2298,7 +2356,11 @@ fn build_container_create_body_with_gpu_devices(
22982356
memory: resource_limits.memory_bytes,
22992357
pids_limit: docker_pids_limit(config.sandbox_pids_limit)?,
23002358
device_requests,
2301-
binds: Some(build_binds(sandbox, config)?),
2359+
binds: {
2360+
let mut binds = build_binds(sandbox, config)?;
2361+
binds.extend(user_bind_strings);
2362+
Some(binds)
2363+
},
23022364
mounts: Some(user_mounts),
23032365
restart_policy: Some(RestartPolicy {
23042366
name: Some(RestartPolicyNameEnum::UNLESS_STOPPED),

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

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -801,17 +801,25 @@ fn build_container_create_body_includes_bind_mounts_when_enabled() {
801801
config.enable_bind_mounts = true;
802802

803803
let body = build_container_create_body(&sandbox, &config).unwrap();
804-
let mounts = body
804+
let binds = body
805805
.host_config
806+
.as_ref()
806807
.unwrap()
807-
.mounts
808-
.expect("driver config mounts should be set");
808+
.binds
809+
.as_ref()
810+
.expect("binds should be set");
809811

810-
assert_eq!(mounts.len(), 1);
811-
assert_eq!(mounts[0].typ, Some(MountTypeEnum::BIND));
812-
assert_eq!(mounts[0].source.as_deref(), Some("/host/path"));
813-
assert_eq!(mounts[0].target.as_deref(), Some("/sandbox/host"));
814-
assert_eq!(mounts[0].read_only, Some(true));
812+
// User bind mount appears after the system binds.
813+
assert!(
814+
binds.iter().any(|b| b == "/host/path:/sandbox/host:ro"),
815+
"expected bind entry '/host/path:/sandbox/host:ro', got {binds:?}"
816+
);
817+
// Bind mounts must not appear in the structured mounts vec.
818+
let mounts = body.host_config.unwrap().mounts.unwrap_or_default();
819+
assert!(
820+
mounts.iter().all(|m| m.typ != Some(MountTypeEnum::BIND)),
821+
"bind mounts should not appear in structured mounts"
822+
);
815823
}
816824

817825
#[test]
@@ -835,13 +843,122 @@ fn driver_config_defaults_enabled_bind_mounts_to_read_only() {
835843
config.enable_bind_mounts = true;
836844

837845
let body = build_container_create_body(&sandbox, &config).unwrap();
838-
let mounts = body
846+
let binds = body
839847
.host_config
840848
.unwrap()
841-
.mounts
842-
.expect("driver config mounts should be set");
849+
.binds
850+
.expect("binds should be set");
843851

844-
assert_eq!(mounts[0].read_only, Some(true));
852+
assert!(
853+
binds
854+
.iter()
855+
.any(|b| b.contains("/host/path:/sandbox/host:ro")),
856+
"default bind mount should be read-only, got {binds:?}"
857+
);
858+
}
859+
860+
#[test]
861+
fn bind_mount_selinux_shared_label() {
862+
let mut sandbox = test_sandbox();
863+
sandbox
864+
.spec
865+
.as_mut()
866+
.unwrap()
867+
.template
868+
.as_mut()
869+
.unwrap()
870+
.driver_config = Some(json_struct(serde_json::json!({
871+
"mounts": [{
872+
"type": "bind",
873+
"source": "/data/shared",
874+
"target": "/sandbox/data",
875+
"read_only": true,
876+
"selinux_label": "shared"
877+
}]
878+
})));
879+
let mut config = runtime_config();
880+
config.enable_bind_mounts = true;
881+
882+
let body = build_container_create_body(&sandbox, &config).unwrap();
883+
let binds = body
884+
.host_config
885+
.unwrap()
886+
.binds
887+
.expect("binds should be set");
888+
889+
assert!(
890+
binds.iter().any(|b| b == "/data/shared:/sandbox/data:ro,z"),
891+
"expected ':ro,z' label, got {binds:?}"
892+
);
893+
}
894+
895+
#[test]
896+
fn bind_mount_selinux_private_label() {
897+
let mut sandbox = test_sandbox();
898+
sandbox
899+
.spec
900+
.as_mut()
901+
.unwrap()
902+
.template
903+
.as_mut()
904+
.unwrap()
905+
.driver_config = Some(json_struct(serde_json::json!({
906+
"mounts": [{
907+
"type": "bind",
908+
"source": "/data/exclusive",
909+
"target": "/sandbox/data",
910+
"read_only": false,
911+
"selinux_label": "private"
912+
}]
913+
})));
914+
let mut config = runtime_config();
915+
config.enable_bind_mounts = true;
916+
917+
let body = build_container_create_body(&sandbox, &config).unwrap();
918+
let binds = body
919+
.host_config
920+
.unwrap()
921+
.binds
922+
.expect("binds should be set");
923+
924+
assert!(
925+
binds.iter().any(|b| b == "/data/exclusive:/sandbox/data:Z"),
926+
"expected ':Z' label, got {binds:?}"
927+
);
928+
}
929+
930+
#[test]
931+
fn bind_mount_without_selinux_label() {
932+
let mut sandbox = test_sandbox();
933+
sandbox
934+
.spec
935+
.as_mut()
936+
.unwrap()
937+
.template
938+
.as_mut()
939+
.unwrap()
940+
.driver_config = Some(json_struct(serde_json::json!({
941+
"mounts": [{
942+
"type": "bind",
943+
"source": "/host/path",
944+
"target": "/sandbox/host",
945+
"read_only": false
946+
}]
947+
})));
948+
let mut config = runtime_config();
949+
config.enable_bind_mounts = true;
950+
951+
let body = build_container_create_body(&sandbox, &config).unwrap();
952+
let binds = body
953+
.host_config
954+
.unwrap()
955+
.binds
956+
.expect("binds should be set");
957+
958+
assert!(
959+
binds.iter().any(|b| b == "/host/path:/sandbox/host"),
960+
"expected no options suffix, got {binds:?}"
961+
);
845962
}
846963

847964
#[test]

0 commit comments

Comments
 (0)