Skip to content

Commit d1dad78

Browse files
committed
feat(driver-podman): add userns config
Add a `userns` option to the Podman compute driver that maps to Podman's user namespace modes. The mode string is split on the first colon into the API's `nsmode` and `value` fields so parameterized values like `auto:size=65536` and `keep-id:uid=1000,gid=1000` are forwarded correctly. When the mode is `auto`, the container spec also sets `idmappings.AutoUserNs = true` as required by the API. An allowlist validates the mode at startup: `auto` and `keep-id` accept optional parameters; `host`, `private`, and `nomap` reject them; everything else is an error. Podman image volumes use overlay mounts internally and the kernel does not support idmapped mounts on overlay (`mount_setattr` returns EINVAL). When userns is configured (any mode except `host`), the driver extracts the supervisor binary from the image to a host-side cache and bind-mounts it instead of using an image volume. Configurable via TOML `userns = "auto"`, CLI `--userns`, or environment variable `OPENSHELL_PODMAN_USERNS`. Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
1 parent dcec2ab commit d1dad78

17 files changed

Lines changed: 1272 additions & 33 deletions

File tree

.agents/skills/debug-openshell-cluster/SKILL.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,25 @@ Common findings:
216216
cannot bypass slirp4netns host-loopback isolation. Do not work around
217217
discovery failures by broadening the primary gateway listener to `0.0.0.0`.
218218

219+
When `userns` is configured (e.g. `userns = "auto"` or `userns = "keep-id"`):
220+
221+
- Supervisor delivery uses bind-mount fallback instead of image volumes because
222+
overlay mounts do not support `idmapped` mounts. The supervisor binary is
223+
extracted from the supervisor image and cached at
224+
`$XDG_DATA_HOME/openshell/podman-supervisor/` (typically
225+
`~/.local/share/openshell/podman-supervisor/`).
226+
- Stale cache: if the supervisor image is updated but the cached binary is not
227+
refreshed, sandbox creation may fail with an ELF validation error or version
228+
mismatch. Remove the cache directory and retry.
229+
- `auto` mode requires subuid/subgid ranges for the current user in
230+
`/etc/subuid` and `/etc/subgid`. If missing, Podman returns a user-namespace
231+
mapping error at container creation.
232+
- `private` mode requires explicit `uidmap` and `gidmap` arrays in the TOML
233+
config (e.g. `uidmap = ["0:1000:1", "1:100000:65536"]`). Without both,
234+
the gateway rejects the config at startup.
235+
- `nomap` (without hyphen) is accepted as input but canonicalized to `no-map`
236+
for Podman's API.
237+
219238
### Step 6: Check Kubernetes Helm Gateways
220239

221240
```bash

architecture/compute-runtimes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ delete, reconciliation removes the row; otherwise it can remain `Deleting`.
117117
| Runtime | Best fit | Sandbox boundary | Notes |
118118
|---|---|---|---|
119119
| Docker | Local development with Docker available. | Container plus nested sandbox namespace. | Uses host networking so loopback gateway endpoints work from the supervisor. |
120-
| Podman | Rootless or single-machine deployments. | Container plus nested sandbox namespace. | Uses the Podman REST API, OCI image volumes, and CDI GPU devices when available. |
120+
| Podman | Rootless or single-machine deployments. | Container plus nested sandbox namespace. | Uses the Podman REST API and CDI GPU devices when available. Delivers the supervisor via OCI image volume by default; falls back to extracting the binary to a host-side cache and bind-mounting it when `userns` is configured (overlay does not support idmapped mounts). |
121121
| Kubernetes | Cluster deployment through Helm. | Pod plus nested sandbox namespace. | Uses Kubernetes API objects, service accounts, secrets, PVC-backed workspace storage, and GPU resources. |
122122
| VM | Experimental microVM isolation. | Per-sandbox libkrun VM. | Managed endpoint-backed driver. The gateway spawns `openshell-driver-vm`, waits for its Unix socket, and then consumes it through the same remote `compute_driver.proto` path used by unmanaged endpoint drivers. The VM driver boots a cached bootstrap `rootfs.ext4`, prepares requested OCI images inside a bootstrap VM with `umoci`, attaches the prepared image disk read-only, and gives each sandbox a writable `overlay.ext4` for merged-root changes and runtime material. The driver persists each accepted launch request beside the overlay and restarts those VMs on driver startup without recreating the overlay. |
123123
| Extension | Out-of-tree drivers operated alongside the gateway. | Whatever boundary the driver implements. | Selected by a non-reserved custom `compute_drivers = ["<name>"]` entry with `[openshell.drivers.<name>].socket_path`, or at launch time by pairing `--drivers <name>` with `--compute-driver-socket=<path>`. Reserved built-in names such as `vm`, `docker`, `podman`, and `kubernetes` cannot be used as unmanaged socket endpoints. The gateway connects to a UDS the operator already provisioned, runs `GetCapabilities`, logs the advertised `driver_name`, and dispatches all sandbox lifecycle calls through `compute_driver.proto`. The driver process and socket lifecycle are operator-owned; the gateway does not spawn, supervise, or remove unmanaged extension drivers. The trust boundary is the socket's filesystem permissions: the operator must ensure only the gateway uid can read/write it. |
@@ -170,7 +170,7 @@ The supervisor must be available inside each sandbox workload:
170170
| Runtime | Delivery model |
171171
|---|---|
172172
| Docker | Bind-mounted local supervisor binary, or a binary extracted from the configured supervisor image. |
173-
| Podman | Read-only OCI image volume containing the supervisor binary. |
173+
| Podman | Read-only OCI image volume by default; host-cached bind mount when `userns` is configured. |
174174
| Kubernetes | Supervisor image side-loaded into the sandbox pod by image volume or init container. |
175175
| VM | Embedded in the guest rootfs bundle. |
176176
| Extension | Defined by the out-of-tree driver. |

crates/openshell-core/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ ipnet = "2"
2727
base64 = { workspace = true }
2828
chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true }
2929
reqwest = { workspace = true, features = ["blocking", "rustls-tls-native-roots"], optional = true }
30-
tar = "0.4"
31-
tempfile = "3"
30+
tar = { version = "0.4", optional = true }
31+
tempfile = { version = "3", optional = true }
3232

3333
[target.'cfg(unix)'.dependencies]
3434
nix = { workspace = true }
@@ -38,6 +38,7 @@ default = ["telemetry"]
3838
## Compile in anonymous telemetry emission support. On by default; disable with
3939
## `--no-default-features` (plus any other features you need) for a build that
4040
## contains no telemetry endpoint, no HTTP client, and no emission code at all.
41+
driver-extraction = ["dep:tar", "dep:tempfile"]
4142
telemetry = ["dep:reqwest", "dep:chrono"]
4243

4344
[build-dependencies]

crates/openshell-core/src/driver_utils.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ pub fn supervisor_image_should_refresh(image: &str) -> bool {
424424
// Supervisor binary extraction helpers (shared by Docker and Podman drivers)
425425
// ---------------------------------------------------------------------------
426426

427+
#[cfg(feature = "driver-extraction")]
427428
/// Extract the payload of the first regular-file entry in a tar archive.
428429
///
429430
/// Container archive endpoints return a single-file tar when `path` points to
@@ -454,6 +455,7 @@ pub fn extract_first_tar_entry(tar_bytes: &[u8]) -> Result<Vec<u8>, String> {
454455
Ok(bytes)
455456
}
456457

458+
#[cfg(feature = "driver-extraction")]
457459
/// Atomically write `bytes` to `final_path` via a sibling temp file.
458460
///
459461
/// Creates parent directories as needed. The temp file is synced, `chmod 755`

crates/openshell-driver-docker/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ license.workspace = true
1111
repository.workspace = true
1212

1313
[dependencies]
14-
openshell-core = { path = "../openshell-core", default-features = false }
14+
openshell-core = { path = "../openshell-core", default-features = false, features = ["driver-extraction"] }
1515

1616
tokio = { workspace = true }
1717
tonic = { workspace = true }

crates/openshell-driver-podman/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ name = "openshell-driver-podman"
1515
path = "src/main.rs"
1616

1717
[dependencies]
18-
openshell-core = { path = "../openshell-core", default-features = false }
18+
openshell-core = { path = "../openshell-core", default-features = false, features = ["driver-extraction"] }
1919

2020
tokio = { workspace = true }
2121
tonic = { workspace = true, features = ["transport"] }

crates/openshell-driver-podman/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ Podman resources after out-of-band container removal or label drift.
360360
| `OPENSHELL_SANDBOX_PROXY_AUTH_FILE` | `--sandbox-proxy-auth-file` | unset | Path to a file containing the proxy credentials as `user:pass`. Staged as a root-only Podman secret so credentials never appear in config or container metadata. Requires the insecure-auth acknowledgement below. |
361361
| `OPENSHELL_SANDBOX_PROXY_AUTH_ALLOW_INSECURE` | `--sandbox-proxy-auth-allow-insecure` | unset | Explicit acknowledgement (`true`) that the credential is sent as cleartext Basic auth over the plain-TCP connection to the `http://` proxy. Required when the auth file is set; rejected when it is not. |
362362
| `OPENSHELL_SANDBOX_PROXY_CONNECT_BY_HOSTNAME` | `--sandbox-proxy-connect-by-hostname` | unset | Send the destination hostname in CONNECT requests instead of a validated IP. Last resort for proxies whose ACLs filter on hostnames: the proxy then resolves the name itself, so sandbox SSRF/`allowed_ips` validation no longer binds the connection. |
363+
| `OPENSHELL_PODMAN_USERNS` | `--userns` | unset | User namespace mode for sandbox containers (e.g. `auto`). When unset, containers use the default user namespace. |
363364

364365
Through the gateway, the same settings are the `https_proxy`, `no_proxy`,
365366
`proxy_auth_file`, `proxy_auth_allow_insecure`, and

crates/openshell-driver-podman/src/client.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,32 @@ impl PodmanClient {
513513
}
514514
}
515515

516+
/// Download a file from a container as a tar archive.
517+
///
518+
/// Calls `GET /libpod/containers/{name}/archive?path={path}` and returns
519+
/// the raw tar bytes. The container does not need to be running.
520+
pub async fn copy_from_container(
521+
&self,
522+
name: &str,
523+
path: &str,
524+
) -> Result<Bytes, PodmanApiError> {
525+
validate_name(name)?;
526+
let encoded_path = url_encode(path);
527+
let (status, bytes) = self
528+
.request(
529+
hyper::Method::GET,
530+
&format!("/libpod/containers/{name}/archive?path={encoded_path}"),
531+
None,
532+
API_TIMEOUT,
533+
)
534+
.await?;
535+
if status.is_success() {
536+
Ok(bytes)
537+
} else {
538+
Err(error_from_response(status.as_u16(), &bytes))
539+
}
540+
}
541+
516542
/// Inspect a container by name or ID.
517543
pub async fn inspect_container(&self, name: &str) -> Result<ContainerInspect, PodmanApiError> {
518544
validate_name(name)?;

0 commit comments

Comments
 (0)