Skip to content
click0Public
forked from yurivict/crate

About

Containerizer for the FreeBSD operating system

Resources

Stars

3 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

crate

Crate is a containerizer for FreeBSD. It packages applications and services into isolated, self-contained containers (.crate files) and runs them in dedicated FreeBSD jails with automatic networking, firewall, and resource cleanup.

Crate containers contain everything needed to run the containerized software — only the crate executable and the FreeBSD kernel are required at runtime.

Language: C++17 | License: BSD 3-Clause | Status: Alpha (active development since 2019)

Українська версія | Comparison with BastilleBSD

Features

Container Management

  • 34 commands: create, run, validate, snapshot, list, info, console, clean, export, import, gui, stack, stats, logs, stop, restart, top, inter-dns, vpn, inspect, migrate, backup, backup-prune, restore, replicate, template, retune, throttle, doctor, vm-wrap, update, profile-from-trace, audit, wrap — all described in crate(5)
  • YAML specification with template inheritance (--template)
  • ZFS integration: snapshots (create/list/restore/delete/diff), COW clones, encryption (AES-256-GCM/CCM, AES-128), dataset attachment
  • Copy-on-Write: ephemeral or persistent modes via ZFS clone or unionfs
  • Export/Import: running containers to .crate archives with SHA256 validation, directory traversal protection, OS version checking
  • Container listing in table or JSON format (crate list -j)
  • Interactive console access (crate console TARGET)
  • pkgbase support for FreeBSD 16+ (--use-pkgbase)

Graphics & Desktop

  • 5 X11 modes: shared (host Xorg), nested (Xephyr), headless (Xvfb), GPU (DRM leasing with nvidia/amdgpu/intel), none
  • VNC: x11vnc with optional password, automatic port allocation
  • noVNC/WebSocket: browser-based access via websockify
  • GUI session manager (crate gui): list, focus, attach, url, tile, screenshot, resize
  • Clipboard isolation: isolated/shared/none modes with directional control (in/out/both)
  • D-Bus isolation: per-container system/session bus policy (allow_own, deny_send)
  • OpenGL/GPU acceleration: hardware rendering with driver auto-detection
  • Video devices: V4L /dev/videoN passthrough

Networking

  • 4 network modes: NAT (default), bridge (if_bridge), passthrough (direct NIC), netgraph (ng_bridge)
  • NAT mode: ipfw-based with automatic 10.0.0.0/8 addressing, outbound control (wan/lan/host/dns), inbound port forwarding (TCP/UDP)
  • Bridge mode: container on physical network via if_bridge with DHCP or static IP
  • Passthrough mode: dedicated physical NIC assigned directly to container via VNET
  • Netgraph mode: ng_bridge + eiface alternative to if_bridge
  • DHCP: synchronous lease acquisition (SYNCDHCP semantics)
  • Static IP: CIDR notation (e.g. 192.168.1.50/24) with gateway
  • IPv6: NAT ULA (fd00:cra7:e::/48), SLAAC for bridge/passthrough/netgraph, static IPv6 addresses
  • Static MAC: deterministic MAC address generation (SHA-256, vendor OUI 58:9c:fc)
  • VLAN: 802.1Q tagging (ID 1-4094) for bridge/passthrough/netgraph modes
  • Multiple interfaces: primary + extra interfaces per container, each with independent mode/IP/VLAN
  • Named networks: define reusable network profiles in system config, reference by name in container specs
  • pf anchors: per-container firewall policy (block_ip, allow_tcp/udp, default_policy)
  • DNS filtering: per-jail unbound with domain blocking (wildcard patterns, nxdomain/redirect)
  • Dynamic firewall slots: unique rule numbers, no conflicts, RAII cleanup

Security

  • Non-root by default: the command runs as the operator's own account mirrored into the jail; a root operator gets the dedicated unprivileged crate account; run/user: root is the explicit opt-in, run/user: www picks a package's account (1.1.64)
  • securelevel: configurable (-1 auto / 0-3, default=2)
  • children.max: configurable (default=0 — no nested jails)
  • CPU pinning: cpuset via security: in YAML
  • RCTL resource limits: memoryuse, pcpu, maxproc, openfiles, and 20+ other resources
  • MAC bsdextended: ugidfw rules for granular access control
  • MAC portacl: port binding restrictions
  • Capsicum: capability-based sandboxing
  • pathnames.h: absolute paths to all external commands (CWE-426 protection)
  • Environment sanitization: clean environ, restore only TERM/DISPLAY/LANG
  • execv (no PATH search) instead of execvp in any privileged context (crated runs as root; crate(1) itself is unprivileged since 1.0.0)
  • lstat for symlink validation (CWE-59 protection)
  • JAIL_OWN_DESC: race-free jail removal (FreeBSD 15+)
  • Archive traversal protection: validates paths before extraction
  • Signal-safe RAII cleanup: guaranteed resource cleanup on SIGINT/SIGTERM
  • Device classes: the jail's /dev shows only basic nodes plus the devices: classes the spec names (sound, gpu, video, input, usb, serial); terminal/devfs_ruleset remains the escape hatch (1.1.62)

Services & IPC

  • Managed services: auto-start/stop in order, rc.conf generation
  • Socket proxying: socat-based host<->jail socket forwarding
  • SysV IPC / POSIX mqueue: configurable per container
  • Tor integration: built-in tor proxy option with optional control port

Lifecycle Hooks

  • run:begin, run:after-create-jail, run:before-start-services, run:before-execute, run:after-execute, run:end
  • create:begin, create:end

Commands

Command Description
crate create -s spec.yml -o app.crate Create a container from specification
crate create -s spec.yml --template base.yml Create with template inheritance
crate run -f app.crate [-- args] Run a container
crate run NAME [-- args] Run a profile by name: spec from ~/.config/crate/profiles/, /usr/local/share/crate/profiles/ or the installed examples, .crate built on first use (1.1.59)
crate run --status-fd N ... Same, plus one JSON object per lifecycle event (jail_created, net_ready, exited, ...) on descriptor N (1.1.65)
crate profile-from-trace -o spec.yml -- CMD [args] Run CMD once on the host under truss(1) and draft a minimal spec from what it touched (1.1.60)
crate audit TARGET [-j] [--offline] What a running container can actually reach, seen from inside: host paths, devices, sockets, net (1.1.61)
crate wrap install PROFILE [--desktop] A launcher in ~/.local/bin (and a .desktop entry) that starts the profile's container; wrap remove, wrap list (1.1.66)
crate validate -s spec.yml Validate a specification
crate snapshot create|list|restore|delete|diff Manage ZFS snapshots
crate list [-j] List running containers (table or JSON)
crate info TARGET Detailed container information
crate console TARGET [-u USER] Interactive shell in a container
crate clean [-n] Clean up orphaned resources (dry-run supported)
crate export TARGET [-o FILE] [-P PASSFILE] [-K SIGNKEY] Export running container to .crate (optional encryption + ed25519 signature)
crate import FILE [-o FILE] [--force] [-P PASSFILE] [-V PUBKEY] Import .crate with validation (auto-decrypts, verifies signature)
crate gui list|focus|attach|url|tile|screenshot|resize GUI session manager
crate top Live, htop-style resource monitor across all running containers (1 Hz; q to quit)

Quick Start

# Create a container from a spec
crate create -s examples/firefox.yml -o firefox.crate

# Run it
crate run -f firefox.crate

# ...or skip both steps: the installed examples are profiles (1.1.59)
crate run firefox

# List running containers
crate list

# Open a shell in a running container
crate console firefox

# Export a running container
crate export firefox -o backup.crate

Encrypted export/import

.crate artefacts are portable — unlike Bastille's place-bound jails — and since 0.5.4 they can also be private. Exports are wrapped in AES-256-CBC + PBKDF2 via openssl enc(1) so the same image can travel between hosts without leaking its filesystem to anyone with network or backup-storage access.

# 1. Write a passphrase to a file (must be mode 0600 — owner-only)
printf 'cRatE2026craTE#UKR' > /etc/crate/secret
chmod 0600 /etc/crate/secret

# 2. Export, encrypted
crate export firefox -P /etc/crate/secret -o firefox.crate
# -> firefox.crate         (AES-256-CBC ciphertext)
# -> firefox.crate.sha256  (over the encrypted bytes)

# 3. scp/rsync to the target host …

# 4. Import — encryption is auto-detected from magic bytes
crate import firefox.crate -P /etc/crate/secret
# Without -P on an encrypted archive, the import fails fast with a clear message.

Notes:

  • The example passphrase cRatE2026craTE#UKR is illustrative — pick your own. PBKDF2 makes brute force expensive but a strong passphrase is still your job.
  • The passphrase file goes in via -kfile <path> to openssl, so the secret never appears on the command line and is not visible via ps.
  • crate refuses any passphrase file that isn't 0600, regular, and non-empty.
  • The .sha256 sidecar covers the ciphertext: verify it out-of-band before decrypting if you want detection of bit-flips on transport.
  • The .sha256 sidecar covers the ciphertext: verify it out-of-band before decrypting if you want detection of bit-flips on transport.
  • For authenticated provenance ("this archive really came from me"), pair -P with -K/-V (ed25519 signing — see below).

Signed export/import (ed25519)

Encryption (the -P flag above) gives you confidentiality. Signing gives you authenticity — a recipient can verify a .crate archive really came from the holder of the matching secret key, even if it travelled through untrusted intermediaries. The two are independent and can be combined.

# 1. One-time: generate an ed25519 keypair
openssl genpkey -algorithm ED25519 -out crate-sign.key
openssl pkey -in crate-sign.key -pubout -out crate-sign.pub
chmod 0600 crate-sign.key
# (publish crate-sign.pub anywhere; keep crate-sign.key secret)

# 2. Sign on export. Combine with -P for confidentiality + authenticity.
crate export firefox -K crate-sign.key -P /etc/crate/secret -o firefox.crate
# -> firefox.crate         (AES-256-CBC ciphertext)
# -> firefox.crate.sha256  (over the encrypted bytes)
# -> firefox.crate.sig     (ed25519 signature over the encrypted bytes)

# 3. On the recipient: verify with the public key.
crate import firefox.crate -V crate-sign.pub -P /etc/crate/secret
# Refuses to import if firefox.crate.sig is missing/mismatched —
# unless --force.

What gets signed: the on-disk archive bytes (including any encryption layer). So:

  • A tampered ciphertext is detected by the signature, even before the recipient enters the passphrase.
  • The public key can verify provenance without holding the passphrase.
  • An unsigned archive imports normally; a signed archive without -V refuses to import (intentionally — opt-in to the looser unsigned mode with --force).

Notes:

  • Implementation: openssl pkeyutl -sign -rawin -inkey ed25519-key. ed25519 produces a fixed 64-byte signature; .sig is binary.
  • Secret-key file rejected unless mode 0600, regular, non-empty.
  • The signature path is <archive>.sig. Verifier exit codes ≠ 0 abort the import unless --force.

Audit log

Every state-changing crate command (create, run, stop, restart, snapshot, export, import, clean, console, gui, stack) appends a one-line JSON record to /var/log/crate/audit.log. Read-only commands (list, info, stats, logs, validate) are skipped to keep the log lean.

$ crate create -s spec.yml -o myapp.crate
...
$ tail -1 /var/log/crate/audit.log | jq .
{
  "ts":      "2026-05-01T20:55:01Z",
  "pid":     12345,
  "uid":     1000,                          // real uid (the user)
  "euid":    0,                             // effective uid (root / sudo / legacy crate.x)
  "gid":     1000,
  "egid":    0,
  "user":    "alice",
  "host":    "build-server",
  "cmd":     "create",
  "target":  "spec.yml",
  "argv":    "'crate' 'create' '-s' 'spec.yml' '-o' 'myapp.crate'",
  "outcome": "ok"
}

Notes:

  • The file lives under Config::Settings::logs (default /var/log/crate); change it via crate.yml.
  • Mode 0640, append-only writes — fits the auditd(8) / syslogd(8) model. Rotate with newsyslog(8).
  • Captures both real and effective uid/gid so reviewers see "user X (uid=1000) acted via euid=0" — relevant whenever crate runs with elevated euid (sudo, or the legacy setuid crate.x build). Since 1.0.0 crate(1) is unprivileged and delegates privileged work to crated(8), which keeps its own per-user audit.
  • Pair outcome: "started" and outcome: "ok" records by pid to measure command duration; failed: <msg> records contain the exception message for post-mortem.

Auto-create bridge interfaces

Bridge mode used to require an operator to pre-create bridge0 (or whichever interface the spec named) before crate run. Specs can now opt into on-demand creation:

options:
    net:
        mode: bridge
        bridge: bridge0
        auto_create_bridge: true
        ip: dhcp

When the bridge already exists, auto_create_bridge is a no-op (so multiple containers can share bridge0 without races). When the bridge is missing and the flag is set, crate creates it (ifconfig bridge0 create up) and destroys it on container teardown only if crate created it in this run. Without the flag, a missing bridge produces a clear error pointing at the option.

The interface name is validated against [A-Za-z0-9_]{1,15} with a required driver prefix and a required unit number — typos like brige0 are caught before they reach ifconfig(8).

Live resource monitor (crate top)

crate top polls all crate-managed jails once per second and renders a table with per-container CPU%, memory, write rate (WRITE/s, RCTL writebps — bytes written per second, not disk usage), and process count. Press q (or Ctrl-C) to quit. CPU% is RCTL pcpu (the kernel's own smoothed figure) and can exceed 100% on multi-core jails — the build-ci row in the example below is using about three cores' worth of CPU:

NAME               JID              IP    CPU%        MEM     WRITE/s  PROC
postgres            12      10.0.0.20    14.2    420.0 MB   12.5 MB/s    23
nginx-edge          15      10.0.0.21     3.1     48.0 MB      0 B/s     7
build-ci            18      10.0.0.22   210.5      1.2 GB  340.0 MB/s    42
3 jails  CPU 227.8%  MEM 1.7 GB  WRITE 352.5 MB/s  PROC 72

When stdout is not a terminal (crate top | grep, watch crate top), the renderer emits one frame and exits. crate top is a read-only command and does not appear in /var/log/crate/audit.log.

Cross-device file shares

files: shares no longer require the host source and the jail-side mount point to live on the same filesystem. When crate run detects a cross-device path pair (host on tmpfs, jail dataset on a different ZFS pool, external disk, etc.), it automatically falls back to a single-file nullfs bind-mount instead of the hard-link path that returns EXDEV. The semantics inside the jail are identical: read/write, in-place edits, ownership and mode all reflect the underlying host file.

# This now works regardless of whether /etc/resolv.conf and the
# jail dataset share a device. crate decides per-file whether to
# hard-link (same device) or nullfs-bind (cross device).
files:
  /etc/resolv.conf: /etc/resolv.conf
  /var/log/myapp.log: /home/me/logs/myapp.log

The strategy table is published in CHANGELOG.md (0.6.0); the pure decision logic lives in lib/share_pure.{h,cpp} with a 9-case ATF test covering every cell of the (host_exists × jail_exists × same_device) matrix.

X11 mode security

crate supports five X11 display modes — they are not equivalent from a security standpoint. crate validate and crate run both emit warnings when an isolation-weak mode is selected.

Mode Isolation Use when
nested (Xephyr) Strong: containers get a private X server desktop apps, default for production
headless (Xvfb) + VNC/noVNC Strong: no host X access at all server/automation/screenshot
gpu (DRM leasing) Strong: dedicated GPU, no host X accelerated workloads
shared None — full read/write access to the host X server only inside fully trusted jails
none n/a no GUI

Why shared is dangerous. Mounting the host's /tmp/.X11-unix into a jail means any process inside that jail can:

  • Read every keystroke typed into any application on the host (X is global, not per-window).
  • Move/raise/iconify/destroy any host window.
  • Take screenshots of the entire desktop.

crate warns about this both at validate time (crate validate spec.yml) and at run time. Operators who deliberately accept the risk can suppress the runtime warning with CRATE_X11_SHARED_ACK=1.

# DON'T (default behaviour for legacy specs)
options: [x11]               # ⚠️ implicit mode=shared

# DO
options: [x11]
x11:
    mode: nested             # private Xephyr server per jail
    resolution: 1920x1080

Headless GUI with browser access

# spec.yml
pkg:
    install: [firefox]
run:
    command: /usr/local/bin/firefox
options: [net]
gui:
    mode: headless
    vnc: true
    novnc: true
    vnc_password: secret
crate create -s spec.yml -o firefox-headless.crate
crate run -f firefox-headless.crate &
crate gui url firefox    # prints noVNC URL for browser access

Wayland compositor inside a jail (gui.mode: compositor)

Unlike gui.mode: wayland (which binds the host compositor's socket so an in-jail client can connect to it), gui.mode: compositor runs a full Wayland compositor — sway, weston, cage, labwc, … — inside the jail. The compositor command is whatever you put in gui.compositor. Two backends:

gui.backend What it does Devices exposed Safety
headless (default) WLR_BACKEND=headless, renders offscreen, exposed over VNC via wayvnc /dev/dri/* only if a render node exists (GPU accel) Safe — no input devices, no host-display conflict
drm WLR_BACKEND=drm, drives the physical GPU + input directly via seatd /dev/dri/* and /dev/input/* ⚠️ The jail effectively owns the screen and can read all input — see the trust-model doc
# Headless sway, viewable over VNC
pkg:
    install: [sway, wayvnc]
options: [net]
gui:
    mode: compositor
    backend: headless        # default; omit for the same effect
    compositor: sway         # or "cage firefox", "weston", "labwc"
    vnc: true                # start wayvnc on 5900 (override with vnc_port)
    # vnc_bind: 0.0.0.0      # default 127.0.0.1 (loopback-only)

wayvnc serves an unauthenticated VNC stream, so it binds to 127.0.0.1 by default — reach it over an SSH tunnel. To expose it on the network, set gui.vnc_bind explicitly (e.g. 0.0.0.0 or a jail address); crate prints a warning when you do, and you should restrict access at the firewall. (Built-in wayvnc authentication is a planned follow-up.)

The drm backend additionally requires seatd reachable on the host (service seatd onestart) or running inside the jail. Because it exposes /dev/input/*, it is opt-in and documented as a privilege surface in docs/trust-model.md.

Network Modes

# NAT (default) — automatic IP, ipfw NAT, outbound control
options:
    net:
        outbound: [wan, dns]
        inbound-tcp:
            8080: 80

# Bridge — container on physical network via if_bridge
options:
    net:
        mode: bridge
        bridge: bridge0
        ip: dhcp
        static-mac: true

# Bridge with static IP and VLAN
options:
    net:
        mode: bridge
        bridge: bridge0
        ip: 192.168.1.100/24
        gateway: 192.168.1.1
        vlan: 100
        ip6: slaac

# Passthrough — dedicated NIC directly to container
options:
    net:
        mode: passthrough
        interface: vtnet1
        ip: dhcp
        ip6: slaac

# Netgraph — ng_bridge alternative to if_bridge
options:
    net:
        mode: netgraph
        interface: em0
        ip: 192.168.1.100/24
        gateway: 192.168.1.1
        static-mac: true

# Multiple interfaces
options:
    net:
        mode: bridge
        bridge: bridge0
        ip: dhcp
        extra:
            - mode: bridge
              bridge: bridge1
              ip: 10.0.0.50/24
              gateway: 10.0.0.1
              vlan: 100

# Using named networks (defined in crate.yml)
options:
    net:
        network: external
        ip: 192.168.1.50/24
        extra:
            - network: internal
              ip: 10.0.0.50/24

Configuration

Configuration files (YAML, higher priority wins):

File Scope
/usr/local/etc/crate.yml System-wide
/usr/local/etc/crate.d/*.yml Drop-in fragments (alphabetical order)
~/.config/crate/crate.yml Per-user

Key options: prefix, cache, logs, zfs_enable, zfs_zpool, network_interface, default_bridge, static_mac_default, bootstrap_method, securelevel, children_max, search_path, compress_xz_options, networks.

Named Networks

Define reusable network profiles in the system config and reference them by name in container specs:

# /usr/local/etc/crate.yml
networks:
    external:
        mode: bridge
        bridge: bridge0
        gateway: 192.168.1.1
        static-mac: true
    internal:
        mode: bridge
        bridge: bridge1
        gateway: 10.0.0.1
        vlan: 100
    dmz:
        mode: netgraph
        interface: em1
        gateway: 172.16.0.1
# container spec (.crate)
options:
    net:
        network: external
        ip: 192.168.1.50/24
        extra:
            - network: internal
              ip: 10.0.0.50/24

Named network fields serve as defaults — explicit values in the container spec always override.

Profiles

crate run NAME without -f (1.1.59, after firejail's <app>.profile + <app>.local) looks a spec up by name, first match wins:

Path Who writes it
~/.config/crate/profiles/NAME.yml the operator
/usr/local/share/crate/profiles/NAME.yml the site (directory created by the operator, root-owned)
/usr/local/share/examples/crate/NAME.yml shipped with crate — the examples double as the profile set

The .crate is built on first use into ~/.local/share/crate/profiles/NAME.crate and reused; it is rebuilt when the profile or its .local is newer. The jail is named after the profile, exactly as with -f NAME.crate. The build is a crate create, which still needs root: an unprivileged operator gets a message with the exact crate create -s … -o … && chown recipe for root (the merged spec is already written), and runs normally once the archive exists.

An optional ~/.config/crate/profiles/NAME.local.yml is merged on top of the profile: maps key by key, lists appended without duplicates, options by option name, scalars replaced. It may add packages, options, limits, run arguments and shares under $HOME:

# ~/.config/crate/profiles/firefox.local.yml
pkg:
    install: [noto-emoji]
run:
    command: /usr/local/bin/firefox --private-window
dirs:
    share:
        - [$HOME/Downloads, $HOME/Downloads]
limits:
    memoryuse: 4G

It may not loosen what the profile author chose: security_advanced, dbus, base, scripts, encrypted, securelevel, socket_proxy and shares of host paths outside your home are refused with the reason. For those, copy the profile into ~/.config/crate/profiles/ and edit the copy. The merged spec is written next to the archive as NAME.yml (generated) and is what +CRATE.SPEC contains. Profile names follow the jail-name rule ([A-Za-z0-9._-]), so the chromium+tor-style examples need a copy under a dash name.

Since 1.1.59 crate create --template uses the same YAML merge and the merged spec goes into the archive; before, the template only affected the build and a --template privacy crate ran without tor, encryption, COW or the DNS filter.

Desktop launchers (crate wrap)

crate wrap install PROFILE (1.1.66, after firejail's firecfg) puts the container in front of the program: it writes ~/.local/bin/PROFILE with exec crate run PROFILE -- "$@", so firefox typed in a shell runs the firefox profile when ~/.local/bin comes before /usr/local/bin in PATH (the command tells you when it does not). --desktop adds ~/.local/share/applications/crate-PROFILE.desktop (Exec=crate run PROFILE -- %U, Terminal=true unless the profile has x11/gui, Icon= the profile name or --icon) so menu entries and file associations start the container too; --no-launcher writes only that, --name picks another launcher name. Both files carry a marker naming the profile: wrap remove PROFILE and wrap list only touch files that carry it, and install refuses to overwrite a file without it unless --force. Everything lives in your home, no privilege is needed.

crate wrap install firefox --desktop     # `firefox` and the menu entry start the container
crate wrap install fetch --name sfetch   # ~/.local/bin/sfetch -> crate run fetch
crate wrap list
crate wrap remove firefox

Spec draft from a trace

crate profile-from-trace -o app.yml -- <command> [args] (1.1.60, after firejail's --build) runs the program once on the host, unsandboxed, under truss -f, and turns the trace into a minimal spec draft:

Seen in the trace Becomes
executed /usr/local programs pkg/install (owners from pkg which; packages only read from are a comment)
files under your home dirs/share / files/share with $HOME; .config/<app>, .local/share/<app>, .mozilla-style state directories listed separately with a "remove to keep it per container" comment; .cache, .Xauthority and login dotfiles skipped
other directories dirs/share, collapsed to the shortest ancestors; base system, /usr/local, /tmp, /var, /dev, /mnt, /media ignored
X11 / D-Bus sockets, /dev/dri, /dev/video*, certificate files x11, dbus/session or dbus/system, gl, video, ssl-certs
outbound connections options/net/outbound by address class (wan, lan, host, dns for port 53); the plain net option when all four are needed
bind+listen, datagram binds inbound-tcp, inbound-udp

Destinations seen, sound/input/USB devices, Wayland sockets, unknown unix sockets and unowned /usr/local files become comments at the end. --trace-file FILE uses a saved truss output instead of running anything; --keep-trace FILE keeps the raw trace. Review the draft, then crate validate -s app.yml and crate create.

Audit from the inside

crate audit <container|jid> (1.1.61, after firejail's --audit) answers the question the spec cannot: what does the running container actually reach? One /bin/sh probe runs inside the jail with base-system tools only (mount, ifconfig, netstat, sysctl, ls, find, sockstat, ps, nc, drill) and its output becomes the PASS/WARN/FAIL table crate doctor uses, or -j JSON:

Category Reported
jail securelevel, hostname and kern.hostid compared with the host's, the allow.* flags as the jail sees them (raw sockets, mount, chflags, sysvipc, socket_af, enforce_statfs, devfs ruleset)
mounts every host path visible through nullfs, rw or ro; procfs, zfs, tmpfs
devfs device classes: gpu, video, sound, input, usb, serial, disks, raw (mem, kmem, bpf, pf)
interfaces addresses and the default route
sockets inet listeners; unix sockets on the filesystem that belong to the host (X11, system/session D-Bus, Wayland, sound server)
processes who runs what inside
connectivity the resolver, one TCP connect to the default gateway, one DNS lookup (--offline skips both, --dns NAME picks the name)

WARN marks an exposure worth a look rather than an error: a whole host area mounted read-write, raw disk or memory devices, keyboard devices, a host socket, a hostid equal to the host's. Exit status is 0, 1 (some WARN) or 2 (the probe did not run). crate doctor looks at the host, crate audit at one container; it needs root or the crated privops socket, like crate console.

Machine identity (privacy:)

A jail shares the host's kern.hostuuid / kern.hostid unless it is created with its own, and crate passed the host's name as the jail hostname, so every jail identified the machine to anything that read them (crate audit has warned about it since 1.1.61). Since 1.1.67 (bwrap/firejail --machine-id, --hostname):

privacy:
    hostid: random        # host | random (a fresh UUID v4 every run) | a literal UUID (stable for the crate)
    hostname: random      # host | random (crate-<8 hex>) | a literal host name

The jail is created with host.hostuuid set and host.hostid derived the way /etc/rc.d/hostid does it (the first 32 bits of md5 of the UUID text); the jail's /etc/hostid gets the UUID so rc(8) inside agrees with the kernel. crate validate warns when tor or i2pd is used without privacy/hostid; templates/privacy.yml and the tor / i2pd examples carry both keys. Under rootless the two parameters travel in create_jail's whitelisted parameter list.

Device classes

Since 1.1.62 every jail gets a private devfs ruleset (firejail's --nosound / --novideo / --no3d / --noinput / --nou2f, inverted): everything in /dev is hidden, the basic nodes are unhidden (null, zero, random, urandom, crypto, tty, ptmx, pts/*, fd/*, stdin/stdout/stderr, fuse, zfs; bpf* with the net option), then the classes the spec names:

devices:
    sound: true      # /dev/dsp*, mixer*, sndstat, audio*, midi*
    gpu: true        # /dev/dri, dri/*, drm/*, nvidia*   (implied by `gl` and gui gpu/auto/compositor)
    video: true      # /dev/video*                        (implied by the `video` option)
    input: true      # /dev/input/* — everything typed on the host is readable; implied by gui compositor's drm backend
    usb: true        # /dev/usb/*, usbctl, ugen*, uhid*, u2f*
    serial: true     # /dev/cua*, ttyu*, ttyU*

devices: [sound, gpu] is the list form; devices: all keeps the host's whole /dev visible, which is what every jail without terminal/devfs_ruleset got before 1.1.62 (disks, memory, keyboards included). terminal/devfs_ruleset: N is still honoured: the operator's ruleset is included in place of the built-in base and the classes go on top. crate validate warns about all, input, usb and serial; crate audit shows the resulting view from inside. The desktop examples carry devices: {sound: true}.

Read-only shares, tmpfs, host /etc entries

Three more knobs from the bwrap/firejail toolbox (1.1.63):

dirs:
    share:
        - $HOME/Downloads                     # read-write, as before
        - $HOME/Documents:ro                  # read-only (nullfs MNT_RDONLY)
        - [$HOME/Music, /mnt/music, ro]       # [jail path, host path, ro|rw]

tmpfs:                                        # a fresh tmpfs at every start
    - /tmp                                    #   root-owned, sticky 01777
    - $HOME/.cache                            #   inside the home: owned by the jail user, 0700
# or with sizes:  tmpfs: {/tmp: 512m, $HOME/.cache: 2g}

etc:
    keep: [localtime, hosts, ssl]             # host /etc entries copied into the jail's /etc at create

A file share is a hard link or a copy and cannot be read-only — share its directory with :ro. tmpfs mounts come after the shares (a tmpfs above a share hides it; crate validate warns) and are refused on /, /dev, /proc, /etc, /bin, /sbin, /lib, /libexec, /usr, /usr/local and /var/run. The jail's /etc is the pristine one from base.txz — etc/keep is the way to carry the host's timezone, hosts file or certificate directory along (firejail's --private-etc, inverted); resolv.conf is written by crate run when the spec has networking. Stack volumes with :ro now reach the mount as read-only too. Under rootless the tmpfs goes through the new mount_tmpfs privops verb, path-gated like mount_nullfs.

Lifecycle stream and run/timeout

crate run --status-fd N (1.1.65; bwrap --json-status-fd) writes one JSON object per line to a descriptor the caller opened, one line per phase, so a script, a CI job or a supervisor gets exact phases instead of grepping logs:

crate run --status-fd 3 -f nginx.crate 3>nginx.status.jsonl
{"event":"started","ts":1761300000,"pid":4242}
{"event":"jail_created","ts":1761300001,"jid":12,"name":"nginx_pid4242","path":"/var/run/crate/jail-nginx-1a2b"}
{"event":"net_ready","ts":1761300001,"mode":"bridge","ip":"10.0.0.7"}
{"event":"mounts_ready","ts":1761300001}
{"event":"services_started","ts":1761300002}
{"event":"healthy","ts":1761300004}
{"event":"exec_started","ts":1761300004,"user":"alice","command":"/run.sh"}
{"event":"exited","ts":1761303604,"code":0,"reason":"exited normally","timed_out":false}
{"event":"teardown_done","ts":1761303605}

A run that fails before the command ran ends with failed (with the error text) and teardown_done. Values are flat strings, integers and booleans; ip appears when the address is known. crate stack up and crated start containers over such a pipe and read it until jail_created instead of polling jls for the jail to appear, so a member that fails to start says why.

What a stack adds to a spec: member — its template, the pool address, the /etc/hosts and resolv.conf scripts, the named volumes — is composed into the member's spec file before the build, so it is what the archived +CRATE.SPEC holds and what the member's crate run executes (until 1.1.68 the member ran its bare spec). A template given by name is found like crate create --template finds it. ${KEY} placeholders stay in the archive; crate run --var KEY=VALUE substitutes them when it reads the spec.

run/timeout: 15m (firejail --timeout; seconds, or s/m/h) starts the command under timeout(1): SIGTERM at the deadline, SIGKILL ten seconds later, exit code 124, "timed_out": true in the exited event, and the usual teardown.

Templates

The templates/ directory provides ready-made base configurations:

Template Description
standard.yml NAT with WAN+DNS, shared Downloads directory
development.yml NAT with full outbound access, shared home, SysV IPC
minimal.yml Bare jail, no network, no X11
privacy.yml Tor proxy, ZFS encryption, ephemeral COW, DNS filtering
network-isolated.yml No network, nested X11 (1280x720)
bridge-dhcp.yml Bridge mode with DHCP and static MAC
bridge-static.yml Bridge mode with static IP and gateway
passthrough.yml Passthrough with DHCP and IPv6 SLAAC
netgraph.yml Netgraph mode with DHCP and static MAC

Examples

The examples/ directory contains ready-to-use specifications:

Desktop applications: firefox, chromium, gimp, thunderbird, meld, mpv, vlc, qbittorrent, qtox, telegram-desktop, xfce-desktop, lxqt-desktop, lxqt-desktop-nested, lxqt-minimal, lxqt-wayland

Headless/GPU modes: firefox-headless, firefox-gpu, chromium-headless, chromium-gpu, gimp-headless, blender-gpu, glxgears-gpu

Network services: gogs, nginx, syncthing, tor, i2pd

Privacy (Tor/I2P): chromium+tor, firefox+i2pd, chromium+i2pd, qbittorrent+i2pd

Utilities: nmap, amass, wget, aria2, fetch, gzip, yt-dlp, xeyes

Thin jail (base/mode: host-ro, 1.1.58): fetch-thin — the same fetch spec with the host's userland mounted read-only instead of a copied base system. The archive holds only /etc /var /root /tmp /home /usr/local; crate run nullfs-mounts /bin /sbin /lib /libexec /rescue /usr/{bin,sbin,lib,lib32,libexec,libdata,share,include} from the host before the jail starts. A few MB instead of hundreds, no big extraction at start, and a host upgrade upgrades every thin jail at its next start. /usr/local stays per container, so pkg/install and crate update --pkg-only work unchanged. The directory list and the host release are frozen in +CRATE.THIN inside the archive; starting on another release branch prints a warning. crate export of a running thin jail archives the writable layer only and yields a valid thin crate.

YAML Specification Reference

Top-level keys supported in .crate / .yml spec files:

Key Description
base mode (full or host-ro — thin jail on nullfs, see below), host-dirs, keep, keep-wildcard, remove — base system layout
pkg install, local-override, nuke — package management
run command, service, user, timeout — what to execute in the container, as whom (1.1.64: the operator's mirrored account by default, crate when the operator is root, root on explicit request, or a jail account) and for how long at most (1.1.65: 90, 15m, 2h → timeout(1), exit 124)
dirs share — shared directories (host<->jail)
files share — shared files (host<->jail)
options net, x11, ssl-certs, tor, video, gl, no-rm-static-libs, dbg-ktrace
scripts Lifecycle hooks (run:begin, run:after-create-jail, etc.)
security securelevel, children_max, cpuset, enforce_statfs, allow_*
limits RCTL resource limits (memoryuse, pcpu, maxproc, etc.)
ipc sysvipc, mqueue, raw_sockets
zfs datasets — additional ZFS datasets to attach
encrypted ZFS encryption (method, keyformat, cipher)
cow Copy-on-Write (mode: ephemeral/persistent, backend: zfs/unionfs)
dns_filter DNS filtering (allow, block, redirect_blocked)
firewall Per-container pf policy (block_ip, allow_tcp/udp, default)
x11 X11 display (mode: nested/shared/none, resolution, clipboard)
gui GUI session (mode: nested/headless/gpu/auto/wayland/compositor, backend, compositor, vnc, novnc, resolution)
clipboard Clipboard isolation (mode: isolated/shared/none, direction)
privacy hostid: host|random|<uuid> — the jail's own kern.hostuuid/kern.hostid and /etc/hostid; hostname: host|random|<name> (1.1.67)
dbus D-Bus policy (system, session, policy/allow_own/deny_send)
services Managed services (managed list, auto_start)
socket_proxy Socket forwarding (share, proxy with host/jail/direction)
terminal Terminal isolation (devfs_ruleset, allow_raw_tty)
devices Device classes visible in /dev: sound, gpu, video, input, usb, serial, or all (1.1.62)
tmpfs Fresh tmpfs at these jail paths every start: [/tmp, $HOME/.cache] or {/tmp: 512m} (1.1.63)
etc keep: [localtime, hosts, ssl] — host /etc entries copied into the jail's pristine /etc at create (1.1.63)
security_advanced Capsicum, MAC bsdextended/portacl, hide_other_jails

Installation

Via FreeBSD ports

cd /usr/ports/sysutils/crate && make install clean

The port supports build OPTIONS (enabled by default unless noted):

Option Description
DAEMON Container lifecycle daemon (crated)
SNMPD SNMP monitoring agent (crate-snmpd)
EXAMPLES Example container specifications
COMPLETIONS Bash/ZSH shell completions (off by default)
ZFS Native ZFS API (libzfs)
IFCONFIG Native interface configuration (libifconfig)
PFCTL Native PF firewall control (libpfctl)
CAPSICUM Capsicum sandbox support (libcasper)
LIBVIRT Virtual machine support (bhyve via libvirt)
X11 X11 display forwarding
VNCSERVER Embedded VNC server (libvncserver) (off by default)
LIBSEAT DRM/GPU session management (libseat)
# Configure options interactively
cd /usr/ports/sysutils/crate && make config && make install clean

From source

make && sudo make install

Dependencies: yaml-cpp, rang (header-only), libjail / libnv / libnetgraph (base). crated additionally needs cpp-httplib (header-only); crate-hub needs cpp-httplib and sqlite3. The release number lives in the VERSION file: the build generates lib/version.h from it, and make check-version confirms port/Makefile and CHANGELOG.md agree with it.

Compile-time feature flags (set =0 to disable):

make HAVE_LIBZFS=1 HAVE_LIBIFCONFIG=1 HAVE_LIBPFCTL=1 HAVE_CAPSICUM=1 \
     WITH_LIBVIRT=1 WITH_X11=1 WITH_LIBSEAT=1 WITH_LIBVNCSERVER=0

All native API wrappers fall back to shell commands when compiled without the corresponding flag.

Make targets

Target Description
make Build crate CLI
make all-daemon Build crate + crated daemon
make all-snmpd Build crate + crate-snmpd agent
make all-hub Build crate + crate-hub aggregator
make install Install crate CLI + man pages
make install-daemon Install crated, RC script, config
make install-snmpd Install crate-snmpd + MIB file
make install-hub Install crate-hub, its web dashboard and man page
make check-version Verify VERSION, port/Makefile and CHANGELOG.md agree
make install-examples Install example specs to /usr/local/share/examples/crate/
make install-completions Install shell completions
make test Compile unit tests and run via kyua

crated — REST API Daemon

crated is a container lifecycle daemon providing a REST API for remote management.

Features

  • Listens on Unix socket (local) and TCP/TLS (remote)
  • Token-based authentication (SHA-256 hashes, roles: viewer/admin)
  • Rate limiting (100 req/s read, 10 req/s write)
  • Prometheus metrics at /metrics
  • FreeBSD RC service integration

API Endpoints

Endpoint Auth Description
GET /healthz — Health check
GET /api/v1/containers — List running containers
GET /api/v1/containers/:name/gui — GUI session info (display, VNC/WS ports)
GET /api/v1/containers/:name/stats yes Resource usage (RCTL)
GET /api/v1/containers/:name/logs yes Logs (?follow=true, ?tail=N)
POST /api/v1/containers/:name/start yes Start container from .crate
POST /api/v1/containers/:name/stop yes Stop container (SIGTERM → SIGKILL)
POST /api/v1/containers/:name/restart yes Stop + start in one call
DELETE /api/v1/containers/:name yes Destroy container
GET /api/v1/containers/:name/snapshots yes List ZFS snapshots
POST /api/v1/containers/:name/snapshots yes Create snapshot (body: {"name":"<snap>"} optional)
DELETE /api/v1/containers/:name/snapshots/:snap yes Delete a snapshot
GET /api/v1/containers/:name/stats/stream yes Server-Sent Events stream of RCTL counters (1 Hz)
GET /api/v1/containers/:name/console (WS) yes RFC 6455 WebSocket — interactive /bin/sh over a PTY (separate listener; admin role)
POST /api/v1/containers/:name/export yes Export running container to /var/run/crate/<name>-<unixtime>.crate; returns {file,size,sha256}
GET /api/v1/exports/:filename yes Stream a previously-exported .crate back to the client
POST /api/v1/imports/:name yes Upload a raw .crate body (octet-stream); atomic-rename into /var/run/crate/<name>.crate
GET /api/v1/host — Host system info
GET /metrics — Prometheus metrics

WebSocket console

The interactive console listens on its own TCP socket (console_ws.port in crated.conf; default 0 = disabled), separate from the HTTP API. It speaks RFC 6455 with version 13. The handshake requires Authorization: Bearer <admin-token>; afterwards the daemon proxies bytes between the WebSocket and a /bin/sh -i running inside the target jail under jexec(8). Server→client traffic uses binary frames so raw TTY escape sequences pass through; client→server frames (text or binary) are written to the shell's stdin. PING/PONG round-trip; CLOSE terminates the shell with SIGTERM.

The bind address goes through getaddrinfo(3) so it accepts 127.0.0.1, ::1, :: (dual-stack), or a hostname:

console_ws:
    port: 9802
    bind: "::"      # dual-stack, or 127.0.0.1 / ::1 for loopback only

SSE stats stream

/stats/stream emits one data: frame per second containing {name, jid, ip, ts, <rctl counters...>}. Clients can consume it with plain JavaScript via new EventSource(...) or with curl --no-buffer. The stream self-terminates with event: end when the jail exits.

Snapshot endpoints

Snapshot names are validated server-side against [A-Za-z0-9._-]{1,64} (. and .. rejected). POST /snapshots with no body generates an auto name like auto_2026-05-01_220000. DELETE /snapshots/:snap is idempotent — repeating it returns 500 with the underlying ZFS error, since zfs destroy itself is not idempotent.

Configuration

# /usr/local/etc/crated.conf
listen:
    unix: /var/run/crate/crated.sock
    tcp_port: 9800
    tcp_bind: 0.0.0.0

tls:
    cert: /usr/local/etc/crate/tls/server.pem
    key: /usr/local/etc/crate/tls/server.key
    ca: /usr/local/etc/crate/tls/ca.pem
    require_client_cert: true

auth:
    tokens:
        - name: ansible
          token_hash: "sha256:..."
          role: admin
        - name: grafana
          token_hash: "sha256:..."
          role: viewer

log:
    file: /var/log/crated.log
    level: info

RC service

# /etc/rc.conf
crated_enable="YES"

# Control
service crated start
service crated stop
service crated status

crate-snmpd — SNMP Monitoring Agent

crate-snmpd is an AgentX subagent that exposes crate counters via SNMP under .1.3.6.1.4.1.59999. Today it serves four scalars — container count, running count, crate version and hostname; the per-container table that CRATE-MIB.txt (installed to /usr/local/share/snmp/mibs/) also defines is not implemented yet (use crated's /metrics for per-container figures). See crate-snmpd(8).

make all-snmpd && sudo make install-snmpd

Testing

The project uses Kyua and ATF (FreeBSD's standard test framework).

make test                     # Build and run all tests
cd tests && kyua test         # Run from tests directory
kyua test unit/               # Unit tests only
kyua test functional/         # Functional tests only
kyua report -v                # Verbose report

Unit tests (C++17, libatf-c++): spec parsing, network options, IPv6, jail lifecycle, error handling.

Functional tests (shell-based ATF): CLI commands (help, version, validate, list).

Compatibility

Supported: FreeBSD 13.0 and later (13.x, 14.x, 15.x). All version-specific features have safe fallbacks.

FreeBSD 15.0 adaptations (automatic, no user action needed):

  • JAIL_OWN_DESC — race-free jail removal via owning descriptor (falls back to jail_remove() on older kernels)
  • epair checksum offload fix — txcsum/txcsum6 disabled unconditionally (harmless on 13.x/14.x)
  • OS version mismatch warning — detects host vs container FreeBSD major version difference
  • ipfw compatibility warning — alerts when FreeBSD 15 host runs a container built with older base

Caveats

  • Inbound network ports can't be accessed from the host running the crate because of this bug, currently they can only be accessed from other hosts.

Project Status

crate is in its alpha stage. Active development since June 2019.

TODO

See the TODO file in the project.

About

Containerizer for the FreeBSD operating system

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages