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 onLinux (KVM), macOS (HVF), Windows (WHP)
InterfacesCLI flash run --secret-proxy, flash proxies · MCP flash_secret_proxy_calls, flash_cli · REST /v1/flashvm/secret-proxies
Guest endpointhttp://<host address>:7730/<token>/…: the NIC's gateway on Linux, .254 of the user-mode NAT segment on macOS and Windows
URL token32 lowercase hex digits (128 unpredictable bits), chosen by the VMM
Credential headerauthorization: Bearer <key> by default; any other header carries the key as it is
LimitsUp to 16 proxies per VM · request bodies up to 32 MiB · 64 concurrent connections · newest 1,024 calls logged per proxy
Capsmax_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

  1. 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.
  2. The relay finds the proxy by the token in the path, then checks the request: one framing (content-length or chunked, never both), no CONNECT or upgrade, no . or .. segments, and a joined URL that stays on the upstream's scheme, host and port.
  3. The call is reserved against max_calls and max_bytes before it is sent, so concurrent calls stay within the cap. Past a cap, the guest receives 429 cap_reached.
  4. Hop-by-hop headers and any copy of the credential header the guest sent are stripped, the key is set, and accept-encoding: identity is sent so the response arrives uncompressed for the redactor.
  5. 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.
  6. 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 PATCH of anything but the caps, returns 422 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_reached past a cap, 413 too_large past 32 MiB, 502 upstream_unreachable when the upstream fails, 404 unknown_proxy for a wrong token, 405 unsupported for CONNECT or 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.

What it does

Related