Skip to main content

Guest exec policy

Confine a single command inside a VM. The request carries a policy; the guest agent applies it with Landlock and seccomp (through fluxvm-procbox) before the command runs, and reports what it actually enforced.

Roadmap and status​

ItemImplemented and type-checkedVerified on hardware
policy on POST /v1/vms/{uuid}/agent and POST /v1/sandboxes/{id}/processyes (unit tests pass)yes, on the VM route (Linux/QEMU guest)
ExecPolicy wire type, procbox-compatible JSONyes (parse and conversion tests)yes (unknown fields ignored, a malformed policy is rejected and not run)
Landlock and seccomp enforcement in the guest agentyesyes (reads and writes outside the policy, TCP connect, namespaces, memory limit, output cap, timeout)
Fail-closed on unenforceable policyyesyes (see below)
ExecEnforcement in the exec responseyesyes
exec allow-list (Landlock execute right; exec_restricted in the response)yesyes, 2026-10-22 (a listed program runs; a script written into a writable dir, an unlisted program and an empty list do not start; Ubuntu guest under QEMU/KVM, VM route)
fluxctl exec --policy FILEyesyes
Agent refuses requests without a tokenyesyes (scripts/test-guest-agent-auth.sh)

Verified by scripts/e2e/guest-exec-policy.sh on an Ubuntu 24.04 guest (kernel 6.8, Landlock ABI 4) under QEMU/KVM; the sandbox process route and other guest kernels are not covered. A guest on Landlock ABI 4 cannot enforce the default policy: scope_ipc (abstract-unix-socket and signal scoping) needs ABI 6, so the guest refuses with policy could not be enforced, command not run rather than running the command unconfined. Set "scope_ipc": false for such guests, or use a newer guest kernel. The response lists what was not enforced (not_enforced, for example device ioctl control needs ABI 5).

Policy JSON​

The shape is the fluxvm-procbox policy JSON, so a policy written for procbox can be passed through unchanged. Unknown fields are ignored (for example max_abi); missing fields take the safe defaults. {} is a valid policy: no filesystem access, no network, IPC scoping on, seccomp errno.

{
"read": ["/bin", "/usr", "/lib", "/lib64", "/etc/ssl"],
"write": ["/tmp/work"],
"tcp_connect": {"ports": [443]},
"tcp_bind": "deny",
"scope_ipc": true,
"seccomp": "errno",
"allow_namespaces": false,
"max_memory": 268435456,
"max_processes": 200,
"cpu_seconds": 30,
"timeout_secs": 30,
"clean_env": true,
"env": [["FOO", "bar"]],
"cwd": "/tmp/work",
"best_effort": false,
"max_output_bytes": 1048576,
"run_as": {"uid": 1000, "gid": 1000},
"isolation": "off",
"allow_unix": false,
"allow_udp": false
}
FieldTypeDefaultMeaning
read, writepaths[]Filesystem paths the command may read or write
execpaths or absentabsentExecutable allow-list, enforced by Landlock's execute right. Absent: read and write paths are also executable. Present: they are not, and only files under these paths run, so a program the command wrote into a writable directory cannot start. Commands run through /bin/sh -c, so list the shell, the programs and the dynamic loader directory (for example /lib64). Needs a Linux guest with Landlock; otherwise the command is refused (or, with best_effort, reported in not_enforced). exec_restricted in the response says it applied. Verified by unit tests only so far; the real-enforcement tests in crates/fluxvm-procbox/tests/confine.rs run on Linux CI.
tcp_connect, tcp_bind"any", "deny" or {"ports": [n, ...]}"deny"Landlock TCP rules
scope_ipcbooltrueScope abstract unix sockets and signals
seccomp"errno", "kill" or null"errno"Denylist action; null turns the filter off
allow_namespacesboolfalsePermit creating namespaces
max_memory, max_processes, cpu_secondsnumbernoneRLIMIT_AS, RLIMIT_NPROC, RLIMIT_CPU
timeout_secsnumbernoneWall-clock timeout. Capped by the request's timeout_seconds: a policy can only shorten it
clean_env, env, cwdfalse, [], noneEnvironment and working directory
best_effortboolfalseRun with what the kernel can enforce and report the rest
max_output_bytesnumberprocbox defaultOutput capture cap
run_as{uid, gid}noneDrop to this id before confining
isolation"off", "auto", "strict""off"Private namespaces with a root holding only granted paths
allow_unix, allow_udpboolfalseKeep those socket families when the network is restricted

Read paths must include /bin and /usr. The agent runs the command as /bin/sh -c <command>. Under a policy the command can read only what read or write lists, so a policy without /bin and /usr cannot even start the shell, and shared libraries usually also need /lib and /lib64. A minimal working read list is ["/bin", "/usr", "/lib", "/lib64"]; add /etc entries the command needs (certificates, resolv.conf).

Enforcement and fail-closed behavior​

Without policy, nothing changes: the command runs unconfined and the response has no enforcement key.

With policy:

  1. The agent converts the policy and runs it through procbox. Landlock confines the filesystem and TCP ports, seccomp denies a syscall denylist, and rlimits, run_as and isolation apply as set.
  2. Strict by default. If the guest kernel cannot enforce something the policy asks for, the command is not run, and the response is an error: policy could not be enforced, command not run: .... Strict mode needs Landlock ABI 3 or later, and the default scope_ipc: true needs ABI 6 (Linux 6.12 or later). On an older guest kernel, either upgrade it, set scope_ipc to false, or set best_effort to true.
  3. With best_effort: true the command runs with what the kernel can enforce, and the gaps are listed in enforcement.not_enforced. Check that list before trusting the run.
  4. A command that outlives its timeout is killed and reported as an error. The timeout is the smaller of the policy's timeout_secs and the request's timeout_seconds (default 30 s).
  5. Procbox sandboxes are already confined by their own spec, so a per-exec policy on one is rejected with 400.

ExecEnforcement result​

The exec response gains enforcement:

{
"result": "exec",
"exit_code": 0,
"stdout": "...",
"stderr": "",
"enforcement": {
"landlock_abi": 6,
"filesystem": true,
"tcp_connect": true,
"tcp_bind": true,
"scope_abstract_unix": true,
"scope_signal": true,
"seccomp": true,
"uid_dropped": false,
"namespaces": false,
"network_isolated": false,
"seccomp_sockets": true,
"not_enforced": []
}
}

not_enforced lists everything the policy asked for that this run did not enforce; it is empty in strict mode. A caller that needs a guarantee should check not_enforced == [] and the specific booleans it relies on, rather than only the exit code.

CLI and REST​

cat > build-policy.json <<'EOF'
{"read": ["/bin", "/usr", "/lib", "/lib64"], "write": ["/tmp/work"], "tcp_connect": "deny"}
EOF
fluxctl exec $VM --policy build-policy.json --timeout-seconds 60 -- 'cd /tmp/work && make'

fluxctl exec --policy FILE parses the JSON locally (unknown fields ignored, missing ones defaulted) and prints the response including enforcement. It runs through the local manager; fluxctl exec is not available with --server, so use REST there:

curl -sS http://127.0.0.1:7788/v1/vms/$VM/agent \
-H 'content-type: application/json' \
-d '{"command":"ls /usr","policy":{"read":["/bin","/usr","/lib","/lib64"]}}'

The same policy field is accepted by POST /v1/sandboxes/{id}/process. See api.md.

Agent token: fail closed​

The guest agent authenticates requests with a token provisioned into the guest. This batch also changes what happens when there is none:

  • Token file present: requests must carry it (unchanged).
  • Token file missing: the agent refuses every request with unauthorized: guest agent has no token provisioned ... and logs an error at startup.
  • FLUXVM_AGENT_ALLOW_INSECURE=1 (also true, yes, on) in the agent's environment restores the old behavior: the agent runs unauthenticated, logs a warning, and any vsock caller can run commands as root.

Compatibility warning for older guest images.

  • An image built with an earlier agent that has no token and relied on running unauthenticated will stop working once its agent is upgraded to this version, until you provision the token or set FLUXVM_AGENT_ALLOW_INSECURE=1. Prefer provisioning the token; the opt-in is for development images.
  • An older agent does not know the policy field. The protocol ignores unknown fields, so it would run the command unconfined and the response would have no enforcement key. When you send a policy, treat a response without enforcement as "not confined", and rebuild the guest image with this agent version before depending on confinement.

This policy governs commands run inside a guest. Which binaries the VMM process may run on the host is a separate control, the VMM Guard exec allow-list; see host-hardening.md.