vk-sandbox is a flag-driven daemon with no configuration file. Most flags
take their default from an environment variable, so the systemd unit can be
driven entirely from EnvironmentFile; an explicit flag always wins over the
environment.
| Flag | Environment | Default | Description |
|---|---|---|---|
--node-name |
VK_NODE_NAME |
vk-sandboxd |
Virtual node name. Must differ from the physical Kubernetes node and any co-located vk-cocoon node |
--node-ip |
VK_NODE_IP |
none | Node InternalIP advertised to the apiserver. Omitted from node addresses when empty |
--listen-addr |
VK_LISTEN_ADDR |
:10260 |
Kubelet API listen address. Must differ from a co-located vk-cocoon, which uses :10250 |
--tls-cert |
VK_TLS_CERT |
none | Kubelet API TLS certificate |
--tls-key |
VK_TLS_KEY |
none | Kubelet API TLS key |
--node-cpu |
VK_NODE_CPU |
4000 |
Advertised node CPU capacity (a scheduling budget only) |
--node-memory |
VK_NODE_MEMORY |
8Ti |
Advertised node memory capacity |
--node-pods |
VK_NODE_PODS |
2000 |
Advertised node max pods |
--kube-api-qps |
– | 200 |
client-go QPS for the Kubernetes clients (status pushes, delete authorization, inventory publish), and the rate of the pod create, delete and status queues |
--kube-api-burst |
– | 400 |
client-go burst on top of --kube-api-qps, and the burst of the pod queues |
--sandboxd-url |
SANDBOXD_URL |
http://127.0.0.1:7777 |
sandboxd base URL |
--sandboxd-advertise-addr |
SANDBOXD_ADVERTISE_ADDR |
host:port of --sandboxd-url |
host:port published in NodeInventory for claim routing |
--sandboxd-token-file |
SANDBOXD_TOKEN_FILE |
none | File holding the sandboxd node API token (surrounding whitespace trimmed) |
--state-path |
VK_STATE_PATH |
/var/lib/vk-sandbox/claims.json |
Claims table persistence path. Required: the binary refuses to start without one, because a node serving real Pods would leak a microVM per claim on restart |
--orphan-scan-interval |
– | 60s |
Audit-only orphan scan cadence; 0 disables the scan |
--publish-inventory |
– | false |
Server-side-apply this node’s NodeInventory for the L3 aggregation layer, stamped with publishedAt |
--publish-interval |
– | 30s |
NodeInventory publish cadence; keep it below the apiserver’s --inventory-stale-after |
--node-labels |
– | sandbox.cocoonstack.io/runtime=sandboxd |
Comma-separated extra node labels, key=value |
--disable-pod-events |
– | false |
Drop the pod controller’s Normal events (one per pod create, update and delete); warnings still reach the apiserver |
| – | VK_LOG_LEVEL |
info |
Log level: debug, info, warn or error; an unknown level stops startup |
--version |
– | false |
Print the build version and exit |
KUBECONFIG is read only when in-cluster configuration is unavailable: the
binary tries the in-cluster service-account config first and falls back to the
kubeconfig at $KUBECONFIG.
The token file is read once at startup, so rotating the sandboxd token requires a restart.
When publishing inventory for a remote operator, set
--sandboxd-advertise-addr to a reachable node address. The loopback default
for --sandboxd-url is suitable for local claims, not remote claim routing.
--node-cpu / --node-memory / --node-pods are a scheduling budget,
not a measurement: the Pod is a placeholder and sandboxd holds the real
microVM. They still have to be set to something, because a node advertising
zero allocatable has every sandbox Pod rejected by the scheduler. Values are
parsed as Kubernetes quantities, so 4000, 8Ti, 2000 are all valid.
Beyond the labels from --node-labels, the node always advertises:
| Field | Value |
|---|---|
Label type |
virtual-kubelet |
Label kubernetes.io/role |
agent |
Label kubernetes.io/hostname |
--node-name |
| Taint | virtual-kubelet.io/provider=sandboxd:NoSchedule |
| Addresses | InternalIP = --node-ip (when set), then Hostname = --node-name |
DaemonEndpoints.kubeletEndpoint.port |
the port parsed out of --listen-addr |
The taint is what keeps ordinary workloads off the virtual node; the sandbox Pod template carries the matching toleration. See Pod contract.
virtual-kubelet serves the kubelet API over TLS only, so a certificate always exists:
--tls-cert and --tls-key are both set and both point at regular
files, that key pair is loaded, and a pair that fails to load stops startup;--node-name, --node-ip, and 127.0.0.1.Self-signing keeps every node’s API surface uniform without depending on a
co-located vk-cocoon’s certificate; the apiserver connects to kubelet-served
routes with InsecureSkipTLSVerify, and for sandboxes those routes are
stubbed regardless. Client certificates are not requested
(tls.NoClientCert), and the minimum version is TLS 1.2.
--state-path holds the durable claim table – the release credentials that
make this provider the only party able to destroy the VMs it delivered. It is
written compactly (shown expanded here) because every claim rewrites it:
{
"claims": {
"default/my-sandbox": {
"id": "sb_...",
"token": "...",
"address": "10.0.0.5:7777",
"podUID": "6f1c...",
"authority": "9a2e...",
"claimedAt": "2026-07-27T02:15:00Z",
"deadline": "2026-07-28T02:15:00Z"
}
}
}
| Field | Description |
|---|---|
id |
sandboxd claim id |
token |
Release credential; never leaves the node |
address |
sandboxd owner_addr for the claim (host:port); the host becomes the Pod IP |
podUID |
UID of the Pod that holds the claim. After a restart the same Pod adopts its claim again even if its controller owner changed. The stale-request guard reads the in-memory pod table, not this field |
authority |
Recorded when the claim is taken or adopted: the Pod’s controller owner UID, or the Pod’s own UID for a bare Pod. A same-name Pod adopts the claim only if it is the Pod that holds it or has this authority (while owner is set, that owner instead); any other Pod claims fresh and the claim is queued for release. Absent in tables from older builds, where a claim without owner is adopted by any same-name Pod, as before |
claimedAt |
When the claim was taken; reported as the Pod start time, which must not move between reads. A table written before this field existed gets it filled in on the next load |
deadline |
The cached lease end. Once it passes, the provider confirms the node’s state: a listed sandbox refreshes the deadline, confirmed absence publishes Failed, and a failed listing defers the decision. Absent in older tables; the vouching pass backfills it |
owner |
The controller owner recorded when a Pod deletion preserved the claim. The owner re-check reads it until the owner is gone and the sandbox released; a replacement Pod adopting the claim clears it |
A top-level releasing list holds claims withdrawn from their pod key, by the
owner re-check or by a Pod of another owner, but not yet released; each is
retried every tick until sandboxd confirms the sandbox gone, so a release that
fails keeps its credential across restarts.
It is written with a tmp-file + atomic rename at mode 0600, its directory
created at 0700, and reloaded on startup. Concurrent Pod creates serialize
their writes, so the file always reflects the newest table rather than whichever
snapshot happened to land last. Treat it as a secret – it
contains release tokens. A missing file is not an error; a malformed one is
fatal at startup rather than silently discarding live authority, and so is a
path the provider cannot write: starting without durable claims would leak one
microVM per claim on the next restart, so the virtual node stays out of
scheduling until the path is fixed.
The permissions the flags imply are shipped in
manifests/rbac.yaml:
the usual virtual-kubelet node/pod/lease/event access, plus get on the
owner CR for the delete-authorization read, plus write access to
nodeinventories when --publish-inventory is on. See
Installation.