Skip to content

Latest commit

 

History

920 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
kern

kern: A fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and AI-generated code.

A real, kernel-enforced container in ~3.5 ms, out of one static binary with no daemon.

Terminal: 'kern box app --image alpine -- echo hello from a real container' prints the greeting, then reports that kern started in 3.5 ms against docker run's 297 ms. A real OCI image, rootless, a static binary, no daemon, on an Intel i7-14700KF, Linux 7.0.

3.5 ms is one machine and one workload: how it was measured

0 RAM at rest · no daemon, no socket, nothing to start · one static binary, libc its only Rust dependency

CI License: Apache-2.0 Runs on

# install the release binary (static, checksum-verified by the script)
curl -fsSL https://raw.githubusercontent.com/getkern/kern/main/install.sh | sh

# a throwaway shell in a real OCI image: rootless, kernel-enforced, a few ms
kern box dev --image alpine -it -- sh

No native Windows: use WSL2. No native macOS: run it in a Linux VM. Install.


What kern is

One binary that manages resources, of which isolation is the first. That is why there is no single row for kern in a comparison table: it is a container runtime, a sandbox, a resource slicer and a stack runner at once, in one static binary with no daemon.

  • A real container. Real OCI images: pull, build from a Dockerfile, commit, push, save/load. A box from an image starts in ~3.5 ms.
  • Rootless, always. User, PID, mount, network, UTS and IPC namespaces, an overlay or read-only root pivoted in, a deny-by-default seccomp allowlist and cgroup v2 limits. One flag, --security-profile untrusted, is the whole hardened bundle.
  • A sandbox for code you did not write. Agent tool-calls, LLM-generated snippets, CI jobs, build steps, notebook cells: a fresh box per call, network off, memory and pid caps, capabilities dropped, and a timeout enforced from outside the box. A timeout, an OOM-kill or a blocked syscall comes back as data on the result, not as an exception. kern-sandbox for Python and Node, and an MCP server for Claude Desktop or Cursor. bindings/python/README.md
  • Resource profiles, not just isolation. CPU (vcpu:), memory, disk (vdisk:) and devices (vgpio:), declared once in a kern.toml and attached by name. kern run applies the same caps to a process on the host, with no sandbox at all, plus --landlock-rw <path> to confine that process's writes with the kernel's own LSM. docs/RESOURCES.md
  • Stacks, in kern's own format or in the one you already have. kern compose <file> up takes a stack.toml ([box.NAME] tables, with the resource profiles above) or a docker-compose.yml, with no conversion step. One stack to one pod, services reaching each other by name.
  • The tools around them. ps, logs, exec, stats, inspect, wait, top (a live TUI), doctor. The Python binding also plugs into LangChain twice: as a code tool, and as an execution policy for its shell middleware.

Its entire Rust dependency tree is libc: JSON and OCI manifests are parsed by hand, and pull shells out to the curl and tar already on the machine rather than linking a TLS stack.

Terminal demo: a kern.toml defines reusable vcpu/vdisk/vgpio (device) profiles; 'kern box train --image alpine vcpu:heavy vdisk:scratch' attaches a 4-vCPU, 8 GB, 2 GB-scratch rootless isolated slice in a few ms (docker run takes ~297 ms); 'kern run vcpu:heavy -- ffmpeg' caps a heavy transcode with no sandbox; 'kern box iot --image alpine vgpio:sensor' exposes only /dev/i2c-1 and nothing else; piping a request into 'kern box fn --image python' runs it in a fresh isolated box per request (serverless style); 'kern compose stack.toml up' brings up a multi-box stack; 'kern top' is the live TUI for boxes, profiles and volumes: CPU, memory, disk and devices, sliced per box, in one static binary, no daemon.

Install

kern needs a Linux kernel with unprivileged user namespaces and cgroup v2. It runs on Linux, WSL2 and ARM boards (Raspberry Pi · Jetson · Arduino UNO Q); there is no native Windows build, use WSL2 (kern ships a pre-baked WSL rootfs).

On a Mac there is no native build either, and there will not be one: macOS has no namespaces and no cgroups. kern runs on a Mac inside a Linux VM (colima, Lima, OrbStack, UTM, or one you already run), where it is the ordinary Linux kern, same binary and same CLI as your CI box. Verified on Apple Silicon with an Ubuntu 24.04 guest. Read docs/INSTALL.md first: two obstacles are certain there, and the resource caps do not bite on a default guest.

The quickest route is the release binary: one static file, no toolchain, and the script verifies its SHA256 before installing it.

curl -fsSL https://raw.githubusercontent.com/getkern/kern/main/install.sh | sh

It picks x86_64 or aarch64 for you, installs to ~/.local/bin (/usr/local/bin as root, or KERN_INSTALL_DIR), and refuses to install a download whose checksum does not match. Verifying by hand instead is two lines:

curl -fsSLO https://github.com/getkern/kern/releases/latest/download/kern-x86_64-unknown-linux-musl.tar.gz{,.sha256}
sha256sum -c kern-x86_64-unknown-linux-musl.tar.gz.sha256 && tar xzf kern-x86_64-unknown-linux-musl.tar.gz

From source is the other route, and the whole dependency tree is one crate (libc), so it is short: clone, build and install took 36 s on a desktop (i7-14700KF), longer on a small ARM board.

# if you do not have Rust yet
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

cargo install --git https://github.com/getkern/kern getkern --locked

That puts kern in ~/.cargo/bin, which rustup adds to your PATH (open a new shell, or source "$HOME/.cargo/env", if kern is not found).

The release also ships an aarch64 binary, a Windows .exe shim and a pre-baked WSL rootfs, each with its own .sha256; the tag is GPG-signed and independently timestamped (provenance/).

kern doctor tells you whether boxes will run here before you try. Boards, WSL2 and the long form: docs/INSTALL.md. Common questions (Docker, bubblewrap, youki, E2B, Windows, the threat model): docs/FAQ.md.

Quickstart

kern box dev --image alpine -it -- sh       # a shell in a real OCI image
kern box svc --image nginx:alpine -d -p 8080:80    # a service, published
kern run --memory 256M --cpus 0.5 -- ./crunch      # cap a process, no sandbox
kern ps                                     # what runs, with PORTS and HEALTH
kern top                                    # live TUI: boxes, profiles, volumes
kern compose stack.toml up                  # a whole stack, one command

Untrusted code, one flag:

kern box job --image python:3.12-slim --security-profile untrusted -- python3 /w/x.py

--security-profile untrusted is the seccomp allowlist + --cap-drop ALL + --read-only in one flag. No network unless you ask, and seccomp is on either way. One runnable example per thing kern does: examples/.

Every read verb also answers in JSON, so nothing has to parse a table:

kern ps --json | jq '.[] | select(.health == "unhealthy") | .name'

Stacks

One file, one command. kern reads its own format, and it reads the docker-compose.yml you already have, unchanged.

# stack.toml - one table per service, keys spelled like the `kern box` flags
[box.db]
image = "postgres:alpine"
env   = ["POSTGRES_PASSWORD=secret", "POSTGRES_DB=app"]

[box.web]
image      = "adminer"
ports      = ["8080:8080"]
depends_on = ["db"]
kern compose stack.toml up          # or point it at your compose.yaml instead

Both official images start, web reaches db by service name, and the port is published. Warm, the web tier serves in ~0.3 s and the stack costs only what postgres and adminer use (~66 MB here), with no daemon underneath. A resource profile attaches here too: vcpu = "heavy" on a service.

A compose file can also name kern's own things, in the spec's extension namespace, and still run under Docker unchanged:

services:
  api:
    image: alpine
    x-kern-vcpu: heavy                   # a resource profile from your kern.toml
    x-kern-security-profile: untrusted   # seccomp allowlist + cap-drop ALL + read-only

kern reads those; every other runtime ignores an x- field, which is what the namespace is for. A typo inside it is reported rather than dropped: a key of ours that does nothing and says nothing is the defect the mechanism exists to avoid.

One constraint comes with the speed: a stack is one pod on one network namespace, so two services cannot both listen on the same container port, even when their published ports differ. kern compose up refuses the collision by name before starting anything. Or pass --no-pod: each service gets its own namespace and the constraint goes away, at the price of name resolution, which is measured rather than assumed. docs/DOCKER-COMPAT.md

Official images that drop to a non-root user (postgres, redis, ...) want uidmap and an /etc/subuid line, and outbound pulls want pasta; both are one apt install, and kern doctor names either if it is missing. This is the local dev loop, not a production orchestrator.

Run an agent's code: Python, Node, MCP

An agent needs somewhere to run what the model just wrote. kern-sandbox is that place, called from your own program: a thin, dependency-free wrapper over the kern binary. Every call runs in a fresh isolated box, with the network off, memory and pid caps, capabilities dropped, output bounded, and a timeout the binding enforces itself.

pip install kern-sandbox        # PyPI   · needs the `kern` binary above, on PATH or $KERN_BIN
npm  install kern-sandbox       # npm    · same
from kern_sandbox import run_code

r = run_code("import platform; print(platform.python_version())")
print(r.stdout)          # ran in a fresh box; a timeout / OOM / blocked escape is data on r.fault
  • Faults are data, not exceptions: a timeout, OOM-kill or blocked syscall is a field on the result, not a raise. A fresh box per call by default; Sandbox keeps a workspace across calls and a warm kernel() keeps one interpreter for sub-millisecond cells (weaker isolation, by choice).
  • Rich results without a Jupyter kernel: the last expression, display() and matplotlib figures come back captured, like a notebook cell.
  • Ships an MCP server (kern-mcp): a dependency-free stdio server that gives Claude Desktop, Cursor or any MCP client a local code interpreter. Point the client at it:
{ "mcpServers": { "kern": { "command": "kern-mcp" } } }

Tools: run_code (python/bash/node), write_file, read_file, list_files. Each call is a fresh network-off box; files persist across calls in a workspace on disk. Setup command, image and the other options: bindings/python/README.md.

Full API, Python and Node: bindings/python/README.md · bindings/node/README.md.

Resource profiles

A slice is declared once in ~/.config/kern/kern.toml and attached by name, to a sandboxed box or a bare process, with the same token.

Three kinds: vcpu: (CPU and memory), vdisk: (a size-capped scratch disk) and vgpio: (device nodes). Two of them, and the anchors they are carved from:

[[cpu]]                     # the host budget a slice is carved from
id    = "cpu:0"
cores = 8.0

[[vcpu]]                    # 1.5 cores and 512 MiB  ->  attach as  vcpu:heavy
name    = "heavy"
backend = "cpu:0"
cpus    = 1.5
memory  = "512m"

[[gpio]]                    # a controller anchor
id = "gpio:0"

[[vgpio]]                   # exactly one device node ->  attach as  vgpio:sensor
name    = "sensor"
backend = "gpio:0"
i2c     = ["/dev/i2c-1"]
kern validate ~/.config/kern/kern.toml       # check it before anything runs
kern box train --image alpine vcpu:heavy vdisk:scratch -- ./train.sh
kern run vcpu:heavy -- ./train.sh            # the same slice, no sandbox
kern box iot --image alpine vgpio:sensor -- ls /dev

Profiles compose: several attach to one box, and an explicit flag beats a profile's own value. Every key is spelled like its CLI flag, so cpus is --cpus and memory is --memory. A backend naming no declared pool is refused when the config is read, not when the box runs. docs/RESOURCES.md has the field-by-field schema.

A vdisk: is a RAM-backed tmpfs when kern runs rootless, whatever its backend says, and an ext4-on-loop image with a real quota when it runs privileged. kern says which one you got, per profile, rather than letting you assume, and the size cap is enforced either way.

vgpio: is chip-granular, not per-line. Asking for pins binds the whole /dev/gpiochipN, and that character device exposes every line of that controller. pins = [17] does not restrict the box to line 17: the kernel has no per-line mount boundary, so the pin list is cooperative metadata rather than a boundary. Naming a device node, as i2c above does, grants that node and nothing else.

kern vs Docker vs Podman

All three columns measured on one host, same workload, same day: an Intel i7-14700KF running Linux 7.0.0, with the method in BENCHMARKS.md.

kern Docker Podman
Daemon no yes (dockerd + containerd) no
Rootless yes, always opt-in yes
Cold start, bare box ~2.3 ms ~297 ms ~293 ms
Cold start, from an OCI image ~3.5 ms ~297 ms ~293 ms
Stop a service (init handles SIGTERM) ~1.9 ms ~310 ms ~380 ms
Resident memory, nothing running 0 154 to 160 MB 0
Footprint one static binary daemon stack multi-binary install
OCI images, pull / build / push yes yes yes
docker-compose.yml yes, read as-is (one caveat) yes partial
Overlay networks, Swarm, CRI no yes partial
GPU on the roadmap yes yes

Performance

Intel i7-14700KF, Linux 7.0.0, the release binary, one script you can run yourself: python3 examples/benchmark.py. Yours will differ with your CPU, kernel and filesystem.

kern bubblewrap runc podman docker
Cold start (bare box) ~2.3 ms ~2.3 ms ~18.6 ms ~293 ms ~297 ms
200 boxes in parallel ~0.11 s ~0.16 s ~0.35 s ~44.8 s ~16.2 s

Three thousand at once take ~2.2 s, and a live box costs ~0.3 MB of memory.

Two honest notes. Nobody wins single-shot latency outright: the floor for unshare + exec is 1 to 2 ms, so the whole top tier sits inside its own noise, and bubblewrap is a launcher with no images, caps or lifecycle. The gap that means something is to the engines, two orders of magnitude above.

Method, per-phase breakdown, board numbers and every caveat: BENCHMARKS.md.

Security

Namespaces, a pivot_root, 16 dangerous capabilities dropped before exec, an always-on seccomp allowlist by default (moby's own default filter minus kern's 35 escape syscalls, which stay hard-killed; a syscall outside the vetted set returns ENOSYS, and the wider denylist is the opt-out via KERN_SECCOMP=denylist), cgroup v2 limits (--require-limits refuses to start unless they bind), and a deny-by-default /dev. Where a boundary is cooperative rather than kernel-enforced, SECURITY.md says so and names the bypass.

You do not have to take it on trust: pentest/ holds five adversarial suites that assert those boundaries against the kernel rather than against kern's own reporting, and they run without a registry account or a network.

sh pentest/run-with-local-registry.sh ./target/release/kern pentest/pentest-ports.sh

Report a vulnerability privately via GitHub Security Advisories or hello@getkern.dev.

Documentation

docs/INSTALL.md install on Linux, WSL2 and ARM boards, from source
docs/DOCKER-COMPAT.md what of Docker works, what does not, and where it differs
docs/RESOURCES.md · docs/CONFIG.md · docs/EGRESS.md the two-verb model with volumes and vdisks, the kern.toml schema, and egress
docs/THREAT_MODEL.md · SECURITY.md · docs/GPU-CLAIMS.md · ROADMAP.md the threat model (structured, then per-mechanism), why a userspace VRAM cap is not a boundary, and the known gaps
BENCHMARKS.md · EDGE.md measurements, and running on a Pi, Jetson or UNO Q
examples/ · blog/ 92 runnable scripts, and longer write-ups
bindings/python/README.md · bindings/node/README.md the kern-sandbox SDK: embed kern in Python or Node

Status

The core is done and the CLI is frozen. 972 Rust, 340 Python and 61 Node tests, clippy-clean and cargo-deny-clean, on Linux, WSL2, Raspberry Pi 5, Jetson Orin Nano and Arduino UNO Q.

Scripts written against the CLI keep working: no verb, no flag and no --json field changes meaning inside a 0.8.x. Coming from 0.7, three things behave differently, and they are listed in the 0.8.0 notes.

What kern is not

  • Not a hypervisor. The boundary is the Linux kernel, so a kernel privilege-escalation bug is an escape. kern is for code you chose to run and whose blast radius you own, not for hostile code from strangers on a kernel you serve other tenants from.
  • Not free of the userns trade. Its isolation is built on an unprivileged user namespace, a fertile source of kernel LPE bugs. SECURITY.md says so before any claim.
  • Not a wall around what you mount in. -v $HOME:/host gives the box your home directory. --net host and --privileged are opt-outs by name.
  • Not a Docker Engine reimplementation. The formats, not the API: no overlay networks, no plugins, no Swarm. docs/DOCKER-COMPAT.md
  • Not a Kubernetes runtime. No CRI. Use containerd or CRI-O.
  • Not shipping GPU slices. On the roadmap. kern doctor reports what a VRAM cap would be worth per GPU; on consumer hardware that is a cooperative quota, NOT a boundary against malicious code. Nothing intercepts a driver call and nothing caps a GPU.

Known gaps: ROADMAP.md.

Contributing

Issues and pull requests are welcome. CONTRIBUTING.md has the workflow and the gates; contributions are covered by the CLA.

Maintainer

Alessandro Polito, @realexhub, Italy. Earlier commits carry @getkerndev, the account the project was published from.

License

Apache-2.0. See LICENSE and TRADEMARK.md.

About

A fast, rootless sandbox and virtual resource runtime for any workload, including untrusted and AI-generated code. Daemonless: a real, kernel-enforced container in ~3.5 ms from an OCI image, out of one static binary

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

369 stars

Watchers

2 watching

Forks

Releases

Contributors

Languages