Skip to content

Repository files navigation

openblox

openblox

CI Tests Integration tests Coverage Lines of Go Go Reference Go 1.25 License: MIT

Run untrusted, AI-generated code on your own hardware. A Go library over Docker and gVisor, small enough to read in an afternoon — no control plane, no database, no scheduler. A sandbox is a container, and the container is the state.

openblox.sh · Docs · Getting started · Production · Architecture · Threat model · Security · Releasing · Discord

Status: pre-release (0.x). The API will change; breaking changes bump the minor version and are listed in the changelog.

Install

Linux on amd64 or arm64, Docker, and gVisor registered as the runsc runtime — or a microVM runtime such as Kata, selected explicitly (how).

The daemon. Your application never touches the Docker socket:

curl -fsSL https://openblox.sh/install.sh -o install.sh
less install.sh          # the bytes you are about to run
sh install.sh

Fetched once and run from disk, so what you read is what executes. Piping curl straight into sh reads one response and runs another.

The script installs one binary and starts nothing. It verifies the published checksum, and the Sigstore build attestation too when the gh CLI is present. Its source is www/install.sh — that URL serves this file.

Pin a version in production, and pick your own target if you want one:

OPENBLOX_VERSION=v0.8.1 OPENBLOX_BIN_DIR=~/.local/bin sh install.sh

The library. For a single process that may hold the Docker socket:

go get github.com/blox-eng/openblox

Use it

backend, err := docker.New()
if err != nil {
    return err
}
defer backend.Close()

// No options: no network, non-root, read-only rootfs, capped CPU/memory/PIDs,
// gVisor runtime, reaped when idle.
sb, err := backend.Create(ctx, "session-1",
    sandbox.WithImage("ghcr.io/blox-eng/openblox-sandbox:latest"))
if err != nil {
    return err
}

res, err := sb.Exec(ctx, sandbox.Command{
    Argv: []string{"python3", "-c", "print(6 * 7)"},
})
fmt.Println(string(res.Stdout)) // 42

That image is the reference sandbox userland; any image that meets the contract works. :latest is fine here — pin a digest anywhere it matters.

In production: run openbloxd

The example above imports the library, so your process holds the Docker socket, which is root-equivalent on the host. In production, run the openbloxd daemon on the host instead. It owns the socket, and your application talks to it over a Unix socket with pkg/brokerclient — the same Backend interface, no Docker access:

application ──unix socket──► openbloxd ──Docker API──► Docker + gVisor ──► sandbox
(no Docker access)           (policy per profile,
                              not settable by requests)

Deployment, verification, compatibility, upgrades and troubleshooting: Running in production.

Where this sits

openblox is the layer below a sandbox platform, not a smaller one.

  your scheduler, your tenancy, your API    ← yours to build, if you ever need it
  ──────────────────────────────────────
  openblox                                  ← isolation, done correctly
  ──────────────────────────────────────
  Docker + gVisor                           ← the boundary itself

One rule decides what belongs here:

How a sandbox is isolated is openblox's problem. Which sandbox runs where is yours.

Egress, capabilities, filesystem, resource caps, lifetime, runtime: openblox's. Placement, queueing, tenancy, metering, snapshots: not openblox's, and not planned. Build those on top when something actually asks for them. That is what a lower layer is for, and it is why there is no control plane to adopt first.

The comparison is libvirt, not OpenStack.

Restrictive by default

The zero value of every option is the most restrictive one. A sandbox created with no options gets:

Isolation gVisor (runsc) — syscalls handled in user space, not by the host kernel
Network no external interface, so no egress and no DNS side channel
Filesystem read-only root, non-root user (root is refused), noexec scratch
Resources bounded CPU, memory (no swap), disk, process count, and captured output
Privileges all capabilities dropped, no-new-privileges
Lifetime commands killed at their timeout; sandboxes reaped when idle and at max age

Relaxing anything is explicit and greppable at the call site. If the host cannot provide the runtime asked for — gVisor, unless you chose a microVM runtime such as Kata — Create fails with ErrRuntimeUnavailable; it never falls back to a weaker boundary.

These are isolation measures, not a guarantee: the boundary is the runtime's — gVisor's by default — and THREAT_MODEL.md lists what is defended, the test behind each claim, and what is not defended. Under Kata two of them differ: /dev/shm is writable and executable, because Kata discards its mount options, and crash recovery is unmeasured (details).

Two levels of the same guarantee. In the library, your code chooses: the defaults are safe, and every relaxation is explicit and greppable at the call site. Through openbloxd the choice stops being the caller's at all — profiles live in the daemon's config file and no request can reach them. A caller names a profile. It cannot name an image, a runtime, a user, an egress policy, or a resource cap.

That is the difference between weakening being visible and weakening being unreachable, and it is the whole reason the daemon exists.

Measuring an implementation

The claims in THREAT_MODEL.md are backed by pkg/conformance, the same adversarial suite pkg/docker runs against itself, expressed against the sandbox.Backend interface rather than against one implementation. Point it at your own backend to find out how it scores — it does not skip:

func TestConformance(t *testing.T) {
    cfg := conformance.Config{
        Name: "mine",
        New:  func() (sandbox.Backend, error) { return mypkg.New() },
    }
    conformance.Run(t, cfg)
    conformance.RunHostLocal(t, cfg)
}

New takes no *testing.T on purpose: the suite, not the implementation, decides what a construction failure means, and there is nothing to skip with.

It has only ever run against pkg/docker: under gVisor on every pull request, and under Kata on amd64 in a separate workflow that does not gate merges, where 21 of 23 properties pass. See the package doc for what each tier covers.

What you get

Exec run a command with a per-call timeout, get stdout, stderr, exit code
Files read and write inside the sandbox without a shell round-trip
Processes start a detached background command, idempotently
Preview links HMAC-signed reverse proxy to a port inside the sandbox
Reaping idle timeout and max age, enforced without a scheduler
openbloxd a policy broker so callers never touch Docker

When not to use it

  • You need tenants isolated from each other at the API. Every caller of one openbloxd can reach every sandbox; tenancy is yours to enforce in front of it.
  • You need a fleet. One host, one daemon. No scheduling, no fairness.
  • You need protection from side channels between co-resident sandboxes. A microVM runtime such as Kata removes the shared kernel, not the shared CPU: microarchitectural side channels remain.
  • You need snapshots, fork, pause/resume, or sub-second cold starts.
  • You cannot run Linux with gVisor or a microVM runtime, or cannot keep that runtime patched.

Most of these are placement rather than isolation, which the rule above puts on your side of the line; the rest are trades made deliberately. None are gaps waiting to be filled. They are the boundary that keeps openblox small enough to be worth reading, and requests to cross it get declined on that basis. See ARCHITECTURE.md for the reasoning.

Supported

Linux on amd64 and arm64, both tested natively in CI against a real gVisor runtime. Docker Engine with runsc registered, or a microVM runtime such as Kata selected explicitly; Kata is measured on amd64 in a separate workflow that does not gate merges. Go 1.25+ for library users. The compatibility matrix covers openbloxd, clients, images, Docker and the runtime.

Status

The badges above are measured from the source on every deploy, not typed here. Three direct dependencies (docker/docker, containerd/errdefs, yaml.v3).

CI runs lint, race-enabled tests, CodeQL and govulncheck (gating on newly reachable vulnerabilities), plus the integration and conformance suites against a real gVisor runtime on amd64 and arm64 — including attacks on the network, filesystem, privileges, resource caps and timeouts.

Every release is cut by CI from a verified commit. The openbloxd binaries are reproducible and ship with SBOMs; binaries and the sandbox image carry Sigstore-signed build provenance. RELEASING.md shows how to verify them.

Written for Blox, where it is the only sandbox backend and replaced a hosted platform. Its own production rollout is gated on migrating its callers off the Docker socket and onto openbloxd. No support SLA.

Security

Report vulnerabilities privately — see SECURITY.md. Please do not open a public issue.

Contributing

Issues and PRs welcome. See CONTRIBUTING.md. Commits follow Conventional Commits; CI enforces it. For a question that is not an issue, there is a Discord.

License

MIT — see LICENSE.

The openbloxd binaries are statically linked, so they also carry the code of their dependencies. Every release attaches THIRD_PARTY_LICENSES.txt with the full licence text of each linked module, generated from what is actually in the binary. Build it yourself with make licenses.

About

Secure sandboxes for running untrusted AI-generated code. A small Go library over Docker + gVisor — no control plane, no database, MIT.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages