Guardrail rules¶
The static scanner (crates/installer-engine, guardrails.rs) reads a compose file and refuses
the ways a service could break out of its own bundle. The same scanner runs at publish time
and at activation time.
F.1 — Ports must be loopback¶
A published port bound to anything other than 127.0.0.1 is flagged as F.1-non-loopback-port.
A bare "8080:8080" or a "0.0.0.0:…" binding publishes on every interface and is refused;
"127.0.0.1:8080:8080" passes.
F.2 — No privilege or host namespaces¶
| Flag | Refused as |
|---|---|
privileged: true |
F.2-privileged |
network_mode: host (or other host namespace) |
F.2-host-namespace |
F.3 — Nothing may read outside the bundle¶
Three ways a compose file can reach host files, all refused unless they resolve inside the bundle directory:
| Source | Rule |
|---|---|
volumes: bind mount |
F.3-host-path-escapes-bundle |
build.context outside the bundle |
F.3-build-context-not-local |
env_file: outside the bundle |
F.3-env-file-escapes-bundle |
the docker socket (/var/run/docker.sock) |
F.3-docker-socket-mount |
measured A bundle carrying privileged: true, network_mode: host,
0.0.0.0:9999:9999, and a /etc bind mount was flagged on all four rules at publish time on
2026-08-27; a bundle with a single loopback port and a bundle-relative mount published clean.
A fixed bypass, recorded honestly
The F.3 host-path check once failed open on an interpolated source: a volume written
${VAR:-/etc} slipped past it and published as clean while mounting the host's /etc.
Four of the project's own demo stacks used exactly that idiom. It was reported and fixed —
the scanner now expands a default and, where it genuinely cannot resolve a value, refuses
rather than allowing it. The measurement above is against the fixed scanner.