- docs/hypervisor-design.md, docs/hypervisor_migration.md: unikernel runtime design and migration plan (previously untracked) - log/2026-03-29-mcp-multinode.md: work log (previously untracked) - CLAUDE.md: mcq lives at a sibling path - engineering-standards.md: document the push target - .gitignore: mcdoc checkout; local configs that have held credentials Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
245 lines
9.9 KiB
Markdown
245 lines
9.9 KiB
Markdown
# Hypervisor-Based Service Isolation -- Design Notes
|
|
|
|
> **Status**: Brainstorming / future direction. This document is NOT
|
|
> an active work item. Agents should ignore this document unless
|
|
> specifically asked to consider it.
|
|
|
|
## Context
|
|
|
|
The metacircular platform runs Go services as rootless podman
|
|
containers orchestrated by MCP. This is a pragmatic execution of ideas
|
|
originally explored in a series of 2015 papers on security kernels,
|
|
environment isolation, and unikernels (see References). Those papers
|
|
describe a richer model than what containers provide: hardware-enforced
|
|
isolation, mandatory inter-environment communication mediation, and
|
|
capability-based access control. This document explores bridging the
|
|
gap by running services as unikernel VMs (specifically Nanos) on the
|
|
MCP control plane.
|
|
|
|
## Motivation: What Containers Don't Give Us
|
|
|
|
The current platform has the W7 security kernel's three properties --
|
|
isolated environments, inter-environment communication (IEC), and
|
|
access mediation -- but implemented cooperatively rather than enforced:
|
|
|
|
| W7 Property | Metacircular Today | Enforcement |
|
|
|---|---|---|
|
|
| Isolated environments | Rootless podman (namespaces/cgroups) | OS-cooperative -- shared kernel, escape CVEs exist |
|
|
| IEC | gRPC/TLS through mc-proxy | Application-cooperative -- services *choose* to route through mc-proxy |
|
|
| Access mediation | MCIAS tokens + per-service policies | Application-level -- services check tokens voluntarily |
|
|
|
|
The topology is right. The enforcement mechanism is weak. Containers
|
|
share a kernel, and any service could bypass mc-proxy to reach the
|
|
Tailnet directly.
|
|
|
|
## What Unikernels Buy Us
|
|
|
|
### Hardware-Enforced Isolation
|
|
|
|
Each service runs in its own VM with its own kernel. There is no
|
|
shared kernel to escape from. The security boundary is the hypervisor
|
|
(KVM), not Linux namespaces. This is the W7 "isolated environments"
|
|
model enforced by hardware, not convention.
|
|
|
|
### Mandatory IEC
|
|
|
|
This is the subtle but powerful part. A container on the Tailnet can
|
|
talk to anything. A unikernel VM with no direct network interface --
|
|
only a virtio-net device connected to a host-only bridge that the
|
|
agent controls -- cannot bypass the mediation layer. If the agent is
|
|
the only Tailnet citizen on the node and VMs can only reach the
|
|
agent's bridge, then mc-proxy stops being a routing convenience and
|
|
becomes the IEC mechanism. Communication between environments is
|
|
mediated by design, not by trust.
|
|
|
|
### Reduced TCB
|
|
|
|
Container TCB: Linux kernel + podman runtime + container image (often
|
|
a full distro). Unikernel TCB: KVM + Nanos runtime + the Go binary.
|
|
No shell, no package manager, no multi-user, no unnecessary syscalls.
|
|
|
|
## The Agent as Security Kernel
|
|
|
|
The MCP agent is already structurally positioned to be the W7 security
|
|
kernel for its node. It manages environment lifecycle, controls the
|
|
IEC layer (mc-proxy routes), provisions credentials (Metacrypt certs),
|
|
and reports to a central authority (master).
|
|
|
|
With unikernels, this role is formalized. The agent becomes the only
|
|
entity with host access. Services exist in VMs that can only
|
|
communicate through agent-controlled channels:
|
|
|
|
- **Network access**: virtio-net bridge under agent control; the agent
|
|
decides what each VM can reach.
|
|
- **Storage access**: 9p/virtio-fs mounts; the agent controls what
|
|
each VM sees on disk.
|
|
- **Credential access**: the agent provisions certs into the VM's
|
|
filesystem before boot.
|
|
- **Identity**: the agent attests to the master what image hash is
|
|
running in each VM.
|
|
|
|
## Why This Is Feasible for Metacircular
|
|
|
|
Several properties of the existing platform make this tractable:
|
|
|
|
- **Go + CGO_ENABLED=0**: Every service already produces a static ELF
|
|
binary. Nanos needs exactly this. The `ops` tool packages them with
|
|
minimal friction.
|
|
|
|
- **Single-process services**: Each service is one Go binary -- no
|
|
sidecars, no shell scripts, no multi-process orchestration. That is
|
|
the unikernel sweet spot.
|
|
|
|
- **Single-operator trust domain**: No multi-tenant capability
|
|
delegation or federated attestation needed. The agent is the
|
|
security kernel for its node; the master is the coordination point.
|
|
|
|
- **mc-proxy already mediates traffic**: The routing mesh is already
|
|
in place. Making it mandatory (rather than optional) for unikernel
|
|
VMs is an incremental change, not a new system.
|
|
|
|
## Design Sketch
|
|
|
|
### Runtime Abstraction
|
|
|
|
The agent gains a `Runtime` interface. Podman is one implementation;
|
|
QEMU/KVM is another. Service definitions gain a `runtime` field:
|
|
|
|
```toml
|
|
name = "mcq"
|
|
runtime = "unikernel" # or "container" (default)
|
|
tier = "worker"
|
|
```
|
|
|
|
Both runtimes coexist. Services can be converted incrementally.
|
|
|
|
### Networking: Host-Only Bridge
|
|
|
|
Each unikernel VM gets a virtio-net device on a host-only bridge. The
|
|
agent runs on the bridge and controls forwarding. VMs cannot reach the
|
|
Tailnet directly. All external communication flows through mc-proxy on
|
|
the host.
|
|
|
|
This is structurally similar to how rootless podman already works
|
|
(container ports are localhost-only, mc-proxy routes to them), but
|
|
with the enforcement moved from convention to network topology.
|
|
|
|
### Storage: 9p Passthrough
|
|
|
|
Unikernel VMs mount `/srv/<service>/` via QEMU's `-virtfs` 9p
|
|
passthrough. Writes go directly to the host filesystem. This makes
|
|
snapshots work the same way as containers -- the agent tars the host
|
|
directory.
|
|
|
|
### Image Building
|
|
|
|
Two options (not mutually exclusive):
|
|
|
|
1. **Build on agent**: Agent extracts the ELF binary from the OCI
|
|
image (pulled from MCR) and runs `ops build` locally.
|
|
2. **Store unikernel images in MCR**: OCI supports arbitrary media
|
|
types. Unikernel `.img` files could be stored as OCI artifacts.
|
|
|
|
Option 1 is simpler to start with. Option 2 is cleaner long-term.
|
|
|
|
### Image Attestation
|
|
|
|
Before booting a unikernel, the agent hashes the image and reports it
|
|
to the master. The master compares against expected hashes from the
|
|
service definition. This is software attestation -- not TPM-based, but
|
|
it closes the "is this what I deployed?" question. It is a stepping
|
|
stone toward measured boot with hardware TPM.
|
|
|
|
### Snapshot Constraints
|
|
|
|
Unikernels have no shell. The `cli` and `exec:` snapshot methods
|
|
don't work. Only `grpc` snapshots are viable for unikernel services
|
|
(the service implements the standard `SnapshotService` RPC). The
|
|
default snapshot method (tar config/db/certs from the host-side 9p
|
|
mount) works unchanged since the agent tars the host directory, not
|
|
the VM filesystem.
|
|
|
|
### Debugging
|
|
|
|
No `podman exec`, no shell. Debugging relies on:
|
|
|
|
- Serial console output from QEMU
|
|
- gRPC health/status endpoints
|
|
- Structured logging to a file on the 9p mount
|
|
- The agent can snapshot and inspect VM state
|
|
|
|
This is a real loss of convenience. It is the price of proper
|
|
isolation -- as noted in the 2015 hypervisor paper, "the nature of
|
|
debugging means that isolation is broken."
|
|
|
|
## Difficulty Assessment
|
|
|
|
| Aspect | Difficulty | Notes |
|
|
|---|---|---|
|
|
| Building unikernel images from Go binaries | Easy | Already static ELF, `ops` handles it |
|
|
| QEMU lifecycle management in agent | Medium | Replace podman calls with qemu-system calls |
|
|
| Networking (host-only bridge + mc-proxy) | Medium | Similar to rootless podman model |
|
|
| Persistent storage via 9p | Medium | Well-supported in QEMU, maps to existing `/srv/` layout |
|
|
| Snapshots | Medium | `grpc` method works; `cli`/`exec` don't |
|
|
| Image attestation | Medium-Low | SHA-256 of image before boot |
|
|
| mc-proxy integration | Low | Just needs a reachable IP:port |
|
|
| Debugging/observability | Annoying | Loss of exec/shell access |
|
|
|
|
The minimum meaningful change is the runtime abstraction + isolated
|
|
networking together. Running a unikernel with full Tailnet access is
|
|
just a heavier container with worse debugging. The isolation properties
|
|
only kick in when the agent mediates all communication.
|
|
|
|
## Progression Path
|
|
|
|
1. **Runtime abstraction in the agent.** `Runtime` interface with
|
|
podman and qemu implementations. Service definitions gain a
|
|
`runtime` field. Both coexist.
|
|
|
|
2. **Isolated networking for unikernel VMs.** Host-only bridge per
|
|
node, agent controls forwarding. mc-proxy becomes the mandatory
|
|
IEC layer for unikernel services.
|
|
|
|
3. **Image attestation.** Agent hashes images before boot, reports to
|
|
master. Master compares against expected values.
|
|
|
|
4. **Capability tokens (longer-term).** MCIAS issues operation-scoped
|
|
tokens instead of identity tokens. The agent's mediation layer
|
|
enforces them at the network boundary. This is independent of
|
|
unikernels but synergizes with mandatory mediation.
|
|
|
|
## Open Questions
|
|
|
|
- **Tailscale integration**: Should unikernel VMs ever be first-class
|
|
Tailnet citizens (via tsnet compiled into the binary), or should the
|
|
agent always mediate? Mandatory mediation is more secure but means
|
|
the agent is on the critical path for all traffic.
|
|
|
|
- **Resource limits**: QEMU VMs need explicit memory and CPU
|
|
allocation. The current container model doesn't declare resource
|
|
requirements. Unikernels would force this.
|
|
|
|
- **Mixed fleet**: During transition, some services run as containers
|
|
and some as unikernels. mc-proxy routes to both. Does the master
|
|
need to know the runtime type for placement decisions?
|
|
|
|
- **ARM support**: Nanos supports aarch64 but the QEMU/KVM story on
|
|
Raspberry Pi (no KVM on all models) may limit unikernels to amd64
|
|
nodes.
|
|
|
|
## References
|
|
|
|
- Rees, J. "A Security Kernel Based on the Lambda Calculus" (W7
|
|
security kernel model -- isolated environments, IEC, access
|
|
mediation)
|
|
- "Containers, isolation, and operating systems for network spaces"
|
|
(2015) -- argues the OS must provide a security kernel; unikernels
|
|
as viable isolation mechanism
|
|
- "A hypervisor for the modern age" (2015) -- problem statement for a
|
|
hypervisor providing proper isolation, IEC, and access mediation
|
|
with a programmatic administrative interface
|
|
- "A content-addressable data store with object capabilities" (Nebula,
|
|
2015) -- capability-based access control model
|
|
- MCP v2 Architecture (`docs/architecture-v2.md`) -- current platform
|
|
design this document builds on
|