AID-I-001.002: MicroVM & Low-Level Sandboxing

Version 1.20261015 · Actionable sub-technique

Employs lightweight Virtual Machines (MicroVMs), user-space kernel sandboxes, and OS-level sandbox primitives to provide a stronger isolation boundary than traditional containers. This is critical for running untrusted code, browser-driven agent tools, or highly sensitive AI workloads where shared-kernel escape, filesystem writes, and uncontrolled network egress must be constrained in layers.

Pillars: infra

Lifecycle: operation

Scope boundary

Owns stronger-than-container execution isolation through microVMs, user-space kernels, WASM capability runtimes, or OS sandbox primitives. It defines isolation strength and permitted host interaction, but not per-invocation freshness, deterministic teardown, or artifact-analysis verdicts.

AID-I-001.001: AID-I-001.002 provides a stronger kernel or hardware isolation boundary; AID-I-001.001 uses conventional shared-kernel containers. Use the container control when its threat model is sufficient, and this control when shared-kernel escape or hostile code requires a stronger boundary.

AID-I-001.003: AID-I-001.002 selects and verifies runtime isolation strength; AID-I-001.003 creates a fresh sandbox for one tool invocation and proves teardown. A microVM can be long-lived, and a single-use sandbox can consume different approved runtime profiles. Isolation strength and one-shot lifecycle are separate controls.

Warning

Low to Medium on Startup Time & CPU/Memory Overhead

Stronger isolation technologies like gVisor or Firecracker impose a greater performance penalty than standard containers.

CPU Overhead: Can introduce a 5% to 15% CPU performance overhead compared to running in a standard container.

Startup Time: Adds a small but measurable delay, typically 5ms to 50ms of additional startup time per instance.

Implementation guidance

AID-I-001.002-G001

Use a stronger-than-container sandbox runtime for high-risk untrusted workloads, selecting either a hardware-virtualized microVM runtime or a userspace-kernel sandbox.
Concept

runtimeClassName selects a CRI handler, but reading that PodSpec field back proves only the request. A production gate must also bind the running Pod UID and node to a fresh, signed node/CRI attestation showing which runtime configuration handled the sandbox. Kubernetes admission and the node attestor remain external enforcement dependencies.

Render the workload from a signed runtime-selection profile

The profile supplies namespace, Pod name, digest-pinned image, RuntimeClass name and expected immutable handler, expected runtime type, runtime-engine digest, node runtime-configuration digest, policy digest, and the attestation validity rule. The cluster provisioner owns RuntimeClass and node configuration; application CI verifies them but does not redefine them.

# File: k8s/deploy_untrusted_runner.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077

root="$(mktemp -d)"
trap 'rm -rf -- "$root"' EXIT
install -m 0400 -- "$RUNTIME_SELECTION_PROFILE" "$root/profile.json"
install -m 0400 -- "$RUNTIME_SELECTION_PROFILE_BUNDLE" "$root/profile.sigstore.json"
cosign verify-blob --key "$RUNTIME_SELECTION_PROFILE_VERIFY_KEY"   --bundle "$root/profile.sigstore.json" "$root/profile.json" >/dev/null
jq -e '
  .schema_version == "aidefend.runtime-selection-profile.v1"
  and (.policy_sha256 | type == "string" and test("^[0-9a-f]{64}$"))
  and (.namespace | type == "string" and length > 0)
  and (.pod_name | type == "string" and length > 0)
  and (.runtime_class_name | type == "string" and length > 0)
  and (.runtime_handler | type == "string" and length > 0)
  and (.runtime_type | type == "string" and length > 0)
  and (.runtime_engine_sha256 | type == "string" and test("^[0-9a-f]{64}$"))
  and (.node_runtime_config_sha256 | type == "string" and test("^[0-9a-f]{64}$"))
  and (.image | type == "string" and test("@sha256:[0-9a-f]{64}$"))
  and (.attestation_max_age_seconds | type == "number" and floor == . and . > 0)
  and (.pod_ready_timeout_seconds | type == "number" and floor == . and . > 0)
  and (.admission_denial_marker | type == "string" and length > 0 and length <= 256)
' "$root/profile.json" >/dev/null

namespace="$(jq -r .namespace "$root/profile.json")"
pod_name="$(jq -r .pod_name "$root/profile.json")"
runtime_class="$(jq -r .runtime_class_name "$root/profile.json")"
expected_handler="$(jq -r .runtime_handler "$root/profile.json")"
expected_runtime_type="$(jq -r .runtime_type "$root/profile.json")"
expected_runtime_engine_sha256="$(jq -r .runtime_engine_sha256 "$root/profile.json")"
expected_node_runtime_config_sha256="$(jq -r .node_runtime_config_sha256 "$root/profile.json")"
image="$(jq -r .image "$root/profile.json")"
policy_sha256="$(jq -r .policy_sha256 "$root/profile.json")"
attestation_max_age="$(jq -r .attestation_max_age_seconds "$root/profile.json")"
pod_ready_timeout="$(jq -r .pod_ready_timeout_seconds "$root/profile.json")"
admission_denial_marker="$(jq -r .admission_denial_marker "$root/profile.json")"

observed_handler="$(kubectl get runtimeclass "$runtime_class" -o json | jq -r .handler)"
[[ "$observed_handler" == "$expected_handler" ]] || {
  echo "RuntimeClass handler differs from signed profile" >&2
  exit 1
}

jq -n   --arg namespace "$namespace"   --arg name "$pod_name"   --arg runtime_class "$runtime_class"   --arg image "$image"   --arg policy_sha256 "$policy_sha256"   '{
    apiVersion:"v1", kind:"Pod",
    metadata:{
      namespace:$namespace, name:$name,
      labels:{"aidefend.io/sandbox-required":"true"},
      annotations:{"aidefend.io/runtime-policy-sha256":$policy_sha256}
    },
    spec:{
      runtimeClassName:$runtime_class,
      automountServiceAccountToken:false,
      restartPolicy:"Never",
      containers:[{
        name:"runner", image:$image,
        securityContext:{
          allowPrivilegeEscalation:false,
          readOnlyRootFilesystem:true,
          capabilities:{drop:["ALL"]}
        }
      }]
    }
  }' > "$root/pod.json"

kubectl apply --server-side --field-manager=aidefend-runtime -f "$root/pod.json"
kubectl wait -n "$namespace" --for=condition=Ready "pod/$pod_name"   --timeout="${pod_ready_timeout}s"

kubectl get pod -n "$namespace" "$pod_name" -o json > "$root/pod-readback.json"
jq -e   --arg runtime_class "$runtime_class"   --arg policy_sha256 "$policy_sha256"   --arg image_digest "${image##*@}" '
    .spec.runtimeClassName == $runtime_class
    and .metadata.annotations["aidefend.io/runtime-policy-sha256"] == $policy_sha256
    and .status.phase == "Running"
    and (.metadata.uid | type == "string" and length > 0)
    and (.spec.nodeName | type == "string" and length > 0)
    and ([.status.containerStatuses[] | select(.name == "runner")] | length == 1)
    and (.status.containerStatuses[] | select(.name == "runner") | .imageID | endswith($image_digest))
  ' "$root/pod-readback.json" >/dev/null
Require a node/CRI attestation and a no-fallback admission test

The node-attestor adapter runs outside the workload identity and queries the node's CRI/provider measurement path. It emits a signed JSON receipt containing exactly the Pod UID, Kubernetes node UID, RuntimeClass name, requested handler, measured runtime type/engine digest, node runtime-config digest, image digest, policy digest, measurement time, and expiry. The adapter may use crictl inspectp or a managed-provider equivalent; a Pod label or application self-report is not acceptable.

# The platform-specific node attestor writes these two files.
install -m 0400 -- "$NODE_RUNTIME_ATTESTATION" "$root/node-attestation.json"
install -m 0400 -- "$NODE_RUNTIME_ATTESTATION_BUNDLE" "$root/node-attestation.sigstore.json"
cosign verify-blob --key "$NODE_RUNTIME_ATTESTOR_VERIFY_KEY"   --bundle "$root/node-attestation.sigstore.json" "$root/node-attestation.json" >/dev/null

pod_uid="$(jq -r .metadata.uid "$root/pod-readback.json")"
node_name="$(jq -r .spec.nodeName "$root/pod-readback.json")"
node_uid="$(kubectl get node "$node_name" -o json | jq -r .metadata.uid)"
now="$(date +%s)"
min_measured_at="$((now - attestation_max_age))"
jq -e   --arg pod_uid "$pod_uid"   --arg node_uid "$node_uid"   --arg runtime_class "$runtime_class"   --arg handler "$expected_handler"   --arg runtime_type "$expected_runtime_type"   --arg runtime_engine_sha256 "$expected_runtime_engine_sha256"   --arg node_runtime_config_sha256 "$expected_node_runtime_config_sha256"   --arg policy_sha256 "$policy_sha256"   --arg image_digest "${image##*@}"   --argjson now "$now"   --argjson min_measured_at "$min_measured_at" '
    (keys | sort) == (["expires_at_unix","image_digest","measured_at_unix","node_runtime_config_sha256","node_uid","pod_uid","policy_sha256","runtime_class_name","runtime_engine_sha256","runtime_handler","runtime_type","schema_version"] | sort)
    and .schema_version == "aidefend.node-runtime-attestation.v1"
    and .pod_uid == $pod_uid
    and .node_uid == $node_uid
    and .runtime_class_name == $runtime_class
    and .runtime_handler == $handler
    and .policy_sha256 == $policy_sha256
    and .image_digest == $image_digest
    and .runtime_type == $runtime_type
    and .runtime_engine_sha256 == $runtime_engine_sha256
    and .node_runtime_config_sha256 == $node_runtime_config_sha256
    and (.measured_at_unix | type == "number" and floor == . and . >= $min_measured_at and . <= $now)
    and (.expires_at_unix | type == "number" and floor == . and . > $now)
  ' "$root/node-attestation.json" >/dev/null

jq '.metadata.generateName = (.metadata.name + "-negative-") | del(.metadata.name, .metadata.uid, .spec.runtimeClassName)'   "$root/pod.json" > "$root/no-runtimeclass.json"
kubectl create --dry-run=client -f "$root/no-runtimeclass.json" >/dev/null
if kubectl create --dry-run=server -f "$root/no-runtimeclass.json"   >/dev/null 2>"$root/admission-denial.txt"; then
  echo "sandbox-required Pod was admitted without RuntimeClass" >&2
  exit 1
fi
if ! grep -F -- "$admission_denial_marker" "$root/admission-denial.txt" >/dev/null; then
  echo "negative admission test failed for an unrecognized reason" >&2
  exit 1
fi

Action: Release only when the Pod is running with the signed image, the RuntimeClass handler matches, the attested runtime type, engine digest, and node configuration digest exactly match the signed profile, the independent attestation binds the exact Pod/node/runtime population and remains fresh under policy, and admission rejects omission. A declared field without runtime attestation is insufficient; a missing attestor or unsupported handler fails closed.

AID-I-001.002-G002

Define strict seccomp-bpf profiles to whitelist only necessary system calls for model inference.
Concept:

A seccomp allowlist is deny-by-default only when defaultAction denies syscalls and the profile explicitly allows the deployment-specific set. A default-allow profile with a short denylist is useful defense in depth, but it is not a least-privilege allowlist. Build the allowlist from complete staging traces for the exact image and workload tests, review additions, and promote the profile with the image.

Step 1: Generate a reviewed deny-by-default profile
# File: tools/build_seccomp_profile.py
from __future__ import annotations

import hashlib
import json
import re
from pathlib import Path


OBSERVED = Path("artifacts/seccomp/observed-syscalls.txt")
REVIEWED_EXTRAS = Path("policy/seccomp/reviewed-extra-syscalls.txt")
OUTPUT = Path("artifacts/seccomp/inference-profile.json")
NAME = re.compile(r"^[a-z0-9_]+$")
DANGEROUS = {
    "bpf", "kexec_load", "keyctl", "mount", "open_by_handle_at",
    "perf_event_open", "ptrace", "reboot", "setns", "unshare",
}


def load_names(path: Path, *, allow_empty: bool = False) -> set[str]:
    names = {
        line.strip()
        for line in path.read_text(encoding="utf-8").splitlines()
        if line.strip() and not line.lstrip().startswith("#")
    }
    if ((not names and not allow_empty)
            or any(not NAME.fullmatch(name) for name in names)):
        raise SystemExit(f"invalid or empty syscall inventory: {path}")
    return names


allowed = load_names(OBSERVED) | load_names(REVIEWED_EXTRAS, allow_empty=True)
forbidden = sorted(allowed & DANGEROUS)
if forbidden:
    raise SystemExit(f"dangerous syscalls require a separate exception: {forbidden}")

profile = {
    "defaultAction": "SCMP_ACT_ERRNO",
    "defaultErrnoRet": 1,
    "architectures": ["SCMP_ARCH_X86_64"],
    "syscalls": [{"names": sorted(allowed), "action": "SCMP_ACT_ALLOW"}],
}
payload = (json.dumps(profile, sort_keys=True, indent=2) + "\n").encode("utf-8")
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_bytes(payload)
OUTPUT.with_suffix(".json.sha256").write_text(
    hashlib.sha256(payload).hexdigest() + "\n", encoding="ascii"
)

The observed inventory must come from successful cold start, readiness/liveness, representative inference, model reload, telemetry, shutdown, and failure-path tests for the exact image digest. Treat the reviewed extras file as code-reviewed policy, not as an escape hatch.

Step 2: Install and bind the exact profile
# Node path: /var/lib/kubelet/seccomp/profiles/aidefend/inference-profile.json
# File: k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inference-api
  namespace: ai-production
spec:
  selector:
    matchLabels:
      app: inference-api
  template:
    metadata:
      labels:
        app: inference-api
    spec:
      automountServiceAccountToken: false
      securityContext:
        seccompProfile:
          type: Localhost
          localhostProfile: aidefend/inference-profile.json
      containers:
      - name: api
        image: registry.example.com/ai/inference-api:2026.04.14
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
Verification

Admission must bind the profile digest to the exact image digest and reject nodes where the file is absent. Run the complete workload suite, then run a purpose-built negative probe that attempts a syscall such as unshare and require EPERM. Read back the pod's resolved seccomp profile and retain the profile bytes/digest, image digest, node rollout population, positive tests, and negative-probe result.

AID-I-001.002-G003

Utilize WebAssembly (WASM) runtimes to run AI models in a high-performance, secure sandbox.
Concept:

Delivery level: reusable module. WASM isolation is practical when inference logic can be compiled and invoked through a strict host boundary. The host controls capabilities; the module gets none by default.

ABI smoke-test fixture

This tiny export is only a deterministic CI fixture for validating the host ABI and negative limit tests; the production model module is separately built, signed, and digest-bound.

// File: inference-engine/src/lib.rs
#[no_mangle]
pub extern "C" fn run_inference(input: i32) -> i32 {
    input * 2
}
Run with a capability-minimal Wasmtime host

The Linker is the capability boundary: the module only receives host functions, WASI handles, directories, environment variables, or stdio streams that you explicitly define. Validate requested imports before instantiation, enable fuel, and bound memory so a hostile module cannot silently gain filesystem/network access or consume unbounded resources.

# File: host_app/run_wasm.py
from __future__ import annotations

import hashlib
import hmac
from dataclasses import dataclass
from pathlib import Path

from wasmtime import Config, Engine, Linker, Module, Store, WasmtimeError


@dataclass(frozen=True)
class VerifiedWasmPolicy:
    policy_sha256: str
    module_sha256: str
    max_fuel: int
    max_memory_bytes: int
    max_table_elements: int
    allowed_imports: frozenset[tuple[str, str]]
    required_exports: frozenset[str]


def run_verified_module(
    module_path: Path,
    policy: VerifiedWasmPolicy,
    input_value: int,
) -> tuple[int, dict[str, object]]:
    if min(policy.max_fuel, policy.max_memory_bytes, policy.max_table_elements) <= 0:
        raise ValueError("verified WASM policy limits must be positive")
    module_bytes = module_path.read_bytes()
    observed_sha256 = "sha256:" + hashlib.sha256(module_bytes).hexdigest()
    if not hmac.compare_digest(observed_sha256, policy.module_sha256):
        raise PermissionError("WASM module digest does not match signed policy")

    config = Config()
    config.consume_fuel = True
    engine = Engine(config)
    module = Module(engine, module_bytes)
    observed_imports = frozenset((item.module, item.name) for item in module.imports)
    if observed_imports != policy.allowed_imports:
        raise PermissionError("WASM import population differs from signed policy")
    observed_exports = frozenset(item.name for item in module.exports)
    if not policy.required_exports.issubset(observed_exports):
        raise PermissionError("required WASM export is absent")

    store = Store(engine)
    store.set_fuel(policy.max_fuel)
    store.set_limits(
        memory_size=policy.max_memory_bytes,
        table_elements=policy.max_table_elements,
        memories=1,
        tables=1,
        instances=1,
    )
    linker = Linker(engine)
    try:
        instance = linker.instantiate(store, module)
        run_inference = instance.exports(store)["run_inference"]
        output = int(run_inference(store, input_value))
    except (KeyError, TypeError, WasmtimeError) as exc:
        raise RuntimeError("bounded WASM execution failed") from exc
    return output, {
        "policy_sha256": policy.policy_sha256,
        "module_sha256": observed_sha256,
        "imports": sorted([list(item) for item in observed_imports]),
        "exports": sorted(observed_exports),
        "fuel_remaining": store.get_fuel(),
    }

Action: Obtain VerifiedWasmPolicy only from a signature-verifying policy adapter, then execute the exact module bytes through this capability-minimal host. Any WASI import, host function, directory, environment variable, stdio stream, network adapter, or clock/random source must appear in the exact signed import set.

AID-I-001.002-G004

Layer OS sandbox primitives beneath microVM or user-space-kernel isolation for local agent and tool runners.
Concept:

MicroVMs and gVisor provide the main isolation boundary, but local agent execution often still needs an OS primitive that limits file writes, process visibility, and network access before the code reaches a managed cluster. Use Landlock or bubblewrap for Linux local execution. On macOS, prefer App Sandbox, managed VDI, or VM/container isolation for production; sandbox-exec/Seatbelt profiles are a deprecated command-line fallback suitable only for local smoke tests and must not be the sole production boundary. Treat network egress as the hardest part: outbound allowlists must be enforced outside the process as well, because application-level checks are easy to miss during redirects, package installation, browser prefetch, and helper subprocesses.

Linux example: run a local tool with bubblewrap and a read-only root
# File: sandbox/run_local_tool.sh
#!/usr/bin/env bash
set -euo pipefail

SANDBOX_ROOT="$(mktemp -d)"
trap 'rm -rf "$SANDBOX_ROOT"' EXIT

mkdir -p "$SANDBOX_ROOT"/{work,tmp,home}
cp "$1" "$SANDBOX_ROOT/work/tool.py"

bwrap   --die-with-parent   --unshare-all   --new-session   --ro-bind /usr /usr   --ro-bind /lib /lib   --ro-bind /lib64 /lib64   --ro-bind /bin /bin   --proc /proc   --dev /dev   --tmpfs /tmp   --dir /run   --chdir /work   --setenv HOME /home/sandbox   --setenv PYTHONNOUSERSITE 1   --bind "$SANDBOX_ROOT/work" /work   --bind "$SANDBOX_ROOT/home" /home/sandbox   --unshare-net   /usr/bin/python3 /work/tool.py

When an in-process Linux runner uses Landlock, bind a specific reviewed library/version and test its actual kernel ABI; do not copy a binding-shaped snippet whose API or handled access rights are unverified. The authoritative production boundary remains the outer microVM/userspace-kernel runtime plus external egress enforcement.

macOS deprecated fallback example: Seatbelt profile for local smoke tests only
; File: sandbox/tool.sb
(version 1)
(deny default)
(allow process-exec)
(allow file-read* (subpath "/usr") (subpath "/bin") (subpath "/System"))
(allow file-read* file-write* (subpath "/tmp/aidefend-tool-work"))
(deny network*)
sandbox-exec -f sandbox/tool.sb /usr/bin/python3 /tmp/aidefend-tool-work/tool.py

Action: Use OS sandbox primitives for local development, self-hosted tool workers, and CI smoke tests, but treat macOS sandbox-exec as deprecated and non-authoritative. Production macOS desktop/app workloads should use App Sandbox entitlement boundaries, managed VDI, or VM/container isolation, and high-risk production invocations still belong in AID-I-001.003 single-use sandboxes with network egress enforced by policy.

Independently verify isolation

Population: enumerate every governed runner, invocation, host/kernel build, outer runtime, sandbox policy, workspace, child process, mount, and credential/network capability; incomplete population coverage is never PASS.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-I-001.002: MicroVM & Low-Level Sandboxing | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version