Harden devcontainer: pin digests, guard SYS_ADMIN, fix mount filters (#20)

Address review feedback from #14–#19:

- Pin base image and uv with SHA256 digests (#15)
- Install fzf from GitHub releases instead of apt (#14)
- Add SYS_ADMIN capability guard to prevent remount attacks (#16)
- Fix mount filter to use target paths instead of source prefixes (#17)
- Fix temp file leak in extract_mounts_to_file (#17)
- Sort apt packages alphabetically (#18)
- Remove unused Tailscale feature (#19)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dan Guido
2026-02-10 06:35:35 -05:00
committed by GitHub
parent 90e3280503
commit a1f3994cda
4 changed files with 44 additions and 25 deletions
+2 -3
View File
@@ -51,6 +51,5 @@ _fzf_compgen_dir() {
fdfind --type d --hidden --follow --exclude .git . "$1"
}
# Source fzf key bindings and completion
source ~/.fzf/key-bindings.zsh
source ~/.fzf/completion.zsh
# Source fzf shell integration (built-in since fzf 0.48+)
eval "$(fzf --zsh)"
+19 -14
View File
@@ -1,6 +1,8 @@
# Claude Code Devcontainer
# Based on Microsoft devcontainer image for better devcontainer integration
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04
ARG UV_VERSION=0.10.0
FROM ghcr.io/astral-sh/uv:${UV_VERSION}@sha256:78a7ff97cd27b7124a5f3c2aefe146170793c56a1e03321dd31a289f6d82a04f AS uv
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04@sha256:d94c97dd9cacf183d0a6fd12a8e87b526e9e928307674ae9c94139139c0c6eae
ARG TZ
ENV TZ="$TZ"
@@ -13,9 +15,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
bubblewrap \
socat \
# Modern CLI tools
fzf \
ripgrep \
fd-find \
ripgrep \
tmux \
zsh \
# Build tools
@@ -23,13 +24,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Utilities
jq \
nano \
vim \
unzip \
vim \
# Network tools (for security testing)
iptables \
ipset \
iproute2 \
dnsutils \
ipset \
iptables \
iproute2 \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Install git-delta
@@ -40,7 +41,17 @@ RUN ARCH=$(dpkg --print-architecture) && \
rm /tmp/git-delta.deb
# Install uv (Python package manager) via multi-stage copy
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY --from=uv /uv /usr/local/bin/uv
# Install fzf from GitHub releases (newer than apt, includes built-in shell integration)
ARG FZF_VERSION=0.67.0
RUN ARCH=$(dpkg --print-architecture) && \
case "${ARCH}" in \
amd64) FZF_ARCH="linux_amd64" ;; \
arm64) FZF_ARCH="linux_arm64" ;; \
*) echo "Unsupported architecture: ${ARCH}" && exit 1 ;; \
esac && \
curl -fsSL "https://github.com/junegunn/fzf/releases/download/v${FZF_VERSION}/fzf-${FZF_VERSION}-${FZF_ARCH}.tar.gz" | tar -xz -C /usr/local/bin
# Create directories and set ownership (combined for fewer layers)
RUN mkdir -p /commandhistory /workspace /home/vscode/.claude /opt && \
@@ -88,12 +99,6 @@ RUN sh -c "$(curl -fsSL https://github.com/deluan/zsh-in-docker/releases/downloa
-p git \
-x
# Download fzf shell integration (Ubuntu 24.04 package doesn't include these)
ARG FZF_VERSION=0.44.1
RUN mkdir -p /home/vscode/.fzf && \
curl -fsSL "https://raw.githubusercontent.com/junegunn/fzf/${FZF_VERSION}/shell/key-bindings.zsh" -o /home/vscode/.fzf/key-bindings.zsh && \
curl -fsSL "https://raw.githubusercontent.com/junegunn/fzf/${FZF_VERSION}/shell/completion.zsh" -o /home/vscode/.fzf/completion.zsh
# Copy zsh configuration
COPY --chown=vscode:vscode .zshrc /home/vscode/.zshrc.custom
+1 -2
View File
@@ -10,8 +10,7 @@
}
},
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/tailscale/codespace/tailscale:1": {}
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"runArgs": [
"--cap-add=NET_ADMIN",
+22 -6
View File
@@ -76,6 +76,19 @@ check_devcontainer_cli() {
fi
}
check_no_sys_admin() {
local workspace="${1:-.}"
local dc_json="$workspace/.devcontainer/devcontainer.json"
[[ -f "$dc_json" ]] || return 0
if jq -e \
'.runArgs[]? | select(test("SYS_ADMIN"))' \
"$dc_json" >/dev/null 2>&1; then
log_error "SYS_ADMIN capability detected in runArgs."
log_error "This defeats the read-only .devcontainer mount."
exit 1
fi
}
get_workspace_folder() {
echo "${1:-$(pwd)}"
}
@@ -101,12 +114,11 @@ extract_mounts_to_file() {
custom_mounts=$(jq -c '
.mounts // [] | map(
select(
(startswith("source=claude-code-bashhistory-") | not) and
(startswith("source=claude-code-config-") | not) and
(startswith("source=claude-code-gh-") | not) and
(startswith("source=${localEnv:HOME}/.gitconfig,") | not) and
# Security: read-only .devcontainer mount prevents container escape on rebuild
(startswith("source=${localWorkspaceFolder}/.devcontainer,") | not)
(contains("target=/commandhistory,") | not) and
(contains("target=/home/vscode/.claude,") | not) and
(contains("target=/home/vscode/.config/gh,") | not) and
(contains("target=/home/vscode/.gitconfig,") | not) and
(contains("target=/workspace/.devcontainer,") | not)
)
) | if length > 0 then . else empty end
' "$devcontainer_json" 2>/dev/null) || true
@@ -114,6 +126,8 @@ extract_mounts_to_file() {
if [[ -n "$custom_mounts" ]]; then
echo "$custom_mounts" >"$temp_file"
echo "$temp_file"
else
rm -f "$temp_file"
fi
}
@@ -207,6 +221,7 @@ cmd_up() {
workspace_folder="$(get_workspace_folder "${1:-}")"
check_devcontainer_cli
check_no_sys_admin "$workspace_folder"
log_info "Starting devcontainer in $workspace_folder..."
devcontainer up --workspace-folder "$workspace_folder"
@@ -218,6 +233,7 @@ cmd_rebuild() {
workspace_folder="$(get_workspace_folder "${1:-}")"
check_devcontainer_cli
check_no_sys_admin "$workspace_folder"
log_info "Rebuilding devcontainer in $workspace_folder..."
devcontainer up --workspace-folder "$workspace_folder" --remove-existing-container