Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ Common findings:
- Rootless networking unavailable: inspect Podman network configuration.
- Sandbox image missing or pull denied: verify image reference and registry credentials.
- 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. Root 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, or supplementary groups must already be able to traverse every parent and write and enter the directory. Podman checks the original image in a networkless temporary probe before attaching the workspace volume, so inspect the probe failure in gateway logs.
- If Podman reports probe cleanup or timeout failures, inspect temporary containers with `podman ps -a --filter name=workdir-probe` and gateway logs. The driver force-removes the probe on every normal success or failure path.
- Supervisor cannot call back: check callback endpoint and gateway logs.
- Gateway exits before becoming healthy with a callback-listener discovery
error: inspect `podman info --debug`, the configured Podman network, and the
Expand Down
7 changes: 7 additions & 0 deletions .agents/skills/openshell-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,13 @@ field wins independently; omitted fields fall back to the image declaration.
An image with no `USER` fails before readiness unless policy supplies both
fields.

Docker and Podman gateways also honor the image's OCI `WORKDIR`. An empty
value, `/`, or `/sandbox` uses the compatibility workspace at `/sandbox`.
Any other value must be an absolute, normalized path that already exists in
the image, contains no symlink components, and is traversable and writable by
the sandbox UID, GID, and supplementary groups. Podman validates that access
against the pinned image before its managed workspace volume covers the path.

### Forward ports

```bash
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 12 additions & 8 deletions architecture/compute-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ The gateway preserves whether each policy process field was omitted. The active
driver then supplies one authoritative identity input to the supervisor:

- Docker and Podman inspect the final sandbox image, pin container creation to
its immutable image ID, and pass its raw OCI `Config.User`. Docker also
resolves the workspace from OCI `Config.WorkingDir` during that inspection.
its immutable image ID, and pass its raw OCI `Config.User`. They also resolve
the workspace from OCI `Config.WorkingDir` during that inspection.
- Kubernetes passes its platform-resolved numeric UID/GID, including OpenShift
SCC-derived values.
- VM keeps its existing guest identity behavior.
Expand All @@ -199,7 +199,7 @@ and uses the same privilege-drop path for direct and SSH children. When a
declaration omits the group, the supervisor fills it with the user's numeric
primary GID. It does not rewrite the account files.

Docker uses an absolute OCI working directory as the workspace. An
Docker and Podman use an absolute OCI working directory as the workspace. An
empty, root (`/`), or explicit `/sandbox` declaration uses `/sandbox`, which
OpenShell creates and owns as a compatibility workspace. Any other workdir must already
exist in the immutable image without symlink components. The completed
Expand All @@ -211,11 +211,15 @@ Path checks reserve the standard OCI runtime namespaces under `/proc`, `/sys`,
and `/dev`, while separate collision checks are derived from actual OpenShell
control paths.
Docker performs the check in the final container before workload launch and
rejects image `VOLUME` declarations that would mask the workdir ancestry. The
resolved workspace is the child cwd and `HOME`; when
`filesystem.include_workdir` is enabled, it becomes the automatic writable
policy path. Podman, Kubernetes/OpenShift, and VM retain their existing
`/sandbox` workspace behavior.
rejects image `VOLUME` declarations that would mask the workdir ancestry.
Podman performs it in a minimal networkless container from the same pinned
image ID before its managed workspace volume covers the path. The probe adopts
the completed process identity and emits an attestation that the final
supervisor must match. The resolved workspace is the child cwd and `HOME`;
when `filesystem.include_workdir` is enabled, it
becomes the automatic writable policy path. Kubernetes/OpenShift keep their
`/sandbox` PVC and `fsGroup` behavior, and VM keeps its `/sandbox` guest
initialization path.

Sandbox creation fails before the workload becomes ready when a required image
identity is absent, malformed, unknown, ambiguous, or resolves to UID/GID 0.
Expand Down
7 changes: 7 additions & 0 deletions crates/openshell-core/src/sandbox_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ pub const SANDBOX_GID: &str = "OPENSHELL_SANDBOX_GID";
/// OCI only for the former contract.
pub const OCI_IMAGE_USER: &str = "OPENSHELL_OCI_IMAGE_USER";

/// Normalized UID/GID/supplementary-group identity attested by the Podman
/// immutable-image workspace probe.
///
/// A non-empty value also asserts that the
/// original workspace was validated before Podman's managed volume covered it.
pub const OCI_WORKSPACE_IDENTITY: &str = "OPENSHELL_OCI_WORKSPACE_IDENTITY";

// The corporate upstream-proxy configuration deliberately has no reserved
// environment variables: it travels on the supervisor's argv
// (`--upstream-proxy` and friends), which a sandbox image cannot forge the
Expand Down
8 changes: 8 additions & 0 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,7 @@ fn build_environment_for_oci_user(
user_env.extend(template.environment.clone());
}
user_env.extend(spec.environment.clone());
user_env.remove(openshell_core::sandbox_env::OCI_WORKSPACE_IDENTITY);
environment.extend(user_env.clone());
if !user_env.is_empty()
&& let Ok(json) = serde_json::to_string(&user_env)
Expand Down Expand Up @@ -2250,6 +2251,13 @@ fn build_environment_for_oci_user(

environment.remove(openshell_core::sandbox_env::SANDBOX_TOKEN);
environment.remove(openshell_core::sandbox_env::SANDBOX_TOKEN_FILE);
// Docker never uses the Podman prevalidation contract. Emit an explicit
// driver-owned empty value so image-baked ENV entries cannot select the
// managed-workspace mutation path for an image-provided workdir.
environment.insert(
openshell_core::sandbox_env::OCI_WORKSPACE_IDENTITY.to_string(),
String::new(),
);
environment.insert(
openshell_core::sandbox_env::OCI_IMAGE_USER.to_string(),
oci_user.to_string(),
Expand Down
9 changes: 9 additions & 0 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ fn test_sandbox() -> DriverSandbox {
}),
resource_requirements: None,
sandbox_token: String::new(),
workspace_validation_identity: None,
}),
status: None,
workspace: String::new(),
Expand Down Expand Up @@ -578,6 +579,10 @@ fn build_environment_protects_oci_identity_metadata() {
(openshell_core::sandbox_env::OCI_IMAGE_USER, "spoofed"),
(openshell_core::sandbox_env::SANDBOX_UID, "9999"),
(openshell_core::sandbox_env::SANDBOX_GID, "9999"),
(
openshell_core::sandbox_env::OCI_WORKSPACE_IDENTITY,
"9999:9999:",
),
] {
spec.environment.insert(key.to_string(), value.to_string());
}
Expand All @@ -590,6 +595,10 @@ fn build_environment_protects_oci_identity_metadata() {
)));
assert!(env.contains(&format!("{}=", openshell_core::sandbox_env::SANDBOX_UID)));
assert!(env.contains(&format!("{}=", openshell_core::sandbox_env::SANDBOX_GID)));
assert!(env.contains(&format!(
"{}=",
openshell_core::sandbox_env::OCI_WORKSPACE_IDENTITY
)));
assert!(!env.iter().any(|entry| entry.ends_with("=spoofed")));
assert!(!env.iter().any(|entry| entry.ends_with("=9999")));
}
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-podman/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ tracing-subscriber = { workspace = true }
thiserror = { workspace = true }
miette = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }

[dev-dependencies]
prost-types = { workspace = true }
Expand Down
35 changes: 24 additions & 11 deletions crates/openshell-driver-podman/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,25 @@ isolation enforcement to the `openshell-sandbox` supervisor binary, which is
sideloaded into each container via an OCI image volume mount.

Before creating the container, the driver inspects the final sandbox image and
captures its immutable image ID and raw OCI `Config.User`. Container creation
uses that image ID with pulling disabled, preventing a mutable tag from changing
between inspection and launch. The supervisor runs as root, resolves omitted
policy identity fields from the image declaration, and drops only agent
children to the completed identity. Named OCI components remain names after
validation; a missing group is filled with the user's numeric primary GID. Explicit
`process.run_as_user` and `process.run_as_group` values take precedence
independently.
captures its immutable image ID, raw OCI `Config.User`, and OCI
`Config.WorkingDir`. Container creation uses that image ID with pulling
disabled, preventing a mutable tag from changing between inspection and launch.
The supervisor runs as root, resolves omitted policy identity fields from the
image declaration, and drops only agent children to the completed identity.
Named OCI components remain names after validation; a missing group is filled
with the user's numeric primary GID. Explicit `process.run_as_user` and
`process.run_as_group` values take precedence independently.

An absolute OCI working directory becomes the agent workspace. An empty,
root (`/`), or explicit `/sandbox` declaration uses `/sandbox`, which OpenShell
creates and owns as a compatibility workspace. For any other workdir, a
resource-limited, networkless probe verifies the original pinned image before
Podman covers the path with the managed workspace volume. The completed process
identity must already be able to traverse every parent and write and enter the
directory, without symlink components or OpenShell control-path collisions.
The final supervisor must match the probe's identity before preparing the
volume. See [Compute runtimes](../../architecture/compute-runtimes.md#process-identity)
for the invariant and probe lifecycle.

For a rootless networking deep dive, see [NETWORKING.md](NETWORKING.md).

Expand Down Expand Up @@ -87,9 +98,11 @@ optional `selinux_label` of `shared` (applies `:z`) or `private` (applies
read-only by default; set `read_only: false` to make them writable. Podman
image and volume mounts do not support `subpath` in OpenShell driver config.
Mount `source` and `target` values must not contain surrounding whitespace.
Mount targets must be absolute container paths and must not replace
the workspace root (`/sandbox`) or overlap OpenShell supervisor files,
`/etc/openshell`, `/etc/openshell-tls`, or `/run/netns`.
Mount targets must be absolute container paths and must not replace the
resolved workspace root or any of its parents. Nested workspace mounts remain
valid. Mounts also must not contain or be contained by concrete OpenShell
control targets such as the supervisor mount, TLS and token files, runtime
socket, or `/run/netns`.

Example named-volume usage:

Expand Down
Loading
Loading