-
Notifications
You must be signed in to change notification settings - Fork 10.9k
feat(devcontainer): add separate secure customer profile #10431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fad8095
2f8c6fc
c0b6e22
28575cd
4634277
58ab6db
2a4fef6
82d4853
b2ef8c9
755e0dd
a30dc10
4c169a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 | ||
|
|
||
| ARG TZ | ||
| ARG DEBIAN_FRONTEND=noninteractive | ||
| ARG NODE_MAJOR=22 | ||
| ARG RUST_TOOLCHAIN=1.92.0 | ||
| ARG CODEX_NPM_VERSION=latest | ||
|
|
||
| ENV TZ="$TZ" | ||
|
|
||
| SHELL ["/bin/bash", "-o", "pipefail", "-c"] | ||
|
|
||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends \ | ||
| build-essential \ | ||
| curl \ | ||
| git \ | ||
| ca-certificates \ | ||
| pkg-config \ | ||
| clang \ | ||
| musl-tools \ | ||
| libssl-dev \ | ||
| libsqlite3-dev \ | ||
| just \ | ||
| python3 \ | ||
| python3-pip \ | ||
| jq \ | ||
| less \ | ||
| man-db \ | ||
| unzip \ | ||
| ripgrep \ | ||
| fzf \ | ||
| fd-find \ | ||
| zsh \ | ||
| dnsutils \ | ||
| iproute2 \ | ||
| ipset \ | ||
| iptables \ | ||
| aggregate \ | ||
| && apt-get clean \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \ | ||
| && apt-get update \ | ||
| && apt-get install -y --no-install-recommends nodejs \ | ||
| && npm install -g corepack@latest "@openai/codex@${CODEX_NPM_VERSION}" \ | ||
| && corepack enable \ | ||
| && corepack prepare pnpm@10.28.2 --activate \ | ||
| && apt-get clean \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| COPY .devcontainer/init-firewall.sh /usr/local/bin/init-firewall.sh | ||
| COPY .devcontainer/post_install.py /opt/post_install.py | ||
| COPY .devcontainer/post-start.sh /opt/post_start.sh | ||
|
|
||
| RUN chmod 500 /usr/local/bin/init-firewall.sh \ | ||
| && chmod 755 /opt/post_start.sh \ | ||
| && chmod 644 /opt/post_install.py \ | ||
| && chown vscode:vscode /opt/post_install.py | ||
|
|
||
| RUN install -d -m 0775 -o vscode -g vscode /commandhistory /workspace \ | ||
| && touch /commandhistory/.bash_history /commandhistory/.zsh_history \ | ||
| && chown vscode:vscode /commandhistory/.bash_history /commandhistory/.zsh_history | ||
|
|
||
| USER vscode | ||
| ENV PATH="/home/vscode/.cargo/bin:${PATH}" | ||
| WORKDIR /workspace | ||
|
|
||
| RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain "${RUST_TOOLCHAIN}" \ | ||
| && rustup component add clippy rustfmt rust-src \ | ||
| && rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| { | ||
| "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json", | ||
| "name": "Codex (Secure)", | ||
| "build": { | ||
| "dockerfile": "Dockerfile.secure", | ||
| "context": "..", | ||
| "args": { | ||
| "TZ": "${localEnv:TZ:UTC}", | ||
| "NODE_MAJOR": "22", | ||
| "RUST_TOOLCHAIN": "1.92.0", | ||
| "CODEX_NPM_VERSION": "latest" | ||
| } | ||
| }, | ||
| "runArgs": [ | ||
| "--cap-add=NET_ADMIN", | ||
| "--cap-add=NET_RAW" | ||
| ], | ||
| "init": true, | ||
| "updateRemoteUserUID": true, | ||
| "remoteUser": "vscode", | ||
| "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", | ||
| "workspaceFolder": "/workspace", | ||
| "mounts": [ | ||
| "source=codex-commandhistory-${devcontainerId},target=/commandhistory,type=volume", | ||
| "source=codex-home-${devcontainerId},target=/home/vscode/.codex,type=volume", | ||
| "source=codex-gh-${devcontainerId},target=/home/vscode/.config/gh,type=volume", | ||
| "source=codex-cargo-registry-${devcontainerId},target=/home/vscode/.cargo/registry,type=volume", | ||
| "source=codex-cargo-git-${devcontainerId},target=/home/vscode/.cargo/git,type=volume", | ||
| "source=codex-rustup-${devcontainerId},target=/home/vscode/.rustup,type=volume", | ||
| "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly" | ||
| ], | ||
| "containerEnv": { | ||
| "RUST_BACKTRACE": "1", | ||
| "CODEX_UNSAFE_ALLOW_NO_SANDBOX": "1", | ||
| "CODEX_ENABLE_FIREWALL": "1", | ||
| "CODEX_INCLUDE_GITHUB_META_RANGES": "1", | ||
| "OPENAI_ALLOWED_DOMAINS": "api.openai.com auth.openai.com github.com api.github.com codeload.github.com raw.githubusercontent.com objects.githubusercontent.com crates.io index.crates.io static.crates.io static.rust-lang.org registry.npmjs.org pypi.org files.pythonhosted.org", | ||
| "CARGO_TARGET_DIR": "/workspace/.cache/cargo-target", | ||
| "GIT_CONFIG_GLOBAL": "/home/vscode/.gitconfig.local", | ||
| "COREPACK_ENABLE_DOWNLOAD_PROMPT": "0", | ||
| "PYTHONDONTWRITEBYTECODE": "1", | ||
| "PIP_DISABLE_PIP_VERSION_CHECK": "1" | ||
| }, | ||
| "remoteEnv": { | ||
| "OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}" | ||
| }, | ||
| "postCreateCommand": "python3 /opt/post_install.py", | ||
| "postStartCommand": "bash /opt/post_start.sh", | ||
| "waitFor": "postStartCommand", | ||
| "customizations": { | ||
| "vscode": { | ||
| "settings": { | ||
| "terminal.integrated.defaultProfile.linux": "zsh", | ||
| "terminal.integrated.profiles.linux": { | ||
| "bash": { | ||
| "path": "bash", | ||
| "icon": "terminal-bash" | ||
| }, | ||
| "zsh": { | ||
| "path": "zsh" | ||
| } | ||
| }, | ||
| "files.trimTrailingWhitespace": true, | ||
| "files.insertFinalNewline": true, | ||
| "files.trimFinalNewlines": true | ||
| }, | ||
| "extensions": [ | ||
| "openai.chatgpt", | ||
| "rust-lang.rust-analyzer", | ||
| "tamasfe.even-better-toml", | ||
| "vadimcn.vscode-lldb", | ||
| "ms-azuretools.vscode-docker" | ||
| ] | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
| IFS=$'\n\t' | ||
|
|
||
| allowed_domains_file="/etc/codex/allowed_domains.txt" | ||
| include_github_meta_ranges="${CODEX_INCLUDE_GITHUB_META_RANGES:-1}" | ||
|
|
||
| if [ -f "$allowed_domains_file" ]; then | ||
| mapfile -t allowed_domains < <(sed '/^\s*#/d;/^\s*$/d' "$allowed_domains_file") | ||
| else | ||
| allowed_domains=("api.openai.com") | ||
| fi | ||
|
|
||
| if [ "${#allowed_domains[@]}" -eq 0 ]; then | ||
| echo "ERROR: No allowed domains configured" | ||
| exit 1 | ||
| fi | ||
|
|
||
| add_ipv4_cidr_to_allowlist() { | ||
| local source="$1" | ||
| local cidr="$2" | ||
|
|
||
| if [[ ! "$cidr" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$ ]]; then | ||
| echo "ERROR: Invalid ${source} CIDR range: $cidr" | ||
| exit 1 | ||
| fi | ||
|
|
||
| ipset add allowed-domains "$cidr" -exist | ||
| } | ||
|
|
||
| configure_ipv6_default_deny() { | ||
| if ! command -v ip6tables >/dev/null 2>&1; then | ||
| echo "ERROR: ip6tables is required to enforce IPv6 default-deny policy" | ||
| exit 1 | ||
| fi | ||
|
|
||
| ip6tables -F | ||
| ip6tables -X | ||
| ip6tables -t mangle -F | ||
| ip6tables -t mangle -X | ||
| ip6tables -t nat -F 2>/dev/null || true | ||
| ip6tables -t nat -X 2>/dev/null || true | ||
|
|
||
| ip6tables -A INPUT -i lo -j ACCEPT | ||
| ip6tables -A OUTPUT -o lo -j ACCEPT | ||
| ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | ||
| ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | ||
|
|
||
| ip6tables -P INPUT DROP | ||
| ip6tables -P FORWARD DROP | ||
| ip6tables -P OUTPUT DROP | ||
|
|
||
| echo "IPv6 firewall policy configured (default-deny)" | ||
| } | ||
|
|
||
| # Preserve docker-managed DNS NAT rules before clearing tables. | ||
| docker_dns_rules="$(iptables-save -t nat | grep "127\\.0\\.0\\.11" || true)" | ||
|
|
||
| iptables -F | ||
| iptables -X | ||
| iptables -t nat -F | ||
| iptables -t nat -X | ||
| iptables -t mangle -F | ||
| iptables -t mangle -X | ||
| ipset destroy allowed-domains 2>/dev/null || true | ||
|
|
||
| if [ -n "$docker_dns_rules" ]; then | ||
| echo "Restoring Docker DNS NAT rules" | ||
| iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true | ||
| iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true | ||
| while IFS= read -r rule; do | ||
| [ -z "$rule" ] && continue | ||
| iptables -t nat $rule | ||
| done <<< "$docker_dns_rules" | ||
| fi | ||
|
|
||
| # Allow DNS resolution and localhost communication. | ||
| iptables -A OUTPUT -p udp --dport 53 -j ACCEPT | ||
| iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT | ||
| iptables -A INPUT -p udp --sport 53 -j ACCEPT | ||
| iptables -A INPUT -p tcp --sport 53 -j ACCEPT | ||
| iptables -A INPUT -i lo -j ACCEPT | ||
| iptables -A OUTPUT -o lo -j ACCEPT | ||
|
|
||
| ipset create allowed-domains hash:net | ||
|
|
||
| for domain in "${allowed_domains[@]}"; do | ||
| echo "Resolving $domain" | ||
| ips="$(dig +short A "$domain" | sed '/^\s*$/d')" | ||
| if [ -z "$ips" ]; then | ||
| echo "ERROR: Failed to resolve $domain" | ||
| exit 1 | ||
| fi | ||
|
|
||
| while IFS= read -r ip; do | ||
| if [[ ! "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then | ||
| echo "ERROR: Invalid IPv4 address from DNS for $domain: $ip" | ||
| exit 1 | ||
| fi | ||
| ipset add allowed-domains "$ip" -exist | ||
| done <<< "$ips" | ||
| done | ||
|
|
||
| if [ "$include_github_meta_ranges" = "1" ]; then | ||
| echo "Fetching GitHub meta ranges" | ||
| github_meta="$(curl -fsSL --connect-timeout 10 https://api.github.com/meta)" | ||
|
|
||
| if ! echo "$github_meta" | jq -e '.web and .api and .git' >/dev/null; then | ||
| echo "ERROR: GitHub meta response missing expected fields" | ||
| exit 1 | ||
| fi | ||
|
|
||
| while IFS= read -r cidr; do | ||
| [ -z "$cidr" ] && continue | ||
| if [[ "$cidr" == *:* ]]; then | ||
| # Current policy enforces IPv4-only ipset entries. | ||
| continue | ||
| fi | ||
| add_ipv4_cidr_to_allowlist "GitHub" "$cidr" | ||
| done < <(echo "$github_meta" | jq -r '((.web // []) + (.api // []) + (.git // []))[]' | sort -u) | ||
| fi | ||
|
|
||
| host_ip="$(ip route | awk '/default/ {print $3; exit}')" | ||
| 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/')" | ||
| iptables -A INPUT -s "$host_network" -j ACCEPT | ||
| iptables -A OUTPUT -d "$host_network" -j ACCEPT | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The rule Useful? React with 👍 / 👎. |
||
|
|
||
| iptables -P INPUT DROP | ||
| iptables -P FORWARD DROP | ||
| iptables -P OUTPUT DROP | ||
|
|
||
| iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | ||
| iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT | ||
| iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT | ||
|
viyatb-oai marked this conversation as resolved.
|
||
|
|
||
| # Reject rather than silently drop to make policy failures obvious. | ||
| iptables -A INPUT -j REJECT --reject-with icmp-admin-prohibited | ||
| iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited | ||
| iptables -A FORWARD -j REJECT --reject-with icmp-admin-prohibited | ||
|
|
||
| configure_ipv6_default_deny | ||
|
|
||
| echo "Firewall configuration complete" | ||
|
|
||
| if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then | ||
| echo "ERROR: Firewall verification failed - was able to reach https://example.com" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! curl --connect-timeout 5 https://api.openai.com >/dev/null 2>&1; then | ||
| echo "ERROR: Firewall verification failed - unable to reach https://api.openai.com" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ "$include_github_meta_ranges" = "1" ] && ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then | ||
| echo "ERROR: Firewall verification failed - unable to reach https://api.github.com" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if curl --connect-timeout 5 -6 https://example.com >/dev/null 2>&1; then | ||
| echo "ERROR: Firewall verification failed - was able to reach https://example.com over IPv6" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Firewall verification passed" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allowing outbound DNS on port 53 to any destination (
OUTPUT ... --dport 53) creates an unfiltered channel to arbitrary IPs. This bypasses the domain allowlist model and lets traffic leave even when non-allowlisted destinations are blocked. DNS should be constrained to Docker's resolver (or explicit trusted resolvers).Useful? React with 👍 / 👎.