Skip to content

Commit fcc0cb1

Browse files
committed
Multiplatform and sandboxing
1 parent 5bcfd26 commit fcc0cb1

7 files changed

Lines changed: 244 additions & 108 deletions

File tree

Dockerfile renamed to .devcontainer/Dockerfile

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
FROM --platform=linux/amd64 python:3.12-slim AS linux-base
1+
FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04 AS linux-base
2+
3+
ARG TARGETARCH
24

35
# Utilities
46
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends build-essential \
5-
sudo curl git htop less rsync screen vim nano wget ca-certificates openssh-client zsh procps psmisc
7+
sudo curl git htop less rsync screen vim nano wget ca-certificates openssh-client zsh procps psmisc \
8+
iptables ipset iproute2 dnsutils aggregate jq
69

710
# Download and install VS Code Server CLI
8-
RUN wget -O /tmp/vscode-server-cli.tar.gz "https://update.code.visualstudio.com/latest/cli-linux-x64/stable" && \
11+
RUN ARCH="$TARGETARCH" && [ "$ARCH" = "amd64" ] && ARCH="x64"; \
12+
wget -O /tmp/vscode-server-cli.tar.gz "https://update.code.visualstudio.com/latest/cli-linux-${ARCH}/stable" && \
913
mkdir -p /usr/local/bin && \
1014
tar -xf /tmp/vscode-server-cli.tar.gz -C /usr/local/bin && \
1115
rm /tmp/vscode-server-cli.tar.gz
@@ -17,7 +21,8 @@ RUN COMMANDS="sacct sacctmgr salloc sattach sbatch sbcast scancel scontrol sdiag
1721
&& chmod +x "/usr/local/bin/$CMD"; done
1822

1923
# Install Claude Code
20-
RUN curl -fsSL https://nodejs.org/dist/v18.20.8/node-v18.20.8-linux-x64.tar.xz \
24+
RUN ARCH="$TARGETARCH" && [ "$ARCH" = "amd64" ] && ARCH="x64"; \
25+
curl -fsSL https://nodejs.org/dist/v18.20.8/node-v18.20.8-linux-${ARCH}.tar.xz \
2126
| tar -xJ -C /usr/local --strip-components=1
2227
RUN npm install -g @anthropic-ai/claude-code
2328

@@ -43,3 +48,15 @@ RUN --mount=type=cache,target=/root/.cache/uv \
4348
--mount=type=bind,source=uv.lock,target=uv.lock \
4449
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
4550
uv sync --frozen --no-install-project --all-groups
51+
52+
# Non-root user
53+
RUN useradd -m -s /bin/bash devuser \
54+
&& echo 'alias claude="claude --dangerously-skip-permissions"' >> /home/devuser/.bashrc
55+
56+
# Firewall init script
57+
COPY .devcontainer/init-firewall.sh /usr/local/bin/init-firewall.sh
58+
RUN chmod +x /usr/local/bin/init-firewall.sh \
59+
&& echo "devuser ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/devuser-firewall \
60+
&& chmod 0440 /etc/sudoers.d/devuser-firewall
61+
62+
USER devuser
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
{
2-
"dockerFile": "Dockerfile",
3-
"build": {"args": {"BUILDPLATFORM": "linux/amd64"}},
4-
"workspaceFolder": "/srv/repo",
2+
"build": {
3+
"dockerfile": "Dockerfile",
4+
"context": ".."
5+
},
6+
"runArgs": [
7+
"--cap-add=NET_ADMIN",
8+
"--cap-add=NET_RAW"
9+
],
10+
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh",
11+
"workspaceFolder": "/srv/repo",
512
"workspaceMount": "source=${localWorkspaceFolder},target=/srv/repo,type=bind",
613
"customizations": {
714
"vscode": {
@@ -13,7 +20,7 @@
1320
"tamasfe.even-better-toml",
1421
"ms-azuretools.vscode-docker",
1522
"ms-toolsai.jupyter",
16-
"anthropic.claude-code"
23+
"anthropic.claude-code"
1724
],
1825
"settings": {
1926
"[python]": {
@@ -26,4 +33,4 @@
2633
}
2734
}
2835
}
29-
}
36+
}

.devcontainer/init-firewall.sh

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
IFS=$'\n\t'
4+
5+
# 1. Extract Docker DNS info BEFORE any flushing
6+
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)
7+
8+
# Flush existing rules and delete existing ipsets
9+
iptables -F
10+
iptables -X
11+
iptables -t nat -F
12+
iptables -t nat -X
13+
iptables -t mangle -F
14+
iptables -t mangle -X
15+
ipset destroy allowed-domains 2>/dev/null || true
16+
17+
# 2. Selectively restore ONLY internal Docker DNS resolution
18+
if [ -n "$DOCKER_DNS_RULES" ]; then
19+
echo "Restoring Docker DNS rules..."
20+
iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
21+
iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
22+
echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
23+
else
24+
echo "No Docker DNS rules to restore"
25+
fi
26+
27+
# Allow DNS, SSH, and loopback before any restrictions
28+
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
29+
iptables -A INPUT -p udp --sport 53 -j ACCEPT
30+
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
31+
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
32+
iptables -A INPUT -i lo -j ACCEPT
33+
iptables -A OUTPUT -o lo -j ACCEPT
34+
35+
# Create ipset with CIDR support
36+
ipset create allowed-domains hash:net
37+
38+
# Fetch GitHub meta and add their IP ranges
39+
echo "Fetching GitHub IP ranges..."
40+
gh_ranges=$(curl -s https://api.github.com/meta)
41+
if [ -z "$gh_ranges" ]; then
42+
echo "ERROR: Failed to fetch GitHub IP ranges"
43+
exit 1
44+
fi
45+
if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
46+
echo "ERROR: GitHub API response missing required fields"
47+
exit 1
48+
fi
49+
echo "Processing GitHub IPs..."
50+
while read -r cidr; do
51+
if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
52+
echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
53+
exit 1
54+
fi
55+
echo "Adding GitHub range $cidr"
56+
ipset add allowed-domains "$cidr"
57+
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)
58+
59+
# Resolve and add allowed domains
60+
for domain in \
61+
"api.anthropic.com" \
62+
"sentry.io" \
63+
"statsig.anthropic.com" \
64+
"statsig.com" \
65+
"registry.npmjs.org" \
66+
"pypi.org" \
67+
"files.pythonhosted.org" \
68+
"storage.googleapis.com" \
69+
"oauth2.googleapis.com" \
70+
"accounts.google.com" \
71+
"dl.google.com"; do
72+
echo "Resolving $domain..."
73+
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
74+
if [ -z "$ips" ]; then
75+
echo "ERROR: Failed to resolve $domain"
76+
exit 1
77+
fi
78+
while read -r ip; do
79+
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
80+
echo "ERROR: Invalid IP from DNS for $domain: $ip"
81+
exit 1
82+
fi
83+
echo "Adding $ip for $domain"
84+
ipset add allowed-domains "$ip" 2>/dev/null || true # ignore duplicates
85+
done < <(echo "$ips")
86+
done
87+
88+
# Allow host network
89+
HOST_IP=$(ip route | grep default | cut -d" " -f3)
90+
if [ -z "$HOST_IP" ]; then
91+
echo "ERROR: Failed to detect host IP"
92+
exit 1
93+
fi
94+
HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
95+
echo "Host network detected as: $HOST_NETWORK"
96+
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
97+
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
98+
99+
# Set default DROP policies
100+
iptables -P INPUT DROP
101+
iptables -P FORWARD DROP
102+
iptables -P OUTPUT DROP
103+
104+
# Allow established/related connections
105+
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
106+
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
107+
108+
# Allow outbound to whitelisted IPs
109+
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
110+
111+
# Reject everything else with feedback
112+
iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited
113+
114+
echo "Firewall configuration complete"
115+
116+
# Verify: blocked
117+
if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
118+
echo "ERROR: Firewall verification failed - reached https://example.com"
119+
exit 1
120+
else
121+
echo "Firewall verification passed - example.com is blocked"
122+
fi
123+
124+
# Verify: allowed
125+
if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
126+
echo "ERROR: Firewall verification failed - cannot reach https://api.github.com"
127+
exit 1
128+
else
129+
echo "Firewall verification passed - api.github.com is reachable"
130+
fi

.dockerignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
*
2-
!/Dockerfile
32
!/pyproject.toml
43
!/uv.lock
4+
!.devcontainer/init-firewall.sh

.github/copilot-instructions.md

Lines changed: 0 additions & 68 deletions
This file was deleted.
Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,20 @@
11
name: Docker
22

3-
# This workflow uses actions that are not certified by GitHub.
4-
# They are provided by a third-party and are governed by
5-
# separate terms of service, privacy policy, and support
6-
# documentation.
7-
83
on:
94
push:
10-
# Publish semver tags as releases.
115
tags: [ 'v*.*.*' ]
126

137
env:
14-
# Use docker.io for Docker Hub if empty
158
REGISTRY: ghcr.io
16-
# github.repository as <account>/<repo>
179
IMAGE_NAME: ${{ github.repository }}
1810

19-
2011
jobs:
2112
build:
2213

2314
runs-on: ubuntu-latest
2415
permissions:
2516
contents: read
2617
packages: write
27-
# This is used to complete the identity challenge
28-
# with sigstore/fulcio when running outside of PRs.
29-
id-token: write
3018

3119
steps:
3220
- name: Checkout repository
@@ -35,22 +23,12 @@ jobs:
3523
- name: Clean up disk space
3624
uses: jlumbroso/free-disk-space@main
3725

38-
# Install the cosign tool except on PR
39-
# https://github.com/sigstore/cosign-installer
40-
- name: Install cosign
41-
if: github.event_name != 'pull_request'
42-
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
43-
with:
44-
cosign-release: 'v2.2.4'
26+
- name: Set up QEMU
27+
uses: docker/setup-qemu-action@v3
4528

46-
# Set up BuildKit Docker container builder to be able to build
47-
# multi-platform images and export cache
48-
# https://github.com/docker/setup-buildx-action
4929
- name: Set up Docker Buildx
5030
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
5131

52-
# Login against a Docker registry except on PR
53-
# https://github.com/docker/login-action
5432
- name: Log into registry ${{ env.REGISTRY }}
5533
if: github.event_name != 'pull_request'
5634
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
@@ -59,21 +37,19 @@ jobs:
5937
username: ${{ github.actor }}
6038
password: ${{ secrets.GITHUB_TOKEN }}
6139

62-
# Extract metadata (tags, labels) for Docker
63-
# https://github.com/docker/metadata-action
6440
- name: Extract Docker metadata
6541
id: meta
6642
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
6743
with:
6844
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
6945

70-
# Build and push Docker image with Buildx (don't push on PR)
71-
# https://github.com/docker/build-push-action
7246
- name: Build and push Docker image
7347
id: build-and-push
7448
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
7549
with:
7650
context: .
51+
file: .devcontainer/Dockerfile
52+
platforms: linux/amd64,linux/arm64
7753
push: ${{ github.event_name != 'pull_request' }}
7854
tags: ${{ steps.meta.outputs.tags }}
7955
labels: ${{ steps.meta.outputs.labels }}
@@ -96,13 +72,12 @@ jobs:
9672
run: |
9773
echo "repository=${GITHUB_REPOSITORY@L}" >> $GITHUB_OUTPUT
9874
99-
# Build and push .sif files for Apptainer
10075
- name: Setup Apptainer
10176
uses: eWaterCycle/setup-apptainer@v2
10277
- name: Build and push Apptainer
10378
env:
10479
TAGS: ${{ steps.meta.outputs.tags }}
10580
run: |
10681
echo ${{ secrets.GITHUB_TOKEN }} | apptainer registry login -u ${{ secrets.GHCR_USERNAME }} --password-stdin docker://ghcr.io
107-
apptainer build container.sif docker://${{ env.REGISTRY }}/${{ steps.lower-repo.outputs.repository }}:latest
82+
apptainer build --arch amd64 container.sif docker://${{ env.REGISTRY }}/${{ steps.lower-repo.outputs.repository }}:latest
10883
echo "${TAGS}" | xargs -I {} apptainer push container.sif oras://{}-sif

0 commit comments

Comments
 (0)