nova-runtime: unified ingress and egress control

· Architecture team

Summary

In nova-runtime, every byte into or out of a NovaScale workload passes through one trusted supervisor per node, enforced from a single set of rules in policyDB. Workload pods run as Kata microVMs with no network device. The supervisor owns their VPC IPs, so each workload is still reachable at a unique IP in the VPC, exactly as today.

The design borrows egress mediation from NVIDIA OpenShell and identity-checked mTLS ingress from Ghostunnel.

Plan of record: nova-runtime runs on Kata. Each pod is a Kata microVM with no network device, and it reaches the supervisor only over vsock. runsc is retired once Kata + nova-runtime passes the evidence under Isolation layer: Kata; plain runc is not used for nova-runtime workloads.

Goals

Non-goals for v1

Why nova-runtime, and not OpenShell or Ghostunnel as they are

Neither tool covers NovaScale on its own. OpenShell confines what a sandboxed agent can reach on the way out; Ghostunnel checks who may connect on the way in. NovaScale needs both directions, for every workload, tied to its own placement, IPs, identities and network. Today it has internet egress open apart from IMDS and RFC1918, secrets travelling in specs, and the per-pod front built but not deployed.

CapabilityOpenShellGhostunnelnova-runtime
Ingress controlNot its focusYes: mTLS, cert-field rules or OPA, once per connectionYes: mTLS, TLS or TCP modes, per request on HTTP and gRPC
Egress controlYes: per binary, host and HTTP requestStatic: one fixed target per client listenerYes: any destination policy allows, per request
One policy for both directionsNo: egress YAML per sandboxNo: flags or OPA per processYes: one policyDB rule compiled into the caller's egress and the callee's ingress
Two independent checks per callNoNoYes: caller's node and callee's node decide separately
Workload identityExecutable path and hash inside the sandboxCertificate of whoever runs itSPIFFE ID per workload; keys held by the supervisor, never by the pod
Unique VPC IP per workloadNo: its own network fenceNoYes: supervisor owns the IP from novalet's IPAM; security groups and flow logs unchanged
Fits NovaScale placement and OVSNo: its own gateway and driversNoYes: execution-controller, novalet, OVS bridges, VPC local DNS
One supervisor for many podsNo: one per sandboxNo: one process per listenerYes: per node, with per-pod identity, credentials, policy and limits
Credentials kept out of workloadsYes: placeholders at approved endpointsNoYes, plus no secret values in specs
Per-app approved syscall profilesNo: fixed process rulesNoYes: signed per image digest, notify tier, placement gate
Isolation layerContainer or microVM driversNone: it is a proxyKata microVM for every workload, no NIC, vsock only
Policy rollout across the fleetGateway per deploymentFiles or OPA per processSigned bundle per node from policy-controller, with a publish gate

What nova-runtime reuses. From OpenShell: the supervisor and sandbox split, egress mediation, credential placeholders, and the rule-change gate. From Ghostunnel: mTLS termination, cert-field and OPA authorization, SPIFFE support and self-sandboxing, carried forward in the existing nova.patch fork.

Architecture

architecture · control plane, node agents, supervisor, workload pods, OVS
architecture · control plane, node agents, supervisor, workload pods, OVS

The supervisor holds every workload IP, key and credential on the node. Each pod is a Kata microVM with no network device that reaches the supervisor only over vsock, and OVS sends each workload /32 to the supervisor's single port.

Components

The supervisor is the only trusted, network-facing part of a workload's path; everything inside the workload pod is assumed hostile. The supervisor grows out of today's per-pod Ghostunnel identity front (novalet/internal/front plus ghostunnel/nova.patch).

ComponentRuns whereTrustedJobBuilt from
Supervisor podOne per node (two instances for HA), own host netns, one port on the workload bridgeYesOwns every workload IP on the node; mTLS ingress; egress mediation; OPA decisions; holds SVIDs and credentialsGhostunnel fork: today's nova.patch plus HTTP mode, connect mode, multi-endpoint, vsock
Workload podKata microVM per placement: no virtio-net, guest netns with lo onlyNoRuns the app as non-root with no capabilitiesKata, with pod networking turned off
VMMHost process per pod (QEMU or Cloud Hypervisor), own netns with only loYesRuns the microVM; seccomp-filteredKata's VMM
nova-relayInside each guest, unprivilegedNoBridges 127.0.0.1 ports to vsock; serves the HTTP proxy for egressNew, small
vsock channelvhost-vsock between guest and hostn/aEgress: the guest dials the supervisor's vsock port. Ingress: the supervisor dials the guest's ports. The host-set peer CID is the pod's identityKata already uses vsock for kata-agent
OVS VPC bridgeNodeYesUplink; delegated subnet to and from the patch portExisting
OVS workload bridgeNodeYesPer-workload /32 to the supervisor port; anti-spoofing on that portExisting bridge, new flows
OPA agentNode, local socketYesEvaluates ingress and egress decisions from the signed bundleExisting (long-poll bundle)
Syscall supervisorNode, host sideYesDecides notified syscalls from each app's approved profileNew
policy-controllerControl planeYesCompiles policyDB rules into one signed bundle per node; signs syscall profilesExisting, new compiler stage
SPIRE agentNodeYesIssues per-pod SVIDs to the supervisor through the Delegated Identity APIExisting SPIFFE IDs, new delegation

SPIFFE IDs keep today's shape, spiffe://…/t/<tenant>/w/<workload>.

Addressing and data path

IPAM and VPC routing do not change; only the last hop on the node does. A workload's IP still comes from the node's delegated subnet, but OVS sends it to the supervisor port instead of a pod port, and the supervisor owns the address.

VPC bridge. The delegated subnet goes in through the patch port; only the subnet comes back out. Internet SNAT, security groups, IMDS and RFC1918 blocking stay where they are today.

priority=100,in_port=uplink,ip,nw_dst=10.64.3.0/26   actions=output:patch-wl
priority=100,in_port=patch-wl,ip,nw_src=10.64.3.0/26 actions=output:uplink
priority=0,in_port=patch-wl                          actions=drop

Workload bridge. novalet adds two flows per endpoint at READY and removes them at drain. Anti-spoofing moves from "each pod port sources its own IP" to "the supervisor port sources only IPs it has registered".

priority=200,in_port=patch-vpc,ip,nw_dst=10.64.3.21 actions=mod_dl_dst:<sup-mac>,output:sup
priority=200,in_port=sup,ip,nw_src=10.64.3.21       actions=output:patch-vpc
priority=10,in_port=sup                             actions=drop

Per-/32 flows are kept even though one supervisor fronts the whole subnet: traffic to an IP that is not READY dies in OVS, and each workload gets its own packet and byte counters. An ARP responder answers the gateway IP that the supervisor netns routes through, as for pods today.

Supervisor netns. A local route makes the workload IP bindable without per-pod interface config (the AnyIP trick). It is added at READY and removed at drain, so the kernel never answers for a workload that is not running.

ip route add local 10.64.3.21/32 dev lo table local

Guest netns. Inside the Kata VM, the same IP goes on the guest's lo before the app starts, so apps that bind to or advertise their own IP (Kafka advertised.listeners, Cassandra broadcast_address) keep working. It is also passed as NOVA_WORKLOAD_IP. No NIC sits behind it: the VM has no virtio-net device.

ip addr add 10.64.3.21/32 dev lo   # inside the guest, before the app starts

Ingress listeners. One listener per exposed (IP, port), not a wildcard. The kernel resets ports that policy does not expose, and the local address of an accepted connection selects the endpoint.

ln, err := net.Listen("tcp", net.JoinHostPort(ep.WorkloadIP, strconv.Itoa(p.Port)))
// accept: conn.LocalAddr() == 10.64.3.21:443 → endpoint → guest vsock port 8080

Egress source address. The supervisor dials from the caller's workload IP, so security groups, firewall allowlists, NAT and flow logs see what they see today. Return traffic is addressed to that IP and comes back to the supervisor.

d := net.Dialer{LocalAddr: &net.TCPAddr{IP: net.ParseIP(ep.WorkloadIP)}}
conn, err := d.DialContext(ctx, "tcp", dest) // only after the egress decision allows it

Calls inside one node. If the destination IP belongs to a workload on the same node, it is local in the supervisor netns: the dial loops back to the supervisor's own listener, both checks still run, and OVS is not involved.

If the bridges run on the OVS-DPDK datapath, the supervisor's kernel-facing port should be a virtio-user or AF_XDP port; tap and internal ports are a slow path there.

Policy model

A rule is written once in policyDB and compiled twice: into egress for the caller and ingress for the callee. The two sides of a call cannot drift apart because they come from the same row.

Rules in policyDB. Today's label-selector policies gain direction-free from / to, an optional L7 block, and an optional credential grant.

- id: x-to-a
  tenant: t
  from: { workload: x }                     # or a label selector
  to:   { workload: service-a, port: https }
  http: { methods: [GET, POST], paths: ["/v1/orders/**"] }
- id: a-to-stripe
  tenant: t
  from: { workload: service-a }
  to:   { external: api.stripe.com, port: 443 }
  http: { methods: [POST], paths: ["/v1/charges"] }
  credential: stripe-live
- id: partner-to-a
  tenant: t
  from: { external_ca: partner-ca, uri: "spiffe://partner/*" }
  to:   { workload: service-a, port: https }
- id: vpc-lb-to-a
  tenant: t
  from: { cidr: 10.64.0.0/16 }
  to:   { workload: service-a, port: health }

Ports belong to the workload. Each exposed port declares how the supervisor terminates it.

ModeTerminationDecided onTypical callers
mtlsmTLS, client cert requiredPeer SPIFFE ID or cert SANs, plus L7Nova workloads, partners with certs
tlsServer cert onlySource CIDR or JWT, plus L7VPC services, load balancers
tcpNone, byte copySource CIDR and portDatabases, legacy protocols

A separate protocol: http | grpc | tcp field turns L7 checks on. gRPC gets per-RPC rules for free, because the method is the :path.

What each rule compiles to.

Rule shapeEgress entryIngress entry
workload → workloadCaller's node bundleCallee's node bundle
workload → external hostCaller's node bundleNone
external CA, CIDR or JWT → workloadNoneCallee's node bundle

The bundle is keyed by workload. Today's allowed_callers[callee] = [caller] in authz.rego grows into rule objects {peer, methods, path_prefixes}, plus a new egress[caller] set.

OPA input. One Rego package, two input shapes:

{"direction":"ingress","target":"service-a","port":"https","peer":"spiffe://…/t/t/w/x","src_ip":"10.64.3.21","method":"GET","path":"/v1/orders/42"}
{"direction":"egress","source":"x","dest_host":"service-a.t.nova","dest_port":443,"method":"GET","path":"/v1/orders/42"}

When decisions run.

Change gate. Before publishing, policy-controller compares the new bundle with the live one. Any of these holds the publish for human approval:

This is a simple diff, not formal proof; OpenShell's prover is the model if it needs to go further.

Identity and credentials

Keys and secrets live only in the supervisor. A compromised workload has no key to steal and no socket except its own to ask through.

SVIDs. Today novalet waits for each pod's SVID (awaitSVID) so the per-pod front can present it. In the new model the supervisor fetches an SVID per pod through SPIRE's Delegated Identity API, using the selectors novalet registers for that pod. The same SVID is:

The workload pod receives no SVID and no key material.

Credentials. Secrets stop travelling as plaintext env values in container_specs.spec over PodAssignments. A spec names a credential (stripe-live); the supervisor fetches it from the secret store, scoped to that workload, and injects it only on egress rules that carry credential:. The app holds a placeholder.

Injection needs the supervisor to see the HTTP request. Two ways, per destination:

  1. Plaintext to the supervisor (default). The app calls http://api.stripe.com through nova-relay; the supervisor originates TLS. No CA in the pod.

  2. Interception. For apps that insist on https://, the supervisor terminates TLS with a per-pod CA trusted inside the pod. More compatible, but the supervisor then holds a CA key per pod.

Isolation between pods sharing a supervisor. Nodes are single tenant, so one supervisor per node is within the tenant boundary. Pods still get least privilege:

Dedicated supervisors. A workload with isolation: dedicated in its spec gets its own supervisor instance. Use it for untrusted agent code, where hostile traffic from inside the pod targets the supervisor's parsers, and a shared supervisor would expose other pods' credentials.

Syscall control and per-app approval

Every app runs under its own approved syscall profile. seccomp-BPF allows common syscalls in the kernel, sends a small sensitive set to a per-node syscall supervisor through seccomp user notification, and denies the rest. A profile is bound to a workload and its image digest, and execution-controller will not place an app without an approved one.

syscall control · approval pipeline and runtime tiers
syscall control · approval pipeline and runtime tiers

Three tiers per profile

TierDecided byExamplesChange without restart
Allowseccomp-BPF in the kernelread, write, futex, epoll_*, socket for AF_UNIX / AF_INET / AF_INET6No: a filter cannot be loosened once installed
NotifySyscall supervisor, through user notificationexecve / execveat checked against the app's binary list and hashes; rules expected to changeYes
Denyseccomp-BPF returns EPERM and logsmount, bpf, ptrace, keyctl, kexec_load, init_module, io_uring_setup, userfaultfd, perf_event_open; clone with namespace flags; clone3 returns ENOSYS so libc falls back to cloneNo

What notify can and cannot prove. A notify reply of "continue" lets the kernel re-read pointer arguments, so a decision based on a path or buffer can race with another thread. Hard limits therefore stay where no race exists: register arguments in BPF, paths in Landlock (including execute rights), and the network fence. Notify adds per-app exec checks with hashes, audit, and rules that can change without a restart.

How the notify channel is wired. In a Kata guest, kata-agent installs the app's seccomp filter in the guest kernel. A small guest shim holds the notify listener and forwards each notification over vsock to the host syscall supervisor, which decides from the app's signed profile and returns allow or EPERM. The shim reads arguments from the guest's /proc/<pid>/mem and checks SECCOMP_IOCTL_NOTIF_ID_VALID before replying. If kata-agent does not accept an OCI listenerPath, the shim installs the notify filter itself, as the app's parent, before exec.

Where it runs. Decisions run on the host, outside the VM, so a compromised guest cannot change the rules; only the forwarding shim runs in the guest. If the shim or the channel fails, notified syscalls fail with ENOSYS, which is fail-closed. A guest-kernel exploit defeats in-guest seccomp, but the VM boundary still holds.

Approval workflow

  1. Record: run the app in staging with every non-baseline syscall sent to notify, allowed and logged (or use the Kubernetes Security Profiles Operator recorder).

  2. Propose: the recorder emits a profile: the allow set, notify rules (binaries and hashes), deny by default.

  3. Review: policy-controller diffs it against the baseline and the last approved profile. Syscalls on the risk list need a security approver; everything else needs the app owner.

  4. Sign: the approved profile is stored in policyDB, keyed by workload and image digest, and signed by policy-controller.

  5. Enforce: execution-controller refuses placement without an approved profile for that digest; novalet verifies the signature before building the OCI spec.

  6. Learn from denials: each denial is logged with the syscall and arguments and can become a proposed change that goes back to step 3. Production never holds a syscall waiting for a human; dev environments may, with a timeout.

Changes this adds

ComponentChange
policyDBSyscall profiles keyed by workload and image digest, with approval state, approver and risk flags
policy-controllerProfile diff and risk flags, approval API, signing
execution-controllerPlacement requires an approved profile for the image digest; ToProto carries the profile reference
novaletFetches and verifies the profile; writes linux.seccomp with listenerPath into the OCI spec; runs the syscall supervisor and holds listener fds
Syscall supervisor (new)Per node; answers notifications from each app's profile; audit events and metrics

Request flows

Each call between Nova workloads is decided twice, by two supervisors that do not trust each other. Y → B follows the same path with its own vsock CID, IPs and SVIDs, so the two flows never share a decision.

request flow · X on node 1 calls service A on node 2
request flow · X on node 1 calls service A on node 2

On node 2, the local IP the connection arrived on selects the backend, and the peer SVID selects the rule.

Variants

How an app reaches its egress socket

In both modes the app speaks plaintext to nova-relay and the supervisor adds mTLS.

Lifecycle

novalet is the only writer of the supervisor's endpoint table; the supervisor never learns anything from a pod. A pod counts as running only after the supervisor has proven it can reach it and holds its SVID.

Placement to READY

  1. execution-controller sends the PodAssignment with the workload IP, ports, port modes, policy reference and syscall profile reference.

  2. novalet assigns the VM a vsock CID and creates the VMM's netns with only lo.

  3. novalet upserts the endpoint as PENDING with the CID and ports. The supervisor requests the delegated SVID, opens listeners on the workload IP, and starts accepting egress from that CID.

  4. novalet starts the Kata VM with no virtio-net and the nova-runtime guest image. The workload IP goes on the guest's lo; nova-relay listens on the guest's vsock ingress ports and forwards to the app's 127.0.0.1 port.

  5. The supervisor health-checks the app over vsock.

  6. novalet adds the supervisor-netns local route and the two OVS flows, then marks the endpoint READY.

  7. novalet reports the pod running; execution-controller moves the placement to RUNNING.

Drain and delete

  1. novalet marks the endpoint DRAINING and removes the OVS ingress flow, so no new connections arrive.

  2. The supervisor stops accepting, lets open connections finish up to a drain timeout, then closes them.

  3. novalet removes the local route, the anti-spoofing flow, the endpoint, and stops the VM.

  4. The IP returns to the pool only after a cooldown, so retries meant for the old workload cannot land on the next one.

Generations. Each endpoint carries a generation that bumps on every pod restart. The supervisor rejects upserts with an older generation and closes connections tied to one.

Endpoint stream. Same pattern as the node/v1 Sync stream: a full snapshot on connect, then deltas. A restarted supervisor rebuilds everything from novalet and the on-disk bundle; it keeps no state of its own.

service SupervisorControl {
  rpc SyncEndpoints(stream SupervisorAck) returns (stream EndpointUpdate);
}
message EndpointUpdate {
  oneof op { Snapshot snapshot = 1; Endpoint upsert = 2; EndpointKey remove = 3; }
}
message Endpoint {
  string workload_id = 1;   // policyDB subject
  string spiffe_id   = 2;
  string pod_uid     = 3;
  uint64 generation  = 4;
  string workload_ip = 5;   // from the node's delegated subnet
  uint32 vsock_cid   = 6;   // guest CID, set by novalet
  repeated Port ports = 7;
  string isolation   = 8;   // shared | dedicated
  State  state       = 9;   // PENDING | READY | DRAINING
}
message Port {
  string name = 1; uint32 port = 2; uint32 target_port = 3;
  enum Mode { MTLS = 0; TLS = 1; TCP = 2; } Mode mode = 4;
  enum Proto { RAW = 0; HTTP = 1; GRPC = 2; } Proto protocol = 5;
}

Supervisor availability. Two instances bind the same addresses with SO_REUSEPORT; the kernel spreads new connections between them. A crash drops only the failed instance's connections, and upgrades start the new version, drain the old one, then stop it. Each instance runs non-root with only CAP_NET_BIND_SERVICE, under Landlock and seccomp.

execution-controller changes

execution-controller keeps owning placement; it learns to describe a supervised workload and refuses to send one to a node that cannot enforce it. File and function names come from an earlier read of nvscale and should be checked against the current tree.

WhereChangeWhy
internal/placement · ContainerSpecAccept runtime_class: nova-runtime (Kata underneath); add ports[] (name, port, target_port, mode, protocol), isolation, credentials[] (names only) and a syscall profile reference to the jsonb specDescribes what the supervisor exposes and how; existing specs parse as before
internal/placement · ParseSpecValidate ports (unique, target_port set, valid mode); refuse secret values in env; refuse identity_front with nova-runtimeKata is the isolation layer of record; the front is superseded
internal/placement · ToProtoEmit a NovaRuntime block on the PodAssignment (ports, isolation, credential names, profile reference) as new core.proto fieldsPorts and modes are structured; a label like IdentityFrontLabel is fine only for a prototype
internal/server · SyncAdd CapNovaRuntime = "nova.io/cap.nova-runtime". sendAssignments fails closed: a nova-runtime placement for a node without the cap goes FAILEDAn older novalet would start the pod with a normal uplink and no policy
Placement filterOnly consider nodes advertising CapNovaRuntime; count each pod's VM memory overhead when fittingEvery Kata pod carries a guest kernel and VMM
Status mappingEndpoint PENDING → CREATING, READY → RUNNING, DRAINING → TERMINATING; failure reasons: VM boot failed, SVID timeout, health check failed, supervisor unavailableA pod that is up but unreachable must not count as running
Service discoveryNo change: supervisors resolve destinations through VPC local DNS, so workload names must keep resolving to workload IPsThe pod has no DNS of its own
pgstoreNo schema change: the new fields live in the container_specs jsonbKeeps the migration small

Syscall profiles add one more check here: placement requires an approved profile for the image digest (see Syscall control and per-app approval).

novalet changes

novalet does most of the new work: it runs the node supervisor, keeps its endpoint table, and builds supervised pods without an uplink. Most of it follows patterns novalet already has for the identity front.

WhereChangeReuses
source/podcontrollerAdvertise CapNovaRuntime in the NodeRegister labels only when Kata, the supervisor and the syscall supervisor are installed and healthyThe two existing caps
New internal/supervisorRuns the node supervisor (two instances, plus one per isolation: dedicated pod) and the syscall supervisor; serves SupervisorControl.SyncEndpoints on a Unix socket; adopts running instances after a novalet restartinternal/front: units that survive novalet restarts, stopOrphanFronts / Adopt
Workload IPAMReturn a released IP to the delegated-subnet pool only after drain completes and a cooldown passesnovalet's existing allocator
manager.startPodInnerFor nova-runtime pods: assign a vsock CID; create the VMM's netns with only lo (skip the novacni Add); upsert PENDING; start the Kata sandbox; wait for the supervisor's health check; then program flows and mark READYThe awaitSVID gate, now waiting on the supervisor
manager.teardownPodDrain sequence: DRAINING, remove the ingress flow, wait, remove route, anti-spoof flow and endpoint, stop the VM, then release the IPExisting teardown ordering
runtime.RunSandboxKata config per pod: pod networking off (no virtio-net), vsock on, the nova-runtime guest image (nova-relay, syscall shim), a guest kernel with Landlock; pass the signed seccomp profile to kata-agent; set HTTP_PROXY, HTTPS_PROXY, NOVA_WORKLOAD_IP; write /etc/hosts entries for fixed dependenciesnovalet's existing microVM launch path
networkPer-/32 ingress and anti-spoof flows on the supervisor port; local routes in the supervisor netns; no pod portExisting OVS flow programming
Supervisor pod networkIts netns gets one port on the workload bridge (virtio-user or AF_XDP on OVS-DPDK), a default route via the bridge gateway, an ARP responder, and VPC local DNS as its resolverToday's pod port setup, once per node
SPIRE registrationKeep registering per-pod entries; authorize the supervisor as a Delegated Identity API callerExisting SVID issuance
Front retirementStop creating per-pod nova-front units for nova-runtime pods; keep the code path for other pods during migrationinternal/front stays until the last workload moves

The per-pod front's job (mTLS ingress plus the OPA check at the handshake) moves into the supervisor unchanged, so nothing is lost while the egress side is built.

Syscall profiles add the guest seccomp wiring through kata-agent, the guest shim, and the per-node syscall supervisor (see Syscall control and per-app approval).

Other changes

Seven more components change; the Ghostunnel fork is the largest.

ComponentChange
policyDBRule schema gains from / to with workload, label selector, external host, cidr, external_ca; optional http block; credential; audit flag. Syscall profiles keyed by workload and image digest
policy-controllerNew compiler stage emits ingress and egress entries per rule; bundle keyed by workload; authz.rego rewritten for both directions and L7; change gate before publish; profile review and signing; opa-system-authz.rego still limits the OPA API to the decision queries
Ghostunnel fork (nova.patch)Many endpoints in one process from the SyncEndpoints stream; listeners per workload IP and port; HTTP mode with a per-request OPA call (httputil.ReverseProxy); connect mode (HTTP CONNECT and SOCKS5) on each pod's vsock port; vsock dialer for ingress; egress dialer with the caller's source IP and SVID; SVIDs from the Delegated Identity API; caller identity to the app via PROXY v2 TLVs or headers; per-endpoint limits; one audit line per decision
novacni / vpg-controllernova-runtime mode: no pod port, since the VM has no NIC; security groups move from per-pod-port matches to matches on the workload IP at the supervisor port, so OVS stays the L3/L4 backstop
KataPod networking off (no virtio-net); vsock for nova-relay and the syscall shim; guest kernel with Landlock and seccomp; VMM seccomp-filtered in an lo-only netns
SPIREEnable the Delegated Identity API and authorize the supervisor's identity
Secret storeCredentials scoped per workload and readable only by the node supervisor

The Envoy + ADS design in ewcarch.html stays the alternative if retries, header routing or rate limiting become requirements; the Ghostunnel fork is chosen for footprint and because Nova already runs it.

Security analysis

The workload is treated as hostile, and so is every other node's supervisor. The largest remaining risk is a compromise of the shared supervisor itself, which exposes the credentials of every pod on that node (one tenant, one node).

ThreatMitigationLeft over
Workload connects out directlyNo virtio-net; only vsock to the supervisorNone while the VM has no NIC
Workload impersonates another podIdentity is the vsock peer CID, which the host setsNone
Workload spoofs a source IPNo NIC; OVS anti-spoofing on the supervisor portNone
Guest-kernel exploitContained by the VM; the VMM is seccomp-filtered in an lo-only netnsA VMM or KVM bug
Unapproved syscallsPer-app signed profile, deny by defaultNotify decisions on pointer arguments are advisory
Workload attacks the supervisor's TLS or HTTP parsingSupervisor is non-root with only CAP_NET_BIND_SERVICE, under Landlock and seccomp; isolation: dedicated for untrusted codeA shared-supervisor compromise exposes that node's credentials
Caller's node supervisor is compromisedCallee's supervisor verifies the peer SVID and runs its own ingress checkCalls the callee already allows that caller
SVID or credential theft from a podKeys and secrets never enter pods; SVIDs are short-livedNone
Exfiltration over DNSPods have no DNS; the supervisor resolves only for allowed destinationsData in allowed requests
Tampered policyBundles and profiles are signed and verified; changes go through the publish gateA bad rule approved by a human
OPA unavailableFail closed; the agent serves the last good bundle from diskL7 ports deny new requests until OPA recovers
Stale IP reuseRelease cooldown and generation checksNone
Cloud metadata (IMDS) accessStill blocked in OVS; policy never allows link-local destinationsNone
Secrets in specsSpecs carry credential names; values stay in the secret storeNone

Isolation layer: Kata (plan of record)

Kata is the isolation layer of record. Each pod gets its own guest kernel behind the hypervisor, so a kernel exploit in the app stays inside its VM, and nova-runtime adds the network, identity, credential and syscall controls on top. runsc is retired after the evidence below; runc is not used for nova-runtime workloads.

Threatrunc + nova-runtimerunsc todayKata todayrunsc + nova-runtimeKata + nova-runtime (plan of record)
Kernel exploit, escape to hostPartial: syscalls reach the host kernelStrong: user-space kernel, few host syscallsStrongest: own guest kernel behind the hypervisorStrongStrongest
Data leaves over the networkStrong: no NIC, policy onlyWeak: internet egress open apart from IMDS and RFC1918WeakStrongStrong
Lateral movementStrong: two checks per call, L7Partial: front + OPA at the handshake (built, not deployed)PartialStrongStrong
Credential theftStrong: never in the podWeak: env values in plaintext specsWeakStrongStrong
Per-app syscall approvalStrongPartialWeakPartialStrong inside the guest, VM boundary behind it
New code to trustSupervisorsNone addedNone addedSupervisorsSupervisors and the syscall shim
CostNativeSyscall overheadVM memory and boot time per podSyscall overheadVM memory and boot time per pod

Kata + nova-runtime is the only column with no Weak or Partial rating on a security threat; its price is the VM cost per pod and the new supervisor code.

Hardening around the VM boundary

Evidence to collect

  1. VM boundary: run public guest-to-host escape proofs of concept for the chosen hypervisor, and confirm the VMM's seccomp filter blocks host syscalls it does not need.

  2. Kernel exploit containment: run public container-escape proofs of concept (Dirty Pipe, CVE-2022-0847, as one example) inside the guest. They may take over the guest but must not reach the host or other pods.

  3. Exfiltration from a hostile pod: raw sockets, DNS, ICMP, IPv6, link-local and IMDS, and smuggling through allowed hosts.

  4. Lateral movement: try every other workload without a rule; both checks must deny.

  5. Secret exposure: search pod env, files and guest memory for credentials and keys.

  6. Supervisor and shim attack: fuzzing and a red-team pass on the TLS, HTTP, CONNECT and vsock paths.

  7. Cost: boot time and memory per pod, and throughput over vsock compared with today's pod ports, for today's workloads.

Retiring runsc. runsc is retired when Kata + nova-runtime contains every exploit in test 2, passes tests 3 to 5 with no findings, has no critical finding in tests 1 and 6, and its cost in test 7 is accepted. Until then, existing runsc workloads stay on runsc.

Compatibility: storage and operations

From the app's side, almost everything keeps working at the same paths. Only two things break outright: UDP, which is denied in v1, and apps that need raw sockets or network privileges in the pod (multicast, ICMP beyond ping, `NET_ADMIN`). In-pod network mounts, iptables-rewriting mesh sidecars and ping keep working once moved: the mount to the host, the sidecar's job and ping to the supervisor. GPUs need guest-image work. This describes Kata's general behavior; novalet's volume code still has to be checked against it.

AreaThingOn Kata + nova-runtimeStatus
StorageConfigMaps, Secrets, emptyDirShared into the guest over virtio-fs at the same paths; check live ConfigMap updates on our Kata versionAs-is
StorageBlock volumes, local disksAttached to the VM as virtio-blk and mounted in the guest at the same pathAs-is
StorageNetwork storage mounted on the host (CSI or novalet)Shared into the guest over virtio-fs; storage traffic leaves from the host, outside the supervisorAs-is
StorageNetwork storage mounted inside the pod today (FUSE such as JuiceFS, NFS client)Works once the mount moves to the host (CSI or novalet): the app keeps the same path, and the pod no longer needs /dev/fuse or CAP_SYS_ADMINChanges
StoragehostPathShared over virtio-fs; refused for untrusted appsRestrict
OperationsLogskata-agent streams stdout and stderr to the runtime shim; same log files as todayAs-is
OperationsExec, attach, exec probeskata-agent runs them inside the guestAs-is
OperationsSidecars in the same podSame VM, shared loopbackAs-is
OperationsCPU and memory metricsVM cgroup on the host plus per-container stats from kata-agent; VM overhead becomes visibleAs-is
OperationsHTTP and TCP probesThe pod IP belongs to the supervisor, so novalet probes through itChanges
OperationsPort-forward and network debuggingNo NIC in the VM; debug through the supervisor or vsockChanges
OperationsApp-side DNS lookupsNo resolver in the pod; proxy mode resolves in the supervisor, raw-TCP apps use the generated /etc/hosts entriesChanges
NetworkingService-mesh sidecars that rewrite iptablesNot needed: the supervisor already does mTLS, identity and L7 policy, so the sidecar is droppedChanges
NetworkingICMP echo (ping)Sent by the supervisor on the pod's behalf, with an ICMP egress rule; see ICMP through the supervisorChanges
DevicesGPUs (Dynamo, vLLM)VFIO passthrough into the VM and an NVIDIA driver in the guest imageNeeds work
ProtocolsUDPDenied in v1Breaks
NetworkingRaw sockets, multicast, other ICMP types, NET_ADMINNo NIC and no network capabilities in the podBreaks

How a host-side mount reaches the container

storage path · host mount to container path in a Kata VM
storage path · host mount to container path in a Kata VM

What to test with our storage

Policy gap. Host-side storage traffic never passes the supervisor, so OVS security groups are its only control unless policyDB gains storage rules.

ICMP through the supervisor

Ping works without giving the pod a NIC: the supervisor sends echo requests on the pod's behalf, the same way it handles TCP egress.

ICMP path · ping from a Kata pod through the supervisor
ICMP path · ping from a Kata pod through the supervisor

The trusted init sets this up in the guest before the app starts; the app itself stays unprivileged.

sysctl -w net.ipv4.ping_group_range="0 2147483647"     # ping without root
ip tuntap add tun0 mode tun user nova-relay && ip link set tun0 up
ip rule add ipproto icmp lookup 100 && ip route add default dev tun0 table 100

The rule is a new protocol: icmp shape in policyDB, compiled to egress only:

- id: x-ping-gateways
  from: { workload: x }
  to:   { cidr: 10.64.0.0/16, protocol: icmp }
  icmp: { types: [echo], rate_per_s: 5 }

Known limitations

Three limitations matter most. UDP is the largest functional gap, because it rules out QUIC / HTTP/3 and app-side DNS over UDP. The shared supervisor is the largest security residual: one compromise exposes every pod's credentials on that node. The unverified assumptions are the largest schedule risk, because any of them can change the design.

KindShortcomingWhat it hitsMitigation or next step
Still breaksUDP denied in v1QUIC / HTTP/3, app-side DNS over UDP, statsd and syslog over UDP, mediaA per-flow UDP relay in the supervisor, like the ICMP path
Still breaksRaw sockets, multicast, traceroute, NET_ADMINtcpdump in the pod, discovery protocols, network toolsCapture and tracing from the supervisor side instead
Still breaksUndeclared inbound portsApps that open listeners at runtimeEvery port declared in the spec; dynamic ports stay unsupported
Still breaksApps doing their own TLS to external hostsNo L7 rules or credential injection on those calls, only host and SNIPlaintext to nova-relay, or per-pod CA interception
CostA VM per podMemory and boot time per pod; lower densityMeasure in phase 0; size the placement filter for VM overhead
CostExtra hopsvsock plus TLS termination on both nodes add latency and supervisor CPUBudget from phase 0 numbers; kTLS on the plaintext leg
Costvirtio-fs storageSlower small-file and metadata-heavy I/OKata caching and DAX; block volumes for heavy I/O
CostGPUsVFIO passthrough and an NVIDIA driver in the guest imageDedicated GPU node pools and a GPU guest image
Security residualShared supervisor per nodeOne compromise exposes every pod's credentials on that nodeSandboxing and fuzzing; isolation: dedicated for untrusted code
Security residualHost-side storage trafficBypasses supervisor policy; only OVS security groups applyStorage rules in policyDB (open question)
Security residualData inside allowed callsExfiltration to allowed hosts; slow timing channelsNarrow L7 rules, rate limits, audit logs
Security residualNotify decisions on pointer argumentsPath or buffer checks can raceHard limits stay in BPF, Landlock and the network fence
OperationalNew code to ownGhostunnel fork, nova-relay, syscall shim, endpoint tableUpstream the generic parts; fuzz the parsers
OperationalHarder debuggingNo port-forward or tcpdump inside the podSupervisor-side debug and replay tooling
OperationalMigration and approval frictionEvery workload needs declared ports, credential names and a profile approved per image digestRecord mode in staging; auto-approve profile changes that add no risk-list syscalls
OperationalUnverified assumptionskata-agent notify hook, networking-off launch path, JuiceFS host mounts, current nvscale codeSpike these before phase 0

Operations

Every decision is logged with who, what and why, so a denied call can be explained from one log line. Performance targets are set from phase 0 measurements, not guessed here.

Observability

Changes in production

Performance to measure in phase 0

Rollout

Phase 0 moves today's ingress protection into the supervisor without changing what it allows, so the riskiest part (IP ownership and OVS flows) ships before any new policy does.

rollout · 4 phases, 3 gates
rollout · 4 phases, 3 gates

Every phase sits behind CapNovaRuntime and runtime_class: nova-runtime, so workloads move one at a time and roll back by returning to their previous runtime class. runsc is retired in phase 3, once Kata + nova-runtime passes the evidence above.

Open questions

Settled: Kata is the isolation layer of record; novalet owns workload IPAM, so the reuse cooldown lives there; supervisors resolve destinations through VPC local DNS.