Files
Triple-C/app/src/components/settings/CertificateSettings.tsx
T
Claude 7a5823cb2b
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
Inject the corporate CA certificate into containers
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

57 lines
2.0 KiB
TypeScript

import { useEffect, useState } from "react";
import { useSettings } from "../../hooks/useSettings";
import CaCertPathInput from "./CaCertPathInput";
const INPUT_CLASS =
"flex-1 min-w-0 px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:border-[var(--accent)]";
/**
* Global corporate CA certificate setting.
*
* Applies to every project unless one overrides it in Project Home → Config →
* Access. Changing it recreates each container on its next start — the
* certificate is copied into the container's trust store once, at start, so
* there is nowhere else for a change to land.
*/
export default function CertificateSettings() {
const { appSettings, saveSettings } = useSettings();
const [path, setPath] = useState(appSettings?.ca_cert_path ?? "");
useEffect(() => {
setPath(appSettings?.ca_cert_path ?? "");
}, [appSettings?.ca_cert_path]);
const commit = async (value: string) => {
if (!appSettings) return;
const next = value.trim() || null;
if (next === appSettings.ca_cert_path) return;
await saveSettings({ ...appSettings, ca_cert_path: next });
};
return (
<div>
<label
className="block text-sm font-medium mb-1"
htmlFor="global-ca-cert-path"
>
Corporate CA Certificate
</label>
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
A certificate file, or a folder of them, for organisations whose network
inspects TLS. Mounted read-only into every container and trusted by
curl, git, npm, pip, Chromium and Claude Code itself. Per-project
settings override this; changing it recreates containers on next start.
</p>
<CaCertPathInput
id="global-ca-cert-path"
value={path}
onChange={setPath}
onCommit={commit}
placeholder="/etc/ssl/certs/corp-root.pem"
emptyHint="Not set — containers trust only the public CAs shipped with the image."
inputClassName={INPUT_CLASS}
/>
</div>
);
}