forked from wrenn/wrenn
v0.2.0 (#50)
Co-authored-by: Tasnim Kabir Sadik <tksadik@omukk.dev> Reviewed-on: wrenn/wrenn#50
This commit is contained in:
120
scripts/cleanup-stale.sh
Executable file
120
scripts/cleanup-stale.sh
Executable file
@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Clean up leftover wrenn host state from crashed/unclean agent exits.
|
||||
# Removes: cloud-hypervisor procs, CH sockets, dm-snapshot devices,
|
||||
# loop devices backing sandbox CoW files AND base rootfs images,
|
||||
# network namespaces, veth interfaces, iptables rules, sandbox CoW
|
||||
# files (optional).
|
||||
#
|
||||
# Does NOT touch: /var/lib/wrenn/images/* files themselves,
|
||||
# /var/lib/wrenn/kernels/*, snapshot directories (cl-*/ with state.json).
|
||||
#
|
||||
# WARNING: base rootfs image loop devices are detached unconditionally.
|
||||
# Run only when no wrenn-agent is alive — a live agent holds those.
|
||||
#
|
||||
# Sudo is invoked per-command. Script itself runs as normal user.
|
||||
#
|
||||
# Usage: bash scripts/cleanup-stale.sh [--delete-cow]
|
||||
|
||||
set -u
|
||||
|
||||
DELETE_COW=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--delete-cow) DELETE_COW=1 ;;
|
||||
-h|--help) sed -n '2,13p' "$0"; exit 0 ;;
|
||||
*) echo "unknown flag: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
SANDBOX_DIR=/var/lib/wrenn/sandboxes
|
||||
|
||||
log() { printf '[cleanup] %s\n' "$*"; }
|
||||
|
||||
# Prime sudo once so subsequent calls don't re-prompt.
|
||||
sudo -v || { echo "sudo required" >&2; exit 1; }
|
||||
|
||||
# 1. Kill leftover cloud-hypervisor processes.
|
||||
# Match via -f because the proc comm is truncated to 15 chars ("cloud-hypervisor" is 16).
|
||||
if pgrep -f '/cloud-hypervisor( |$)' >/dev/null; then
|
||||
log "killing cloud-hypervisor procs"
|
||||
sudo pkill -TERM -f '/cloud-hypervisor( |$)' || true
|
||||
for _ in 1 2 3 4 5; do
|
||||
pgrep -f '/cloud-hypervisor( |$)' >/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
sudo pkill -KILL -f '/cloud-hypervisor( |$)' 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# 2. Remove stale CH API sockets.
|
||||
for sock in /tmp/ch-*.sock; do
|
||||
[[ -e "$sock" ]] || continue
|
||||
log "rm $sock"
|
||||
sudo rm -f "$sock"
|
||||
done
|
||||
|
||||
# 3. Remove all dm-snapshot devices with wrenn- prefix.
|
||||
while read -r name _; do
|
||||
[[ -z "$name" || "$name" == "No" ]] && continue
|
||||
case "$name" in
|
||||
wrenn-*)
|
||||
log "dmsetup remove $name"
|
||||
sudo dmsetup remove --retry "$name" || sudo dmsetup remove --force "$name" || true
|
||||
;;
|
||||
esac
|
||||
done < <(sudo dmsetup ls --target snapshot 2>/dev/null)
|
||||
|
||||
# 4. Detach loop devices backing sandbox CoW files and base rootfs images.
|
||||
IMAGES_DIR=/var/lib/wrenn/images
|
||||
while IFS= read -r line; do
|
||||
dev=${line%%:*}
|
||||
backing=${line#*(} # strip up to first '('
|
||||
backing=${backing%)} # strip trailing ')'
|
||||
backing=${backing% (deleted)} # strip kernel '(deleted)' marker
|
||||
case "$backing" in
|
||||
"$SANDBOX_DIR"/*.cow|"$IMAGES_DIR"/*)
|
||||
log "losetup -d $dev ($backing)"
|
||||
sudo losetup -d "$dev" || true
|
||||
;;
|
||||
esac
|
||||
done < <(losetup -a)
|
||||
|
||||
# 5. Tear down wrenn network namespaces + host veth.
|
||||
if [[ -d /run/netns ]]; then
|
||||
for ns in /run/netns/wrenn-ns-*; do
|
||||
[[ -e "$ns" ]] || continue
|
||||
name=$(basename "$ns")
|
||||
idx=${name#wrenn-ns-}
|
||||
veth="wrenn-veth-$idx"
|
||||
log "deleting netns $name and host veth $veth"
|
||||
sudo ip link del "$veth" 2>/dev/null || true
|
||||
sudo ip netns del "$name" || true
|
||||
done
|
||||
fi
|
||||
|
||||
# 6. Strip any remaining wrenn-veth interfaces.
|
||||
for link in $(ip -o link show | awk -F': ' '{print $2}' | cut -d@ -f1 | grep '^wrenn-veth-' || true); do
|
||||
log "ip link del $link"
|
||||
sudo ip link del "$link" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# 7. Remove host iptables rules referencing wrenn-veth interfaces.
|
||||
for table in filter nat; do
|
||||
sudo iptables-save -t "$table" 2>/dev/null | grep 'wrenn-veth-' | while read -r line; do
|
||||
[[ "$line" == -A* ]] || continue
|
||||
log "iptables -t $table -D ${line#-A }"
|
||||
# shellcheck disable=SC2086
|
||||
sudo iptables -t "$table" -D ${line#-A } 2>/dev/null || true
|
||||
done
|
||||
done
|
||||
|
||||
# 8. Optionally delete sandbox CoW files.
|
||||
if (( DELETE_COW )); then
|
||||
for f in "$SANDBOX_DIR"/*.cow; do
|
||||
[[ -e "$f" ]] || continue
|
||||
log "rm $f"
|
||||
sudo rm -f "$f"
|
||||
done
|
||||
fi
|
||||
|
||||
log "done"
|
||||
@ -1,385 +0,0 @@
|
||||
#!/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 ""
|
||||
@ -38,7 +38,9 @@ IMAGE_NAME="$2"
|
||||
OUTPUT_DIR="${WRENN_IMAGES_PATH}/${IMAGE_NAME}"
|
||||
OUTPUT_FILE="${OUTPUT_DIR}/rootfs.ext4"
|
||||
MOUNT_DIR="/tmp/wrenn-rootfs-build"
|
||||
TAR_FILE="/tmp/wrenn-rootfs-export-${IMAGE_NAME}.tar"
|
||||
# IMAGE_NAME may contain slashes (e.g. teams/<team>/<id>); flatten them so the
|
||||
# temp tar is a single file in /tmp rather than a path into a missing dir.
|
||||
TAR_FILE="/tmp/wrenn-rootfs-export-${IMAGE_NAME//\//_}.tar"
|
||||
|
||||
# Verify the container exists.
|
||||
if ! docker inspect "${CONTAINER}" > /dev/null 2>&1; then
|
||||
@ -121,16 +123,24 @@ if [ -z "${TINI_BIN}" ]; then
|
||||
aarch64) TINI_ARCH="arm64" ;;
|
||||
*) echo "ERROR: Unsupported architecture: ${ARCH}"; exit 1 ;;
|
||||
esac
|
||||
# Use the statically linked tini so the binary runs regardless of the
|
||||
# guest's libc (glibc on Ubuntu/Arch/Fedora, musl on Alpine).
|
||||
TINI_VERSION="v0.19.0"
|
||||
TINI_URL="https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TINI_ARCH}"
|
||||
TINI_TMP="/tmp/tini-${TINI_ARCH}"
|
||||
echo " Downloading tini ${TINI_VERSION} (${TINI_ARCH})..."
|
||||
TINI_URL="https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-${TINI_ARCH}"
|
||||
TINI_TMP="/tmp/tini-static-${TINI_ARCH}"
|
||||
echo " Downloading tini ${TINI_VERSION} static (${TINI_ARCH})..."
|
||||
curl -fsSL "${TINI_URL}" -o "${TINI_TMP}"
|
||||
chmod +x "${TINI_TMP}"
|
||||
TINI_BIN="${TINI_TMP}"
|
||||
fi
|
||||
sudo mkdir -p "${MOUNT_DIR}/sbin"
|
||||
sudo cp "${TINI_BIN}" "${MOUNT_DIR}/sbin/tini"
|
||||
# On usr-merged distros (e.g. Fedora) /sbin is a symlink to /usr/bin, so a tini
|
||||
# already at /usr/bin/tini IS /sbin/tini — copying onto itself errors. Skip then.
|
||||
if [ "${TINI_BIN}" -ef "${MOUNT_DIR}/sbin/tini" ]; then
|
||||
echo " tini already at /sbin/tini (usr-merged); skipping copy"
|
||||
else
|
||||
sudo cp "${TINI_BIN}" "${MOUNT_DIR}/sbin/tini"
|
||||
fi
|
||||
sudo chmod 755 "${MOUNT_DIR}/sbin/tini"
|
||||
|
||||
# Step 6: Verify injected binaries and required container packages.
|
||||
@ -141,7 +151,7 @@ ls -la "${MOUNT_DIR}/usr/local/bin/envd" "${MOUNT_DIR}/usr/local/bin/wrenn-init"
|
||||
echo ""
|
||||
echo "==> Checking required container packages..."
|
||||
MISSING_PKGS=""
|
||||
for bin in socat chronyd curl git; do
|
||||
for bin in socat chronyd chronyc curl git; do
|
||||
if ! find "${MOUNT_DIR}" -name "${bin}" -type f 2>/dev/null | head -1 | grep -q .; then
|
||||
MISSING_PKGS="${MISSING_PKGS} ${bin}"
|
||||
fi
|
||||
|
||||
@ -1,32 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# update-debug-rootfs.sh — Build envd and inject it (plus wrenn-init + tini) into the debug rootfs.
|
||||
# update-minimal-rootfs.sh — Rebuild envd and inject it (plus wrenn-init + tini)
|
||||
# into the system base rootfs images.
|
||||
#
|
||||
# This script:
|
||||
# 1. Builds a fresh envd static binary via make
|
||||
# 2. Mounts the rootfs image
|
||||
# 3. Copies envd, wrenn-init, and tini into the image
|
||||
# 4. Unmounts cleanly
|
||||
# 1. Builds a fresh envd static binary via make (once)
|
||||
# 2. For each system base rootfs (ubuntu/alpine/arch/fedora): mounts it,
|
||||
# copies envd + wrenn-init + tini in, and unmounts cleanly
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/update-debug-rootfs.sh [rootfs_path]
|
||||
# bash scripts/update-minimal-rootfs.sh [rootfs_path]
|
||||
#
|
||||
# Defaults to /var/lib/wrenn/images/minimal/rootfs.ext4
|
||||
# With no argument it updates all four system base rootfs images under
|
||||
# ${WRENN_DIR}/images/teams/<platform>/<id>/rootfs.ext4
|
||||
# With a path argument it updates only that single rootfs.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
WRENN_DIR="${WRENN_DIR:-/var/lib/wrenn}"
|
||||
ROOTFS="${1:-${WRENN_DIR}/images/minimal/rootfs.ext4}"
|
||||
MOUNT_DIR="/tmp/wrenn-rootfs-update"
|
||||
|
||||
if [ ! -f "${ROOTFS}" ]; then
|
||||
echo "ERROR: Rootfs not found at ${ROOTFS}"
|
||||
exit 1
|
||||
# base36(all-zeros UUID) = platform team that owns every system base template.
|
||||
PLATFORM_TEAM_B36="0000000000000000000000000"
|
||||
|
||||
# System base template IDs (well-known reserved IDs 0..3). Single-digit IDs, so
|
||||
# the 25-char base36 string is just the zero-padded decimal.
|
||||
SYSTEM_TEMPLATE_IDS=(0 1 2 3)
|
||||
|
||||
# Resolve which rootfs images to update.
|
||||
ROOTFS_LIST=()
|
||||
if [ $# -ge 1 ]; then
|
||||
ROOTFS_LIST=("$1")
|
||||
else
|
||||
for tid in "${SYSTEM_TEMPLATE_IDS[@]}"; do
|
||||
tmpl_b36="$(printf '%025d' "${tid}")"
|
||||
ROOTFS_LIST+=("${WRENN_DIR}/images/teams/${PLATFORM_TEAM_B36}/${tmpl_b36}/rootfs.ext4")
|
||||
done
|
||||
fi
|
||||
|
||||
# Step 1: Build envd.
|
||||
# Step 1: Build envd (once).
|
||||
echo "==> Building envd..."
|
||||
cd "${PROJECT_ROOT}"
|
||||
make build-envd
|
||||
@ -42,64 +56,84 @@ if ! ldd "${ENVD_BIN}" | grep -q "statically linked"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Mount the rootfs.
|
||||
echo "==> Mounting rootfs at ${MOUNT_DIR}..."
|
||||
mkdir -p "${MOUNT_DIR}"
|
||||
sudo mount -o loop,rw "${ROOTFS}" "${MOUNT_DIR}"
|
||||
|
||||
cleanup() {
|
||||
echo "==> Unmounting rootfs..."
|
||||
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
|
||||
rmdir "${MOUNT_DIR}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Step 3: Copy files into rootfs.
|
||||
echo "==> Installing envd..."
|
||||
sudo mkdir -p "${MOUNT_DIR}/usr/local/bin"
|
||||
sudo cp "${ENVD_BIN}" "${MOUNT_DIR}/usr/local/bin/envd"
|
||||
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/envd"
|
||||
|
||||
echo "==> Installing wrenn-init..."
|
||||
sudo cp "${PROJECT_ROOT}/images/wrenn-init.sh" "${MOUNT_DIR}/usr/local/bin/wrenn-init"
|
||||
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/wrenn-init"
|
||||
|
||||
echo "==> Installing tini..."
|
||||
TINI_BIN=""
|
||||
# 1. Already in the rootfs?
|
||||
for p in "${MOUNT_DIR}/usr/bin/tini" "${MOUNT_DIR}/sbin/tini" "${MOUNT_DIR}/usr/local/bin/tini"; do
|
||||
if [ -f "$p" ]; then TINI_BIN="$p"; break; fi
|
||||
done
|
||||
# 2. Available on the host?
|
||||
if [ -z "${TINI_BIN}" ]; then
|
||||
for p in /usr/bin/tini /usr/local/bin/tini /sbin/tini; do
|
||||
if [ -f "$p" ]; then TINI_BIN="$p"; break; fi
|
||||
# resolve_tini ROOTFS_MOUNT — echo a path to a tini binary suitable for the
|
||||
# mounted rootfs. Prefers one already in the image, then a static download.
|
||||
resolve_tini() {
|
||||
local mount_dir="$1" p tini_arch arch
|
||||
for p in "${mount_dir}/usr/bin/tini" "${mount_dir}/sbin/tini" "${mount_dir}/usr/local/bin/tini"; do
|
||||
if [ -f "$p" ]; then echo "$p"; return; fi
|
||||
done
|
||||
fi
|
||||
# 3. Download from GitHub releases.
|
||||
if [ -z "${TINI_BIN}" ]; then
|
||||
ARCH="$(uname -m)"
|
||||
case "${ARCH}" in
|
||||
x86_64) TINI_ARCH="amd64" ;;
|
||||
aarch64) TINI_ARCH="arm64" ;;
|
||||
*) echo "ERROR: Unsupported architecture: ${ARCH}"; exit 1 ;;
|
||||
arch="$(uname -m)"
|
||||
case "${arch}" in
|
||||
x86_64) tini_arch="amd64" ;;
|
||||
aarch64) tini_arch="arm64" ;;
|
||||
*) echo "ERROR: Unsupported architecture: ${arch}" >&2; exit 1 ;;
|
||||
esac
|
||||
TINI_VERSION="v0.19.0"
|
||||
TINI_URL="https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TINI_ARCH}"
|
||||
TINI_TMP="/tmp/tini-${TINI_ARCH}"
|
||||
echo " Downloading tini ${TINI_VERSION} (${TINI_ARCH})..."
|
||||
curl -fsSL "${TINI_URL}" -o "${TINI_TMP}"
|
||||
chmod +x "${TINI_TMP}"
|
||||
TINI_BIN="${TINI_TMP}"
|
||||
# Static tini runs under any libc (glibc or musl).
|
||||
local tmp="/tmp/tini-static-${tini_arch}"
|
||||
if [ ! -f "${tmp}" ]; then
|
||||
echo " Downloading tini v0.19.0 static (${tini_arch})..." >&2
|
||||
curl -fsSL "https://github.com/krallin/tini/releases/download/v0.19.0/tini-static-${tini_arch}" -o "${tmp}"
|
||||
chmod +x "${tmp}"
|
||||
fi
|
||||
echo "${tmp}"
|
||||
}
|
||||
|
||||
# inject_rootfs ROOTFS — mount, copy guest binaries in, unmount.
|
||||
inject_rootfs() {
|
||||
local rootfs="$1" tini_bin
|
||||
echo ""
|
||||
echo "==> Updating ${rootfs}"
|
||||
|
||||
mkdir -p "${MOUNT_DIR}"
|
||||
sudo mount -o loop,rw "${rootfs}" "${MOUNT_DIR}"
|
||||
|
||||
local mounted=1
|
||||
cleanup_mount() {
|
||||
if [ "${mounted}" = "1" ]; then
|
||||
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
|
||||
rmdir "${MOUNT_DIR}" 2>/dev/null || true
|
||||
mounted=0
|
||||
fi
|
||||
}
|
||||
trap cleanup_mount RETURN
|
||||
|
||||
sudo mkdir -p "${MOUNT_DIR}/usr/local/bin"
|
||||
sudo cp "${ENVD_BIN}" "${MOUNT_DIR}/usr/local/bin/envd"
|
||||
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/envd"
|
||||
|
||||
sudo cp "${PROJECT_ROOT}/images/wrenn-init.sh" "${MOUNT_DIR}/usr/local/bin/wrenn-init"
|
||||
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/wrenn-init"
|
||||
|
||||
tini_bin="$(resolve_tini "${MOUNT_DIR}")"
|
||||
sudo mkdir -p "${MOUNT_DIR}/sbin"
|
||||
# On usr-merged distros (e.g. Fedora) /sbin -> /usr/bin, so a tini already at
|
||||
# /usr/bin/tini IS /sbin/tini — copying onto itself errors. Skip then.
|
||||
if [ "${tini_bin}" -ef "${MOUNT_DIR}/sbin/tini" ]; then
|
||||
echo " tini already at /sbin/tini (usr-merged); skipping copy"
|
||||
else
|
||||
sudo cp "${tini_bin}" "${MOUNT_DIR}/sbin/tini"
|
||||
fi
|
||||
sudo chmod 755 "${MOUNT_DIR}/sbin/tini"
|
||||
|
||||
ls -la "${MOUNT_DIR}/usr/local/bin/envd" "${MOUNT_DIR}/usr/local/bin/wrenn-init" "${MOUNT_DIR}/sbin/tini"
|
||||
cleanup_mount
|
||||
}
|
||||
|
||||
# Step 2: Update each rootfs that exists.
|
||||
UPDATED=0
|
||||
for rootfs in "${ROOTFS_LIST[@]}"; do
|
||||
if [ ! -f "${rootfs}" ]; then
|
||||
echo "==> Skipping (not found): ${rootfs}"
|
||||
continue
|
||||
fi
|
||||
inject_rootfs "${rootfs}"
|
||||
UPDATED=$((UPDATED + 1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [ "${UPDATED}" -eq 0 ]; then
|
||||
echo "==> No rootfs images updated. Build them first with: make images"
|
||||
exit 1
|
||||
fi
|
||||
sudo mkdir -p "${MOUNT_DIR}/sbin"
|
||||
sudo cp "${TINI_BIN}" "${MOUNT_DIR}/sbin/tini"
|
||||
sudo chmod 755 "${MOUNT_DIR}/sbin/tini"
|
||||
|
||||
# Step 4: Verify.
|
||||
echo ""
|
||||
echo "==> Installed files:"
|
||||
ls -la "${MOUNT_DIR}/usr/local/bin/envd" "${MOUNT_DIR}/usr/local/bin/wrenn-init" "${MOUNT_DIR}/sbin/tini"
|
||||
|
||||
echo ""
|
||||
echo "==> Done. Rootfs updated: ${ROOTFS}"
|
||||
echo "==> Done. Updated ${UPDATED} rootfs image(s)."
|
||||
|
||||
Reference in New Issue
Block a user