1
0
forked from wrenn/wrenn
This commit is contained in:
2026-04-16 19:24:25 +00:00
parent 172413e91e
commit 605ad666a0
239 changed files with 19966 additions and 3454 deletions

View File

385
scripts/prepare-wrenn-user.sh Executable file
View File

@ -0,0 +1,385 @@
#!/usr/bin/env bash
#
# prepare-wrenn-user.sh — Create the wrenn system user and configure minimal privileges.
#
# Creates a locked-down 'wrenn' system user that can run wrenn-agent and wrenn-cp
# with only the privileges they need. The agent binary gets Linux capabilities
# via setcap — no sudo is configured for the wrenn user at all. If an attacker
# compromises the wrenn user, they cannot escalate via sudo.
#
# What this script does:
# 1. Creates the 'wrenn' system user (bash shell for debugging, no home dir)
# 2. Creates required directories with correct ownership
# 3. Sets Linux capabilities on wrenn-agent and all child binaries
# 4. Installs an apt hook to restore capabilities after package updates
# 5. Installs a sudoers drop-in (comment-only, no grants — absence is the cage)
# 6. Ensures required kernel modules are loaded
# 7. Writes systemd unit files for both wrenn-agent and wrenn-cp
#
# Usage:
# sudo bash scripts/prepare-wrenn-user.sh
#
# Prerequisites:
# - wrenn-agent binary at /usr/local/bin/wrenn-agent
# - wrenn-cp binary at /usr/local/bin/wrenn-cp
# - firecracker binary at /usr/local/bin/firecracker
# - libcap2-bin installed (for setcap)
set -euo pipefail
# ── Guard ────────────────────────────────────────────────────────────────────
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root."
exit 1
fi
# ── Configuration ────────────────────────────────────────────────────────────
WRENN_USER="wrenn"
WRENN_GROUP="wrenn"
WRENN_DIR="/var/lib/wrenn"
AGENT_BIN="/usr/local/bin/wrenn-agent"
CP_BIN="/usr/local/bin/wrenn-cp"
FC_BIN="/usr/local/bin/firecracker"
RESTORE_CAPS_SCRIPT="/etc/wrenn/restore-caps.sh"
# ── 1. Create system user ───────────────────────────────────────────────────
if id "${WRENN_USER}" &>/dev/null; then
echo "==> User '${WRENN_USER}' already exists, skipping creation."
else
echo "==> Creating system user '${WRENN_USER}'..."
useradd \
--system \
--no-create-home \
--home-dir "${WRENN_DIR}" \
--shell /bin/bash \
"${WRENN_USER}"
fi
# Add wrenn to kvm group for /dev/kvm access.
if getent group kvm &>/dev/null; then
usermod -aG kvm "${WRENN_USER}"
echo "==> Added '${WRENN_USER}' to 'kvm' group."
fi
# ── 2. Create directories with correct ownership ────────────────────────────
echo "==> Setting up directories..."
directories=(
"${WRENN_DIR}"
"${WRENN_DIR}/images"
"${WRENN_DIR}/kernels"
"${WRENN_DIR}/sandboxes"
"${WRENN_DIR}/snapshots"
"${WRENN_DIR}/logs"
"/run/netns"
)
for dir in "${directories[@]}"; do
mkdir -p "${dir}"
done
# Only chown wrenn-owned dirs (not /run/netns which is system-managed).
for dir in "${WRENN_DIR}" "${WRENN_DIR}/images" "${WRENN_DIR}/kernels" \
"${WRENN_DIR}/sandboxes" "${WRENN_DIR}/snapshots" "${WRENN_DIR}/logs"; do
chown "${WRENN_USER}:${WRENN_GROUP}" "${dir}"
chmod 750 "${dir}"
done
# ── 3. Set capabilities on binaries ─────────────────────────────────────────
#
# These capabilities replace full root access. The wrenn-agent binary gets
# exactly the capabilities it needs for:
#
# CAP_SYS_ADMIN — network namespaces (netns create/enter), mount namespaces
# (unshare -m), losetup, dmsetup, mount/umount
# CAP_NET_ADMIN — veth/TAP creation (netlink), iptables rules, IP forwarding,
# routing table manipulation
# CAP_NET_RAW — raw socket access (needed by iptables internally)
# CAP_SYS_PTRACE — reading /proc/self/ns/net (netns.Get)
# CAP_KILL — sending SIGTERM/SIGKILL to Firecracker processes
# CAP_DAC_OVERRIDE — accessing /dev/loop*, /dev/mapper/*, /dev/net/tun,
# /proc/sys/net/ipv4/ip_forward
# CAP_MKNOD — creating device nodes (dm-snapshot)
#
# The 'ep' suffix means Effective + Permitted (granted at exec time).
echo "==> Setting capabilities on wrenn-agent..."
if [[ ! -f "${AGENT_BIN}" ]]; then
echo "WARNING: ${AGENT_BIN} not found, skipping setcap. Install the binary first."
else
setcap \
cap_sys_admin,cap_net_admin,cap_net_raw,cap_sys_ptrace,cap_kill,cap_dac_override,cap_mknod+ep \
"${AGENT_BIN}"
echo " Capabilities set on ${AGENT_BIN}:"
getcap "${AGENT_BIN}"
fi
# Firecracker also needs capabilities when spawned by a non-root parent.
# CAP_NET_ADMIN is required for network device access inside the netns.
if [[ -f "${FC_BIN}" ]]; then
setcap cap_net_admin,cap_sys_admin,cap_dac_override+ep "${FC_BIN}"
echo " Capabilities set on ${FC_BIN}:"
getcap "${FC_BIN}"
fi
# ── Helper: resolve binary path and apply setcap ────────────────────────────
#
# Uses `command -v` to find the binary in PATH (handles /usr/bin vs /usr/sbin
# differences across distros), then `readlink -f` to resolve symlinks so that
# setcap hits the real inode (important for iptables-nft/alternatives).
setcap_binary() {
local name="$1" caps="$2"
local bin
bin=$(command -v "$name" 2>/dev/null) || {
echo " WARNING: ${name} not found in PATH, skipping."
return 0
}
bin=$(readlink -f "$bin")
setcap "$caps" "$bin"
echo " $(getcap "$bin")"
}
# The child binaries invoked by wrenn-agent (iptables, losetup, dmsetup, etc.)
# also need capabilities since they'll be exec'd by a non-root user.
echo "==> Setting capabilities on child binaries..."
setcap_binary iptables "cap_net_admin,cap_net_raw+ep"
setcap_binary iptables-save "cap_net_admin,cap_net_raw+ep"
setcap_binary ip "cap_sys_admin,cap_net_admin+ep"
setcap_binary sysctl "cap_net_admin+ep"
setcap_binary losetup "cap_sys_admin,cap_dac_override+ep"
setcap_binary blockdev "cap_sys_admin,cap_dac_override+ep"
setcap_binary dmsetup "cap_sys_admin,cap_dac_override,cap_mknod+ep"
setcap_binary e2fsck "cap_sys_admin,cap_dac_override+ep"
setcap_binary resize2fs "cap_sys_admin,cap_dac_override+ep"
setcap_binary dd "cap_dac_override+ep"
setcap_binary unshare "cap_sys_admin+ep"
setcap_binary mount "cap_sys_admin,cap_dac_override+ep"
# ── 4. Persist capabilities across package updates ──────────────────────────
#
# apt/dpkg overwrites binaries on package updates, which strips the xattr-based
# capabilities set by setcap. This installs:
# - /etc/wrenn/restore-caps.sh: re-applies setcap to all child binaries
# - /etc/apt/apt.conf.d/99-wrenn-setcap: apt post-invoke hook that calls it
echo "==> Installing capability restore hook..."
mkdir -p /etc/wrenn
cat > "${RESTORE_CAPS_SCRIPT}" << 'RESTORE'
#!/usr/bin/env bash
#
# restore-caps.sh — Re-apply Linux capabilities to wrenn child binaries.
# Called automatically by apt after package updates (see /etc/apt/apt.conf.d/99-wrenn-setcap).
# Can also be run manually: sudo /etc/wrenn/restore-caps.sh
set -euo pipefail
setcap_binary() {
local name="$1" caps="$2"
local bin
bin=$(command -v "$name" 2>/dev/null) || return 0
bin=$(readlink -f "$bin")
setcap "$caps" "$bin" 2>/dev/null || true
}
# wrenn-agent and firecracker (only if present — they aren't package-managed).
[[ -f /usr/local/bin/wrenn-agent ]] && \
setcap cap_sys_admin,cap_net_admin,cap_net_raw,cap_sys_ptrace,cap_kill,cap_dac_override,cap_mknod+ep \
/usr/local/bin/wrenn-agent 2>/dev/null || true
[[ -f /usr/local/bin/firecracker ]] && \
setcap cap_net_admin,cap_sys_admin,cap_dac_override+ep \
/usr/local/bin/firecracker 2>/dev/null || true
# Child binaries (these are the ones wiped by apt).
setcap_binary iptables "cap_net_admin,cap_net_raw+ep"
setcap_binary iptables-save "cap_net_admin,cap_net_raw+ep"
setcap_binary ip "cap_sys_admin,cap_net_admin+ep"
setcap_binary sysctl "cap_net_admin+ep"
setcap_binary losetup "cap_sys_admin,cap_dac_override+ep"
setcap_binary blockdev "cap_sys_admin,cap_dac_override+ep"
setcap_binary dmsetup "cap_sys_admin,cap_dac_override,cap_mknod+ep"
setcap_binary e2fsck "cap_sys_admin,cap_dac_override+ep"
setcap_binary resize2fs "cap_sys_admin,cap_dac_override+ep"
setcap_binary dd "cap_dac_override+ep"
setcap_binary unshare "cap_sys_admin+ep"
setcap_binary mount "cap_sys_admin,cap_dac_override+ep"
RESTORE
chmod 755 "${RESTORE_CAPS_SCRIPT}"
cat > /etc/apt/apt.conf.d/99-wrenn-setcap << 'APT'
// Re-apply Linux capabilities to wrenn child binaries after any package update.
// Capabilities (xattr) are stripped when dpkg overwrites a binary.
DPkg::Post-Invoke { "/etc/wrenn/restore-caps.sh"; };
APT
echo " Installed ${RESTORE_CAPS_SCRIPT} and apt post-invoke hook."
# ── 5. Device access ────────────────────────────────────────────────────────
#
# /dev/kvm — handled by kvm group membership above
# /dev/net/tun — needs to be accessible by wrenn user
echo "==> Configuring device access..."
# Ensure /dev/net/tun is accessible (udev rule for persistence across reboots).
cat > /etc/udev/rules.d/99-wrenn.rules << 'UDEV'
# Allow wrenn user access to TUN device for TAP networking.
SUBSYSTEM=="misc", KERNEL=="tun", GROUP="wrenn", MODE="0660"
UDEV
udevadm control --reload-rules 2>/dev/null || true
echo " Installed udev rule for /dev/net/tun."
# ── 6. Kernel modules ───────────────────────────────────────────────────────
echo "==> Ensuring kernel modules are loaded..."
modules=(dm_snapshot dm_mod loop tun)
for mod in "${modules[@]}"; do
if ! lsmod | grep -q "^${mod}"; then
modprobe "${mod}" 2>/dev/null && echo " Loaded ${mod}" || echo " WARNING: Could not load ${mod}"
else
echo " ${mod} already loaded."
fi
done
# Persist across reboots.
for mod in "${modules[@]}"; do
grep -qxF "${mod}" /etc/modules-load.d/wrenn.conf 2>/dev/null || echo "${mod}" >> /etc/modules-load.d/wrenn.conf
done
echo " Module persistence written to /etc/modules-load.d/wrenn.conf."
# ── 7. Sudoers ──────────────────────────────────────────────────────────────
#
# The wrenn user has no sudo grants. The absence of a grant is the cage — an
# explicit "!ALL" deny is weaker due to known bypasses (CVE-2019-14287).
# This file exists purely as documentation for operators running `sudo -l`.
echo "==> Writing sudoers drop-in..."
cat > /etc/sudoers.d/wrenn << 'SUDOERS'
# Wrenn system user — no sudo access permitted.
# All privilege is granted via Linux capabilities on specific binaries (setcap).
# This file contains no active rules. The absence of any grant is intentional
# and is the strongest way to deny escalation.
#
# Do not add rules here. If the wrenn user needs new privileges, use setcap
# on the specific binary instead.
SUDOERS
chmod 440 /etc/sudoers.d/wrenn
visudo -c -f /etc/sudoers.d/wrenn
echo " /etc/sudoers.d/wrenn installed and validated."
# ── 8. Systemd units ────────────────────────────────────────────────────────
echo "==> Writing systemd service files..."
cat > /etc/systemd/system/wrenn-agent.service << 'UNIT'
[Unit]
Description=Wrenn Host Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=wrenn
Group=wrenn
EnvironmentFile=-/etc/wrenn/agent.env
# The binary has capabilities set via setcap. These systemd directives ensure
# the capabilities are inherited into the process at exec time.
AmbientCapabilities=CAP_SYS_ADMIN CAP_NET_ADMIN CAP_NET_RAW CAP_SYS_PTRACE CAP_KILL CAP_DAC_OVERRIDE CAP_MKNOD
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_NET_ADMIN CAP_NET_RAW CAP_SYS_PTRACE CAP_KILL CAP_DAC_OVERRIDE CAP_MKNOD
# IMPORTANT: must be false — child binaries (iptables, losetup, dmsetup, etc.)
# have their own file capabilities via setcap which must be honored at exec time.
NoNewPrivileges=false
# Enable IP forwarding before the agent starts. The "+" prefix runs this
# directive as root (bypassing User=wrenn) so it can write to procfs.
ExecStartPre=+/bin/sh -c 'sysctl -w net.ipv4.ip_forward=1'
ExecStart=/usr/local/bin/wrenn-agent --address ${WRENN_ADVERTISE_ADDR}
Restart=on-failure
RestartSec=5
# File descriptor limits (Firecracker + loop devices + sockets).
LimitNOFILE=65536
LimitNPROC=4096
# Protect host filesystem — only allow access to what's needed.
ProtectHome=true
ReadWritePaths=/var/lib/wrenn /tmp /run/netns /dev/mapper
ReadOnlyPaths=/usr/local/bin/firecracker
[Install]
WantedBy=multi-user.target
UNIT
cat > /etc/systemd/system/wrenn-cp.service << 'UNIT'
[Unit]
Description=Wrenn Control Plane
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=wrenn
Group=wrenn
EnvironmentFile=-/etc/wrenn/cp.env
# Control plane is fully unprivileged — no capabilities needed.
NoNewPrivileges=true
CapabilityBoundingSet=
ExecStart=/usr/local/bin/wrenn-cp
Restart=on-failure
RestartSec=5
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/tmp
[Install]
WantedBy=multi-user.target
UNIT
mkdir -p /etc/wrenn
touch /etc/wrenn/agent.env /etc/wrenn/cp.env
chmod 640 /etc/wrenn/agent.env /etc/wrenn/cp.env
chown root:${WRENN_GROUP} /etc/wrenn/agent.env /etc/wrenn/cp.env
systemctl daemon-reload
echo " wrenn-agent.service and wrenn-cp.service installed."
# ── Done ─────────────────────────────────────────────────────────────────────
echo ""
echo "=== Setup complete ==="
echo ""
echo "Next steps:"
echo " 1. Copy wrenn-agent and wrenn-cp binaries to /usr/local/bin/"
echo " 2. Edit /etc/wrenn/agent.env with WRENN_CP_URL and WRENN_ADVERTISE_ADDR"
echo " 3. Edit /etc/wrenn/cp.env with DATABASE_URL and other control plane config"
echo " 4. systemctl enable --now wrenn-agent"
echo " 5. systemctl enable --now wrenn-cp"
echo ""
echo "Security summary:"
echo " - wrenn user: bash shell (for debugging), no home, no sudo (no grants in sudoers)"
echo " - wrenn-agent: runs as wrenn with 7 capabilities via setcap (not root)"
echo " - wrenn-cp: runs as wrenn with zero capabilities"
echo " - Capabilities auto-restored after apt upgrades via /etc/wrenn/restore-caps.sh"
echo ""

View File

@ -86,7 +86,7 @@ sudo mkfs.ext4 -F "${OUTPUT_FILE}"
# Step 4: Mount and populate.
echo "==> Mounting image at ${MOUNT_DIR}..."
mkdir -p "${MOUNT_DIR}"
sudo mount -o loop "${OUTPUT_FILE}" "${MOUNT_DIR}"
sudo mount -o loop,rw "${OUTPUT_FILE}" "${MOUNT_DIR}"
echo "==> Extracting container filesystem..."
sudo tar xf "${TAR_FILE}" -C "${MOUNT_DIR}"

View File

View File

@ -1,233 +0,0 @@
#!/usr/bin/env bash
#
# test-host.sh — Integration test for the Wrenn host agent.
#
# Prerequisites:
# - Host agent running: sudo ./builds/wrenn-agent
# - Firecracker installed at /usr/local/bin/firecracker
# - Kernel at /var/lib/wrenn/kernels/vmlinux
# - Base rootfs at /var/lib/wrenn/images/minimal.ext4 (with envd + wrenn-init baked in)
#
# Usage:
# ./scripts/test-host.sh [agent_url]
#
# The agent URL defaults to http://localhost:50051.
set -euo pipefail
AGENT="${1:-http://localhost:50051}"
BASE="/hostagent.v1.HostAgentService"
SANDBOX_ID=""
# Colors for output.
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
pass() { echo -e "${GREEN}PASS${NC}: $1"; }
fail() { echo -e "${RED}FAIL${NC}: $1"; exit 1; }
info() { echo -e "${YELLOW}----${NC}: $1"; }
rpc() {
local method="$1"
local body="$2"
curl -s -X POST \
-H "Content-Type: application/json" \
"${AGENT}${BASE}/${method}" \
-d "${body}"
}
# ──────────────────────────────────────────────────
# Test 1: List sandboxes (should be empty)
# ──────────────────────────────────────────────────
info "Test 1: List sandboxes (expect empty)"
RESULT=$(rpc "ListSandboxes" '{}')
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -q '"sandboxes"' || echo "${RESULT}" | grep -q '{}' && \
pass "ListSandboxes returned" || \
fail "ListSandboxes failed"
# ──────────────────────────────────────────────────
# Test 2: Create a sandbox
# ──────────────────────────────────────────────────
info "Test 2: Create a sandbox"
RESULT=$(rpc "CreateSandbox" '{
"template": "minimal",
"vcpus": 1,
"memoryMb": 512,
"timeoutSec": 300
}')
echo " Response: ${RESULT}"
SANDBOX_ID=$(echo "${RESULT}" | python3 -c "import sys,json; print(json.load(sys.stdin)['sandboxId'])" 2>/dev/null) || \
fail "CreateSandbox did not return sandboxId"
echo " Sandbox ID: ${SANDBOX_ID}"
pass "Sandbox created: ${SANDBOX_ID}"
# ──────────────────────────────────────────────────
# Test 3: List sandboxes (should have one)
# ──────────────────────────────────────────────────
info "Test 3: List sandboxes (expect one)"
RESULT=$(rpc "ListSandboxes" '{}')
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -q "${SANDBOX_ID}" && \
pass "Sandbox ${SANDBOX_ID} found in list" || \
fail "Sandbox not found in list"
# ──────────────────────────────────────────────────
# Test 4: Execute a command
# ──────────────────────────────────────────────────
info "Test 4: Execute 'echo hello world'"
RESULT=$(rpc "Exec" "{
\"sandboxId\": \"${SANDBOX_ID}\",
\"cmd\": \"/bin/sh\",
\"args\": [\"-c\", \"echo hello world\"],
\"timeoutSec\": 10
}")
echo " Response: ${RESULT}"
# stdout is base64-encoded in Connect RPC JSON.
STDOUT=$(echo "${RESULT}" | python3 -c "
import sys, json, base64
r = json.load(sys.stdin)
print(base64.b64decode(r['stdout']).decode().strip())
" 2>/dev/null) || fail "Exec did not return stdout"
[ "${STDOUT}" = "hello world" ] && \
pass "Exec returned correct output: '${STDOUT}'" || \
fail "Expected 'hello world', got '${STDOUT}'"
# ──────────────────────────────────────────────────
# Test 5: Execute a multi-line command
# ──────────────────────────────────────────────────
info "Test 5: Execute multi-line command"
RESULT=$(rpc "Exec" "{
\"sandboxId\": \"${SANDBOX_ID}\",
\"cmd\": \"/bin/sh\",
\"args\": [\"-c\", \"echo line1; echo line2; echo line3\"],
\"timeoutSec\": 10
}")
echo " Response: ${RESULT}"
LINE_COUNT=$(echo "${RESULT}" | python3 -c "
import sys, json, base64
r = json.load(sys.stdin)
lines = base64.b64decode(r['stdout']).decode().strip().split('\n')
print(len(lines))
" 2>/dev/null)
[ "${LINE_COUNT}" = "3" ] && \
pass "Multi-line output: ${LINE_COUNT} lines" || \
fail "Expected 3 lines, got ${LINE_COUNT}"
# ──────────────────────────────────────────────────
# Test 6: Pause the sandbox
# ──────────────────────────────────────────────────
info "Test 6: Pause sandbox"
RESULT=$(rpc "PauseSandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
echo " Response: ${RESULT}"
# Verify status is paused.
LIST=$(rpc "ListSandboxes" '{}')
echo "${LIST}" | grep -q '"paused"' && \
pass "Sandbox paused" || \
fail "Sandbox not in paused state"
# ──────────────────────────────────────────────────
# Test 7: Exec should fail while paused
# ──────────────────────────────────────────────────
info "Test 7: Exec while paused (expect error)"
RESULT=$(rpc "Exec" "{
\"sandboxId\": \"${SANDBOX_ID}\",
\"cmd\": \"/bin/echo\",
\"args\": [\"should fail\"]
}")
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -qi "not running\|error\|code" && \
pass "Exec correctly rejected while paused" || \
fail "Exec should have failed while paused"
# ──────────────────────────────────────────────────
# Test 8: Resume the sandbox
# ──────────────────────────────────────────────────
info "Test 8: Resume sandbox"
RESULT=$(rpc "ResumeSandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
echo " Response: ${RESULT}"
# Verify status is running.
LIST=$(rpc "ListSandboxes" '{}')
echo "${LIST}" | grep -q '"running"' && \
pass "Sandbox resumed" || \
fail "Sandbox not in running state"
# ──────────────────────────────────────────────────
# Test 9: Exec after resume
# ──────────────────────────────────────────────────
info "Test 9: Exec after resume"
RESULT=$(rpc "Exec" "{
\"sandboxId\": \"${SANDBOX_ID}\",
\"cmd\": \"/bin/sh\",
\"args\": [\"-c\", \"echo resumed ok\"],
\"timeoutSec\": 10
}")
echo " Response: ${RESULT}"
STDOUT=$(echo "${RESULT}" | python3 -c "
import sys, json, base64
r = json.load(sys.stdin)
print(base64.b64decode(r['stdout']).decode().strip())
" 2>/dev/null) || fail "Exec after resume failed"
[ "${STDOUT}" = "resumed ok" ] && \
pass "Exec after resume works: '${STDOUT}'" || \
fail "Expected 'resumed ok', got '${STDOUT}'"
# ──────────────────────────────────────────────────
# Test 10: Destroy the sandbox
# ──────────────────────────────────────────────────
info "Test 10: Destroy sandbox"
RESULT=$(rpc "DestroySandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
echo " Response: ${RESULT}"
pass "Sandbox destroyed"
# ──────────────────────────────────────────────────
# Test 11: List sandboxes (should be empty again)
# ──────────────────────────────────────────────────
info "Test 11: List sandboxes (expect empty)"
RESULT=$(rpc "ListSandboxes" '{}')
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -q "${SANDBOX_ID}" && \
fail "Destroyed sandbox still in list" || \
pass "Sandbox list is clean"
# ──────────────────────────────────────────────────
# Test 12: Destroy non-existent sandbox (expect error)
# ──────────────────────────────────────────────────
info "Test 12: Destroy non-existent sandbox (expect error)"
RESULT=$(rpc "DestroySandbox" '{"sandboxId": "sb-nonexist"}')
echo " Response: ${RESULT}"
echo "${RESULT}" | grep -qi "not found\|error\|code" && \
pass "Correctly rejected non-existent sandbox" || \
fail "Should have returned error for non-existent sandbox"
echo ""
echo -e "${GREEN}All tests passed!${NC}"

View File

@ -45,7 +45,7 @@ fi
# Step 2: Mount the rootfs.
echo "==> Mounting rootfs at ${MOUNT_DIR}..."
mkdir -p "${MOUNT_DIR}"
sudo mount -o loop "${ROOTFS}" "${MOUNT_DIR}"
sudo mount -o loop,rw "${ROOTFS}" "${MOUNT_DIR}"
cleanup() {
echo "==> Unmounting rootfs..."