Skip to content
Closed
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
15 changes: 15 additions & 0 deletions docs/sandbox-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ container. Configure the Host with an authenticated `OPENGUI_SHELL_SANDBOX_ENDPO
- `OPENGUI_SHELL_RESOLV_CONF`: trusted resolver configuration mounted read-only;
- optional `OPENGUI_SHELL_DEPLOY_CREDENTIALS`: repository-root to deploy-key mappings.

Run `sandbox-doctor.ts` with the broker's `OPENGUI_SHELL_IMAGE` configuration and the same
rootless Docker context used by the broker before deployment, restart, upgrade, or rollback.
The doctor inspects the configured image locally and fails if it is missing or Docker cannot
inspect it; it never pulls or restores an image. Without `OPENGUI_SHELL_IMAGE`, the image check
is skipped for deployments that do not use the separate broker. A broker on another machine
must be checked on that machine, not against the web Host's Docker daemon.

A successful preflight or web `/api/health` response does not establish shell readiness. Verify
an actual authenticated restricted-Account shell call in a disposable granted Project, including
a read-only grant and denied access outside the grant. Repeat after cleanup and upgrades. This
doctor does not install image retention, recurring probes, alerts, or recovery automation.
Immutable image IDs/digests do not prevent image deletion; restore and verify the required image
before retrying deployment or rolling back. Do not use broad image/container/system pruning as
a readiness test.

The Host re-resolves the actor's policy immediately before every tool effect. Restricted shell is
enabled only when the actor has grants and the broker is configured. Every invocation gets a new
gVisor container with only those canonical grant roots mounted; `read` grants are mounted read-only
Expand Down
70 changes: 68 additions & 2 deletions scripts/sandbox-doctor.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { sandboxHostFindings } from "./sandbox-doctor.ts";
import { describe, expect, test, vi } from "vitest";
import { inspectSandboxHost, sandboxHostFindings, type DockerCommand } from "./sandbox-doctor.ts";

describe("sandbox Host doctor", () => {
test("accepts a rootless Docker daemon with runsc and private deployment files", () => {
Expand All @@ -14,6 +14,72 @@ describe("sandbox Host doctor", () => {
).toEqual([]);
});

test("rejects an unavailable configured shell image even when Docker and files are healthy", () => {
expect(
sandboxHostFindings({
securityOptions: ["name=rootless"],
runtimes: ["runc", "runsc"],
workspace: { exists: true, directory: true, mode: 0o750 },
deployKey: { exists: true, directory: false, mode: 0o600 },
knownHosts: { exists: true, directory: false, mode: 0o644 },
shellImageAvailable: false,
}),
).toContain("OPENGUI_SHELL_IMAGE is not available in the selected Docker daemon");
});

test.each([
{ label: "present image", status: 0, stdout: `sha256:${"a".repeat(64)}\n`, available: true },
{ label: "missing image", status: 1, stdout: "", available: false },
{ label: "Docker error", status: null, stdout: "", available: false },
{ label: "invalid inspection output", status: 0, stdout: "null\n", available: false },
])(
"inspects the configured image without pulling it: $label",
({ status, stdout, available }) => {
const docker = vi.fn<DockerCommand>((arguments_) => {
if (arguments_[0] === "image") {
return { status, stdout, stderr: "fixture diagnostic with private content" };
}
return {
status: 0,
stdout: arguments_[2]?.includes("SecurityOptions") ? '["name=rootless"]' : "runsc\n",
stderr: "",
};
});
const inspection = inspectSandboxHost(
{
OPENGUI_SHELL_IMAGE: "fixture/image:release",
OPENGUI_SHELL_BROKER_TOKEN: "fixture-secret",
},
docker,
);
expect(inspection.shellImageAvailable).toBe(available);
expect(docker).toHaveBeenCalledWith([
"image",
"inspect",
"--format",
"{{.Id}}",
"--",
"fixture/image:release",
]);
expect(docker).toHaveBeenCalledTimes(3);
const diagnostics = JSON.stringify(sandboxHostFindings(inspection));
expect(diagnostics).not.toContain("fixture-secret");
expect(diagnostics).not.toContain("private content");
expect(diagnostics).not.toContain("fixture/image");
},
);

test("does not require or inspect an image for broker-free deployments", () => {
const docker = vi.fn<DockerCommand>((arguments_) => ({
status: 0,
stdout: arguments_[2]?.includes("SecurityOptions") ? '["name=rootless"]' : "runsc\n",
stderr: "",
}));
expect(inspectSandboxHost({}, docker).shellImageAvailable).toBeUndefined();
expect(docker).toHaveBeenCalledTimes(2);
expect(docker.mock.calls.every(([arguments_]) => arguments_[0] === "info")).toBe(true);
});

test("reports unsafe or incomplete hosts without exposing credential contents", () => {
expect(
sandboxHostFindings({
Expand Down
42 changes: 36 additions & 6 deletions scripts/sandbox-doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export type SandboxHostInspection = {
workspace: PathStatus;
deployKey: PathStatus;
knownHosts: PathStatus;
/** Unset for deployments that do not configure the separate shell broker. */
shellImageAvailable?: boolean;
};

export type DockerCommand = (arguments_: string[]) => {
status: number | null;
stdout: string;
stderr: string;
};

export function sandboxHostFindings(inspection: SandboxHostInspection) {
Expand All @@ -32,6 +40,9 @@ export function sandboxHostFindings(inspection: SandboxHostInspection) {
if (!inspection.knownHosts.exists) {
findings.push("GitHub known-hosts file does not exist");
}
if (inspection.shellImageAvailable === false) {
findings.push("OPENGUI_SHELL_IMAGE is not available in the selected Docker daemon");
}
return findings;
}

Expand All @@ -45,28 +56,44 @@ function pathStatus(path: string | undefined): PathStatus {
}
}

function dockerOutput(format: string) {
const result = spawnSync("docker", ["info", "--format", format], {
const runDocker: DockerCommand = (arguments_) => {
const result = spawnSync("docker", arguments_, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
};

function dockerOutput(format: string, docker: DockerCommand) {
const result = docker(["info", "--format", format]);
if (result.status !== 0) {
throw new Error(result.stderr.trim() || "docker info failed");
}
return result.stdout.trim();
}

export function inspectSandboxHost(environment: NodeJS.ProcessEnv = process.env) {
const securityOptions = JSON.parse(dockerOutput("{{json .SecurityOptions}}")) as string[];
const runtimes = dockerOutput("{{range $name, $_ := .Runtimes}}{{println $name}}{{end}}")
export function inspectSandboxHost(
environment: NodeJS.ProcessEnv = process.env,
docker: DockerCommand = runDocker,
) {
const securityOptions = JSON.parse(dockerOutput("{{json .SecurityOptions}}", docker)) as string[];
const runtimes = dockerOutput("{{range $name, $_ := .Runtimes}}{{println $name}}{{end}}", docker)
.split(/\s+/u)
.filter(Boolean);
const image = environment.OPENGUI_SHELL_IMAGE;
let shellImageAvailable: boolean | undefined;
if (image !== undefined) {
const result = docker(["image", "inspect", "--format", "{{.Id}}", "--", image]);
shellImageAvailable =
result.status === 0 && /^sha256:[a-f0-9]{64}$/u.test(result.stdout.trim());
}
return {
securityOptions,
runtimes,
workspace: pathStatus(environment.OPENGUI_WORKSPACE),
deployKey: pathStatus(environment.OPENGUI_GITHUB_DEPLOY_KEY_FILE),
knownHosts: pathStatus(environment.OPENGUI_GITHUB_KNOWN_HOSTS_FILE),
shellImageAvailable,
} satisfies SandboxHostInspection;
}

Expand All @@ -79,7 +106,10 @@ function main() {
process.exitCode = 1;
return;
}
console.info("Sandbox Host is ready: rootless Docker, runsc, workspace, and GitHub files OK");
console.info(
"Sandbox Host preflight passed: Docker, runsc, files, and any configured shell image OK. " +
"This is not proof of usable shell execution; run an authenticated restricted-grant probe.",
);
} catch (error) {
console.error(
`Sandbox Host inspection failed: ${error instanceof Error ? error.message : String(error)}`,
Expand Down
Loading