No description
  • C 56.9%
  • Scheme 39.6%
  • Makefile 3.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Berkeley 9ed8d60152 examples: website-hosting tutorial + build-web-rootfs helper
Make it easy for others to build and host sites with Esquema:

- README: new "Tutorial: host a website with Esquema" section — build a
  rootfs, describe the deployment as a <container>, run it, verify, and the
  privileged-port (<1024) sysctl note.
- examples/build-web-rootfs.sh: assemble a BusyBox web rootfs (httpd + full
  applet userland, mount points, minimal /etc), locating busybox from the
  store or `guix build`.
- examples/deploy-web.scm: serve examples/rootfs-web with busybox httpd inside
  an Esquema container, sharing the host net so it is reachable, binding
  /gnu/store read-only for busybox's libraries; port via $ESQ_PORT.
- examples/rootfs-web: a polished landing page for the demo.
- .gitignore: keep the page + layout, ignore the regenerable busybox binary,
  its applet symlinks and run-time mount targets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:30:15 -03:00
c Harden Esquema into a secure rootless container runtime 2026-07-01 22:14:09 -03:00
examples examples: website-hosting tutorial + build-web-rootfs helper 2026-07-04 11:30:15 -03:00
scheme/esquema Harden Esquema into a secure rootless container runtime 2026-07-01 22:14:09 -03:00
.gitignore examples: website-hosting tutorial + build-web-rootfs helper 2026-07-04 11:30:15 -03:00
LICENSE Initial commit 2026-02-08 22:36:30 +00:00
Makefile Harden Esquema into a secure rootless container runtime 2026-07-01 22:14:09 -03:00
manifest.scm Harden Esquema into a secure rootless container runtime 2026-07-01 22:14:09 -03:00
README.md examples: website-hosting tutorial + build-web-rootfs helper 2026-07-04 11:30:15 -03:00

Esquema — a Guile-native container runtime

Esquema is a minimal, security-first, rootless container runtime built natively in Scheme. It integrates with GNU Guix and Shepherd to give reproducible, strongly-isolated environments without a daemon, without root, and without YAML — just declarative Scheme.

Containers are first-class Scheme objects. Isolation is explicit, fine-grained and secure by default.


Security model

Esquema confines an untrusted payload behind defence-in-depth. Every layer is applied, in the correct order, inside a clone/fork child that runs only async-signal-safe C between fork and execve:

  1. User namespace (rootless). Runs as an unprivileged user; the payload is root inside the namespace, mapped to your ordinary uid outside.
  2. Mount / PID / UTS / IPC / net / cgroup namespaces. Full process, filesystem, hostname, IPC and network isolation.
  3. pivot_root into the rootfs, with the host mount tree detached so it is unreachable, a fresh /proc, and minimal /dev.
  4. All capabilities dropped — bounding set emptied, ambient cleared, capset zeroed, securebits locked (SECBIT_NOROOT, so uid-0-in-ns confers nothing), and PR_SET_NO_NEW_PRIVS set so setuid binaries cannot escalate.
  5. seccomp-BPF allowlist — default ENOSYS, with a KILL_PROCESS deny-set for ptrace, mount, unshare, setns, bpf, keyctl, module and kexec syscalls, etc. Non-native (incl. x32) ABIs are killed.
  6. cgroups v2 limits (best-effort under rootless delegation) — memory.max, pids.max, cpu.max.

This is verified by the test-suite (see Testing): the payload cannot see host PIDs, cannot reach the host filesystem, has an empty capability set, is killed on a denied syscall, and sees only loopback networking.

How this compares

Property Esquema Docker/Podman Jails / LXC guix shell
Language-native (Scheme)
Daemon-free (Docker)
Rootless by default partial
User namespace isolation n/a (BSD) only --container
seccomp syscall filtering (allowlist) (coarse)
Capability drop + no-new-privs partial
Declarative in one language (Scheme) (YAML/OCI) (Guix)

A plain guix shell gives a reproducible environment but no kernel-level isolation (shared namespaces, no seccomp, full capabilities). Esquema adds the isolation while keeping the Scheme-native, daemon-free ergonomics. Container startup is ~13 ms (≈8 ms over a bare execve).


Getting started

Build the shared library and run the smoke test inside the pinned environment:

guix shell -m manifest.scm -- make smoke

Build a self-contained demo rootfs and launch a container:

guix shell -m manifest.scm -- sh examples/build-rootfs.sh examples/rootfs-min
guix shell -m manifest.scm -- env ESQUEMA_LIBDIR=$PWD \
  guile -L scheme examples/hello.scm

Define and run a container from Scheme:

(use-modules (esquema runtime) (esquema container))

(define box
  (make-container "web" "/path/to/rootfs"
                  (list "/bin/httpd" "-p" "8080")
                  #:hostname   "web"
                  #:rootfs-ro? #t                       ; seal the root
                  #:limits     (make-limits (* 256 1024 1024) ; memory.max bytes
                                            128              ; pids.max
                                            50000 100000))) ; cpu 50%
;; forks, isolates, execve()s the payload, waits, returns the exit status:
(run-container box)

make-container is secure by default: all namespaces, seccomp on, and every capability dropped unless you opt out (#:seccomp? #f, #:drop-caps? #f, #:namespaces '(user mount pid ...)).


Tutorial: host a website with Esquema

Serve a real, browser-reachable website from inside an isolated Esquema container — rootless, seccomp-filtered, every capability dropped. Three steps.

1. Build a web rootfs

A rootfs is just a directory. The helper populates one with a full BusyBox userland (its httpd applet is the web server) plus the standard mount points:

examples/build-web-rootfs.sh examples/rootfs-web
# drop your own files in examples/rootfs-web/www/ (index.html, assets, …)

BusyBox on Guix is dynamically linked, so the container bind-mounts /gnu/store read-only at run time to resolve its loader and libraries — no copying, and the store is world-readable already. (For a fully self-contained image, put a statically-linked server in bin/ instead and skip the bind.)

2. Describe the deployment

A container is a value. examples/deploy-web.scm:

(use-modules (esquema runtime) (esquema container))

(define port (or (getenv "ESQ_PORT") "8081"))

(run-container
 (make-container "esquema-web" "/absolute/path/to/examples/rootfs-web"
                 (list "/bin/httpd" "-f" "-v" "-p" port "-h" "/www")
                 #:hostname   "esquema-web"
                 ;; Drop 'net to SHARE the host network so the port is reachable;
                 ;; keep it to isolate networking (then only loopback exists).
                 #:namespaces '(user mount pid uts ipc cgroup)
                 #:mounts     '(("/gnu/store" "gnu/store" #t))   ; ro: busybox libs
                 #:limits     (make-limits (* 128 1024 1024) 64 #f #f)))

Everything except networking stays isolated: the payload is PID 1 in its own PID/mount/UTS/IPC namespace, pivot_root-ed into the rootfs, with an empty capability set and the seccomp filter loaded.

3. Run it

# from a checkout (library on ESQUEMA_LIBDIR):
ESQ_PORT=8081 guix shell -m manifest.scm -- \
  env ESQUEMA_LIBDIR=$PWD guile -L scheme examples/deploy-web.scm

# or, once installed from the securityops channel (guix install esquema):
ESQ_PORT=8081 guile examples/deploy-web.scm

Verify and see the isolation the visitor's server runs under:

curl -s http://localhost:8081/ | head        # your page
# what the server process itself sees:
guile -c '(use-modules (esquema runtime)(esquema container))
 (run-container (make-container "x" "'$PWD'/examples/rootfs-web"
   (list "/bin/sh" "-c" "id -u; hostname; grep -E \"Cap|Seccomp\" /proc/self/status; ls /")
   #:mounts (quote (("/gnu/store" "gnu/store" #t)))))'
#  -> uid=0 (in-ns)  host=x  CapEff 0000000000000000  Seccomp: 2  / = just the rootfs

Ports below 1024

A rootless container can only bind an unprivileged port (≥ 1024 by default), so 8081 works out of the box but 80/81 do not. To serve on a privileged port, lower the threshold once (reversible), then use it:

sudo sysctl -w net.ipv4.ip_unprivileged_port_start=81   # revert with =1024
ESQ_PORT=81 guile examples/deploy-web.scm

To keep it running across reboots, wrap it in the Shepherd service (below) or a guix home Shepherd service.


Testing

guix shell -m manifest.scm -- make check       # C tests + functional + security + cppcheck
guix shell -m manifest.scm -- make test-perf   # startup latency / overhead / leak guard
guix shell -m manifest.scm -- make static      # gcc -fanalyzer
guix shell -m manifest.scm -- make sanitize    # ASan + UBSan build and run

The suite covers functional behaviour (scheme/esquema/tests/functional.scm), isolation and escape attempts with positive+negative controls (security.scm), C-level enforcement including the seccomp SIGSYS kill (tests/c/test_primitives.c), and performance/leak guards (performance.scm). The C library builds warning-clean under -Wall -Wextra -Werror with FORTIFY, stack-protector/clash protection, full RELRO, a non-executable stack and CF-protection.


Guix service

(esquema esquema-service) provides a Shepherd service type to supervise a container as part of a Guix system configuration.

Esquema empowers secure, reproducible, declarative containerization for GNU Guix — for development, CI/CD and lightweight server deployments.