Commit Graph
2 Commits
Author SHA1 Message Date
Claude ae3ca8cda4 Fix root scrub deleting host files through symlinked parents (C1)
SNAPSHOT_SCRUB_PATHS is expanded by /bin/sh inside the container as root.
For an entry ending `/*` the parent is a path *component*, resolved by both
the glob expansion and the `rm -rf`. The agent has passwordless sudo, so
`ln -s /workspace/myproject /var/log/apt` turned the next commit into a
recursive delete of the user's real files on the host — reproduced end to
end against a live container.

snapshot_scrub_script now routes every deletion through one `scrub_in`
function that validates the parent before touching anything inside it:
`cd -P` for a TOCTOU-free handle, `pwd -P` equality to reject a symlinked
component, a hardcoded containment allowlist that is deliberately not
derived from the path list, and an st_dev comparison against `/` so a bind
mount or a volume is refused even though it is not a symlink. It fails
closed when `stat` is missing.

Also:
- /tmp/triple-c-drops/* and /tmp/clipboard_*.png are age-limited to 14 days
  instead of scrubbed unconditionally. They hold the user's own files, and
  scrubbing them meant "drop a file, change a setting, lose it silently";
  removing them from the list would restore unbounded growth instead.
- M11: scrub_writable_layer returns a ScrubOutcome and skips cleanly when
  the container is not running, so a migration no longer logs "could not
  run … committing anyway" on every run.
- M12: the scrub exec is bounded by a 120s timeout; on expiry it logs and
  lets the commit proceed.
- The script is now fold-safe (self-terminating lines, no `#` comments).
  disk.rs folds it onto one `RUN` line and the previous form was a
  `"do" unexpected` syntax error there, so compaction had been scrubbing
  nothing at all.

Tests: the substring check on the script text is replaced by a behavioural
test that runs the real generated script with a real symlink planted in a
throwaway tree, plus structural tests over each containment construct and
a `sh -n` check of the folded form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
2026-08-23 11:11:57 -07:00
Claude 7a5823cb2b Inject the corporate CA certificate into containers
Build App / compute-version (pull_request) Successful in 6s
Build App / build-macos (pull_request) Successful in 2m30s
Build App / build-windows (pull_request) Successful in 5m16s
Build Container / build-container (pull_request) Successful in 10m15s
Build App / build-linux (pull_request) Successful in 6m35s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
Behind a TLS-terminating corporate proxy every HTTPS call inside a container
fails — npm, pip, git, curl, the browser-view pane, and Claude Code's own API
requests. There was no mechanism at all: installing the certificate by hand
inside a container is lost on Reset and had to be repeated per project.

A global CA path in AppSettings with a per-project override on Project, taking
either a single certificate file or a directory. It is bind-mounted read-only
at /tmp/.host-ca (mirroring /tmp/.host-ssh and /tmp/.host-aws) and applied by
entrypoint.sh on every start, so it survives recreation, migration and Reset.

Four things this gets right that are easy to get wrong:

* update-ca-certificates globs *.crt case-sensitively, so a .pem that is merely
  copied in is ignored in silence. Certificates are renamed, by
  container_cert_name() in Rust and a mirrored few lines of shell.
* The system store only serves curl/git/apt. Node — and so Claude Code itself —
  needs NODE_EXTRA_CA_CERTS, Python needs REQUESTS_CA_BUNDLE/SSL_CERT_FILE, and
  Chromium reads neither: it wants ~/.pki/nssdb, seeded with certutil
  (libnss3-tools, added to the image).
* Those vars are set from Rust at creation, never exported by the entrypoint —
  a terminal is a docker exec and sees nothing the entrypoint exported. They are
  emitted empty when no CA is configured, since docker commit bakes env into the
  snapshot image.
* triple-c.ca-fingerprint hashes the certificate bytes as well as the path, so
  a CA rotated in at the same location still forces a recreation.

Verified end to end against a real container and a self-signed CA: curl, node,
python and git all complete a TLS handshake against a server signed by it and
all three fail in the same container without it; the env vars are visible from
a docker exec session; the store is cleaned when the setting is cleared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
2026-08-10 10:40:21 -07:00