Merge branch 'master' of gitbox.bradina.localdomain:branderson/dotfiles

This commit is contained in:
2026-07-19 13:03:55 -07:00
4 changed files with 51 additions and 31 deletions
+16 -5
View File
@@ -15,7 +15,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
git \
openssh-client \
procps \
sudo \
fzf \
zsh \
man-db \
@@ -82,9 +81,21 @@ RUN mkdir -p /home/node/.local/share/nvim /home/node/.local/state/nvim /home/nod
ARG GIT_DELTA_VERSION=0.18.2
RUN ARCH=$(dpkg --print-architecture) && \
wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
sudo dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
# gosu lets entrypoint.sh (root) drop to the node user for the actual
# session after setting up the firewall, without leaving any setuid/sudo
# escalation path available to node afterward. Verified against the
# published SHA256SUMS since it runs as root.
ARG GOSU_VERSION=1.19
RUN ARCH=$(dpkg --print-architecture) && \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-${ARCH}" && \
wget -O /tmp/gosu.sha256sums "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/SHA256SUMS" && \
grep -E "gosu-${ARCH}\$" /tmp/gosu.sha256sums | sed "s#gosu-${ARCH}#/usr/local/bin/gosu#" | sha256sum -c - && \
rm /tmp/gosu.sha256sums && \
chmod +x /usr/local/bin/gosu
# Install the latest stable Neovim release. Debian's packaged version is too
# old for this config's plugins (blink.cmp etc. need a recent Neovim), so
# fetch the current release from GitHub directly rather than pinning a
@@ -140,9 +151,9 @@ COPY config/nvim /home/node/.config/nvim
USER root
RUN chmod +x /usr/local/bin/init-firewall.sh /usr/local/bin/entrypoint.sh /usr/local/bin/gitea-pr /usr/local/bin/claude && \
chown -R node:node /home/node/.config/nvim && \
echo "node ALL=(root) NOPASSWD:SETENV: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
chmod 0440 /etc/sudoers.d/node-firewall && \
echo 'export PATH="$PATH:/usr/local/share/npm-global/bin"' > /etc/profile.d/npm-global-path.sh
USER node
# Entrypoint runs as root (no USER node here) so it can set up the firewall
# directly, with no sudo/setuid escalation path available to node at any
# point afterward. It drops to node itself via gosu before execing nvim.
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+16 -8
View File
@@ -80,13 +80,16 @@ since `~/.claude` is shared.
On start, the container runs an allowlist-only `iptables`/`ipset` firewall
(`init-firewall.sh`), matching the reference dev container. Allowed by
default: DNS, SSH (port 22, for git-over-ssh to any host), the Docker
bridge/host subnet, GitHub's published IP ranges, `registry.npmjs.org`,
`api.anthropic.com`, and whatever hosts `GITEA_SSH_HOST`/`GITEA_API` resolve
to. Set `ALLOWED_DOMAINS` (space-separated) in the environment to allow
more (e.g. `crates.io` for a Rust project needing more than what mason/npm
already cover). Everything else is rejected. Set `ENABLE_FIREWALL=0` to
disable it entirely (e.g. for debugging a blocked domain).
default: DNS resolution via Docker's embedded resolver (loopback only, so
never leaves the container), the Docker host itself (not its whole subnet),
GitHub's published IP ranges, `registry.npmjs.org`, `api.anthropic.com`, and
whatever hosts `GITEA_SSH_HOST`/`GITEA_API` resolve to — that last group
isn't port-restricted, so git-over-ssh to GitHub/Gitea works without a
separate blanket "port 22 to anywhere" rule. Set `ALLOWED_DOMAINS`
(space-separated) in the environment to allow more (e.g. `crates.io` for a
Rust project needing more than what mason/npm already cover). Everything
else is rejected. Set `ENABLE_FIREWALL=0` to disable it entirely (e.g. for
debugging a blocked domain).
If your Gitea host is only resolvable via a LAN-local DNS server, make sure
the Docker host itself can resolve it — containers inherit the host's
@@ -104,6 +107,11 @@ resolver by default.
- Don't mount other host secrets (`~/.ssh` private keys, cloud credential
files) into the container. Git push auth goes through the forwarded SSH
agent instead, which never exposes the key material itself.
- The `node` user has no `sudo`/setuid path back to root once the container
is up: `entrypoint.sh` runs as root, sets up the firewall directly, then
drops to `node` via `gosu` before execing `nvim`. A compromised session
can't re-run `init-firewall.sh` with a wider allowlist to open its own
exfiltration path.
## Files
@@ -111,7 +119,7 @@ resolver by default.
| --- | --- |
| `Dockerfile` | Base image, dev tools, nvim, Claude Code + wrapper, `gitea-pr` |
| `init-firewall.sh` | Egress allowlist firewall, run at container start |
| `entrypoint.sh` | Runs the firewall, then execs `nvim` |
| `entrypoint.sh` | Runs as root: sets up the firewall, drops to `node` via `gosu`, execs `nvim` |
| `claude-wrapper.sh` | Shadows `claude` on `PATH` to always add `--dangerously-skip-permissions` |
| `docker-compose.yml` | Wires up mounts/env; invoked via `bin/claude-sandbox`, not directly |
+5 -2
View File
@@ -11,7 +11,10 @@ fi
extra_domains="${GITEA_SSH_HOST:-} ${gitea_api_host} ${ALLOWED_DOMAINS:-}"
if [ "${ENABLE_FIREWALL:-1}" = "1" ]; then
sudo EXTRA_ALLOWED_DOMAINS="${extra_domains}" /usr/local/bin/init-firewall.sh
EXTRA_ALLOWED_DOMAINS="${extra_domains}" /usr/local/bin/init-firewall.sh
fi
exec nvim "$@"
# Drop root -> node for the actual session; node has no sudo/setuid path
# back to root after this, so it can't re-run init-firewall.sh itself to
# widen the allowlist mid-session.
exec gosu node nvim "$@"
+14 -16
View File
@@ -26,16 +26,10 @@ else
echo "No Docker DNS rules to restore"
fi
# First allow DNS and localhost before any restrictions
# Allow outbound DNS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
# Allow inbound DNS responses
iptables -A INPUT -p udp --sport 53 -j ACCEPT
# Allow outbound SSH (covers git-over-ssh to any allowed host, e.g. Gitea)
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
# Allow inbound SSH responses
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
# Allow localhost
# Allow localhost. DNS resolution goes entirely through Docker's embedded
# resolver at 127.0.0.11 (see resolv.conf), which stays on loopback, so this
# also covers DNS - no need for a separate "UDP/53 to anywhere" rule, which
# would just be an unrestricted-destination exfil channel.
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
@@ -118,12 +112,12 @@ if [ -z "$HOST_IP" ]; then
exit 1
fi
HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
echo "Host network detected as: $HOST_NETWORK"
echo "Host detected as: $HOST_IP"
# Set up remaining iptables rules
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
# Only the host itself, not its whole subnet - other machines on the same
# LAN/VPC have no reason to be reachable from this container.
iptables -A INPUT -s "$HOST_IP" -j ACCEPT
iptables -A OUTPUT -d "$HOST_IP" -j ACCEPT
# Set default policies to DROP first
iptables -P INPUT DROP
@@ -134,7 +128,11 @@ iptables -P OUTPUT DROP
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Then allow only specific outbound traffic to allowed domains
# Then allow only specific outbound traffic to allowed domains. This isn't
# port-restricted, so it also covers git-over-ssh (port 22) to GitHub and
# GITEA_SSH_HOST/GITEA_API, which are already in the ipset above - no
# separate "TCP/22 to anywhere" rule needed (that would allow SSH, or any
# raw TCP posing as it, to any host on the internet).
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
# Explicitly REJECT all other outbound traffic for immediate feedback