Skip to main content

Security

Model​

Job code is treated as hostile. The main barrier is the VM; the second is the confinement of the process that runs the VM.

The libkrun documentation is explicit: the guest and the VMM share the same security context. A VM escape lands inside the rc vmm process, with its privileges. That is why isolation is the VM plus the confinement of that process — and that confinement currently differs by platform.

Layers of a job (on both platforms)​

LayerHow
one VM per jobevery job runs in a fresh VM, destroyed at the end; nothing carries over from one job to the next
read-only rootthe root is an ext4 mounted read-only (/dev/vda); writes only go to the /tmp tmpfs (256 MiB)
ephemeral statethe tmpfs disappears with the VM
no networkkrun_disable_implicit_vsock + krun_add_vsock(ctx, 0): no TSI, no network interface
no virtiofsthe root is a disk, not a shared host directory; virtiofs would not stop the guest from reaching other host directories
per-language ceilingsvCPU and RAM limited in the catalog; default timeout 10 s, maximum 300 s, with SIGKILL on the process group
input limitsstdin ≤ 1,000,000 bytes, up to 32 files, code ≤ 4 MiB, names without /, without .., not starting with .
parent dies, VM diesno VM is left orphaned if the host goes down

Per platform​

ItemmacOSLinux
hypervisorHypervisor.frameworkKVM
permission to create a VMcom.apple.security.hypervisor entitlement on the binaryaccess to /dev/kvm
VM dies with its parentwatch pipe on fd 3Pdeathsig
timeoutSIGKILL on the process groupSIGKILL on the process group
unprivileged uid/gid in the VM process—supported by the spec (krun_setuid/krun_setgid)
Landlockdoes not exist on macOSapplied to the vmm process of every job VM
seccompdoes not exist on macOSdenylist applied to all threads (TSYNC)

On macOS, confinement of the vmm process comes down to the entitlement and the process group.

Confinement on Linux​

Before handing the process over to libkrun, the vmm of a job VM (disk root, no network) applies:

MechanismWhat is allowedWhat is denied
Landlock (files)reading the root disk and already-loaded libraries, /proc, /sys; writing to the console and to the VM's own directory (run/<id>/); /dev/kvm with ioctlthe rest of the file system
Landlock (network, ABI ≥ 4)nothingevery TCP bind and connect (and UDP on ABI 10)
Landlock (scope, ABI ≥ 6)—signals and abstract sockets outside the process itself
Landlock (Unix socket, ABI ≥ 9)the VM's own socketconnect to another VM's socket
seccompeverything elseexecve, ptrace, mount, unshare/setns, kernel modules, kexec, bpf, perf_event_open, keyring, clock, swap, reboot, userfaultfd and the like (EPERM)

libkrunfw is loaded before Landlock, because libkrun only opens it inside krun_start_enter and that open would fail after confinement. seccomp is a denylist, not an allowlist, on purpose: an allowlist would require measuring every libkrun syscall on a Linux host with KVM, and a single missing one would bring down every VM.

Measured on Ubuntu 26.04 (kernel 7.0, aarch64, Landlock ABI 8), with the process itself confined: reading a file outside the policy, writing next to the disk, TCP, execve and personality denied; reading the disk and creating its own socket allowed. Without confinement, the same probe succeeds at everything (proof of failure).

Not measured: a full microVM under confinement on a Linux host with KVM (the test VM has no /dev/kvm).

Known gap: with Landlock ABI < 9 the kernel does not control connect on a path-based Unix socket; one VM's process can still reach another's socket. rc doctor warns about it. RUNNER_SANDBOX=off disables confinement for diagnostics, with a warning on stderr.

Persistent machines​

A machine gives up part of a job's isolation (it keeps state and may have network access), and every opening is declared and has a floor:

LayerHow
network off by defaultwithout --net (or net, or -p alone), the machine has no network card; with -p and no --net, the host reaches the port, but the guest cannot get out
egress policyuserspace gateway inside the vmm process itself, no TSI: allow-host by name, CIDR, --block-private; a name outside the list gets NXDOMAIN; an IP only counts if it came from an allowed name. See Network and volumes
floor no rule can opencloud metadata and all of link-local; the host loopback only with --allow-cidr 127.0.0.1, never through an IP learned from DNS (DNS rebinding)
logged denialsevery denied egress goes to egress-denials.log in the machine directory
credentials without reading themthe guest sees a placeholder; the gateway swaps it for the real value only on TLS to the binding's host, with a CA belonging to the machine itself (name constraints). A placeholder in the URL is rejected with 403. See Credentials
secrets--secret-env/--secret-file are read at start and sent to the guest in memory only; they are not kept in the registry or on the machine's disk
SSH agentthe guest uses the host's keys through a forwarded socket; the private key never enters the VM
:ro volumethe protection is on the device, not just the mount option: mount -o remount,rw by the guest's root does not open the volume (tested)
nested virtualization--nested is rejected where the host does not nest; through the API, only with rc serve --allow-nested-virt
displaythe viewer only listens on the host's 127.0.0.1, with no password

What a machine does not protect: the guest's root can read the environment of its own processes, so a secret handed to the machine is within reach of the code running in it. Credential substitution also does not prevent the server from returning the value in the response body.

What the end-to-end test proves​

PropertyProof
no networka connection to 1.1.1.1 fails inside the VM; with the network turned on deliberately, the test fails
read-only rootwriting to /etc fails
no state between jobsthe file written to /tmp by one job does not exist in the next job
memory ceilingallocating 2 GiB in a 512 MiB VM fails, without affecting the host
timeoutthe job in an infinite loop is killed and no VM survives

The API​

The HTTP API has no authentication. By default it only listens on 127.0.0.1. Do not expose it beyond loopback without an authenticating proxy in front of it.

Image build​

The image pull happens on the host. The rest of the build runs in its own VM, with network access, because installing e2fsprogs and GNU tar and the warmup (apk add, for example) need it. The build VM's network never reaches the job VM. The Alpine base tarball used by the build VM has its sha256 verified before use. Language images come from the official OCI images.

Open items​

  • validate a full microVM under Landlock + seccomp on a Linux host with KVM;
  • API authentication (currently delegated to a proxy);
  • Unix socket isolation between VMs on kernels with Landlock ABI < 9.