Home
Why Sandlock
How It Works Use Cases Comparison Security Model
Docs
Documentation Home Getting Started CLI Reference Python SDK Sandbox Reference
Products
Overview Sandbox HTTP API Sandbox Scheduler
GitHub Schedule a Demo
Sandlock / Docs /OCI Runtime

OCI Runtime and Kubernetes

sandlock-oci is a thin OCI-compatible front end over sandlock-core. Where runc builds a container from namespaces, cgroups, and pivot_root, it produces a namespace-less container confined by Landlock and seccomp, running unprivileged.

How it fits in

sandlock-oci parses an OCI bundle, translates config.json into a Sandlock policy, and drives the sandbox lifecycle on behalf of a higher-level runtime. Each call from that runtime maps to one subcommand. A long-lived supervisor daemon owns the sandbox between calls, and an in-sandbox sandlock-init acts as PID 1 so the workload and every exec'd process share one sandbox and one seccomp supervisor.

  containerd / CRI-O / kubelet
            │  (OCI runtime calls)
            ▼
      sandlock-oci  ──fork──>  supervisor daemon
            │                       │
            │                       ▼
            │                  sandlock-init  (confined PID 1 in the sandbox)
            │                       │  fork + execve
            │                       ▼
            │                   workload  (+ any exec'd siblings)
            ▼
      sandlock-core
   Landlock · seccomp · seccomp-notify · COW

Lifecycle

CommandEffect
create <id> -b <bundle>Spawn the supervisor, build the sandbox, park the child before execve, and save state
start <id>Release the parked child so it execve's the workload
state <id>Print state.json, reconciled against liveness
kill <id> <signal>Forward a signal to the workload, or to its group with -a
delete <id>Shut the supervisor down and remove the state directory
exec <id> <cmd>Run a sibling process in the same sandbox
listList all sandboxes managed by sandlock-oci
checkpoint <id>Snapshot a running sandbox to an image directory
restore <id>Recreate and resume a sandbox from a checkpoint image
checkReport the host kernel's Landlock support

The create then start split matches the OCI two-phase model. create forks the child and installs the full policy, meaning Landlock, seccomp-notify, resource limits, and the network ACL, with the child parked just before execve; start releases it. The supervisor reports the child PID back to the caller over a pipe, so the handshake involves no sleep and no race.

Driving it directly

shell
$ sandlock-oci create mycontainer -b /path/to/bundle
$ sandlock-oci start  mycontainer
$ sandlock-oci state  mycontainer
$ sandlock-oci exec   mycontainer sh -c 'echo hello'
$ sandlock-oci kill   mycontainer SIGTERM
$ sandlock-oci delete mycontainer

To use it under a higher-level runtime, point that runtime at the sandlock-oci binary as its OCI runtime.

Spec translation

config.json is mapped to a Sandlock policy by intent rather than by replaying Linux container primitives. A memory limit becomes a memory limit; it does not become a cgroup.

OCI fieldSandlock mapping
root.pathchroot target for the sandbox
root.readonlyGrants the rootfs read-only instead of read-write
mounts (bind)fs_mount, read-only or read-write from the options
mounts (tmpfs)Host-backed scratch directory, isolated and cleaned on delete
mounts (proc)Host /proc mounted read-only, virtualized by seccomp
mounts (sysfs)Host /sys mounted read-only
process.cwdWorking directory
process.envEnvironment, started clean then populated from the spec
process.userRun-as uid and gid, with a user namespace only if needed
linux.resources.memory.limitmax_memory
linux.resources.pids.limitmax_processes
linux.resources.cpu.quota / periodSub-core max_cpu throttle
linux.resources.cpu.cpusCPU affinity via sched_setaffinity
linux.namespacesIgnored by design

Supported spec versions are 1.0.x, 1.1.x, and 1.2.x. A bundle declaring any other version is rejected at create rather than silently mis-mapped, which is the difference between a clear failure and a container that quietly runs with less confinement than its author expected.

Mount types with no safe namespace-less equivalent, namely devpts, mqueue, cgroup, and cgroup2, are skipped. Port binding is remapped so in-container servers can bind() without colliding on host ports.

Sandlock policy in annotations

The OCI runtime specification has no field for an outbound allowlist or a transparent proxy, so the parts of a Sandlock policy it cannot express travel as annotations under the io.sandlock. prefix.

Entries are separated by ;, not ,. A comma is already meaningful inside a network spec, where host:80,443 is one rule covering two ports.

Annotation Maps to
io.sandlock.network.allownet_allow
io.sandlock.network.denynet_deny
io.sandlock.network.allow_bindnet_allow_bind
io.sandlock.network.deny_bindnet_deny_bind
io.sandlock.network.port_remapport_remap. On by default here, unlike everywhere else; see below. Set "false" to opt out.
io.sandlock.http.allowhttp_allow
io.sandlock.http.denyhttp_deny
io.sandlock.http.portshttp_ports
io.sandlock.config.http_cahttp_ca. A host-side file, resolved relative to the bundle.
io.sandlock.config.http_keyhttp_key. Host-side, bundle-relative.
io.sandlock.config.http_ca_outhttp_ca_out. Host-side, bundle-relative.
io.sandlock.config.http_inject_cahttp_inject_ca. These are sandbox-virtual paths, passed through as written and resolved through the chroot and mounts.

Why port remapping defaults on

Container workloads legitimately run servers. Sandlock gates bind() default-deny, so without remapping an in-container server fails with EACCES and the container exits, which then hangs any readiness probe waiting on it. Remapped binds are emulated on the on-behalf path and succeed, taking the requested port when it is free and a fresh host port only on a conflict, so co-located sandboxes never collide.

On Kubernetes

Pod annotations only reach the runtime if the containerd handler is told to forward them. Without pod_annotations, the keys are silently dropped and the pod runs with no network policy at all.

containerd
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.sandlock]
  runtime_type    = "io.containerd.runc.v2"
  pod_annotations = ["io.sandlock.*"]
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.sandlock.options]
    BinaryName = "/usr/local/bin/sandlock-oci"
A pod, image unchanged
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: sandlock
handler: sandlock
---
apiVersion: v1
kind: Pod
metadata:
  name: agent
  annotations:
    io.sandlock.network.allow: "api.internal:443;10.0.0.0/8:5432"
    io.sandlock.http.allow: "POST api.internal/v1/*"
    io.sandlock.config.http_inject_ca: "/etc/ssl/certs/ca-certificates.crt"
spec:
  runtimeClassName: sandlock
  containers:
    - name: agent
      image: python:3.12-slim

runc compatibility

The CLI accepts the global and per-command flags containerd and CRI-O pass to runc, so it can be wired in as a drop-in replacement. Flags that do not apply to a namespace-less, cgroup-less runtime are accepted and ignored: --systemd-cgroup, --rootless, --no-pivot, --no-new-keyring, and --debug.

Fatal errors are appended to the --log file in text or JSON form, so the containerd shim surfaces the real failure reason rather than a generic one.

The supervisor daemon exits with the workload's status, re-raising the killing signal where applicable, so a reaping shim sees a wait-status that mirrors the workload exactly as it would from runc.

State

State lives under $XDG_RUNTIME_DIR/sandlock-oci for unprivileged users and /run/sandlock-oci for root, overridable with the global --root flag.

Requirements and limits

  • Linux 6.12 or newer, for Landlock ABI v6. Run sandlock-oci check to confirm.
  • A built sandlock-core; this crate depends on it directly.
  • The workload and all exec'd processes share one sandbox and one seccomp supervisor, through the in-sandbox sandlock-init PID 1.
  • exec is non-TTY only. -t and --console-socket are accepted for runc compatibility but ignored, as there is no PTY support yet.

What you gain and what you give up. You gain kernel-enforced confinement without namespaces, cgroups, or privilege. You give up the parts of the container model that are namespaces: a container-private PID space and network stack. If your workload depends on those specifically, rather than on the isolation they happen to provide, runc remains the right runtime for it.