Why

One kernel, many tenants, nothing wasted

Jails are the oldest and most efficient way to run isolated services, and ZFS makes them cheap to clone, snapshot, move and back up. JailOps puts a modern control plane on that: an API, projects and trust, real IPv6 for every jail in any cloud, managed updates, encrypted backups and fleets declared in git. This page says why each of those matters, and what JailOps leaves out on purpose.

At a glance

  • Efficient

    A jail is a process tree with its own view of the system, not a machine. No guest kernel, no hypervisor, no memory set aside for an operating system that is not yours. A thin jail is a ZFS clone of one shared, immutable image generation.

  • Reachable

    A real public IPv6 address per jail, on a routed prefix or on a shared switched /64, with a built-in neighbor discovery proxy for the second case. No NAT puzzle, no port juggling.

  • Maintained

    Update policies per jail: check, notify or apply on request, with a snapshot first, a health check, and a root swap back when an OS update fails.

  • Recoverable

    Encrypted, deduplicated backups to a node that holds no key, restore tests that prove the backup can be read, and rollback scopes that keep data and OS apart.

  • Declarative

    A git repository of YAML describes networks, projects, jails and their secrets. jailops-sync plans, applies, detects drift and rolls out upgrades with a canary.

  • Small and sharp

    Pure Go, single static binaries, an unprivileged daemon and a small root helper that does not trust it. FreeBSD 15.1 and later, pkgbase first.

The idea behind it

Quality of life for FreeBSD people.

  • A better day for the people who run FreeBSD. JailOps exists to make everyday work with jails, ZFS and networking more pleasant: an API, a console, a backup you can restore and a fleet you can read in git. That includes people who are less conservative about tooling and want a modern control plane, and it is meant with respect for those who prefer plain jail.conf and shell scripts, which remain a fine choice.
  • Enough for about 95% of needs. For most jail and small hosting setups, one host or a handful, a few dozen jails, backups and IPv6, JailOps aims to be all you need, and nothing more to learn.
  • The heavy 5% deserves a big tool. JailOps does not try to replace the specialised ones. Building large sets of packages from the ports tree, for example, is a job for poudriere. Use the right tool for the job and let JailOps manage the jails around it.
  • A companion for packages: PkgOps (source). A separate project and repository that builds native FreeBSD .pkg packages by cross-compiling Go on Linux or macOS, and publishes them as signed static repositories that stock pkg can use, with no FreeBSD builder needed. Install, upgrade and signature checks were run against real pkg on FreeBSD 15.1 arm64; amd64 hardware, GitHub Pages hosting and boot-time rc.d behaviour are not yet verified. The pkg.jailops.net repository is not live yet.

Why jails

Isolation without the machine.

  • Density and speed. Many jails share one kernel and one page cache, and a jail starts as fast as its services do. A host that would hold a few virtual machines holds many more jails, because each one costs what it runs and nothing for the operating system under it.
  • One thing to patch. The kernel is the host's. Updating the host updates every jail's kernel, and a jail's userland is updated from an image generation, not reinstalled.
  • A single, mature mechanism. FreeBSD jails have been in production for more than twenty years. JailOps uses jail(8), VNET, PF and ZFS and adds nothing underneath them: no second runtime, no image format other than ZFS, no virtual machines.
  • Linux too, when you need it. Alpine and Debian run as jails through the Linuxulator. This part is Experimental and only run on arm64 so far.

Jails share the host kernel. A tenant that needs a hard boundary against a hostile neighbour needs its own pool, its own host, or both; the design document says so plainly.

Why ZFS, natively

The storage layer is the product.

  • Cloning is free. A thin jail is a clone of an immutable generation of the image. Twenty jails from one generation store the differences, not twenty copies.
  • Upgrades are a root swap. The new generation is cloned beside the old one, /etc is merged three ways, the roots are renamed, and a failed health check swaps back. The data datasets never move.
  • Rollback has scopes. Roll the OS back and keep what was written since, or roll everything back after typing the jail's name and seeing how many bytes you are discarding.
  • Segmentation on a cloud VM. One NIC and one /64 are enough: carve a /80 network per project, never shared, with cross-project traffic closed by default and a firewall policy per jail in its PF anchor. No VLANs, extra NICs, VMs or overlays. See segmentation.
  • Your big pools work. Register a pool of spinning disks as a storage, put volumes on it with quotas, or mount an existing archive dataset read-only into a jail. See big pools and existing data.

Why any cloud

A /64 is all it takes.

Implemented, tested with fakesVerified on FreeBSD 15.1 (arm64 and amd64)

The rented server you can get almost anywhere comes with one IPv4 address and a /64 of IPv6. With IPv4 you juggle ports and NAT; with that /64 you can give every jail a public address of its own and stop juggling. JailOps carves networks out of it, allocates addresses, writes them down, answers for them when the provider's network needs to be asked, and puts a firewall in front of each one.

  • Routed prefix: the provider sends a prefix to your host, which forwards. No proxy at all.
  • Shared switched /64: the provider's router asks who owns each address, and JailOps answers with the host's MAC address, installs a host route, and announces new addresses. It runs for every proxy-mode network, and the operator approves it once in priv.conf.
  • Dual stack: the same network also holds an IPv4 subnet behind NAT, with port forwards (rdr) for the services that still need IPv4.
  • Cheap to repeat: a fleet of small servers gets the same declaration with a different prefix per host.

The details, with diagrams and commands, are on the IPv6 and cloud setups page.

Cloudflare in front, or direct

Both, per service.

Implemented, tested with fakesInbound TCP from the internet not yet run end to end

A public address per jail makes two shapes easy, and they mix freely on one host.

  • Behind Cloudflare (proxied). The hostname is proxied, the origin is the jail's own address, and the jail's firewall accepts the web ports only from Cloudflare. Nothing else on the internet can reach the origin, so it cannot be attacked around the proxy. Put Cloudflare's origin certificate in the jail as a sealed secret, and it is stored encrypted and opened only when a process in the jail needs it.
  • Direct (DNS only). SSH, mail, WireGuard, a game server or a database replica gets an AAAA record that points straight at the jail. No tunnel, no port mapping, no shared address. The per-jail firewall policy is the access control.
a web jail that only Cloudflare can reach
# copy the current ranges from Cloudflare's published list (cloudflare.com/ips)
$ jailops firewall address-set create cloudflare \
    --address 173.245.48.0/20 --address 103.21.244.0/22 \
    --address 2400:cb00::/32 --address 2606:4700::/32

$ jailops firewall policy create edge --default-in deny --default-out allow \
    --rule "allow in tcp from @cloudflare port 80,443"

$ jailops jail create site --project shop --image freebsd-15@stable \
    --network web --storage default --firewall-policy edge

# a mail jail, direct: its own policy, its own AAAA record
$ jailops firewall policy create mail --default-in deny --default-out allow \
    --rule "allow in tcp port 25,465,587,993"
  • The address set is yours to keep current; jailops-sync can hold it in the fleet repository so every host gets the same list.
  • A project policy sets a floor that a jail's own policy cannot weaken, and traffic between projects is closed unless a rule opens it.
  • Not yet run end to end: TCP and HTTP from the internet into a jail. Inbound ICMP to a jail on a switched /64 was run for fifty minutes (only the first minute, while the router learned the address, lost packets), and the PF matrix passes, but a real web request through Cloudflare is still to be shown. The status page keeps that open.

Updates and backups, without drama

The boring work, done on a schedule.

Implemented, tested with fakesBackup and restore verified on FreeBSD 15.1 (arm64)
  • Update policy per jail. manual does nothing unless you ask, notify checks on a schedule and tells you, managed applies inside a window. An update takes a snapshot first, runs the jail's health check, and for an OS update swaps the root back when the check fails.
  • Packages and the OS are separate. Packages are updated from the host with pkg or apk; the OS follows a newer image generation by root swap. Application data is never rolled back for you.
  • Fleets upgrade with a canary. A rollout moves one jail, waits for the health gate, then goes on in batches.
  • Backups the server cannot read. The host cuts, compresses and encrypts; the backup node, on a directory or S3, holds no key. Policies select jails by label, keep daily, weekly and monthly copies, and run restore tests that prove a backup can be read.
maintenance and backup, example
$ jailops set site updates.mode managed updates.schedule "0 3 * * 0" \
    updates.packages true updates.os true update.healthcheck /usr/local/bin/check
$ jailops update check
$ jailops update history site

$ jailops backup policy create nightly --selector tier=web --target vault \
    --schedule "30 2 * * *" --keep-daily 7 --keep-weekly 4 --keep-monthly 6

Not yet shown: package and generation updates on a real jail, and restore on a second host in a drill. The status page lists what has run on FreeBSD and what has not.

Management, from one jail to a fleet

  • One API. The CLI, the Go SDK, jailops-sync and the two web consoles use the same REST API. Nothing is second-class, and everything you click is something you can script.
  • Projects and trust. A project is granted networks and storage and has its own quotas. Identities are pinned certificates, tokens and users, each with a role and a project scope. There is no default project, and an empty scope never means everything.
  • Git is the source of truth. Describe the fleet in YAML, keep secrets with SOPS and age, and let jailops-sync compare, apply and report drift. It only touches what it owns.
  • Secrets stay sealed. A jail's environment can hold values encrypted to the host's own key; the daemon opens them only when it starts a process in the jail.
  • Every host stands alone. A host has its own API and console. Hubs and fleets are additive, so the smallest setup, one server, is complete.

Why trust it

  • It says what it has not done. Every area carries a tag, and the status page lists what ran on a real FreeBSD host, on which architecture, and what did not.
  • Least privilege by construction. The daemon runs as an ordinary user. A small root helper performs a fixed list of typed operations on allow-lists the host operator writes, and treats the daemon as untrusted.
  • Tested against the real thing. Beyond unit tests against fakes, a clean-install run and a traffic matrix of about 180 probes run on real FreeBSD hosts, so a firewall rule that silently does nothing is caught.
  • Built for where FreeBSD is going. pkgbase first, 15.1 and later, a new release becomes a row in the acceptance matrix. See the direction we track.
  • Open source, BSD licensed, and the web consoles make no external requests.

Web and API: always optional, you decide the exposure

  • The web consoles are optional. Switch them off with web.enabled: false, or build without them using -tags noweb. The API and jailops-sync never need them.
  • Nothing listens on the network by default. The API is on a unix socket. The optional TCP listener is TLS 1.3 only, with no plain-http mode, and takes your own certificate (reloaded without a restart). Callers use pinned mTLS client certificates or scoped API tokens.
  • Three recipes: local only, behind Cloudflare or a reverse proxy, or direct TLS with mTLS. See exposure.md.

Not yet: automatic certificates (ACME) and a require-client-certificate mode.

What it is not

  • Not a virtual machine manager. No bhyve, no hypervisor and no OCI containers: jails only, on purpose. If you want virtual machines and jails in one place, there are other tools worth a look.
  • Not a replacement for thinking about isolation. Jails share a kernel. Treat a hostile tenant as a reason for a separate host.
  • Not finished. It is young software, still before 1.0. Cluster mode, replication between hosts and self-healing fleets are planned, not built.

Get started All features