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
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
This commit is contained in:
@@ -155,6 +155,36 @@ ruff, the OAuth login, `~/.claude.json`, skills, transcripts, scheduler tasks an
|
||||
re-attach for free when a container is recreated from a *different* image — which is what makes
|
||||
base-image migration cheap.
|
||||
|
||||
### Corporate CA certificates (`docker/ca_certs.rs`, `entrypoint.sh`)
|
||||
|
||||
A global `AppSettings::ca_cert_path` with a per-project `Project::ca_cert_path` override, accepting
|
||||
a single certificate file **or** a directory. Follows the SSH/AWS host-mount pattern: read-only
|
||||
bind mount at `/tmp/.host-ca`, applied by the entrypoint on every start, so it survives recreation,
|
||||
migration and Reset. Four things here are not obvious:
|
||||
|
||||
- **`update-ca-certificates` globs `*.crt`, case-sensitively.** A `.pem` that is merely copied into
|
||||
`/usr/local/share/ca-certificates/` is ignored in total silence. Certificates are *renamed* —
|
||||
`container_cert_name()` in Rust, mirrored in a few lines of shell in `entrypoint.sh` (the Rust
|
||||
side carries the unit tests). A single-file mount lands at `/tmp/.host-ca/<name>.crt` so the
|
||||
entrypoint only ever sees a directory and the file keeps a recognisable name.
|
||||
- **The system store is not enough.** Only curl/git/apt read it. Node — and therefore Claude Code
|
||||
itself — needs `NODE_EXTRA_CA_CERTS`; Python/requests need `REQUESTS_CA_BUNDLE`/`SSL_CERT_FILE`;
|
||||
Chrome/Chromium read neither and want their own NSS database at `~/.pki/nssdb`, seeded with
|
||||
`certutil` (`libnss3-tools`, added to the image for this). The NSS step warns and continues if
|
||||
`certutil` is missing rather than failing the start.
|
||||
- **Those env vars are set from Rust at creation, never exported by the entrypoint.** A terminal
|
||||
session is a `docker exec`, which inherits the container's configured env and sees nothing the
|
||||
entrypoint exported — the same lesson that made `$BROWSER` an image-level `ENV`. The bundle path
|
||||
is deterministic (`/etc/ssl/certs/ca-certificates.crt`), so Rust can set them up front. They are
|
||||
emitted **empty** when no CA is configured, for the `MANAGED_AUTH_KEYS` reason: `docker commit`
|
||||
bakes env into the snapshot image. Empty is safe — verified on Ubuntu 24.04 that curl, `openssl
|
||||
s_client` and Python's `ssl` behave exactly as with the vars unset.
|
||||
- **`triple-c.ca-fingerprint` covers the certificate *bytes*, not just the path.** Replacing a
|
||||
rotated CA at the same location must recreate the container; the copy inside is made once, at
|
||||
start, so nothing else would notice. The entrypoint is stamped/idempotent on restart, and
|
||||
actively **removes** `triple-c-*.crt` when the setting is cleared — `/usr/local/share` rides the
|
||||
project's snapshot image, so turning the feature off has to undo, not merely stop.
|
||||
|
||||
### Container Lifecycle
|
||||
|
||||
Containers use a **stop/start** model (not create/destroy). Installed packages persist across stops. The `.claude` config dir uses a named Docker volume (`triple-c-claude-config-{projectId}`), nested inside the home volume (`triple-c-home-{projectId}`), so OAuth tokens and Claude Code config survive container stop/start *and* container recreation.
|
||||
|
||||
Reference in New Issue
Block a user