mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 08:02:19 +00:00
feat: add custom sandbox Dockerfile with sudo support
- Add Dockerfile.sandbox with sudo, writable passwd/shadow, and entrypoint that registers dynamic UIDs for full root access inside container - Remove playwright MCP server (npx not available in sandbox) - Move sandbox host_commands/image config to global workmux config - Remove git from host_commands to prevent infinite fork bomb via shims Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,6 @@
|
||||
"svelte": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.svelte.dev/mcp"
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -45,4 +45,4 @@ files:
|
||||
sandbox:
|
||||
enabled: false
|
||||
toolchain: off
|
||||
host_commands: ["cargo", "npm", "npx", "node", "git"]
|
||||
# image and host_commands configured in global ~/.config/workmux/config.yaml
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
git \
|
||||
iptables \
|
||||
gosu \
|
||||
sudo \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "ALL ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/sandbox \
|
||||
&& chmod 0440 /etc/sudoers.d/sandbox \
|
||||
&& chmod 666 /etc/passwd /etc/group /etc/shadow
|
||||
|
||||
# Embed network init script (sets up iptables firewall, then drops privileges)
|
||||
RUN cat <<'SCRIPT' > /usr/local/bin/network-init.sh
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${WM_PROXY_HOST:-}" ] && [ -n "${WM_PROXY_PORT:-}" ]; then
|
||||
# Resolve hostnames to ALL IPs (multi-A records, round-robin DNS)
|
||||
PROXY_IPS=$(getent ahostsv4 "$WM_PROXY_HOST" | awk '{print $1}' | sort -u)
|
||||
RPC_HOST="${WM_RPC_HOST:-$WM_PROXY_HOST}"
|
||||
RPC_IPS=$(getent ahostsv4 "$RPC_HOST" | awk '{print $1}' | sort -u)
|
||||
|
||||
if [ -z "$PROXY_IPS" ] || [ -z "$RPC_IPS" ]; then
|
||||
echo "network-init: failed to resolve proxy/RPC host" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# IPv4: default deny outbound
|
||||
iptables -P OUTPUT DROP
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# Allow DNS (UDP/TCP 53) to configured nameservers.
|
||||
# Without this, any hostname resolution hangs until timeout.
|
||||
if [ -f /etc/resolv.conf ]; then
|
||||
grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | while read -r ns; do
|
||||
iptables -A OUTPUT -d "$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -A OUTPUT -d "$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
fi
|
||||
|
||||
# Allow ALL resolved proxy IPs (handles multi-A DNS)
|
||||
for ip in $PROXY_IPS; do
|
||||
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_PROXY_PORT" -j ACCEPT
|
||||
done
|
||||
|
||||
# Allow ALL resolved RPC IPs
|
||||
if [ -n "${WM_RPC_PORT:-}" ]; then
|
||||
for ip in $RPC_IPS; do
|
||||
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_RPC_PORT" -j ACCEPT
|
||||
done
|
||||
fi
|
||||
|
||||
# Reject (not drop) everything else to fail fast instead of hanging
|
||||
iptables -A OUTPUT -j REJECT
|
||||
|
||||
# IPv6: block entirely to prevent leaks (fail closed)
|
||||
if ip6tables -L -n >/dev/null 2>&1; then
|
||||
ip6tables -P OUTPUT DROP
|
||||
ip6tables -A OUTPUT -o lo -j ACCEPT
|
||||
ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
ip6tables -A OUTPUT -j REJECT
|
||||
else
|
||||
# If ip6tables unavailable, disable IPv6 via sysctl as fallback
|
||||
if ! sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null; then
|
||||
echo "network-init: failed to block IPv6 (neither ip6tables nor sysctl available)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add sandbox user to passwd so sudo works with arbitrary UIDs.
|
||||
if ! getent passwd "${WM_TARGET_UID}" >/dev/null 2>&1; then
|
||||
echo "sandbox:x:${WM_TARGET_UID}:${WM_TARGET_GID}:sandbox:/tmp:/bin/sh" >> /etc/passwd
|
||||
echo "sandbox:x:${WM_TARGET_GID}:" >> /etc/group 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Fix PTY ownership so the unprivileged user can read/write the terminal.
|
||||
# Docker allocates the PTY as root; after gosu drops privileges, the user
|
||||
# has no access to /dev/pts/0 unless we transfer ownership.
|
||||
if [ -t 0 ]; then
|
||||
chown "${WM_TARGET_UID}:${WM_TARGET_GID}" "$(tty)"
|
||||
fi
|
||||
|
||||
# Drop privileges and exec the user command.
|
||||
# gosu resets HOME via getpwuid() which returns "/" for UIDs not in /etc/passwd.
|
||||
# Preserve the container's HOME=/tmp so agents find their config dirs.
|
||||
exec gosu "${WM_TARGET_UID}:${WM_TARGET_GID}" env HOME=/tmp "$@"
|
||||
SCRIPT
|
||||
RUN chmod +x /usr/local/bin/network-init.sh
|
||||
|
||||
# Install workmux (needed for sandbox RPC)
|
||||
RUN curl -fsSL https://raw.githubusercontent.com/raine/workmux/main/scripts/install.sh | bash
|
||||
|
||||
# --- claude agent ---
|
||||
# Install Claude Code, then move to a globally accessible path so the binary
|
||||
# is reachable when running as an arbitrary non-root UID (--user uid:gid).
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash && \
|
||||
target="$(readlink -f /root/.local/bin/claude)" && \
|
||||
mv /root/.local/share/claude /opt/claude && \
|
||||
ln -s "/opt/claude/versions/$(basename "$target")" /usr/local/bin/claude && \
|
||||
# Claude Code checks $HOME/.local/bin/claude when installMethod is "native".
|
||||
# Container sets HOME=/tmp, so place a symlink to suppress the warning.
|
||||
mkdir -p /tmp/.local/bin && \
|
||||
ln -s /usr/local/bin/claude /tmp/.local/bin/claude && \
|
||||
chmod 777 /tmp/.local
|
||||
|
||||
# Entrypoint that registers the dynamic UID in /etc/passwd so sudo works.
|
||||
# workmux passes --user UID:GID directly, bypassing network-init.sh.
|
||||
RUN cat <<'ENTRY' > /usr/local/bin/entrypoint.sh
|
||||
#!/bin/sh
|
||||
if ! getent passwd "$(id -u)" >/dev/null 2>&1; then
|
||||
echo "sandbox:x:$(id -u):$(id -g):sandbox:/tmp:/bin/sh" >> /etc/passwd
|
||||
fi
|
||||
if ! getent group "$(id -g)" >/dev/null 2>&1; then
|
||||
echo "sandbox:x:$(id -g):" >> /etc/group
|
||||
fi
|
||||
if ! grep -q "^sandbox:" /etc/shadow 2>/dev/null; then
|
||||
echo "sandbox:*:19000:0:99999:7:::" >> /etc/shadow
|
||||
fi
|
||||
exec "$@"
|
||||
ENTRY
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
Reference in New Issue
Block a user