diff --git a/agent-vm.nix b/agent-vm.nix index 0cde7b5..14bc9ce 100644 --- a/agent-vm.nix +++ b/agent-vm.nix @@ -8,6 +8,8 @@ let net = import ./network.nix; privateMatch = { Name = net.agentPrivateInterface; }; + caBundle = "/etc/ssl/certs/ca-certificates.crt"; + caDir = "/etc/ssl/certs"; in { imports = @@ -95,14 +97,19 @@ in # Common SDK trust-store env vars. NixOS's security.pki.certificateFiles # adds the CA to /etc/ssl/certs/ca-certificates.crt, which curl, git, - # and OpenSSL pick up automatically. Node, Python `requests`, and a few - # other ecosystems read their CA bundle from a hardcoded path or env - # var instead, so set those explicitly. Pi, Claude Code, Codex, etc. - # are all Node CLIs — without NODE_EXTRA_CA_CERTS they'd fail TLS to - # every allowlisted host the moment MITM is enabled. + # and OpenSSL pick up automatically. Node, Python, pip, uv, botocore, + # and Nix dev shells can otherwise prefer package-local CA bundles + # that do not include our per-instance MITM CA. environment.variables = { - NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt"; - SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt"; - REQUESTS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; + NODE_EXTRA_CA_CERTS = caBundle; + NIX_SSL_CERT_FILE = caBundle; + SSL_CERT_FILE = caBundle; + SSL_CERT_DIR = caDir; + REQUESTS_CA_BUNDLE = caBundle; + CURL_CA_BUNDLE = caBundle; + GIT_SSL_CAINFO = caBundle; + PIP_CERT = caBundle; + AWS_CA_BUNDLE = caBundle; + UV_NATIVE_TLS = "true"; }; } diff --git a/home.nix b/home.nix index a71e14f..5c4c4e4 100644 --- a/home.nix +++ b/home.nix @@ -11,6 +11,8 @@ let net = import ./network.nix; pi-coding-agent = import ./pi/pi-coding-agent.nix { inherit pkgs; }; + caBundle = "/etc/ssl/certs/ca-certificates.crt"; + caDir = "/etc/ssl/certs"; in { # Per-user git identity, generated by `rootcell` from the host's `git config`. @@ -84,6 +86,19 @@ in recursive = true; }; + home.sessionVariables = { + NODE_EXTRA_CA_CERTS = caBundle; + NIX_SSL_CERT_FILE = caBundle; + SSL_CERT_FILE = caBundle; + SSL_CERT_DIR = caDir; + REQUESTS_CA_BUNDLE = caBundle; + CURL_CA_BUNDLE = caBundle; + GIT_SSL_CAINFO = caBundle; + PIP_CERT = caBundle; + AWS_CA_BUNDLE = caBundle; + UV_NATIVE_TLS = "true"; + }; + programs.bash = { enable = true; enableCompletion = true; @@ -99,6 +114,38 @@ in programs.direnv = { enable = true; nix-direnv.enable = true; + stdlib = '' + rootcell_ca_bundle=${caBundle} + rootcell_ca_dir=${caDir} + + rootcell_export_ca_bundle() { + if [ -r "$rootcell_ca_bundle" ]; then + export NODE_EXTRA_CA_CERTS="$rootcell_ca_bundle" + export NIX_SSL_CERT_FILE="$rootcell_ca_bundle" + export SSL_CERT_FILE="$rootcell_ca_bundle" + export SSL_CERT_DIR="$rootcell_ca_dir" + export REQUESTS_CA_BUNDLE="$rootcell_ca_bundle" + export CURL_CA_BUNDLE="$rootcell_ca_bundle" + export GIT_SSL_CAINFO="$rootcell_ca_bundle" + export PIP_CERT="$rootcell_ca_bundle" + export AWS_CA_BUNDLE="$rootcell_ca_bundle" + export UV_NATIVE_TLS=true + fi + } + + rootcell_wrap_direnv_loader() { + local fn="$1" + local original="rootcell_original_$fn" + if declare -F "$fn" >/dev/null && ! declare -F "$original" >/dev/null; then + eval "$(declare -f "$fn" | sed "1s/^$fn/$original/")" + eval "$fn() { $original \"\$@\"; local status=\$?; rootcell_export_ca_bundle; return \$status; }" + fi + } + + rootcell_export_ca_bundle + rootcell_wrap_direnv_loader use_flake + rootcell_wrap_direnv_loader use_nix + ''; }; # git-delta as the git pager. The `enableGitIntegration` flag writes the diff --git a/src/rootcell/rootcell.ts b/src/rootcell/rootcell.ts index fb012fb..83d6548 100644 --- a/src/rootcell/rootcell.ts +++ b/src/rootcell/rootcell.ts @@ -159,6 +159,33 @@ function shellQuote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`; } +const AGENT_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; +const AGENT_CA_DIR = "/etc/ssl/certs"; +const AGENT_CA_ENV = [ + ["NODE_EXTRA_CA_CERTS", AGENT_CA_BUNDLE], + ["NIX_SSL_CERT_FILE", AGENT_CA_BUNDLE], + ["SSL_CERT_FILE", AGENT_CA_BUNDLE], + ["SSL_CERT_DIR", AGENT_CA_DIR], + ["REQUESTS_CA_BUNDLE", AGENT_CA_BUNDLE], + ["CURL_CA_BUNDLE", AGENT_CA_BUNDLE], + ["GIT_SSL_CAINFO", AGENT_CA_BUNDLE], + ["PIP_CERT", AGENT_CA_BUNDLE], + ["AWS_CA_BUNDLE", AGENT_CA_BUNDLE], + ["UV_NATIVE_TLS", "true"], +] as const; + +function agentCaEnv(bundlePath = AGENT_CA_BUNDLE): readonly string[] { + return AGENT_CA_ENV.map(([key, value]) => `${key}=${value === AGENT_CA_BUNDLE ? bundlePath : value}`); +} + +function exportAgentCaEnvScript(bundlePath: string): string { + return agentCaEnv(bundlePath).map((entry) => `export ${entry}`).join("\n"); +} + +function sudoAgentCaEnvScript(): string { + return AGENT_CA_ENV.map(([key]) => ` ${key}="$${key}" \\`).join("\n"); +} + function nixStringList(values: readonly string[]): string { return `[ ${values.map(nixString).join(" ")} ]`; } @@ -292,9 +319,7 @@ export class RootcellApp { env: [ ...injectedSecretEnv, `AWS_REGION=${this.config.awsEc2?.region ?? process.env.AWS_REGION ?? "us-east-1"}`, - "NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt", - "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt", - "REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt", + ...agentCaEnv(), ], }); } @@ -971,15 +996,9 @@ sudo nixos-rebuild switch --flake ${shellQuote(this.guestFlakeRef(this.nixosConf await this.bootstrapAgentFirewallTrust(); await this.runNixosSwitch("agent", ` set -e -export NIX_SSL_CERT_FILE=/tmp/agent-vm-bootstrap-ca-bundle.crt -export SSL_CERT_FILE=/tmp/agent-vm-bootstrap-ca-bundle.crt -export GIT_SSL_CAINFO=/tmp/agent-vm-bootstrap-ca-bundle.crt -export REQUESTS_CA_BUNDLE=/tmp/agent-vm-bootstrap-ca-bundle.crt +${exportAgentCaEnvScript("/tmp/agent-vm-bootstrap-ca-bundle.crt")} sudo env \\ - NIX_SSL_CERT_FILE="$NIX_SSL_CERT_FILE" \\ - SSL_CERT_FILE="$SSL_CERT_FILE" \\ - GIT_SSL_CAINFO="$GIT_SSL_CAINFO" \\ - REQUESTS_CA_BUNDLE="$REQUESTS_CA_BUNDLE" \\ +${sudoAgentCaEnvScript()} nixos-rebuild switch --flake ${shellQuote(this.guestFlakeRef(this.nixosConfiguration("agent")))} `); await this.runAgentHomeManager(); @@ -1038,10 +1057,7 @@ fi private async runAgentHomeManager(): Promise { const script = ` set -e -export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -export GIT_SSL_CAINFO=/etc/ssl/certs/ca-certificates.crt -export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +${exportAgentCaEnvScript(AGENT_CA_BUNDLE)} nix run ${shellQuote(this.guestFlakeRef("home-manager"))} -- switch --flake ${shellQuote(this.guestFlakeRef(this.config.guestUser))} `; const attempts = 4;