Skip to content
Fuse
Esc
↑↓navigate↵open⌘Jpreview
On this page

Security hardening

TLS, CIDR allowlisting, fail-closed mode, and secrets encryption

Fuse’s auth model is deliberately simple: a single shared bearer token, no users, no roles. See Authentication for the full model. This page covers the operational hardening controls available on top of that model.

TLS

export ORCH_TLS_CERT=/path/to/cert.pem
export ORCH_TLS_KEY=/path/to/key.pem

Both unset (the default) serves plain HTTP. Setting both switches to HTTPS and flips the session cookie’s Secure attribute on, an HttpOnly cookie without Secure would otherwise be sendable over an accidental plaintext connection. Setting only one of the two fails at startup with a message naming the missing variable, so a half-configured deployment can never quietly serve plaintext while you believe TLS is on. Confirm the scheme=https field on the listening line the orchestrator logs at boot rather than assuming TLS came up. There is no built-in certificate rotation, mutual TLS, or cipher-suite configuration, terminate TLS at a reverse proxy in front of the orchestrator if you need any of that.

Session cookies behind a TLS proxy

export ORCH_SECURE_COOKIES=true

When a reverse proxy terminates TLS (the topology recommended above), the orchestrator itself speaks plain HTTP and cannot tell that the browser’s connection was encrypted. Set ORCH_SECURE_COOKIES=true so the session cookie, which carries the master token for seven days, still gets its Secure attribute. Without it that cookie is sendable over any plaintext hop.

The orchestrator deliberately ignores X-Forwarded-Proto and friends: any client can send those headers, so trusting them would let a caller decide whether their own session cookie is protected.

ORCH_SECURE_COOKIES Result
unset (default) Secure when the orchestrator terminates TLS itself, else off
true always Secure, for TLS-terminating proxy deployments
false never Secure, for plaintext local development only

Any other value fails at startup. Running plaintext without an explicit setting logs a warning at boot naming this variable.

CIDR allowlisting

export ORCH_ALLOWED_CIDRS=10.0.0.0/8,192.168.1.0/24

Comma-separated CIDR blocks. Requests from outside every listed range are rejected with 403, before bearer auth even runs. Empty (the default) means no IP restriction. Invalid CIDR syntax fails the orchestrator at startup rather than silently ignoring the bad entry, so a typo here is caught immediately, not discovered later as an unexpected open door.

/metrics, /health, /ready, and /v1/version are mounted outside the allowlist (on the outer mux, alongside but not behind the router that installs the CIDR middleware), so they stay reachable from any source address. /metrics in particular exposes fleet size and route names, restrict these four at the reverse proxy or the host firewall if that matters to you.

Fail-closed production mode

export ORCH_REQUIRE_AUTH=true

Without this, the orchestrator boots happily with no auth configured at all, convenient for local dev, dangerous to leave on by accident in production. Setting ORCH_REQUIRE_AUTH=true makes the orchestrator refuse to start unless ORCH_AUTH_TOKEN, TOKEN_ENCRYPTION_KEY, and DATABASE_URL are all set, a missing one fails fast at boot with the specific missing variable named, rather than silently running in a weaker mode than intended. Set this in every production deployment.

Secrets encryption at rest

export TOKEN_ENCRYPTION_KEY=$(openssl rand -hex 32)

Must be exactly 64 hex characters (32 bytes); anything else fails at boot with a specific error rather than silently truncating or padding. Used to encrypt three things at rest with AES-256-GCM before they’re written to the state store: per-VM guest-agent tokens, per-host agent bearer tokens, and per-VM secrets. Losing this key doesn’t lock you out of the orchestrator itself, but nothing it encrypted can be decrypted afterward. If you ever have to regenerate it, rotate every environment’s token via fuse environment rotate-token, re-register every host (its stored agent token is unreadable, which surfaces later as a firecracker create vm: http 401 at environment-create time rather than as an error at boot), and re-supply any per-VM secrets.

Master token vs. API keys

Prefer revocable API keys over the master token for anything automated (CI runners, other services). The master token can mint and revoke keys; a compromised API key can only do what its holder was scoped to do at the API level (everything an authenticated caller can do, except manage other keys), and it can be individually revoked without rotating the master secret and re-distributing it everywhere.

Same-origin deployment only

The session-cookie flow (SameSite=Lax, no CORS middleware) only supports a browser SPA served same-origin through a reverse proxy in front of both the static assets and the API. A genuinely cross-origin deployment (the SPA on a different domain than the API) isn’t supported today, the browser blocks the credentialed /login request at the CORS preflight before auth even runs. Front any browser-based dashboard through a same-origin reverse proxy rather than attempting a cross-origin setup. See Authentication for the full cookie model.

Request size limits

Every JSON endpoint decodes through a bounded reader, so no single request can make the orchestrator allocate without limit. Exceeding a limit returns 413 with the error code payload_too_large. The limits count bytes actually read, so they apply to chunked requests as well as ones that declare a Content-Length.

Endpoint Limit
POST /login 4 KiB
POST /v1/environments, exec 1 MiB
all other JSON endpoints 64 KiB

An inline manifest plus secrets plus a startup script must fit inside the 1 MiB ceiling. Reference a manifest instead of inlining it if you are near that.

The host agents apply their own ceilings, configurable per host: 1 MiB for control-plane calls (FC_AGENT_MAX_BODY_BYTES / QEMU_AGENT_MAX_BODY_BYTES) and 64 MiB for guest file uploads (FC_AGENT_MAX_UPLOAD_BYTES / QEMU_AGENT_MAX_UPLOAD_BYTES). They reject chunked request bodies outright, which nothing in Fuse sends. FC_AGENT_REQUEST_TIMEOUT / QEMU_AGENT_REQUEST_TIMEOUT (60s) bound a client that opens a connection and then stalls; interactive attach sessions are exempt, since idling is what they are for.

Host agent tokens are a separate concern

FIRECRACKER_TOKEN (and QEMU_AGENT_TOKEN for GPU hosts) authenticate the orchestrator to each host agent, entirely independent of the operator-facing API token above. Rotating one doesn’t affect the other. See Firecracker host setup and GPU host setup.

Exposed guest ports are unauthenticated

An expose: entry in a Fusefile becomes a plain DNAT rule on the host agent: iptables rewrites <host>:<host port> to <guest ip>:<guest port> on the host’s default-route interface, TCP only. Nothing sits in that path. There is no gateway, no reverse proxy, no TLS termination, and no token check in front of exposed traffic, so whatever the guest service does for authentication is the whole of it. Fuse adds nothing. The address handed back after fuse up is a bare host:port authority with no scheme, for that reason.

None of the controls above reach these ports. ORCH_ALLOWED_CIDRS is middleware on the orchestrator’s HTTP router, so it filters API calls, not packets to a published port; ORCH_TLS_CERT terminates TLS for the API only; the host agent tokens guard the agent’s own port. Anything listening on an exposed port is reachable by anyone who can reach the host.

That is the intended default for a sandbox tool rather than an oversight, but it puts the burden on the Fusefile author. If a service needs authentication, implement it in the service, or put something you control in front of the host, a reverse proxy or load balancer that terminates TLS and checks credentials, and publish nothing directly. A host firewall works too, with one caveat on Firecracker hosts: fc-expose.sh inserts an INPUT ... ACCEPT for the published port at the top of the chain, ahead of a deny rule added later.

The reserved-port rules are a related but narrower control. expose: rejects the guest agent’s management port (9550) and SSH (22) outright, and rejects privileged ports below 1024 unless the entry sets allow_reserved: true, with the host agent re-checking both before it installs any rule. That protects Fuse’s own control plane from being published. It says nothing about the services you publish on purpose.

Proxy egress is a policy, not a filter

An environment created with egress: proxy has exactly one path out of the host: the proxy endpoint the orchestrator handed it. The enforcement is on the host’s tap, in iptables, and is two things together: the FORWARD ... ACCEPT that a direct environment’s tap gets is never installed, and an explicit FORWARD -i <tap> -o <iface> -j DROP is installed in its place. The drop is there because the FORWARD chain has no default deny, so omission alone would enforce nothing on a host whose policy is ACCEPT. Nothing inside the guest can reach that rule, so proxy mode cannot be bypassed from inside: a workload that ignores the proxy, opens raw sockets, or rewrites its own resolv.conf gets no egress. The guest’s resolver points at its own loopback with nothing listening, so a name lookup fails closed rather than leaking to a resolver outside the tunnel. A proxy that dies after boot leaves the environment with no egress, never direct egress; the orchestrator reports that as egress.health: unhealthy and does not repair it.

That is the whole of the guarantee, and it is worth being exact about what it is not:

  • No content inspection. Nothing looks at what goes through the proxy. A proxy-mode environment can fetch anything the backend can reach.
  • No allowlist. There is no per-environment destination policy. If you need one, the place to apply it is the backend: a Zero Trust organisation’s gateway rules for cloudflare-warp, nothing for mock.
  • The mock is not private. mock is a socks5 server inside the orchestrator process, egressing from wherever that process runs. It proves the policy is enforced; it hides nothing and is registered only when ORCH_EGRESS_MOCK=true is set, with a warning at startup.

The endpoint is not a secret. It is the host side of the vm’s tap (socks5h://10.200.<N>.1:<port>), reachable only from inside that vm, and it is deliberately written to a world-readable /etc/fuse/egress.json so a non-root browser harness can find it. The provider’s credentials are a separate matter: they stay in the orchestrator’s configuration, travel to the host agent per provision request, and are never written to the agent’s state, a log line, or the guest. See WARP egress host setup.

What this doesn’t cover

Broader infrastructure hardening, network segmentation between hosts, host OS patching, Firecracker’s own jailer configuration, isn’t implemented by Fuse’s Go code and is genuinely general infrastructure-security practice rather than something specific to this project. Apply your organization’s standard hardening baseline to the machines running the orchestrator and host agents, the controls on this page are what Fuse itself provides on top of that.

Was this page helpful?