Network
Secret relay
The host adds a credential to a VM's API calls, so the VM uses a key it never holds.
The FlashVMM secret relay is a host-side HTTP forwarder inside each VMM that lets a FlashVM call an authenticated API through a local endpoint while the credential is added on the host, masked from every response, logged per call and capped.
A FlashVM is given one or more named proxies at create. Its workload receives FLASH_PROXY_<NAME>_URL, a plain-HTTP URL on its own host address, and calls it exactly as it would call the upstream. The VMM's frame path maps that endpoint to the relay, the relay adds the key in the configured header and forwards over the host's TLS, and the answer streams back. Paired with --egress none (see egress engine), the guest reaches its APIs and nothing else, and holds no key at any point.
Key facts
| Runs on | Linux (KVM), macOS (HVF), Windows (WHP) |
|---|---|
| Interfaces | CLI flash run --secret-proxy, flash proxies · MCP flash_secret_proxy_calls, flash_cli · REST /v1/flashvm/secret-proxies |
| Guest endpoint | http://<host address>:7730/<token>/…: the NIC's gateway on Linux, .254 of the user-mode NAT segment on macOS and Windows |
| URL token | 32 lowercase hex digits (128 unpredictable bits), chosen by the VMM |
| Credential header | authorization: Bearer <key> by default; any other header carries the key as it is |
| Limits | Up to 16 proxies per VM · request bodies up to 32 MiB · 64 concurrent connections · newest 1,024 calls logged per proxy |
| Caps | max_calls and max_bytes (request plus response body), uncapped by default, changeable live |
How it works
Setup
Each proxy is NAME=KEYFILE,upstream=URL[,header=H][,max-calls=N][,max-bytes=B]. flash reads the key file as its owner and hands the VMM the key itself; on Linux, flashnet opens the file before capabilities are dropped and passes it as an inherited descriptor. The VMM validates every proxy at create: a name of [a-z0-9_-]{1,32}, an https:// (or http://) upstream with a host and no user, password, query or fragment, a key of at least 8 bytes on one line, and a header other than hop-by-hop, host, content-length, accept-encoding or expect. The key is held in memory that is zeroed when dropped, and the header value is marked sensitive.
The path of one call
- The guest opens TCP to its host address on port 7730. The egress engine rewrites the destination to the relay's listener (an ephemeral port on the host) and fixes the IP and TCP checksums; replies are rewritten back. These frames stay on the host and sit outside the egress policy.
- The relay finds the proxy by the token in the path, then checks the request: one framing (
content-lengthorchunked, never both), noCONNECTor upgrade, no.or..segments, and a joined URL that stays on the upstream's scheme, host and port. - The call is reserved against
max_callsandmax_bytesbefore it is sent, so concurrent calls stay within the cap. Past a cap, the guest receives429 cap_reached. - Hop-by-hop headers and any copy of the credential header the guest sent are stripped, the key is set, and
accept-encoding: identityis sent so the response arrives uncompressed for the redactor. - The response streams back to the guest as it arrives (server-sent events included). Every header and body byte passes the redactor, which masks the key in raw form, lowercase and uppercase hex, percent-encoding,
\u-escapes and standard and URL-safe base64 at all three alignments, including matches split across chunk boundaries. - The call is logged as
{seq, t, method, path, status, bytes, duration_ms}. The path is recorded without its query.
Per platform
Each OS uses its own VMM with the shared flash-relay crate: Linux listens on NIC 0's gateway address and answers only that VM's own guest address; macOS (HVF engine) and Windows listen on the host's loopback, which the built-in user-mode NAT exposes to the guest as .254 of its segment. The relay is synchronous threads on every OS, so the same code serves all three.
Restore and fork
A snapshot records each proxy's name, upstream, header, caps and token, never the key. A restore gives each key again, and the guest's existing URL keeps working because the token is kept. A forked child inherits its parent's proxies with its own counters.
Interfaces
CLI
# A model API the guest may call 200 times, and nothing else on the network
flash run --bundle ./bundle --vcpus 2 --mem-mib 1024 --id agent \
--nic tap=t0 --egress none \
--secret-proxy model=/home/me/model.key,upstream=https://api.example.com,header=x-api-key,max-calls=200 \
--detach
# Inside the guest
flash exec agent -- sh -c 'curl -s "$FLASH_PROXY_MODEL_URL/v1/models"'
# Proxies, counters, and one proxy's calls
flash proxies agent
flash proxies agent model --json
# Raise or lift a cap on the running VM
flash proxies agent model --max-calls 500
flash proxies agent model --max-bytes none
MCP
flash_secret_proxy_calls(vm_id: "agent")
flash_secret_proxy_calls(vm_id: "agent", name: "model", since: 0)
flash_api(vm_id: "agent", method: "PATCH", path: "/v1/flashvm/secret-proxies/model", body: {"max_calls": null})
REST
GET /v1/flashvm/secret-proxies
GET /v1/flashvm/secret-proxies/{proxy_name}
PATCH /v1/flashvm/secret-proxies/{proxy_name} {"max_calls": 500, "max_bytes": null}
GET /v1/flashvm/secret-proxies/{proxy_name}/calls?since=0
{"calls": [{"seq": 1, "t": 1793750400123, "method": "POST", "path": "/v1/messages",
"status": 200, "bytes": 2048, "duration_ms": 640}], "next": 1}
Proxies are given in the create body as secret_proxies. A proxy view carries name, env, url, upstream, header, key_file, max_calls, max_bytes, calls, bytes, refused, and never the key. Contract:
- A bad proxy at create, or a
PATCHof anything but the caps, returns422 invalid_secret_proxy. - A restore that omits the key for a proxy the snapshot's guest holds returns
422 secret_proxy_missing. - An unknown proxy name returns
404 unknown_secret_proxy. - Inside the guest:
429 cap_reachedpast a cap,413 too_largepast 32 MiB,502 upstream_unreachablewhen the upstream fails,404 unknown_proxyfor a wrong token,405 unsupportedforCONNECTor upgrades.
Guarantees
- The key stays on the host. It lives in the VMM's memory only, outside every API answer, log line and snapshot record.
- Echoes are masked. An upstream that reflects the key, raw or encoded, delivers it to the guest masked, in headers and bodies alike, and upstream error text is masked too.
- Calls stay on the upstream. Path tricks, smuggled framing and tunnels are refused before anything is sent.
- Caps hold under concurrency. Each call is reserved before it goes out.
- Every call is accounted. Method, path, status, bytes and duration per call, with running totals per proxy.
See secret proxies and GPU pools.