explain
Security model (trust, proxy, secrets)
What grain isolates, what it trusts, and how proxy and secrets fit.
What grain gives you
- Hardware virtualization boundary between host and guest (QEMU/HVF or Firecracker/KVM)
- Ephemeral disks by default so experiments do not linger
- Optional egress filtering so guests only reach allowed HTTP(S) destinations
- Optional API tokens if the TCP API is exposed beyond localhost
What grain does not claim
- Multi-tenant hard isolation between untrusted co-tenants on one host without additional hardening
- Multi-user RBAC or per-user API tokens — intentionally out of scope (see Single-tenant model)
- A substitute for your OS firewall and disk encryption
- Perfect secrecy if you inject secrets into the guest filesystem — the guest process can read them
Single-tenant / single-operator model
Grain is one operator (or one cooperating team) per data_dir / daemon. That is the product model, not a temporary gap waiting for RBAC.
| Control | Meaning |
|---|---|
One data_dir owner | Default ~/.grain is created 0700 for a single OS user. Disks, keys, images, and state under that tree belong to that owner. |
Unix socket 0600 | grain.sock is owner-only. Any local process running as that user can drive the full control plane without a Bearer token. |
api_token is a shared secret | One Bearer value authenticates the whole daemon. It is not per-user identity, roles, or multi-token RBAC. Put SSO/mTLS at a reverse proxy if you need user identity in front. |
| Shared physical host | For untrusted co-tenants: run separate OS users (each with its own data_dir and daemon) or separate hosts. Do not share one grain control plane across hostile tenants. |
| Trusted team lab | One daemon + one token for peers who already trust each other is fine — everyone with the token is equivalent. Ops: Remote sandbox host; happy path: Remote lab. |
Anyone who holds the token (or the socket) can create VMs, exec in guests, and read host-injected secrets. Treat the API like root on that lab.
Trust boundaries
You (operator)
→ host grain daemon (trusted)
→ hypervisor
→ guest (less trusted workload / agent code)
→ host egress proxy (trusted policy + secrets)
→ internetSecrets: two patterns
- Inject — materialize a file in the guest. Use for TLS keys and app config files.
- Proxy inject — guest uses a placeholder path to the proxy; real
Authorizationis added on the host. Prefer this for cloud API tokens.
Host data directory
data_dir(default~/.grain), plusvms/,images/, andlogs/, are created with mode 0700- Unix socket (
grain.sock) is 0600 - Per-VM
meta.jsonis written 0600 (host paths, ports, tags) - Secrets already use
0700/0600underdata_dir/secrets/ - Grain does not rewrite modes of pre-existing directories; tighten manually on shared hosts if an older install used
0755
Network exposure
- Default API bind
127.0.0.1is intentional - Proxy default
0.0.0.0:3128is intentional so SLIRP guests can reach10.0.2.2— restrict with host firewall if the machine is multi-user or public - Set
api_tokenif anything other than local clients can reach the TCP API
Guest agent trust model
grain-agent is an unauthenticated HTTP server inside each guest (default listen :7475). Anyone who can open TCP (or vsock) to that port can exec, shell, and read/write files as the agent process (often root or uid 1000). Isolation is therefore about who can reach the agent, not agent-side tokens.
| Path | Who may reach the agent | Auth |
|---|---|---|
| Default (SLIRP) | Host process dials 127.0.0.1:<agent_port> hostfwd → guest :7475 | Hostfwd is loopback-only — other machines cannot hit it |
| Remote CLI / SDK | Client → daemon API (Bearer / unix socket) → daemon dials agent on the host | Daemon is authenticated; agent itself still has no token |
| virtio-vsock | Host kernel path to guest CID:7475 | Same trust as local host processes with vsock access |
network: overlay | Any peer VM on the shared L2 can dial guest :7475 on the overlay NIC | None — peers can control each other’s agents |
Implications:
- Do not publish guest port 7475 with
-P/ hostfwd to non-loopback, and do not re-bind hostfwds as0.0.0.0. - Prefer remote access via authenticated API proxy (
GRAIN_API+GRAIN_TOKEN), not by tunneling raw agent ports. - Treat every guest on an overlay as the same trust domain.
Details: Guest agent, Overlay network.
Overlay network (shared L2)
Default network: slirp keeps each VM on an isolated user-net. network: overlay adds a second NIC on a shared multicast L2 (230.0.0.1:4242) so guests can talk to each other. That is intentional for multi-VM labs — and it means guest-to-guest isolation is gone for anything listening on all interfaces (including the agent on :7475).
Use overlay only among workloads that may fully trust one another. Untrusted multi-tenant guests must stay on slirp (or separate hosts). Guide: Overlay network.
Shared / remote hosts
Running grain on a team machine so developers create sandboxes remotely is a supported ops pattern, not multi-tenant SaaS and not multi-user RBAC. One token ≈ one trust domain; see Single-tenant model.
- Set
api_token; daemon will not bind a non-loopbackapiwithout one - Prefer
api: 127.0.0.1:7474+ SSH tunnel or TLS reverse proxy - The control plane is cleartext HTTP unless you terminate TLS in front; Bearer tokens on bare
http://host:7474are sniffable on the LAN - Remote CLI:
GRAIN_API/--api+GRAIN_TOKEN(CLI warns once on non-loopback cleartexthttp://;GRAIN_INSECURE_HTTP=1silences) - Firewall port 7474 (and egress proxy 3128) — do not leave control plane open to the internet
- Resource caps; published ports stay on host loopback
- Avoid
network: overlayacross different users’ sandboxes on a shared host - Keep MCP on loopback (or behind the same token/firewall story) — MCP tools inherit control-plane power
Happy path: Remote lab. Ops: Remote sandbox host.
Images
Only pull images from sources you trust (ubuntu-cloud, grain-ubuntu from your releases, alpine-cloud from Alpine).
Pull requires a digest before install:
- Pinned catalog SHA-256 for
ubuntu-cloudandalpine-cloud - Companion
.sha256sidecar forgrain-ubuntu(empty catalog pin)
If neither pin nor sidecar is available, pull fails closed (no silent unverified install). See Images.