Runs OpenCode 2 with full permissions while containing its mistakes: - egress filter: squid in a container per project; squid.conf is rendered from layered egress.conf files (package, user, project, local) with allow/block/connect rules and expect-pass/expect-block self-tests - netlock: nftables rejects direct egress for the opencode-sandbox group (installed once with sudo as a systemd unit) - bubblewrap: read-only host, writable project, worktrees and state; credentials hidden; ssh keys via the host's ssh-agent; session bus replaced by an xdg-dbus-proxy limited to notifications; no permission prompts (sudo and systemd-run denied); refuses to run unsandboxed - per-project state in ~/.local/share/opencode-sandbox/<name> - worktrees: `wt add|rm|list` with project hooks - skills mounted per project; the opencode-sandbox skill explains the environment to the agent - plugins as submodules: context-panel, skill-compact-resurrect; global config adds opencode-notifier and cc-safety-net - README in English and Russian
90 lines
3.1 KiB
Bash
Executable File
90 lines
3.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Netlock, run as root (opencode-sandbox netlock setup|teardown calls it through sudo).
|
|
# Processes whose primary group is GROUP may only talk to loopback (where the egress filter
|
|
# listens) and to DNS servers from resolv.conf; everything else is rejected. The rules are
|
|
# loaded by a systemd unit and survive reboots.
|
|
# netlock-root.sh setup <group> <user>
|
|
# netlock-root.sh teardown
|
|
set -euo pipefail
|
|
|
|
TABLE=opencode_sandbox
|
|
CONF_DIR=/etc/opencode-sandbox
|
|
RULES="${CONF_DIR}/netlock.nft"
|
|
UNIT=opencode-sandbox-netlock.service
|
|
UNIT_PATH="/etc/systemd/system/${UNIT}"
|
|
|
|
[[ ${EUID} -eq 0 ]] || { echo "must run as root" >&2; exit 1; }
|
|
|
|
setup() {
|
|
local group=$1 user=${2:-} gid ns dns_rules=""
|
|
getent group "${group}" >/dev/null || groupadd "${group}"
|
|
gid=$(getent group "${group}" | cut -d: -f3)
|
|
if [[ -n ${user} ]] && ! id -nG "${user}" | tr ' ' '\n' | grep -qx "${group}"; then
|
|
usermod -aG "${group}" "${user}"
|
|
echo " ${user} added to group ${group} (log out and in, or use the new group via sg)"
|
|
fi
|
|
|
|
# The loopback resolver (systemd-resolved) is covered by the loopback rule; non-local
|
|
# nameservers are allowed one by one, not port 53 anywhere
|
|
while read -r ns; do
|
|
case "${ns}" in
|
|
127.* | ::1) ;;
|
|
*:*) dns_rules+=" meta skgid ${gid} ip6 daddr ${ns} meta l4proto { tcp, udp } th dport 53 accept"$'\n' ;;
|
|
*) dns_rules+=" meta skgid ${gid} ip daddr ${ns} meta l4proto { tcp, udp } th dport 53 accept"$'\n' ;;
|
|
esac
|
|
done < <(awk '$1 == "nameserver" { print $2 }' /etc/resolv.conf)
|
|
|
|
mkdir -p "${CONF_DIR}"
|
|
cat >"${RULES}" <<EOF
|
|
# Generated by opencode-sandbox (lib/netlock-root.sh) — edit the script, not this file.
|
|
table inet ${TABLE} {
|
|
chain output {
|
|
type filter hook output priority 0; policy accept;
|
|
meta skgid ${gid} ip daddr 127.0.0.0/8 accept
|
|
meta skgid ${gid} ip6 daddr ::1 accept
|
|
${dns_rules} meta skgid ${gid} reject with icmpx admin-prohibited
|
|
}
|
|
}
|
|
EOF
|
|
chmod 644 "${RULES}"
|
|
|
|
cat >"${UNIT_PATH}" <<EOF
|
|
[Unit]
|
|
Description=opencode-sandbox netlock (nftables, group ${group})
|
|
Wants=network-pre.target
|
|
Before=network-pre.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStartPre=-/usr/sbin/nft delete table inet ${TABLE}
|
|
ExecStart=/usr/sbin/nft -f ${RULES}
|
|
ExecStop=/usr/sbin/nft delete table inet ${TABLE}
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
systemctl daemon-reload
|
|
systemctl enable "${UNIT}" >/dev/null 2>&1
|
|
systemctl restart "${UNIT}"
|
|
echo "✓ netlock: group ${group} (gid ${gid}), unit ${UNIT} enabled"
|
|
echo " allowed: 127.0.0.0/8, ::1${dns_rules:+, DNS servers from resolv.conf}"
|
|
}
|
|
|
|
teardown() {
|
|
if systemctl list-unit-files "${UNIT}" >/dev/null 2>&1; then
|
|
systemctl disable --now "${UNIT}" >/dev/null 2>&1 || true
|
|
rm -f "${UNIT_PATH}"
|
|
systemctl daemon-reload
|
|
fi
|
|
nft list table inet "${TABLE}" >/dev/null 2>&1 && nft delete table inet "${TABLE}"
|
|
rm -rf "${CONF_DIR}"
|
|
echo "✓ netlock removed (unit ${UNIT}, table ${TABLE}); the group is kept"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
setup) shift; setup "$@" ;;
|
|
teardown) teardown ;;
|
|
*) echo "usage: $0 setup <group> [user] | teardown" >&2; exit 1 ;;
|
|
esac
|