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:
@@ -3,6 +3,7 @@ import { open } from "@tauri-apps/plugin-dialog";
|
||||
import type { Project } from "../../../../lib/types";
|
||||
import Button from "../../../ui/Button";
|
||||
import Field, { ConfigGroup, inputClass } from "../../../ui/Field";
|
||||
import CaCertPathInput from "../../../settings/CaCertPathInput";
|
||||
import EnvVarsEditor from "../../EnvVarsEditor";
|
||||
import PortMappingsEditor from "../../PortMappingsEditor";
|
||||
|
||||
@@ -20,12 +21,14 @@ export default function AccessSection({
|
||||
disabledReason,
|
||||
}: Props) {
|
||||
const [sshKeyPath, setSshKeyPath] = useState(project.ssh_key_path ?? "");
|
||||
const [caCertPath, setCaCertPath] = useState(project.ca_cert_path ?? "");
|
||||
const [gitName, setGitName] = useState(project.git_user_name ?? "");
|
||||
const [gitEmail, setGitEmail] = useState(project.git_user_email ?? "");
|
||||
const [gitToken, setGitToken] = useState(project.git_token ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
setSshKeyPath(project.ssh_key_path ?? "");
|
||||
setCaCertPath(project.ca_cert_path ?? "");
|
||||
setGitName(project.git_user_name ?? "");
|
||||
setGitEmail(project.git_user_email ?? "");
|
||||
setGitToken(project.git_token ?? "");
|
||||
@@ -114,6 +117,24 @@ export default function AccessSection({
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Corporate CA certificate"
|
||||
hint="Overrides the global certificate for this project only. A certificate file, or a folder of them, trusted inside the container by curl, git, npm, pip, Chromium and Claude Code."
|
||||
>
|
||||
{(id) => (
|
||||
<CaCertPathInput
|
||||
id={id}
|
||||
value={caCertPath}
|
||||
onChange={setCaCertPath}
|
||||
onCommit={(value) => save({ ca_cert_path: value.trim() || null })}
|
||||
disabled={disabled}
|
||||
placeholder="/etc/ssl/certs/corp-root.pem"
|
||||
emptyHint="Using the global certificate from Settings → Certificates."
|
||||
inputClassName={`${inputClass} min-w-0`}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div className="pt-2 border-t border-[var(--border-color)]">
|
||||
<span className="block text-[13px] font-medium text-[var(--text-primary)]">
|
||||
Environment variables
|
||||
|
||||
Reference in New Issue
Block a user