I drew the same network diagram several times before I admitted that I did not like what it was saying. Each version looked normal, which was part of the problem.
Each version began with a normal virtual NIC inside the guest. From there came the familiar machinery: an address, a route, a TAP device or bridge on the host, DNS, NAT, firewall rules, and a cleanup path for all of it. Linux knew how to use the result, which made the design feel reassuringly ordinary.
But one question kept bothering me: at what exact moment did the workload receive permission to talk to the outside world?
The honest answer was not “when the firewall allowed a packet.” It was earlier, when I attached a routable network device to the guest. The firewall was trying to take back pieces of an authority the runtime had already granted.
For a conventional VM, that can be perfectly reasonable. For a system intended to run untrusted and AI-generated code, I wanted a different default.
So I removed the NIC.
The diagram that changed the design
The ordinary path looks roughly like this:
flowchart LR
W[Workload] --> S[Guest socket]
S --> K[Guest TCP/IP stack]
K --> N[Virtual NIC]
N --> T[Host TAP or bridge]
T --> F[Firewall and NAT]
F --> I[External network]
Nothing in that diagram is inherently unsafe. The problem is where the security decision lives. The guest already has a network stack attached to a network device; policy is applied after that fact.
MVM starts from the other side of the boundary:
flowchart LR
W[Workload] --> A[Narrow guest adapter]
A --> V[Authenticated vsock channel]
V --> G[Host-owned gateway]
G --> P[Policy, limits, secrets, audit]
P --> I[External network]
The workload can ask for a connection. It cannot create one by itself. The host receives the request, binds it to the machine and its signed execution plan, evaluates the destination and current policy, and only then opens the outbound socket.
That sounds like a subtle rearrangement. It changes the ownership model completely.
The host makes the last connection
The load-bearing property is simple: the host originates every external connection.
A workload does not receive a reusable API token and a general route to the internet. It sends a constrained request across a machine-scoped channel. The host can decide that this boot, under this plan, may connect to this destination, using this protocol, for this long, and within this byte budget.
That gives MVM one place to enforce rules that become much harder once authority has crossed into the guest:
- egress is denied unless the plan allows it
- a secret can be bound to one destination and inserted at the final hop
- bytes, active flows, and lifetimes can be capped
- private and reserved ranges can be refused
- the decision can be audited without logging the payload
- a restored or stale session cannot simply reconnect with yesterday’s authority
The workload remains useful. It just does not receive a general-purpose network identity as a side effect of booting.
Keeping normal sockets without giving the NIC back
The first version of this idea was narrow: expose a small set of host services over vsock and require workloads to use those services directly. That is still the strongest shape for code that only needs a few known operations.
It is not enough for everything.
A great deal of software expects ordinary sockets. Rewriting Python libraries, package managers, HTTP libraries, and language runtimes around a custom transport would turn the security boundary into a compatibility project.
MVM therefore has an opt-in L3-over-vsock path. The guest sees a point-to-point TUN interface named mvm0 and can use its normal TCP/IP stack. It still does not receive a virtio NIC, a TAP-backed Ethernet device, a MAC address, a bridge, or an L2 broadcast domain.
flowchart TB
APP[Application] --> SOCK[Normal guest socket]
SOCK --> TCP[Guest TCP/IP stack]
TCP --> TUN[mvm0 point-to-point TUN]
TUN --> FRAME[Bounded IP frame]
FRAME --> VSOCK[vsock control and data channels]
VSOCK --> NETD[Machine-scoped host gateway]
NETD --> POLICY[Admission and flow policy]
POLICY --> OUT[Host networking]
The application keeps the interface it already understands. The host keeps control of the transport that actually reaches the world.
That compromise is what made the design practical.
Removing the NIC did not remove the hard work
It removed the second path around the gateway. Everything else became our responsibility.
The first parser on the host, for example, has to treat the guest as hostile. A length field cannot be allowed to trigger an unbounded allocation. Frames need fixed headers, explicit endianness, hard limits, and rejection before allocation. IPv6 extension headers need a bounded walk. Unknown message types must fail closed.
Backpressure matters just as much. A guest can write faster than the host can connect; an upstream server can return data faster than the guest can read it. DNS requests can accumulate. UDP peers can multiply. A workload does not need to be malicious to exhaust a host.
The permission is not merely “may send.” It is closer to:
Every queue and flow table needs a ceiling because every unbounded dimension quietly widens the grant.
Identity is another place where convenient shortcuts become dangerous. A vsock CID, IP address, port, or Unix socket path is a transport coordinate, not a durable machine identity. All of them can be reused. MVM binds the session to host-owned boot state instead:
VmInstanceIdentity {
node_id,
vm_id,
boot_id,
plan_digest,
}
A network lease belongs to this boot, under this exact admitted plan. Restoring a machine creates a new session. A socket left on disk cannot resurrect an old grant.
DNS has to obey the same model. A domain allow-list is mostly theater if the guest can choose another resolver, connect directly to an IP, follow a CNAME outside policy, or keep using an address after the binding expires. The controlled path normalizes names, evaluates the same egress policy, pins approved answers for a bounded lifetime, and blocks private or reserved destinations unless the plan explicitly permits them.
And then there is teardown—the part that looks like housekeeping until it fails. Routes, listeners, leases, processes, namespaces, and firewall state must disappear after clean shutdown, crashes, partial startup, and cancellation. If a forwarding rule survives the VM that owned it, authority has survived without an owner.
That is a security bug, not a cleanup annoyance.
Some combinations have to be refused
Cross-platform abstractions tend to make backends look more alike than they really are.
If a signed plan requires L3-over-vsock and the guarantee that no routable guest NIC exists, the selected backend has to provide both. A backend that cannot enforce the requested shape should fail admission and name the missing capability.
It should not quietly substitute “something close.”
The same rule applies to encrypted traffic. MVM can authorize a TLS connection by workload identity, destination, port, and plan. It cannot inspect or rewrite arbitrary end-to-end ciphertext unless it deliberately terminates TLS. Secret substitution and content redaction only work where MVM owns a cleartext boundary.
A plan that requires those guarantees cannot choose a transport that makes them impossible. The honest behavior is to refuse the combination before the guest boots.
I have come to prefer a smaller feature whose boundary is real over a larger one whose security story changes underneath the same interface.
What I was really trying to remove
“No NIC” is not a claim that virtual networking is broken. It is not a performance trick either.
Ask where the route begins
Draw the path from an untrusted workload to the public internet. Mark the first point where it receives a route, a socket, a credential, or a reusable channel. That point—not the last firewall rule—is where the authority entered the system.
If you want to narrow the boundary, move that first decision to a place the host can name, authorize, limit, and audit.
I was trying to remove ambient network authority.
The guest does not get a route merely because it exists. Network access becomes an explicit request against a signed contract, and the host can answer concrete questions before opening a connection: Who is asking? Which boot is this? Which plan admitted it? Where is it trying to go? How much may it send? Can this backend enforce the requested guarantee? What evidence should remain afterward?
The price is real. MVM has to own framing, flow control, DNS, identity, policy, and teardown.
After drawing the conventional diagram enough times, I decided that was the right place to spend the complexity: on the trusted side of a narrow boundary, where there is no second route around the answer. For MVM, the safest NIC is no NIC.