-
Notifications
You must be signed in to change notification settings - Fork 454
feat: add sandbox.agent.runtime: gvisor for gVisor container runtime support #44796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9e0513c
81ff5b1
35185d8
90c25bb
6fb0b2b
5a8aabb
c772544
7a451a0
e7fc1d7
51b942c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # ADR-44796: Use gVisor (runsc) as an Optional Container Runtime for Agent Containers | ||
|
|
||
| **Date**: 2026-07-10 | ||
| **Status**: Draft | ||
| **Deciders**: @lpcox | ||
|
|
||
| --- | ||
|
|
||
| ### Context | ||
|
|
||
| Agent containers in gh-aw run under a standard Docker runtime backed by Linux cgroups and namespaces. This provides process-level isolation but leaves the host kernel's system call surface fully accessible to agent code. Any vulnerability in a system call handler can be exploited to escape the container. For higher-assurance workloads, operators need a runtime that interposes on all syscalls and runs them through a user-space kernel, substantially reducing the kernel attack surface exposed to untrusted agent code. The AWF daemon already supports `container.containerRuntime: "gvisor"` in its stdin config (gh-aw-firewall#6093), but the compiler had no way to emit that setting or install the runtime on the runner. This PR wires up the missing compiler side: a new `sandbox.agent.runtime` frontmatter field, an auto-generated pre-agent gVisor install step, and the AWF config JSON plumbing that passes `containerRuntime: "gvisor"` to AWF. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smoke-test review: the AWFContainerRuntimeMinVersion placeholder (v0.28.0) should be tracked so it is updated when the firewall release ships. |
||
|
|
||
| ### Decision | ||
|
|
||
| We will add `sandbox.agent.runtime: gvisor` as an optional frontmatter field. When set, the compiler (1) emits a GitHub Actions step before the AWF install step that downloads `runsc` and `containerd-shim-runsc-v1` from `storage.googleapis.com/gvisor`, installs them to `/usr/local/bin`, registers them with Docker via `sudo runsc install`, restarts Docker with `systemctl restart`, and verifies the runtime with a test container, and (2) passes `containerRuntime: "gvisor"` in the AWF stdin config JSON so AWF starts the agent container under `runsc` — once the effective AWF version is at or above `AWFContainerRuntimeMinVersion` (currently a placeholder at `v0.28.0` pending the release of gh-aw-firewall#6093). One incompatible combination is rejected at compile time: `runtime: gvisor` + `runner.topology: arc-dind` (systemd unavailable on ARC DinD). The `runtime: gvisor` option is compatible with both `sudo: false` (default, network-isolation mode) and `sudo: true` — the gVisor install step uses shell-level sudo commands independently of the `sandbox.agent.sudo` setting. The existing `sudo: true` deprecation warning continues to apply regardless of `runtime`. | ||
|
|
||
| ### Alternatives Considered | ||
|
|
||
| #### Alternative 1: Standard Docker runtime (status quo) | ||
|
|
||
| Continue using only Linux cgroups/namespaces for agent container isolation without adding a new runtime option. System calls from inside the container reach the host kernel directly. This approach requires zero compiler changes and no runner setup time, and it already satisfies many workloads. It was rejected because it does not meet the kernel-level isolation requirement: a single exploited syscall can escape the container, and operators running sensitive workloads need a stronger isolation boundary. | ||
|
|
||
| #### Alternative 2: Kata Containers | ||
|
|
||
| Kata Containers provide VM-level isolation — each container runs in a lightweight QEMU/Firecracker VM with a dedicated kernel. This gives stronger isolation guarantees than gVisor in some threat models. It was not chosen because: (a) Kata requires KVM hardware virtualization support, which is not available in all GitHub-hosted runner configurations; (b) the AWF daemon already has native support for `containerRuntime: "gvisor"` but not for Kata; and (c) gVisor's installation footprint (two binaries + `runsc install`) is far smaller than Kata's, reducing runner setup time and dependency surface. | ||
|
|
||
| #### Alternative 3: Seccomp / AppArmor profiles | ||
|
|
||
| Reduce the syscall surface by attaching a restrictive seccomp profile or AppArmor policy to the agent container, without changing the OCI runtime. This has near-zero overhead and no special hardware requirements. It was not chosen because it still routes all permitted syscalls through the real host kernel — a zero-day in a whitelisted syscall path remains exploitable. Profiles also require ongoing maintenance as agent behavior evolves, and they do not satisfy an isolation requirement that explicitly demands a user-space kernel intercept layer. | ||
|
|
||
| ### Consequences | ||
|
|
||
| #### Positive | ||
| - Agent containers configured with `runtime: gvisor` run under gVisor's `runsc` OCI runtime, which interposes on all system calls via a user-space kernel, substantially reducing the host kernel attack surface. | ||
| - Compile-time validation catches the known incompatible combination (`arc-dind`) before a workflow is submitted, giving authors a clear error message rather than a runtime failure. | ||
| - The change is fully backward-compatible: existing workflows that do not set `sandbox.agent.runtime` are unaffected and continue to run under the default Docker runtime. | ||
| - `runtime: gvisor` is compatible with `sudo: false` (the default, network-isolation mode): the install step uses shell-level sudo commands independently of the `sandbox.agent.sudo` setting, so operators can have both gVisor kernel isolation and AWF network isolation. | ||
| - Removing the special case for `AgentRuntimeGVisor` in `validateStrictSandboxCustomization` simplifies the strict-mode validation path. | ||
|
|
||
| #### Negative | ||
| - `runtime: gvisor` is incompatible with `runner.topology: arc-dind`, restricting which runner configurations can use this feature. | ||
| - The gVisor install step adds observable setup time (binary downloads, Docker restart, verification container run) to every workflow execution that enables this option. | ||
| - The `containerRuntime` field in the AWF config is gated behind `AWFContainerRuntimeMinVersion` (placeholder `v0.28.0`) until gh-aw-firewall#6093 is released; until then, the install step runs but AWF uses its default runtime. | ||
|
|
||
| #### Neutral | ||
| - The `systemctl restart docker` (rather than `reload`) constraint documented in the implementation will need to be preserved in future modifications to the install step: Docker's SIGHUP reload does not call `setHostGatewayIP()`, which breaks `--add-host host.docker.internal:host-gateway`. | ||
| - AWF translates the `"gvisor"` string in its config to `"runsc"` internally; the compiler intentionally passes the human-readable label rather than the internal runtime name. | ||
| - Future container runtime options (e.g., Kata) can follow the same `AgentRuntime` type + `sandbox.agent.runtime` enum pattern established here. | ||
|
|
||
| --- | ||
|
|
||
| *ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -330,3 +330,60 @@ func generateDockerComposeInstallStep() GitHubActionStep { | |
| ` docker compose version`, | ||
| }) | ||
| } | ||
|
|
||
| // generateGVisorInstallStep creates a GitHub Actions step that downloads, installs, | ||
| // and verifies the gVisor (runsc) container runtime. This step must run BEFORE the | ||
| // AWF invocation step so that Docker can start the agent container under runsc. | ||
| // | ||
| // Key implementation notes: | ||
| // - Pins to constants.DefaultGVisorVersion rather than "latest" for reproducible, | ||
| // verifiable installs. Each binary is verified against its official SHA-512 file | ||
| // before being installed with root privileges, matching the pattern used by the | ||
| // adjacent AWF installer. | ||
| // - Uses uname -m directly (x86_64, aarch64) — gVisor download URLs use raw arch names. | ||
| // - Restarts Docker with `systemctl restart` (NOT reload): Docker's SIGHUP reload does | ||
| // not call setHostGatewayIP(), which breaks --add-host host.docker.internal:host-gateway. | ||
| // - Downloads both runsc and containerd-shim-runsc-v1; the shim is required for Docker's | ||
| // containerd integration. | ||
| func generateGVisorInstallStep() GitHubActionStep { | ||
| version := constants.DefaultGVisorVersion | ||
| return GitHubActionStep([]string{ | ||
| " - name: Install gVisor (runsc)", | ||
| " run: |", | ||
| " set -euo pipefail", | ||
| "", | ||
| ` echo "::group::Install gVisor (runsc)"`, | ||
| ` ARCH=$(uname -m)`, | ||
| ` URL="https://storage.googleapis.com/gvisor/releases/release/` + version + `/${ARCH}"`, | ||
| ` echo "Downloading runsc ` + version + ` for ${ARCH}..."`, | ||
| ` curl -fsSL "${URL}/runsc" -o /tmp/runsc`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] No checksum verification for downloaded binaries — a compromised or MITM-replaced The AWF install step explicitly calls out SHA256 verification as a supply-chain protection. The gVisor curl downloads have no equivalent check. 💡 Suggested fixgVisor publishes signed checksums at curl -fsSL "${URL}/runsc" -o /tmp/runsc
curl -fsSL "${URL}/runsc.sha512" -o /tmp/runsc.sha512
sha512sum -c /tmp/runsc.sha512
curl -fsSL "${URL}/containerd-shim-runsc-v1" -o /tmp/containerd-shim-runsc-v1
curl -fsSL "${URL}/containerd-shim-runsc-v1.sha512" -o /tmp/containerd-shim-runsc-v1.sha512
sha512sum -c /tmp/containerd-shim-runsc-v1.sha512This mirrors the supply-chain protection described in @copilot please address this. |
||
| ` curl -fsSL "${URL}/runsc.sha512" -o /tmp/runsc.sha512`, | ||
| ` echo "Verifying SHA-512 for runsc..."`, | ||
| ` (cd /tmp && sha512sum -c runsc.sha512)`, | ||
| ` curl -fsSL "${URL}/containerd-shim-runsc-v1" -o /tmp/containerd-shim-runsc-v1`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: No integrity verification of downloaded binaries The This is a supply-chain risk: a MITM or compromised GCS bucket could deliver a malicious binary that runs as root inside the agent container. Suggested fix: curl -fsSL "${URL}/runsc" -o /tmp/runsc
curl -fsSL "${URL}/runsc.sha512" -o /tmp/runsc.sha512
sha512sum -c /tmp/runsc.sha512
curl -fsSL "${URL}/containerd-shim-runsc-v1" -o /tmp/containerd-shim-runsc-v1
curl -fsSL "${URL}/containerd-shim-runsc-v1.sha512" -o /tmp/containerd-shim-runsc-v1.sha512
sha512sum -c /tmp/containerd-shim-runsc-v1.sha512@copilot please address this. |
||
| ` curl -fsSL "${URL}/containerd-shim-runsc-v1.sha512" -o /tmp/containerd-shim-runsc-v1.sha512`, | ||
| ` echo "Verifying SHA-512 for containerd-shim-runsc-v1..."`, | ||
| ` (cd /tmp && sha512sum -c containerd-shim-runsc-v1.sha512)`, | ||
| ` sudo install -m 755 /tmp/runsc /usr/local/bin/runsc`, | ||
| ` sudo install -m 755 /tmp/containerd-shim-runsc-v1 /usr/local/bin/containerd-shim-runsc-v1`, | ||
| ` runsc --version`, | ||
| ` echo "::endgroup::"`, | ||
| "", | ||
| ` echo "::group::Register runsc as Docker runtime"`, | ||
| ` sudo runsc install`, | ||
| ` # IMPORTANT: Must use restart (not reload).`, | ||
| ` # Docker's SIGHUP reload does NOT call setHostGatewayIP(), so`, | ||
| ` # --add-host host.docker.internal:host-gateway breaks for any`, | ||
| ` # container started after a reload-only config change.`, | ||
| ` sudo systemctl restart docker`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 Suggested fixUse a portable restart + readiness wait: # Portable full restart (not SIGHUP/reload) on both systemd and non-systemd runners
sudo service docker restart 2>/dev/null || sudo systemctl restart docker
# Wait for daemon to be ready
timeout 30 bash -c 'until docker info &>/dev/null; do sleep 1; done'
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] A failed Docker restart leaves the runner in a broken state mid-job rather than surfacing a clear compile-time or early-runtime error. 💡 Options
Option 1 is preferred — fail fast at compile time with a clear message. @copilot please address this. |
||
| ` echo "Docker runtimes:"`, | ||
| ` docker info --format '{{.Runtimes}}' || docker info | grep -i runtime`, | ||
| ` echo "::endgroup::"`, | ||
| "", | ||
| ` echo "::group::Verify gVisor works"`, | ||
| ` docker pull hello-world`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reliability: Verification step pulls from Docker Hub, which may be blocked by sandbox network policy When Consider using @copilot please address this. |
||
| ` docker run --rm --runtime=runsc hello-world`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 Suggested fixVerify gVisor using only local tools — no image pull needed: # Confirm runsc binary is functional — no network required
runsc --version
runsc --help &>/dev/null && echo "runsc OK"If a container smoke-test is truly needed, use the Docker daemon's built-in |
||
| ` echo "✅ gVisor runtime verified"`, | ||
| ` echo "::endgroup::"`, | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -266,6 +266,14 @@ func (c *Compiler) extractAgentSandboxConfig(agentVal any) *AgentSandboxConfig { | |
| } | ||
| } | ||
|
|
||
| // Extract runtime (container runtime for the agent container) | ||
| if runtimeVal, hasRuntime := agentObj["runtime"]; hasRuntime { | ||
| if runtimeStr, ok := runtimeVal.(string); ok { | ||
| agentConfig.Runtime = AgentRuntime(runtimeStr) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unvalidated runtime string is passed downstream without allowlist check: any arbitrary string from the workflow YAML is cast directly to 💡 Suggested fixValidate against the allowlist of known if runtimeStr, ok := runtimeVal.(string); ok {
rt := AgentRuntime(runtimeStr)
switch rt {
case AgentRuntimeGVisor:
agentConfig.Runtime = rt
frontmatterExtractionSecurityLog.Printf("Extracted sandbox.agent.runtime: %s", runtimeStr)
default:
// Return a parse/validation error — unknown runtime
return nil, fmt.Errorf("sandbox.agent.runtime: unsupported value %q (supported: gvisor)", runtimeStr)
}
}This is particularly important in a file named
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] Unknown runtime values are silently accepted — an invalid The JSON schema rejects unknown values via the 💡 Suggested fixAdd a check after extracting the value: switch agentConfig.Runtime {
case AgentRuntimeGVisor, "":
// valid
default:
return nil, fmt.Errorf("sandbox.agent.runtime: unknown value %q; valid values: gvisor", runtimeStr)
}This makes the Go layer the authoritative validator rather than relying solely on the JSON schema. @copilot please address this. |
||
| frontmatterExtractionSecurityLog.Printf("Extracted sandbox.agent.runtime: %s", runtimeStr) | ||
| } | ||
| } | ||
|
|
||
| // Extract model-fallback (AWF API proxy model fallback enable/disable flag) | ||
| if mfVal, hasMF := agentObj["model-fallback"]; hasMF { | ||
| switch v := mfVal.(type) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Smoke-test review: consider linking the referenced issue gh-aw-firewall#6093 for traceability once it lands.