Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 effective global-or-sandbox 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=openshell-workdir-probe` and gateway logs. The driver force-removes the exact attempt-unique probe on every normal success or failure path. A gateway process crash can leave a stopped probe; verify its generated name and image before removing it manually. The driver deliberately does not sweep containers by a forgeable label or name prefix.
- 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.

25 changes: 16 additions & 9 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 @@ -210,12 +210,19 @@ uses kernel effective-access checks so POSIX ACL and LSM decisions are honored.
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.
Both drivers reject image `VOLUME` declarations that would mask the workdir
ancestry. Docker performs the check in the final container before workload
launch. 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 identity source from the effective global-or-sandbox policy, or
discovers the image policy when neither exists, and emits a normalized
attestation that the final supervisor must match. This internal Podman-only
contract is required because the managed volume hides the original image tree
before the final supervisor starts. 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
6 changes: 6 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,11 @@ fn build_environment_for_oci_user(

environment.remove(openshell_core::sandbox_env::SANDBOX_TOKEN);
environment.remove(openshell_core::sandbox_env::SANDBOX_TOKEN_FILE);
// Ignore image-provided Podman probe attestations.
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
44 changes: 33 additions & 11 deletions crates/openshell-driver-podman/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,27 @@ 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 gateway supplies the identity source from the effective global-or-sandbox
policy, or requests image-policy discovery when neither exists. The final
supervisor must match the probe's normalized 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 +100,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 Expand Up @@ -291,6 +306,13 @@ sequenceDiagram

D->>P: pull_image(supervisor, "missing")
D->>P: pull_image(sandbox_image, policy)
D->>P: inspect_image(sandbox_image)

opt Non-default OCI workdir
D->>P: create + start validation probe
D->>P: wait + read bounded logs
D->>P: force-remove exact probe name
end

D->>P: create_volume(workspace)
Note over D: On failure below, rollback volume
Expand Down
Loading
Loading