Pulls the 5 privileged helpers out of deploy/files/usr/local/{libexec,sbin}/
into top-level scripts/{libexec,sbin}/. They are application-inherent code
(invoked at runtime via sudo from l4d2host/l4d2web), not deploy artifacts —
the previous nesting under deploy/files/ confused source-of-truth with
install-target FHS layout.
deploy/ now means "reference exemplar": README explaining the target
layout, plus example sudoers / sysctl / sandbox-resolv.conf / env
templates / curated systemd units (the ones ckn-bw's reactor emits).
Anyone building a fresh deployment (other than ckn-bw) reads this tree.
Dead static artifacts deleted: left4me-apply-cake helper, left4me-cake
+ left4me-nft-mark service units, cake.env, left4me-mark.nft, and the
superseded deploy-test-server.sh installer.
Tests split to match the new shape:
- scripts/tests/{test_overlay,test_script_sandbox,test_systemctl_helper,
test_journalctl_helper,test_helpers_use_fixed_paths,test_sudoers_grants}.py
with shared fixtures in conftest.py
- deploy/tests/test_example_units.py (renamed from test_deploy_artifacts.py)
— slimmed to lock down the curated example units, sysctl, env templates
l4d2host/tests/test_overlay_helper.py: helper-source path updated to
scripts/libexec/left4me-overlay (was building the path segment-by-segment
under deploy/files/, missed by the path-prefix grep during pre-flight).
Runtime install-target paths (/usr/local/{libexec,sbin}/) unchanged, so
l4d2host/service_control.py, l4d2web/services/overlay_builders.py, the
sudoers grants, and the systemd units all keep their existing path
references.
Requires the matching ckn-bw change to bundles/left4me/items.py
(install_left4me_scripts repointed from /opt/left4me/src/deploy/files/...
to /opt/left4me/src/scripts/...). Left4me lands first so a fresh
git_deploy exposes the new source path before the bundle apply runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
5.1 KiB
Bash
Executable file
109 lines
5.1 KiB
Bash
Executable file
#!/bin/bash
|
|
# Privileged sandbox launcher for left4me script overlays.
|
|
#
|
|
# Invoked via sudo by the web user with two arguments:
|
|
# <overlay_id> numeric overlay id; bind-mounts /var/lib/left4me/overlays/<id>
|
|
# read-write at /overlay inside the sandbox.
|
|
# <script_path> absolute path to a bash file already written by the web app;
|
|
# bind-mounted read-only at /script.sh inside the sandbox.
|
|
#
|
|
# The script runs as a transient systemd .service with the full hardening
|
|
# surface: cgroup limits + walltime kill, NoNewPrivileges, ProtectSystem,
|
|
# ProtectHome, kernel-tunable / -module / -log protection, namespace
|
|
# restriction, address-family restriction, capability bounding (empty),
|
|
# seccomp filter (@system-service @network-io), MemoryDenyWriteExecute,
|
|
# LockPersonality, RestrictSUIDSGID. Network namespace is *not* restricted —
|
|
# scripts must reach the public internet to download workshop / l4d2center
|
|
# / cedapug content. PID namespace is shared with the host (no
|
|
# PrivatePID= directive in systemd); host PIDs are visible via /proc but
|
|
# not signal-able due to UID mismatch.
|
|
set -euo pipefail
|
|
|
|
# Self-wrap into PID 1's mount namespace before doing anything mount-related.
|
|
# The web app's left4me-web.service has PrivateTmp=true, which gives it a
|
|
# private mount namespace. When the worker invokes us via sudo, we inherit
|
|
# that namespace; our `mount --bind` would land there. systemd-run below
|
|
# spawns transient units in PID 1's namespace (where they don't see the
|
|
# private bind), so the sandbox would bind onto an empty staging dir and
|
|
# permission-deny on every write. The sentinel env var avoids an exec loop.
|
|
if [[ "${L4D2_SANDBOX_IN_PID1_MNT_NS:-}" != "1" ]]; then
|
|
exec env L4D2_SANDBOX_IN_PID1_MNT_NS=1 \
|
|
/usr/bin/nsenter --mount=/proc/1/ns/mnt -- "$0" "$@"
|
|
fi
|
|
|
|
[[ $# -eq 2 ]] || { echo "usage: $0 <overlay_id> <script>" >&2; exit 64; }
|
|
|
|
OVERLAY_ID=$1
|
|
SCRIPT=$2
|
|
|
|
[[ "$OVERLAY_ID" =~ ^[0-9]+$ ]] || { echo "bad overlay id" >&2; exit 64; }
|
|
OVERLAY_DIR=/var/lib/left4me/overlays/$OVERLAY_ID
|
|
[[ -d $OVERLAY_DIR ]] || { echo "no overlay dir at $OVERLAY_DIR" >&2; exit 65; }
|
|
[[ -f $SCRIPT ]] || { echo "no script at $SCRIPT" >&2; exit 65; }
|
|
|
|
if [[ "${LEFT4ME_SCRIPT_SANDBOX_DRY_RUN:-}" == "1" ]]; then
|
|
echo "DRY RUN: overlay_id=$OVERLAY_ID script=$SCRIPT overlay_dir=$OVERLAY_DIR"
|
|
exit 0
|
|
fi
|
|
|
|
# Pre-create an idmapped bind of the overlay dir, then point the sandbox's
|
|
# BindPaths at that staging path. The bind translates the sandbox's writing
|
|
# uid (l4d2-sandbox) back to left4me on disk, so all overlay content
|
|
# (script-built and workshop) is uniformly left4me-owned. Map direction:
|
|
# `--map-users=<disk_uid>:<mount_uid>:1` with disk=left4me, mount=sandbox —
|
|
# a process inside the bind with uid sandbox sees its uid as itself, and
|
|
# writes get translated to disk-uid left4me. Verified on kernel 6.12 that
|
|
# idmap propagates through systemd-run's plain re-bind of the staging path.
|
|
LEFT4ME_UID=$(id -u left4me)
|
|
LEFT4ME_GID=$(id -g left4me)
|
|
SANDBOX_UID=$(id -u l4d2-sandbox)
|
|
SANDBOX_GID=$(id -g l4d2-sandbox)
|
|
STAGING=/var/lib/left4me/tmp/sandbox-idmap-${OVERLAY_ID}
|
|
|
|
# trap fires even on errors / signals so the staging bind doesn't outlive
|
|
# this invocation. Idempotent if the staging is already gone.
|
|
cleanup_staging() {
|
|
umount "$STAGING" 2>/dev/null || true
|
|
rmdir "$STAGING" 2>/dev/null || true
|
|
}
|
|
trap cleanup_staging EXIT
|
|
|
|
# A leftover staging mount from a SIGKILLed prior run can be reset by
|
|
# umounting first, then re-binding fresh on the same path.
|
|
umount "$STAGING" 2>/dev/null || true
|
|
mkdir -p "$STAGING"
|
|
mount --bind \
|
|
--map-users="${LEFT4ME_UID}:${SANDBOX_UID}:1" \
|
|
--map-groups="${LEFT4ME_GID}:${SANDBOX_GID}:1" \
|
|
"$OVERLAY_DIR" "$STAGING"
|
|
|
|
SCRIPT_RC=0
|
|
systemd-run --quiet --collect --wait --pipe \
|
|
--unit="left4me-script-${OVERLAY_ID}-$$" \
|
|
--slice=l4d2-build.slice \
|
|
-p OOMScoreAdjust=500 \
|
|
-p User=l4d2-sandbox -p Group=l4d2-sandbox \
|
|
-p UMask=0022 \
|
|
-p NoNewPrivileges=yes \
|
|
-p ProtectSystem=strict -p ProtectHome=yes \
|
|
-p PrivateTmp=yes -p PrivateDevices=yes -p PrivateIPC=yes \
|
|
-p ProtectKernelTunables=yes -p ProtectKernelModules=yes \
|
|
-p ProtectKernelLogs=yes -p ProtectControlGroups=yes \
|
|
-p RestrictNamespaces=yes \
|
|
-p RestrictAddressFamilies="AF_INET AF_INET6 AF_UNIX" \
|
|
-p RestrictSUIDSGID=yes -p LockPersonality=yes \
|
|
-p MemoryDenyWriteExecute=yes \
|
|
-p SystemCallFilter="@system-service @network-io" \
|
|
-p SystemCallArchitectures=native \
|
|
-p CapabilityBoundingSet= -p AmbientCapabilities= \
|
|
-p IPAddressDeny="127.0.0.0/8 ::1/128 169.254.0.0/16 fe80::/10 224.0.0.0/4 ff00::/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fc00::/7" \
|
|
-p TemporaryFileSystem="/etc /var/lib" \
|
|
-p BindReadOnlyPaths="/etc/left4me/sandbox-resolv.conf:/etc/resolv.conf /etc/ssl /etc/ca-certificates /etc/nsswitch.conf /etc/alternatives ${SCRIPT}:/script.sh" \
|
|
-p BindPaths="${STAGING}:/overlay" \
|
|
-p WorkingDirectory=/overlay \
|
|
-p Environment="HOME=/tmp PATH=/usr/bin:/usr/sbin OVERLAY=/overlay" \
|
|
-p MemoryMax=4G -p MemorySwapMax=0 -p TasksMax=512 \
|
|
-p CPUQuota=200% -p RuntimeMaxSec=3600 \
|
|
-- /bin/bash /script.sh || SCRIPT_RC=$?
|
|
|
|
exit $SCRIPT_RC
|