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:
@@ -78,6 +78,7 @@ pub(crate) async fn create_container_for_project(
|
||||
settings.timezone.as_deref(),
|
||||
settings.global_claude_code_settings.as_ref(),
|
||||
settings.default_ssh_key_path.as_deref(),
|
||||
settings.ca_cert_path.as_deref(),
|
||||
settings.default_git_user_name.as_deref(),
|
||||
settings.default_git_user_email.as_deref(),
|
||||
)
|
||||
@@ -406,6 +407,7 @@ pub async fn start_project_container(
|
||||
settings.timezone.as_deref(),
|
||||
settings.global_claude_code_settings.as_ref(),
|
||||
settings.default_ssh_key_path.as_deref(),
|
||||
settings.ca_cert_path.as_deref(),
|
||||
settings.default_git_user_name.as_deref(),
|
||||
settings.default_git_user_email.as_deref(),
|
||||
).await.unwrap_or(false);
|
||||
|
||||
@@ -155,6 +155,78 @@ pub async fn detect_aws_config() -> Result<Option<String>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// What the UI shows next to a corporate CA certificate path.
|
||||
///
|
||||
/// Errors are returned *inside* the payload rather than as `Err` so the field
|
||||
/// can render its own inline message while the user is still typing — a toast
|
||||
/// per keystroke would be unusable. The same check runs again, as a hard error,
|
||||
/// when the container is created.
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct CaCertInfo {
|
||||
pub exists: bool,
|
||||
pub is_directory: bool,
|
||||
/// How many certificate files were found.
|
||||
pub cert_count: usize,
|
||||
/// The names they will be installed as inside the container. Surfacing
|
||||
/// these makes the silent `.pem` → `.crt` rename visible, which is the one
|
||||
/// step users most often do by hand and get wrong.
|
||||
pub installed_names: Vec<String>,
|
||||
/// Why the path is unusable, if it is.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn inspect_ca_cert_path(path: String) -> Result<CaCertInfo, String> {
|
||||
use crate::docker::ca_certs;
|
||||
|
||||
let trimmed = path.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(CaCertInfo {
|
||||
exists: false,
|
||||
is_directory: false,
|
||||
cert_count: 0,
|
||||
installed_names: Vec::new(),
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
|
||||
let p = std::path::Path::new(trimmed);
|
||||
let exists = p.exists();
|
||||
let is_directory = p.is_dir();
|
||||
|
||||
match ca_certs::resolve(Some(trimmed)) {
|
||||
Ok(Some(resolved)) => Ok(CaCertInfo {
|
||||
exists,
|
||||
is_directory,
|
||||
cert_count: resolved.cert_files.len(),
|
||||
installed_names: resolved
|
||||
.cert_files
|
||||
.iter()
|
||||
.map(|f| {
|
||||
ca_certs::container_cert_name(
|
||||
&f.file_name().unwrap_or_default().to_string_lossy(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
error: None,
|
||||
}),
|
||||
Ok(None) => Ok(CaCertInfo {
|
||||
exists,
|
||||
is_directory,
|
||||
cert_count: 0,
|
||||
installed_names: Vec::new(),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => Ok(CaCertInfo {
|
||||
exists,
|
||||
is_directory,
|
||||
cert_count: 0,
|
||||
installed_names: Vec::new(),
|
||||
error: Some(e),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_aws_profiles() -> Result<Vec<String>, String> {
|
||||
let mut profiles = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user