From 9c4fea93bc3b0c486207bebc94fa664b4cde9f4d Mon Sep 17 00:00:00 2001 From: pptx704 Date: Thu, 16 Apr 2026 16:56:04 +0600 Subject: [PATCH] Added host preparation script and updated claude md --- CLAUDE.md | 6 +- scripts/prepare-wrenn-user.sh | 385 ++++++++++++++++++++++++++++++++++ 2 files changed, 389 insertions(+), 2 deletions(-) create mode 100755 scripts/prepare-wrenn-user.sh diff --git a/CLAUDE.md b/CLAUDE.md index c104a7b..33b1f06 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ User SDK → HTTPS/WS → Control Plane → Connect RPC → Host Agent → HTTP/ | Binary | Module | Entry point | Runs as | |--------|--------|-------------|---------| | wrenn-cp | `git.omukk.dev/wrenn/wrenn` | `cmd/control-plane/main.go` | Unprivileged | -| wrenn-agent | `git.omukk.dev/wrenn/wrenn` | `cmd/host-agent/main.go` | Root (NET_ADMIN + /dev/kvm) | +| wrenn-agent | `git.omukk.dev/wrenn/wrenn` | `cmd/host-agent/main.go` | `wrenn` user with capabilities (SYS_ADMIN, NET_ADMIN, NET_RAW, SYS_PTRACE, KILL, DAC_OVERRIDE, MKNOD) via setcap; also accepts root | | envd | `git.omukk.dev/wrenn/wrenn/envd` (standalone `envd/go.mod`) | `envd/main.go` | PID 1 inside guest VM | envd is a **completely independent Go module**. It is never imported by the main module. The only connection is the protobuf contract. It compiles to a static binary baked into rootfs images. @@ -86,7 +86,9 @@ Startup (`cmd/control-plane/main.go`) is a thin wrapper: `cpserver.Run(cpserver. **Packages:** `internal/hostagent/`, `internal/sandbox/`, `internal/vm/`, `internal/network/`, `internal/devicemapper/`, `internal/envdclient/`, `internal/snapshot/` -Startup (`cmd/host-agent/main.go`) wires: root check → enable IP forwarding → clean up stale dm devices → `sandbox.Manager` (containing `vm.Manager` + `network.SlotAllocator` + `devicemapper.LoopRegistry`) → `hostagent.Server` (Connect RPC handler) → HTTP server. +**Production deployment:** `scripts/prepare-wrenn-user.sh` creates the `wrenn` system user, sets Linux capabilities (setcap) on wrenn-agent and all child binaries (iptables, losetup, dmsetup, etc.), installs an apt hook to restore capabilities after package updates, configures udev rules for `/dev/net/tun`, loads required kernel modules, and writes systemd unit files for both services. No sudo grants — all privilege is via capabilities. + +Startup (`cmd/host-agent/main.go`) wires: root/capabilities check → enable IP forwarding → clean up stale dm devices → `sandbox.Manager` (containing `vm.Manager` + `network.SlotAllocator` + `devicemapper.LoopRegistry`) → `hostagent.Server` (Connect RPC handler) → HTTP server. - **RPC Server** (`internal/hostagent/server.go`): implements `hostagentv1connect.HostAgentServiceHandler`. Thin wrapper — every method delegates to `sandbox.Manager`. Maps Connect error codes on return. - **Sandbox Manager** (`internal/sandbox/manager.go`): the core orchestration layer. Maintains in-memory state in `boxes map[string]*sandboxState` (protected by `sync.RWMutex`). Each `sandboxState` holds a `models.Sandbox`, a `*network.Slot`, and an `*envdclient.Client`. Runs a TTL reaper (every 10s) that auto-destroys timed-out sandboxes. diff --git a/scripts/prepare-wrenn-user.sh b/scripts/prepare-wrenn-user.sh new file mode 100755 index 0000000..c48eef0 --- /dev/null +++ b/scripts/prepare-wrenn-user.sh @@ -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 ""