Skip to content

Build-time installs + root-entrypoint firewall#397

Merged
krokicki merged 3 commits into
mainfrom
podman-devcontainer-buildtime-hardening
Jun 26, 2026
Merged

Build-time installs + root-entrypoint firewall#397
krokicki merged 3 commits into
mainfrom
podman-devcontainer-buildtime-hardening

Conversation

@krokicki

@krokicki krokicki commented Jun 23, 2026

Copy link
Copy Markdown
Member

Chained on top of #396 (base: podman-devcontainer).

Restructures container setup so that no phase is simultaneously networked, credentialed, privileged, and unfirewalled. Previously post-create.sh ran many networked installs (pixi, npm, Playwright browsers, the Claude curl | bash installer, Codex) while it had full network access, passwordless sudo, and the credentials mounted, all before the firewall came up.

The work is split so the untrusted, CDN-fetching installs happen at image-build time (where no credentials are mounted), and the credentialed runtime phase is unprivileged and firewalled.

Changes

  • Dockerfile: install Node (build-time only), the Claude and Codex CLIs, and the pinned Playwright browser (chromium, matching the project's @playwright/test 1.56.1) at build time; install bubblewrap + socat; bake "no sudo for vscode" into the image (rm /etc/sudoers.d/vscode); add a root ENTRYPOINT + CMD.
  • entrypoint.sh (new): runs as root at container start, fixes the .pixi volume ownership if needed, brings up the egress firewall (fail-closed), signals readiness, then execs the long-running command.
  • post-create.sh: now runs as the unprivileged vscode user after the firewall is up (waits on a readiness flag). Only pixi install --locked, dev-install, and the UI-test JS deps remain — all hitting allowlisted endpoints only. No sudo, no non-allowlisted CDN access.
  • devcontainer.json: overrideCommand: false so the image ENTRYPOINT/CMD run; containerUser: root (for the entrypoint) with remoteUser: vscode (lifecycle commands and shells); PLAYWRIGHT_BROWSERS_PATH for the baked-in browser.

Resulting phases

Phase Runs as Network Credentials
Build (Dockerfile) root open none mounted
Start (entrypoint) root open until firewall applied none
postCreate + agents vscode (no sudo) firewalled (allowlist) mounted

Verification

Rebuilt under rootless Podman, confirmed on the running container:

  • /etc/sudoers.d/vscode is gone; sudo fails; vscode cannot iptables -F.
  • Claude, Codex, the chromium-1194 browser, bwrap, and socat are all baked into the image.
  • The firewall is verified up at entrypoint time (example.com blocked, api.github.com reachable) before post-create's first network call; 0 port-22/subnet rules; DNS pinned.
  • The agent task path (dc.sh exec, used by container-claude/-codex/-shell) runs as vscode, not root, even though the container starts as root for the entrypoint.
  • Both agents run (claude 2.1.187, codex-cli 0.142.0).

Notes

  • Fail-closed on every start: the entrypoint re-runs the firewall on each container start, so a GitHub-meta-fetch failure during an outage would block startup. This is the intended secure default.

@StephanPreibisch @JaneliaSciComp/fileglancer

krokicki and others added 2 commits June 23, 2026 17:19
…ia root entrypoint

Collapse the highly-trusted setup window. Previously post-create.sh ran a
lot of networked installs (pixi, npm, Playwright browsers, the Claude
curl|bash installer, Codex) while it had full network, passwordless sudo,
and the credentials mounted, before the firewall came up. This restructures
setup so no phase is simultaneously networked, credentialed, privileged, and
unfirewalled.

- Dockerfile: install Node (build-only), the Claude and Codex CLIs, and the
  pinned Playwright browser (chromium, v1.56.1) at build time -- where no
  credentials are mounted, so a poisoned dependency can't exfiltrate tokens.
  Also install bubblewrap + socat. Bake "no sudo for vscode" into the image
  (rm /etc/sudoers.d/vscode). Add a root ENTRYPOINT + CMD.
- entrypoint.sh (new): runs as root at container start, fixes the .pixi
  volume ownership if needed, brings up the egress firewall (fail-closed),
  signals readiness, then execs the long-running command.
- post-create.sh: now runs as unprivileged vscode AFTER the firewall is up
  (waits on the readiness flag). Only pixi install --locked, dev-install,
  and UI-test JS deps remain -- all hitting allowlisted endpoints only. No
  sudo, no non-allowlisted CDN access.
- devcontainer.json: overrideCommand=false so our ENTRYPOINT/CMD run;
  containerUser=root (entrypoint) with remoteUser=vscode (lifecycle/shells);
  PLAYWRIGHT_BROWSERS_PATH for the baked-in browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The container rootfs (~/.cache) is fuse-overlayfs under rootless Podman,
which pixi flags as a network filesystem and redirects per-run to an
ephemeral /tmp dir (emitting a warning and losing the cache across
rebuilds). Point PIXI_CACHE_DIR at the local xfs .pixi volume via
containerEnv so it applies to every container process (entrypoint,
postCreate, and all exec sessions); the cache is now quiet and persistent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@krokicki krokicki changed the title Build-time installs + root-entrypoint firewall (collapse the trusted setup window) Build-time installs + root-entrypoint firewall Jun 23, 2026
…-machine setup

Rewrite docs/DevContainer.md to cover both the rootless-Podman support and
the build-time/firewall hardening:

- Runtime selection via FG_CONTAINER_RUNTIME and how dc.sh uses it.
- Linux rootless Podman setup: podman + deps, subuid/subgid, the ~/.bashrc
  exports (FG_CONTAINER_RUNTIME=podman, PIXI_CACHE_DIR on local scratch),
  optional scratch-backed storage.conf, and ipset module persistence.
- Security model: unprivileged agent (no sudo) with host root-shell for
  maintenance, credential-free build-time installs, root entrypoint that
  brings up the firewall first, the tamper-proof firewall (pinned DNS, no
  blanket port 22, no host subnet), and the keep-id credential mapping.
- Container vs host pixi cache (PIXI_CACHE_DIR) and the baked-in Playwright
  browser.
- Architecture of the .devcontainer/ files; updated GPU section for Podman
  CDI. Dropped the outdated standalone-CLI section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Base automatically changed from podman-devcontainer to main June 26, 2026 15:01
@krokicki krokicki merged commit 45eec8c into main Jun 26, 2026
5 checks passed
@krokicki krokicki deleted the podman-devcontainer-buildtime-hardening branch June 26, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant