|
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> |
||
|---|---|---|
| c | Harden Esquema into a secure rootless container runtime | |
| examples | examples: website-hosting tutorial + build-web-rootfs helper | |
| scheme/esquema | Harden Esquema into a secure rootless container runtime | |
| .gitignore | examples: website-hosting tutorial + build-web-rootfs helper | |
| LICENSE | Initial commit | |
| Makefile | Harden Esquema into a secure rootless container runtime | |
| manifest.scm | Harden Esquema into a secure rootless container runtime | |
| README.md | examples: website-hosting tutorial + build-web-rootfs helper | |
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:
- User namespace (rootless). Runs as an unprivileged user; the payload is root inside the namespace, mapped to your ordinary uid outside.
- Mount / PID / UTS / IPC / net / cgroup namespaces. Full process, filesystem, hostname, IPC and network isolation.
pivot_rootinto the rootfs, with the host mount tree detached so it is unreachable, a fresh/proc, and minimal/dev.- All capabilities dropped — bounding set emptied, ambient cleared,
capsetzeroed,securebitslocked (SECBIT_NOROOT, so uid-0-in-ns confers nothing), andPR_SET_NO_NEW_PRIVSset so setuid binaries cannot escalate. - seccomp-BPF allowlist — default
ENOSYS, with aKILL_PROCESSdeny-set forptrace,mount,unshare,setns,bpf,keyctl, module and kexec syscalls, etc. Non-native (incl. x32) ABIs are killed. - 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.