Skip to content

MicroVM Administration

CYPT71 edited this page Aug 21, 2026 · 2 revisions

MicroVM administration

secure-oci microvm exposes one operational vocabulary over two deliberately different backends:

Backend Intended environment Lifecycle owner Guest input
native Linux host, Docker, or Podman with KVM foreground process, systemd, or container runtime verified local OCI layout
kubevirt Kubernetes with KubeVirt KubeVirt VirtualMachine digest-pinned external-kernel-boot image

The backends share names, CPU and memory bounds, structured logs, and the start, stop, restart, status, logs, and delete operations where the platform supports them. They do not pretend their boot artifacts are interchangeable: a normal application OCI image does not directly contain the kernel and initramfs paths required by KubeVirt.

Native CLI

Foreground development run:

secure-oci microvm run \
  --backend=native --name=example --layout=/srv/layouts/example \
  --memory-mib=256 --vcpus=2 \
  -p 127.0.0.1:8080:8080 \
  -p 127.0.0.1:8443:8443

Persistent host service (Linux with systemd):

sudo secure-oci microvm start --backend=native --name=example \
  --layout=/srv/layouts/example --memory-mib=256 --vcpus=2 --port=8080
sudo secure-oci microvm status --backend=native --name=example
sudo secure-oci microvm logs --backend=native --name=example
sudo secure-oci microvm restart --backend=native --name=example
sudo secure-oci microvm stop --backend=native --name=example
sudo secure-oci microvm delete --backend=native --name=example

start creates a transient unit named platform-factory-<name>.service. systemd owns the process identity and restart policy; the CLI does not trust reusable PID files. Logs are returned as journald JSON. The default listener is 127.0.0.1; selecting --listen-address=0.0.0.0 is an explicit exposure decision.

Docker and Podman

Build the host-side manager image:

docker build -f Dockerfile.microvm-manager -t platform-factory-manager:local .

Run one named container per microVM:

docker run -d --name microvm-example \
  --device=/dev/kvm --cap-drop=ALL \
  --security-opt=no-new-privileges --read-only \
  --tmpfs=/tmp:rw,nosuid,nodev,noexec \
  -v "$PWD/oci-image:/layout:ro" \
  -v platform-factory-cache:/opt/secure-oci/.cache/microvm \
  -p 127.0.0.1:8080:8080 \
  platform-factory-manager:local \
  microvm run --backend=native --name=example --layout=/layout \
  --listen-address=0.0.0.0 --port=8080

Use podman in place of docker for Podman. Administration then uses the runtime's normal, audited lifecycle:

docker inspect microvm-example
docker logs -f microvm-example
docker restart microvm-example
docker stop microvm-example
docker rm microvm-example

The device grant is mandatory and security-sensitive. Never use --privileged, never mount the Docker/Podman socket, and mount layouts read-only. Rootless Podman works only when the host grants the user access to /dev/kvm. The first uncached kernel build requires network access; production operators should pre-populate and lock down the named kernel cache.

Kubernetes with KubeVirt

Prerequisites:

  1. KubeVirt is installed and its VirtualMachine CRD is available.
  2. virtctl and kubectl target the intended cluster.
  3. The external-kernel-boot image matches the requested architecture and is pinned by a full sha256 digest.
  4. Admission policy verifies that boot image's signature and provenance.

Prepare the boot-image context from a verified application layout, then build and publish it:

secure-oci microvm package --layout=oci-image --output=kubevirt-boot
docker build -t ghcr.io/acme/example-boot:candidate kubevirt-boot
docker push ghcr.io/acme/example-boot:candidate

The generated scratch image contains /boot/kernel and /boot/initramfs.cpio.gz, owned by UID/GID 107 as required by KubeVirt. Resolve the registry digest after publication and use only the name@sha256:... reference below. KubeVirt consumes it through domain.firmware.kernelBoot.container; it is not attached as a VM disk.

Render for review without changing the cluster:

secure-oci microvm create --backend=kubevirt \
  --name=example --namespace=production --arch=amd64 \
  --image=ghcr.io/acme/example-boot@sha256:<64-hex-digest> \
  --memory-mib=256 --vcpus=2 > example-vm.json

Apply explicitly, then administer:

secure-oci microvm create --backend=kubevirt --apply \
  --name=example --namespace=production --arch=amd64 \
  --image=ghcr.io/acme/example-boot@sha256:<64-hex-digest> \
  --memory-mib=256 --vcpus=2
secure-oci microvm start --backend=kubevirt --name=example --namespace=production
secure-oci microvm status --backend=kubevirt --name=example --namespace=production
secure-oci microvm logs --backend=kubevirt --name=example --namespace=production
secure-oci microvm restart --backend=kubevirt --name=example --namespace=production
secure-oci microvm stop --backend=kubevirt --name=example --namespace=production
secure-oci microvm delete --backend=kubevirt --name=example --namespace=production

Creation is dry-run by default. Cluster mutation requires --apply. Lifecycle operations use the current kubeconfig and are therefore subject to Kubernetes RBAC and API audit logs. Give operators rights only in the target namespace.

Residual risks

  • KVM/QEMU remains a privileged host boundary even without container capabilities; keep QEMU and the host kernel patched.
  • A KubeVirt boot image is more powerful than an application image. Verify its signature, provenance, SBOM, architecture, and digest independently.
  • The native container needs a writable kernel cache and temporary directory. Compromise of either cache can affect later boots unless artifacts are verified and the cache is periodically rebuilt from pinned sources.
  • 0.0.0.0 exposes the QEMU forwarder to the container or host network. Prefer loopback publication and a separate authenticated ingress.

Clone this wiki locally