Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .devcontainer/dc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Wrapper around the devcontainers CLI that selects the container runtime via
# FG_CONTAINER_RUNTIME (default: docker). This keeps devcontainer.json runtime-
# neutral and lets the same pixi tasks work with Docker/Colima on Mac and
# rootless Podman on Linux. Set FG_CONTAINER_RUNTIME=podman to opt into Podman.
set -euo pipefail

runtime="${FG_CONTAINER_RUNTIME:-docker}"
sub="$1"
shift

docker_path="$runtime"
extra=()

if [[ "$runtime" == "podman" ]]; then
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Route podman through our shim so host-user -> vscode (uid 1000) mapping is
# applied (see podman-shim/podman). Must keep the basename `podman` so the
# CLI still detects the Podman runtime.
docker_path="$script_dir/podman-shim/podman"
# Disable the CLI's "renumber remote user to host uid" step on `up`: it would
# need an unsafe ~1M subuid range on this shared host. The shim handles the
# mapping instead. (--update-remote-user-uid-default is only valid for `up`.)
if [[ "$sub" == "up" ]]; then
extra+=(--update-remote-user-uid-default never)
fi
fi

exec npx @devcontainers/cli "$sub" \
--docker-path "$docker_path" \
${extra[@]+"${extra[@]}"} \
"$@"
35 changes: 17 additions & 18 deletions .devcontainer/init-firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ else
echo "No Docker DNS rules to restore"
fi

# First allow DNS and localhost before any restrictions
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p udp --sport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
# Allow DNS, but ONLY to the resolvers configured in /etc/resolv.conf -- not to
# any host. This closes "send DNS to an arbitrary external resolver" tunneling.
# (Outbound SSH is intentionally NOT opened wholesale: SSH to GitHub still works
# because GitHub's ranges are in the allowed-domains set below, while SSH to any
# other host is blocked.)
while read -r ns; do
[[ "$ns" =~ ^[0-9.]+$ ]] || continue
echo "Allowing DNS to resolver $ns"
iptables -A OUTPUT -p udp -d "$ns" --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp -d "$ns" --dport 53 -j ACCEPT
done < <(awk '/^nameserver/ {print $2}' /etc/resolv.conf)

# Allow localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

Expand Down Expand Up @@ -111,19 +119,10 @@ for domain in "${ALLOWED_DOMAINS[@]}"; do
done < <(echo "$ips")
done

# Get host IP from default route
HOST_IP=$(ip route | grep default | cut -d" " -f3)
if [ -z "$HOST_IP" ]; then
echo "ERROR: Failed to detect host IP"
exit 1
fi

HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
echo "Host network detected as: $HOST_NETWORK"

# Set up remaining iptables rules
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
# Note: the host's local /24 subnet is intentionally NOT allowed wholesale.
# Reaching the host or specific internal services should go through an entry in
# the allowed-domains set above, not blanket subnet access (which enables
# lateral movement to neighboring machines).

# Set default policies to DROP
iptables -P INPUT DROP
Expand Down
29 changes: 29 additions & 0 deletions .devcontainer/podman-shim/podman
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Podman shim, used ONLY on the rootless-Podman path (wired up by dc.sh via
# --docker-path). It must be named `podman` so the devcontainers CLI still
# detects the Podman runtime.
#
# Why this exists: the CLI forces `--userns=keep-id` on `podman run`, which maps
# the host user onto the SAME uid inside the container (e.g. 990465). We disable
# the CLI's uid-renumber step because making it succeed would require a subuid
# range of ~1,000,000 ids, which on this shared LDAP host would overlap real
# users' uids. Instead we upgrade the flag to keep-id:uid=1000,gid=1000 so the
# host user maps onto the container's vscode user (uid/gid 1000). Bind-mounted
# files then appear owned by vscode and are writable, using only the existing
# small subuid range. Every other invocation passes straight through.
set -euo pipefail

PODMAN_BIN="${FG_PODMAN_BIN:-/usr/bin/podman}"

if [[ "${1:-}" == "run" ]]; then
rewritten=()
for arg in "$@"; do
case "$arg" in
--userns=keep-id) arg="--userns=keep-id:uid=1000,gid=1000" ;;
esac
rewritten+=("$arg")
done
exec "$PODMAN_BIN" "${rewritten[@]}"
fi

exec "$PODMAN_BIN" "$@"
10 changes: 10 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ if ! command -v codex &> /dev/null; then
pixi run npm install -g @openai/codex
fi

# Lock down the firewall: now that setup is complete, revoke the vscode user's
# passwordless sudo. The agent runs as unprivileged vscode, which cannot touch
# iptables/ipset without root, so the egress allowlist can no longer be flushed
# or bypassed from inside the container. NET_ADMIN/NET_RAW remain in the image
# but are unusable without root, so this neutralizes them for the agent.
# For maintenance you can still get a root shell from the HOST side:
# podman exec -u root <container> bash (or: docker exec -u root ...)
echo "Revoking in-container sudo to lock the firewall..."
sudo rm -f /etc/sudoers.d/vscode

echo ""
echo "=========================================="
echo "Dev container setup complete!"
Expand Down
4 changes: 2 additions & 2 deletions pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ dev-launch-secure = "python fileglancer/dev_launch.py"
migrate = "alembic -c fileglancer/alembic.ini upgrade head"
migrate-create = "alembic -c fileglancer/alembic.ini revision --autogenerate"
stamp-db = "python -m fileglancer.stamp_db"
container-rebuild = "npx @devcontainers/cli up --workspace-folder . --remove-existing-container"
container-shell = "npx @devcontainers/cli exec --workspace-folder . bash"
container-claude = "npx @devcontainers/cli exec --workspace-folder . bash -lc 'pixi run claude --permission-mode auto'"
container-codex = "npx @devcontainers/cli exec --workspace-folder . bash -lc 'pixi run codex --dangerously-bypass-approvals-and-sandbox'"
container-rebuild = "bash .devcontainer/dc.sh up --workspace-folder . --remove-existing-container"
container-shell = "bash .devcontainer/dc.sh exec --workspace-folder . bash"
container-claude = "bash .devcontainer/dc.sh exec --workspace-folder . bash -lc 'pixi run claude --permission-mode auto'"
container-codex = "bash .devcontainer/dc.sh exec --workspace-folder . bash -lc 'pixi run codex --dangerously-bypass-approvals-and-sandbox'"
claude = "claude"
codex = "codex"

Expand Down
Loading