diff --git a/internal/devbox/devbox.go b/internal/devbox/devbox.go index 4c3f82421ce..e964ca7d67e 100644 --- a/internal/devbox/devbox.go +++ b/internal/devbox/devbox.go @@ -738,6 +738,15 @@ func (d *Devbox) computeEnv( for k, v := range nixEnv { env[k] = v } + + // The Nix dev-env sets the SSL certificate-bundle variables to a Nix + // store path whenever a package pulls in nss-cacert (e.g. httpie, + // python). That clobbers a value the user deliberately set in their own + // environment — most importantly a corporate MITM/proxy CA bundle — + // breaking outbound TLS for the rest of the project. Restore the user's + // own value so custom CA bundles keep working, mirroring how `nix shell` + // leaves these untouched. See jetify-com/devbox#2604. + preserveUserSSLCertFiles(env, originalEnv) } slog.Debug("nix environment PATH", "path", env["PATH"]) @@ -1100,6 +1109,27 @@ var ignoreDevEnvVar = map[string]bool{ "UID": true, } +// sslCertFileEnvVars are the certificate-bundle environment variables that +// Nix's build environment sets (via packages such as nss-cacert, pulled in by +// httpie, python, etc.). They point at a Nix store CA bundle, which is the right +// default when the user hasn't set one themselves but is wrong when they have: +// a user who exports one of these — typically to a corporate MITM/proxy CA +// bundle — needs that value preserved for outbound TLS to keep working. +var sslCertFileEnvVars = []string{"NIX_SSL_CERT_FILE", "SSL_CERT_FILE"} + +// preserveUserSSLCertFiles restores the user's own certificate-bundle variables +// (taken from userEnv, the ambient environment captured before the Nix dev-env +// is layered on) into env, so that a value the user explicitly set wins over the +// Nix store path injected by the dev-env. Variables the user did not set are +// left as-is, keeping the Nix default. See jetify-com/devbox#2604. +func preserveUserSSLCertFiles(env, userEnv map[string]string) { + for _, key := range sslCertFileEnvVars { + if val, ok := userEnv[key]; ok && val != "" { + env[key] = val + } + } +} + func (d *Devbox) ProjectDirHash() string { return cachehash.Bytes([]byte(d.projectDir)) } diff --git a/internal/devbox/devbox_test.go b/internal/devbox/devbox_test.go index b0429dfd0d2..69e1e549a51 100644 --- a/internal/devbox/devbox_test.go +++ b/internal/devbox/devbox_test.go @@ -120,6 +120,68 @@ func TestComputeDevboxPathWhenRemoving(t *testing.T) { assert.NotEqual(t, path, path2, "path should not be the same") } +// testNixVars is a nix.Nixer mock whose PrintDevEnv returns a configurable set +// of exported variables. Used to exercise how computeEnv layers the Nix +// dev-env on top of the ambient environment. +type testNixVars struct { + vars map[string]string +} + +func (n *testNixVars) PrintDevEnv(ctx context.Context, args *nix.PrintDevEnvArgs) (*nix.PrintDevEnvOut, error) { + variables := map[string]nix.Variable{} + for k, v := range n.vars { + variables[k] = nix.Variable{Type: "exported", Value: v} + } + return &nix.PrintDevEnvOut{Variables: variables}, nil +} + +func TestPreserveUserSSLCertFiles(t *testing.T) { + const userBundle = "/Library/Application Support/Netskope/STAgent/data/nscacert_combined.pem" + const nixBundle = "/nix/store/abc-nss-cacert-3.108/etc/ssl/certs/ca-bundle.crt" + + t.Run("restores user value when set", func(t *testing.T) { + env := map[string]string{"NIX_SSL_CERT_FILE": nixBundle, "SSL_CERT_FILE": nixBundle} + userEnv := map[string]string{"NIX_SSL_CERT_FILE": userBundle, "SSL_CERT_FILE": userBundle} + preserveUserSSLCertFiles(env, userEnv) + assert.Equal(t, userBundle, env["NIX_SSL_CERT_FILE"]) + assert.Equal(t, userBundle, env["SSL_CERT_FILE"]) + }) + + t.Run("keeps nix value when user did not set one", func(t *testing.T) { + env := map[string]string{"NIX_SSL_CERT_FILE": nixBundle} + preserveUserSSLCertFiles(env, map[string]string{}) + assert.Equal(t, nixBundle, env["NIX_SSL_CERT_FILE"]) + }) + + t.Run("ignores empty user value", func(t *testing.T) { + env := map[string]string{"NIX_SSL_CERT_FILE": nixBundle} + preserveUserSSLCertFiles(env, map[string]string{"NIX_SSL_CERT_FILE": ""}) + assert.Equal(t, nixBundle, env["NIX_SSL_CERT_FILE"]) + }) +} + +// TestComputeEnvPreservesUserSSLCertFile is a regression test for +// jetify-com/devbox#2604: adding a package that pulls in nss-cacert (e.g. +// httpie) must not clobber a NIX_SSL_CERT_FILE the user set in their own +// environment (e.g. a corporate MITM CA bundle). +func TestComputeEnvPreservesUserSSLCertFile(t *testing.T) { + const userBundle = "/Library/Application Support/Netskope/STAgent/data/nscacert_combined.pem" + const nixBundle = "/nix/store/abc-nss-cacert-3.108/etc/ssl/certs/ca-bundle.crt" + + d := devboxForTesting(t) + d.nix = &testNixVars{vars: map[string]string{ + "PATH": "/tmp/my/path", + "NIX_SSL_CERT_FILE": nixBundle, + }} + + t.Setenv("NIX_SSL_CERT_FILE", userBundle) + + env, err := d.computeEnv(t.Context(), false /*use cache*/, devopt.EnvOptions{}) + require.NoError(t, err, "computeEnv should not fail") + assert.Equal(t, userBundle, env["NIX_SSL_CERT_FILE"], + "the user's NIX_SSL_CERT_FILE should win over the nix dev-env value") +} + func devboxForTesting(t *testing.T) *Devbox { path := t.TempDir() _, err := devconfig.Init(path)