55def3732e
test / stage-firecracker-inputs (pull_request) Successful in 2s
test / integration-docker (pull_request) Successful in 32s
test / unit (pull_request) Successful in 36s
lint / lint (push) Failing after 52s
test / build-infra (pull_request) Successful in 4m5s
tracker-policy-pr / check-pr (pull_request) Successful in 8s
test / integration-firecracker (pull_request) Successful in 2m2s
test / coverage (pull_request) Successful in 2m7s
test / publish-infra (pull_request) Has been skipped
Podman avoids the CAP_SYS_ADMIN requirement that makes rootless Docker impossible here: with no subordinate UID range configured it falls back to a single-UID self-mapping, which an unprivileged process may write itself, so newuidmap is never invoked. The image build therefore strips /etc/subuid and /etc/subgid entries rather than adding them. The agent-facing surface is unchanged — docker and docker compose talk to podman's Docker-compatible API socket. Two device nodes need relaxing as root inside the bottle (0666 on /dev/fuse and /dev/net/tun); both already exist and neither needs a capability the bottle lacks, unlike CAP_SYS_ADMIN. Verified live on macOS 26 / Apple Container 1.0.0: service starts with zero added capabilities, compose pulls and serves from the workspace on a published port, and a nested container cannot reach the network outside the egress path. Known limitation, not yet addressed: the agent base image is Debian bookworm, whose podman 4.3.1 swallows container exit codes through the compat API (docker run returns 0 regardless). podman 5.4.2 on trixie fixes it; the base bump is a separate PR. The acceptance test asserts on an in-band marker rather than an exit code so it cannot false-pass in the meantime. Nested containers give no isolation from the agent itself — root inside one is the agent user outside it. They are a build/test convenience; the bottle remains the security boundary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnMp8SUGH57hZX7192Rv2F
90 lines
2.7 KiB
Bash
Executable File
90 lines
2.7 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
uid="$(id -u)"
|
|
if [ "$uid" -eq 0 ]; then
|
|
echo "refusing to run rootless podman as root" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for command in podman docker fuse-overlayfs slirp4netns; do
|
|
command -v "$command" >/dev/null 2>&1 || {
|
|
echo "missing rootless podman prerequisite: $command" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
# The inverse of the rootless-Docker check, and the whole point of the podman
|
|
# variant: a subordinate range would push podman onto newuidmap, which cannot
|
|
# write a multi-range uid_map without CAP_SYS_ADMIN in this guest. An empty
|
|
# range keeps it on the single-UID self-mapping an unprivileged process may
|
|
# write itself.
|
|
if grep -q "^$(id -un):" /etc/subuid 2>/dev/null; then
|
|
echo "unexpected subordinate UID range for $(id -un): podman would" >&2
|
|
echo "require CAP_SYS_ADMIN via newuidmap in this guest" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for device in /dev/fuse /dev/net/tun; do
|
|
[ -r "$device" ] && [ -w "$device" ] || {
|
|
echo "device $device is not readable/writable by $(id -un)" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp/bot-bottle-podman-run}"
|
|
config="$HOME/.config/containers"
|
|
mkdir -p "$XDG_RUNTIME_DIR" "$config"
|
|
chmod 700 "$XDG_RUNTIME_DIR"
|
|
|
|
# ignore_chown_errors is required, not incidental: with a single-UID mapping
|
|
# there is no second UID for image layers to be chowned to, so layers that
|
|
# record other owners would otherwise fail to extract.
|
|
cat > "$config/storage.conf" <<'CONF'
|
|
[storage]
|
|
driver="overlay"
|
|
[storage.options.overlay]
|
|
mount_program="/usr/bin/fuse-overlayfs"
|
|
ignore_chown_errors="true"
|
|
CONF
|
|
|
|
# No cgroup delegation reaches this guest, so asking podman to manage cgroups
|
|
# fails; events_logger=file avoids the journald socket that is equally absent.
|
|
cat > "$config/containers.conf" <<'CONF'
|
|
[containers]
|
|
cgroups="disabled"
|
|
[engine]
|
|
cgroup_manager="cgroupfs"
|
|
events_logger="file"
|
|
CONF
|
|
|
|
# Registry pulls egress through the bottle's proxy like everything else. The
|
|
# token-bearing proxy URL is already in the agent's environment; persisting it
|
|
# inside this disposable VM does not broaden its authority.
|
|
python3 - <<'PY'
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy", "")
|
|
no_proxy = os.environ.get("NO_PROXY") or os.environ.get("no_proxy", "")
|
|
config = {"proxies": {"default": {
|
|
"httpProxy": proxy,
|
|
"httpsProxy": proxy,
|
|
"noProxy": no_proxy,
|
|
}}}
|
|
path = Path.home() / ".docker" / "config.json"
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(config), encoding="utf-8")
|
|
path.chmod(0o600)
|
|
PY
|
|
|
|
if docker info >/dev/null 2>&1; then
|
|
exit 0
|
|
fi
|
|
|
|
log=/tmp/bot-bottle-rootless-podman.log
|
|
nohup podman system service --time=0 \
|
|
"unix://$XDG_RUNTIME_DIR/podman.sock" \
|
|
>"$log" 2>&1 </dev/null &
|