From 4cb560e84f4302927a8655b5c555732c468d3487 Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 12:25:43 +0200 Subject: [PATCH 1/9] Envd support for custom CA --- packages/envd/internal/api/api.gen.go | 12 +++ packages/envd/internal/api/init.go | 33 +++++++ packages/envd/internal/api/init_test.go | 109 ++++++++++++++++++++++++ packages/envd/internal/api/store.go | 7 ++ packages/envd/pkg/version.go | 2 +- packages/envd/spec/envd.yaml | 19 +++++ 6 files changed, 181 insertions(+), 1 deletion(-) diff --git a/packages/envd/internal/api/api.gen.go b/packages/envd/internal/api/api.gen.go index 75bdd14d5e..45bea2b82b 100644 --- a/packages/envd/internal/api/api.gen.go +++ b/packages/envd/internal/api/api.gen.go @@ -23,6 +23,15 @@ const ( File EntryInfoType = "file" ) +// CACertificate A CA certificate to install into the system trust store +type CACertificate struct { + // Cert PEM-encoded CA certificate + Cert string `json:"cert"` + + // Name Filename (without extension) for the certificate + Name string `json:"name"` +} + // ComposeRequest defines model for ComposeRequest. type ComposeRequest struct { // Destination Destination file path for the composed file @@ -174,6 +183,9 @@ type PostInitJSONBody struct { // AccessToken Access token for secure access to envd service AccessToken *SecureToken `json:"accessToken,omitempty"` + // CaCertificates CA certificates to install into the system trust store + CaCertificates *[]CACertificate `json:"caCertificates,omitempty"` + // DefaultUser The default user to use for operations DefaultUser *string `json:"defaultUser,omitempty"` diff --git a/packages/envd/internal/api/init.go b/packages/envd/internal/api/init.go index 1dd422e8a8..2e95d2c486 100644 --- a/packages/envd/internal/api/init.go +++ b/packages/envd/internal/api/init.go @@ -8,7 +8,9 @@ import ( "io" "net/http" "net/netip" + "os" "os/exec" + "path/filepath" "sync" "time" @@ -215,6 +217,10 @@ func (a *API) SetData(ctx context.Context, logger zerolog.Logger, data PostInitJ a.defaults.Workdir = data.DefaultWorkdir } + if data.CaCertificates != nil && len(*data.CaCertificates) > 0 { + go a.installCACerts(context.WithoutCancel(ctx), *data.CaCertificates) + } + if data.VolumeMounts != nil { var wg sync.WaitGroup for _, volume := range *data.VolumeMounts { @@ -253,6 +259,33 @@ func (a *API) setupNfs(ctx context.Context, nfsTarget, path string) { } } +// installCACerts writes PEM-encoded CA certificates to the system trust store +// and runs update-ca-certificates once to make them trusted by all TLS clients. +func (a *API) installCACerts(ctx context.Context, certs []CACertificate) { + certDir := a.certDir + + if err := os.MkdirAll(certDir, 0o755); err != nil { + a.logger.Error().Err(err).Msg("failed to create ca-certificates directory") + + return + } + + for _, c := range certs { + // Use filepath.Base to strip any directory components from the name. + certPath := certDir + "/" + filepath.Base(c.Name) + ".crt" + if err := os.WriteFile(certPath, []byte(c.Cert), 0o644); err != nil { + a.logger.Error().Err(err).Str("name", c.Name).Msg("failed to write CA certificate") + + return + } + } + + data, err := exec.CommandContext(ctx, "update-ca-certificates").CombinedOutput() + + logFn := a.getLogger(err) + logFn.Str("output", string(data)).Msg("update-ca-certificates") +} + func (a *API) SetupHyperloop(address string) { a.hyperloopLock.Lock() defer a.hyperloopLock.Unlock() diff --git a/packages/envd/internal/api/init_test.go b/packages/envd/internal/api/init_test.go index 9877104e09..a3088dc188 100644 --- a/packages/envd/internal/api/init_test.go +++ b/packages/envd/internal/api/init_test.go @@ -2,6 +2,13 @@ package api import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" "os" "path/filepath" "strings" @@ -18,6 +25,29 @@ import ( utilsShared "github.com/e2b-dev/infra/packages/shared/pkg/utils" ) +// generateTestCACert creates a minimal self-signed CA certificate and returns +// it as a PEM-encoded string. The cert is not written to disk. +func generateTestCACert(t *testing.T) string { + t.Helper() + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Test CA"}, + NotBefore: time.Now().Add(-time.Minute), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + BasicConstraintsValid: true, + } + + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) +} + func TestSimpleCases(t *testing.T) { t.Parallel() testCases := map[string]func(string) string{ @@ -585,3 +615,82 @@ func TestSetData(t *testing.T) { assert.Equal(t, "value", val) }) } + +func TestInstallCACerts(t *testing.T) { + t.Parallel() + ctx := context.Background() + + newAPIWithTempCertDir := func(t *testing.T) (*API, string) { + t.Helper() + api := newTestAPI(nil, &mockMMDSClient{}) + api.certDir = t.TempDir() + return api, api.certDir + } + + t.Run("writes single cert file with .crt extension", func(t *testing.T) { + t.Parallel() + api, certDir := newAPIWithTempCertDir(t) + certPEM := generateTestCACert(t) + + api.installCACerts(ctx, []CACertificate{{Name: "my-proxy-ca", Cert: certPEM}}) + + content, err := os.ReadFile(filepath.Join(certDir, "my-proxy-ca.crt")) + require.NoError(t, err) + assert.Equal(t, certPEM, string(content)) + }) + + t.Run("writes multiple cert files", func(t *testing.T) { + t.Parallel() + api, certDir := newAPIWithTempCertDir(t) + cert1 := generateTestCACert(t) + cert2 := generateTestCACert(t) + + api.installCACerts(ctx, []CACertificate{ + {Name: "ca-one", Cert: cert1}, + {Name: "ca-two", Cert: cert2}, + }) + + content, err := os.ReadFile(filepath.Join(certDir, "ca-one.crt")) + require.NoError(t, err) + assert.Equal(t, cert1, string(content)) + + content, err = os.ReadFile(filepath.Join(certDir, "ca-two.crt")) + require.NoError(t, err) + assert.Equal(t, cert2, string(content)) + }) + + t.Run("strips directory traversal from name", func(t *testing.T) { + t.Parallel() + api, certDir := newAPIWithTempCertDir(t) + certPEM := generateTestCACert(t) + + api.installCACerts(ctx, []CACertificate{{Name: "../../../etc/evil", Cert: certPEM}}) + + // filepath.Base("../../../etc/evil") == "evil", so the file lands inside certDir + content, err := os.ReadFile(filepath.Join(certDir, "evil.crt")) + require.NoError(t, err) + assert.Equal(t, certPEM, string(content)) + + // Nothing should have escaped the temp dir + _, err = os.ReadFile("/etc/evil.crt") + assert.True(t, os.IsNotExist(err)) + }) + + t.Run("cert content is valid PEM", func(t *testing.T) { + t.Parallel() + api, certDir := newAPIWithTempCertDir(t) + certPEM := generateTestCACert(t) + + api.installCACerts(ctx, []CACertificate{{Name: "valid-ca", Cert: certPEM}}) + + raw, err := os.ReadFile(filepath.Join(certDir, "valid-ca.crt")) + require.NoError(t, err) + + block, _ := pem.Decode(raw) + require.NotNil(t, block, "expected valid PEM block in written file") + assert.Equal(t, "CERTIFICATE", block.Type) + + _, err = x509.ParseCertificate(block.Bytes) + require.NoError(t, err, "expected parseable X.509 certificate") + }) +} diff --git a/packages/envd/internal/api/store.go b/packages/envd/internal/api/store.go index 2524206226..498b6bfb56 100644 --- a/packages/envd/internal/api/store.go +++ b/packages/envd/internal/api/store.go @@ -37,8 +37,14 @@ type API struct { lastSetTime *utils.AtomicMax initLock sync.Mutex + + // certDir is the directory where CA certificates are written before + // update-ca-certificates is run. Overridable in tests. + certDir string } +const systemCertDir = "/usr/local/share/ca-certificates" + func New(l *zerolog.Logger, defaults *execcontext.Defaults, mmdsChan chan *host.MMDSOpts, isNotFC bool) *API { return &API{ logger: l, @@ -48,6 +54,7 @@ func New(l *zerolog.Logger, defaults *execcontext.Defaults, mmdsChan chan *host. mmdsClient: &DefaultMMDSClient{}, lastSetTime: utils.NewAtomicMax(), accessToken: &SecureToken{}, + certDir: systemCertDir, } } diff --git a/packages/envd/pkg/version.go b/packages/envd/pkg/version.go index e15331fdfa..d51e146561 100644 --- a/packages/envd/pkg/version.go +++ b/packages/envd/pkg/version.go @@ -1,3 +1,3 @@ package pkg -const Version = "0.5.8" +const Version = "0.5.9" diff --git a/packages/envd/spec/envd.yaml b/packages/envd/spec/envd.yaml index 7dc0b3a9e8..f3af3ad7ee 100644 --- a/packages/envd/spec/envd.yaml +++ b/packages/envd/spec/envd.yaml @@ -64,6 +64,11 @@ paths: defaultWorkdir: type: string description: The default working directory to use for operations + caCertificates: + type: array + description: CA certificates to install into the system trust store + items: + $ref: "#/components/schemas/CACertificate" responses: "204": description: Env vars set, the time and metadata is synced with the host @@ -378,6 +383,20 @@ components: username: type: string description: User for setting ownership and resolving relative paths + CACertificate: + type: object + description: A CA certificate to install into the system trust store + additionalProperties: false + required: + - name + - cert + properties: + name: + type: string + description: Filename (without extension) for the certificate + cert: + type: string + description: PEM-encoded CA certificate VolumeMount: type: object description: Volume mount configuration From 0f2ce4870ee7fe1e75b005e3da8ef9a7f2035765 Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 12:26:00 +0200 Subject: [PATCH 2/9] Removed outdated and unused package.json --- packages/envd/package.json | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 packages/envd/package.json diff --git a/packages/envd/package.json b/packages/envd/package.json deleted file mode 100644 index b79080125a..0000000000 --- a/packages/envd/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@e2b/envd", - "version": "0.0.0-automated", - "description": "E2B Env Daemon", - "repository": "https://github.com/e2b-dev/infra", - "homepage": "https://e2b.dev", - "bugs": "https://github.com/e2b-dev/infra/issues", - "license": "MIT", - "author": "E2B", - "type": "module", - "url": "https://github.com/e2b-dev/infra/releases/download/v{{version}}/{{bin_name}}_{{version}}_{{platform}}_{{arch}}.tar.gz", - "bin": "bin/envd", - "release": { - "branches": [ - "main" - ] - } -} \ No newline at end of file From 8547ca74aff6803b460db5f026edf7a4d1462cef Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 14:09:38 +0200 Subject: [PATCH 3/9] Env init CA injecting sync so there is no data race --- packages/envd/internal/api/init.go | 12 +++++++----- tests/integration/internal/envd/generated.go | 12 ++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/envd/internal/api/init.go b/packages/envd/internal/api/init.go index 2e95d2c486..8a800b03c7 100644 --- a/packages/envd/internal/api/init.go +++ b/packages/envd/internal/api/init.go @@ -218,7 +218,7 @@ func (a *API) SetData(ctx context.Context, logger zerolog.Logger, data PostInitJ } if data.CaCertificates != nil && len(*data.CaCertificates) > 0 { - go a.installCACerts(context.WithoutCancel(ctx), *data.CaCertificates) + a.installCACerts(ctx, *data.CaCertificates) } if data.VolumeMounts != nil { @@ -260,7 +260,10 @@ func (a *API) setupNfs(ctx context.Context, nfsTarget, path string) { } // installCACerts writes PEM-encoded CA certificates to the system trust store -// and runs update-ca-certificates once to make them trusted by all TLS clients. +// and runs update-ca-certificates to register them with the OS. +// +// This call is intentionally synchronous: certs must be fully installed before +// user processes start or TLS connections through the proxy will fail. func (a *API) installCACerts(ctx context.Context, certs []CACertificate) { certDir := a.certDir @@ -280,10 +283,9 @@ func (a *API) installCACerts(ctx context.Context, certs []CACertificate) { } } - data, err := exec.CommandContext(ctx, "update-ca-certificates").CombinedOutput() - + out, err := exec.CommandContext(ctx, "update-ca-certificates").CombinedOutput() logFn := a.getLogger(err) - logFn.Str("output", string(data)).Msg("update-ca-certificates") + logFn.Str("output", string(out)).Msg("update-ca-certificates") } func (a *API) SetupHyperloop(address string) { diff --git a/tests/integration/internal/envd/generated.go b/tests/integration/internal/envd/generated.go index f254034502..83586e87a8 100644 --- a/tests/integration/internal/envd/generated.go +++ b/tests/integration/internal/envd/generated.go @@ -27,6 +27,15 @@ const ( File EntryInfoType = "file" ) +// CACertificate A CA certificate to install into the system trust store +type CACertificate struct { + // Cert PEM-encoded CA certificate + Cert string `json:"cert"` + + // Name Filename (without extension) for the certificate + Name string `json:"name"` +} + // ComposeRequest defines model for ComposeRequest. type ComposeRequest struct { // Destination Destination file path for the composed file @@ -178,6 +187,9 @@ type PostInitJSONBody struct { // AccessToken Access token for secure access to envd service AccessToken *SecureToken `json:"accessToken,omitempty"` + // CaCertificates CA certificates to install into the system trust store + CaCertificates *[]CACertificate `json:"caCertificates,omitempty"` + // DefaultUser The default user to use for operations DefaultUser *string `json:"defaultUser,omitempty"` From 700c11d247cfc38eb9b064d4063115fbcd5f57cb Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 14:09:53 +0200 Subject: [PATCH 4/9] Test re-running init with different certs on existing sbx --- packages/envd/internal/api/init_test.go | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/envd/internal/api/init_test.go b/packages/envd/internal/api/init_test.go index a3088dc188..cc8b109e32 100644 --- a/packages/envd/internal/api/init_test.go +++ b/packages/envd/internal/api/init_test.go @@ -693,4 +693,35 @@ func TestInstallCACerts(t *testing.T) { _, err = x509.ParseCertificate(block.Bytes) require.NoError(t, err, "expected parseable X.509 certificate") }) + + t.Run("re-init with one cert updates its content and leaves other cert untouched", func(t *testing.T) { + t.Parallel() + api, certDir := newAPIWithTempCertDir(t) + + certAv1 := generateTestCACert(t) + certB := generateTestCACert(t) + + // First init: two certs. + api.installCACerts(ctx, []CACertificate{ + {Name: "ca-a", Cert: certAv1}, + {Name: "ca-b", Cert: certB}, + }) + + // Second init: only cert-a with new content — cert-b is not mentioned. + certAv2 := generateTestCACert(t) + api.installCACerts(ctx, []CACertificate{ + {Name: "ca-a", Cert: certAv2}, + }) + + // cert-a must hold the new content. + contentA, err := os.ReadFile(filepath.Join(certDir, "ca-a.crt")) + require.NoError(t, err) + assert.Equal(t, certAv2, string(contentA), "ca-a should have updated content after second init") + assert.NotEqual(t, certAv1, string(contentA), "ca-a should no longer hold first-round content") + + // cert-b must still be present and unchanged. + contentB, err := os.ReadFile(filepath.Join(certDir, "ca-b.crt")) + require.NoError(t, err) + assert.Equal(t, certB, string(contentB), "ca-b should be unchanged after second init") + }) } From f181ed7e9ac3f7e476ba6c7929432d7205177046 Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 14:11:12 +0200 Subject: [PATCH 5/9] Envd CA implemented in orch --- packages/orchestrator/benchmark_test.go | 2 +- .../orchestrator/cmd/create-build/main.go | 2 +- .../orchestrator/cmd/resume-build/main.go | 2 +- .../orchestrator/cmd/smoketest/smoke_test.go | 2 +- packages/orchestrator/pkg/factories/run.go | 2 +- packages/orchestrator/pkg/sandbox/envd.go | 12 ++ .../orchestrator/pkg/sandbox/envd/envd.gen.go | 12 ++ .../orchestrator/pkg/sandbox/envd_test.go | 115 ++++++++++++++++++ .../pkg/sandbox/network/egressproxy.go | 29 ++++- packages/orchestrator/pkg/sandbox/sandbox.go | 8 ++ .../orchestrator/pkg/tcpfirewall/proxy.go | 4 + 11 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 packages/orchestrator/pkg/sandbox/envd_test.go diff --git a/packages/orchestrator/benchmark_test.go b/packages/orchestrator/benchmark_test.go index bf21c90ab7..830a76709b 100644 --- a/packages/orchestrator/benchmark_test.go +++ b/packages/orchestrator/benchmark_test.go @@ -185,7 +185,7 @@ func BenchmarkBaseImageLaunch(b *testing.B) { b.Cleanup(templateCache.Stop) sandboxes := sandbox.NewSandboxesMap() - sandboxFactory := sandbox.NewFactory(config.BuilderConfig, networkPool, devicePool, featureFlags, hoststats.NewNoopDelivery(), cgroup.NewNoopManager(), sandboxes) + sandboxFactory := sandbox.NewFactory(config.BuilderConfig, networkPool, devicePool, featureFlags, hoststats.NewNoopDelivery(), cgroup.NewNoopManager(), network.NewNoopEgressProxy(), sandboxes) dockerhubRepository, err := dockerhub.GetRemoteRepository(b.Context()) require.NoError(b, err) diff --git a/packages/orchestrator/cmd/create-build/main.go b/packages/orchestrator/cmd/create-build/main.go index 4cff864073..cb4aad596b 100644 --- a/packages/orchestrator/cmd/create-build/main.go +++ b/packages/orchestrator/cmd/create-build/main.go @@ -299,7 +299,7 @@ func doBuild( defer templateCache.Stop() buildMetrics, _ := metrics.NewBuildMetrics(noop.MeterProvider{}) - sandboxFactory := sandbox.NewFactory(c.BuilderConfig, networkPool, devicePool, featureFlags, hoststats.NewNoopDelivery(), cgroup.NewNoopManager(), sandboxes) + sandboxFactory := sandbox.NewFactory(c.BuilderConfig, networkPool, devicePool, featureFlags, hoststats.NewNoopDelivery(), cgroup.NewNoopManager(), network.NewNoopEgressProxy(), sandboxes) builder := build.NewBuilder( builderConfig, l, featureFlags, sandboxFactory, diff --git a/packages/orchestrator/cmd/resume-build/main.go b/packages/orchestrator/cmd/resume-build/main.go index 684c88d6cf..c17ae3b9d0 100644 --- a/packages/orchestrator/cmd/resume-build/main.go +++ b/packages/orchestrator/cmd/resume-build/main.go @@ -1052,7 +1052,7 @@ func run(ctx context.Context, buildID string, iterations int, coldStart, noPrefe if verbose { fmt.Println("🔧 Creating sandbox factory...") } - factory := sandbox.NewFactory(config.BuilderConfig, networkPool, devicePool, flags, hoststats.NewNoopDelivery(), cgroup.NewNoopManager(), sandboxes) + factory := sandbox.NewFactory(config.BuilderConfig, networkPool, devicePool, flags, hoststats.NewNoopDelivery(), cgroup.NewNoopManager(), network.NewNoopEgressProxy(), sandboxes) fmt.Printf("📦 Loading %s...\n", buildID) tmpl, err := cache.GetTemplate(ctx, buildID, false, false) diff --git a/packages/orchestrator/cmd/smoketest/smoke_test.go b/packages/orchestrator/cmd/smoketest/smoke_test.go index 3718dadb8e..fa358f06f6 100644 --- a/packages/orchestrator/cmd/smoketest/smoke_test.go +++ b/packages/orchestrator/cmd/smoketest/smoke_test.go @@ -228,7 +228,7 @@ func newTestInfra(t *testing.T, ctx context.Context) *testInfra { ti.closers = append(ti.closers, func(ctx context.Context) { sandboxProxy.Close(ctx) }) // Factory + Builder - factory := sandbox.NewFactory(orcConfig.BuilderConfig, networkPool, devicePool, flags, hoststats.NewNoopDelivery(), cgroup.NewNoopManager(), sandboxes) + factory := sandbox.NewFactory(orcConfig.BuilderConfig, networkPool, devicePool, flags, hoststats.NewNoopDelivery(), cgroup.NewNoopManager(), network.NewNoopEgressProxy(), sandboxes) ti.factory = factory buildMetrics, _ := metrics.NewBuildMetrics(noop.MeterProvider{}) diff --git a/packages/orchestrator/pkg/factories/run.go b/packages/orchestrator/pkg/factories/run.go index 609ee4fc90..c474cd19e3 100644 --- a/packages/orchestrator/pkg/factories/run.go +++ b/packages/orchestrator/pkg/factories/run.go @@ -532,7 +532,7 @@ func run(config cfg.Config, opts Options) (success bool) { closers = append(closers, closer{"network pool", networkPool.Close}) // sandbox factory - sandboxFactory := sandbox.NewFactory(config.BuilderConfig, networkPool, devicePool, featureFlags, hostStatsDelivery, cgroupManager, sandboxes) + sandboxFactory := sandbox.NewFactory(config.BuilderConfig, networkPool, devicePool, featureFlags, hostStatsDelivery, cgroupManager, egressSetup.Proxy, sandboxes) // isolated filesystems cache (for nfs proxy) builder := chrooted.NewBuilder(config) diff --git a/packages/orchestrator/pkg/sandbox/envd.go b/packages/orchestrator/pkg/sandbox/envd.go index 13a08c3a78..c4b717d818 100644 --- a/packages/orchestrator/pkg/sandbox/envd.go +++ b/packages/orchestrator/pkg/sandbox/envd.go @@ -16,6 +16,7 @@ import ( "go.uber.org/zap" "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/envd" + "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/network" "github.com/e2b-dev/infra/packages/shared/pkg/consts" "github.com/e2b-dev/infra/packages/shared/pkg/logger" "github.com/e2b-dev/infra/packages/shared/pkg/telemetry" @@ -42,6 +43,7 @@ func (s *Sandbox) doRequestWithInfiniteRetries( DefaultUser: utils.DerefOrDefault(s.Config.Envd.DefaultUser, ""), DefaultWorkdir: utils.DerefOrDefault(s.Config.Envd.DefaultWorkdir, ""), VolumeMounts: s.convertMounts(s.Config.VolumeMounts), + CaCertificates: s.convertCACertificates(s.CACertificates), } for { @@ -95,6 +97,16 @@ func (s *Sandbox) convertMounts(mounts []VolumeMountConfig) []envd.VolumeMount { return results } +func (s *Sandbox) convertCACertificates(certs []network.CACertificate) []envd.CACertificate { + results := make([]envd.CACertificate, 0, len(certs)) + + for _, cert := range certs { + results = append(results, envd.CACertificate{Name: cert.Name, Cert: cert.Cert}) + } + + return results +} + func (s *Sandbox) initEnvd(ctx context.Context) (e error) { ctx, span := tracer.Start(ctx, "envd-init", trace.WithAttributes(telemetry.WithEnvdVersion(s.Config.Envd.Version))) defer func() { diff --git a/packages/orchestrator/pkg/sandbox/envd/envd.gen.go b/packages/orchestrator/pkg/sandbox/envd/envd.gen.go index f77d914b4b..f435ebbb42 100644 --- a/packages/orchestrator/pkg/sandbox/envd/envd.gen.go +++ b/packages/orchestrator/pkg/sandbox/envd/envd.gen.go @@ -18,6 +18,15 @@ const ( File EntryInfoType = "file" ) +// CACertificate A CA certificate to install into the system trust store +type CACertificate struct { + // Cert PEM-encoded CA certificate + Cert string `json:"cert"` + + // Name Filename (without extension) for the certificate + Name string `json:"name"` +} + // ComposeRequest defines model for ComposeRequest. type ComposeRequest struct { // Destination Destination file path for the composed file @@ -169,6 +178,9 @@ type PostInitJSONBody struct { // AccessToken Access token for secure access to envd service AccessToken SecureToken `json:"accessToken,omitempty"` + // CaCertificates CA certificates to install into the system trust store + CaCertificates []CACertificate `json:"caCertificates,omitempty"` + // DefaultUser The default user to use for operations DefaultUser string `json:"defaultUser,omitempty"` diff --git a/packages/orchestrator/pkg/sandbox/envd_test.go b/packages/orchestrator/pkg/sandbox/envd_test.go new file mode 100644 index 0000000000..49e188df7c --- /dev/null +++ b/packages/orchestrator/pkg/sandbox/envd_test.go @@ -0,0 +1,115 @@ +package sandbox + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/coreos/go-iptables/iptables" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/envd" + "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/network" +) + +// mockEgressProxy is a test EgressProxy that returns a fixed set of CA certificates. +type mockEgressProxy struct { + certs []network.CACertificate +} + +func (m *mockEgressProxy) OnSlotCreate(_ *network.Slot, _ *iptables.IPTables) error { return nil } +func (m *mockEgressProxy) OnSlotDelete(_ *network.Slot, _ *iptables.IPTables) error { return nil } +func (m *mockEgressProxy) CACertificates() []network.CACertificate { return m.certs } + +// newTestSandboxWithCerts builds a minimal Sandbox that has CACertificates set — +// mirroring what Factory.CreateSandbox does with f.egressProxy.CACertificates(). +func newTestSandboxWithCerts(certs []network.CACertificate) *Sandbox { + return &Sandbox{ + Metadata: &Metadata{ + internalConfig: internalConfig{EnvdInitRequestTimeout: 5 * time.Second}, + Config: NewConfig(Config{}), + Runtime: RuntimeMetadata{SandboxID: "test-sandbox"}, + }, + CACertificates: certs, + } +} + +func TestConvertCACertificates(t *testing.T) { + t.Parallel() + + t.Run("converts network certs to envd certs preserving name and PEM", func(t *testing.T) { + t.Parallel() + + proxy := &mockEgressProxy{ + certs: []network.CACertificate{ + {Name: "proxy-ca", Cert: "-----BEGIN CERTIFICATE-----\nABC\n-----END CERTIFICATE-----\n"}, + {Name: "custom-ca", Cert: "-----BEGIN CERTIFICATE-----\nDEF\n-----END CERTIFICATE-----\n"}, + }, + } + + sbx := newTestSandboxWithCerts(proxy.CACertificates()) + result := sbx.convertCACertificates(sbx.CACertificates) + + require.Len(t, result, 2) + assert.Equal(t, "proxy-ca", result[0].Name) + assert.Equal(t, "-----BEGIN CERTIFICATE-----\nABC\n-----END CERTIFICATE-----\n", result[0].Cert) + assert.Equal(t, "custom-ca", result[1].Name) + assert.Equal(t, "-----BEGIN CERTIFICATE-----\nDEF\n-----END CERTIFICATE-----\n", result[1].Cert) + }) + + t.Run("returns empty slice for nil certs", func(t *testing.T) { + t.Parallel() + sbx := newTestSandboxWithCerts(nil) + result := sbx.convertCACertificates(nil) + assert.Empty(t, result) + }) +} + +// TestEnvdInitSendsCACertificates verifies the full injection chain: +// EgressProxy.CACertificates() → Sandbox.CACertificates → POST /init body. +func TestEnvdInitSendsCACertificates(t *testing.T) { + // Not parallel: overrides the package-level sandboxHttpClient. + + proxy := &mockEgressProxy{ + certs: []network.CACertificate{ + {Name: "proxy-ca", Cert: "-----BEGIN CERTIFICATE-----\nPROXY\n-----END CERTIFICATE-----\n"}, + {Name: "custom-ca", Cert: "-----BEGIN CERTIFICATE-----\nCUSTOM\n-----END CERTIFICATE-----\n"}, + }, + } + + // Simulate what Factory.CreateSandbox does when assigning egress proxy certs. + sbx := newTestSandboxWithCerts(proxy.CACertificates()) + + var captured envd.PostInitJSONBody + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "/init", r.URL.Path) + + err := json.NewDecoder(r.Body).Decode(&captured) + require.NoError(t, err) + + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + // Temporarily swap the package-level client so the sandbox reaches our test server. + orig := sandboxHttpClient + sandboxHttpClient = http.Client{Timeout: 5 * time.Second} + defer func() { sandboxHttpClient = orig }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, _, err := sbx.doRequestWithInfiniteRetries(ctx, http.MethodPost, server.URL+"/init") + require.NoError(t, err) + + require.Len(t, captured.CaCertificates, 2) + assert.Equal(t, proxy.certs[0].Name, captured.CaCertificates[0].Name) + assert.Equal(t, proxy.certs[0].Cert, captured.CaCertificates[0].Cert) + assert.Equal(t, proxy.certs[1].Name, captured.CaCertificates[1].Name) + assert.Equal(t, proxy.certs[1].Cert, captured.CaCertificates[1].Cert) +} diff --git a/packages/orchestrator/pkg/sandbox/network/egressproxy.go b/packages/orchestrator/pkg/sandbox/network/egressproxy.go index 42a8a34bf0..c80377876c 100644 --- a/packages/orchestrator/pkg/sandbox/network/egressproxy.go +++ b/packages/orchestrator/pkg/sandbox/network/egressproxy.go @@ -4,13 +4,38 @@ import ( "github.com/coreos/go-iptables/iptables" ) +type CACertificate struct { + // Name is the filename (without extension) used when installing the cert into the trust store. + Name string + + // Cert is the PEM-encoded CA certificate. + Cert string +} + type EgressProxy interface { OnSlotCreate(s *Slot, tables *iptables.IPTables) error OnSlotDelete(s *Slot, tables *iptables.IPTables) error + + CACertificates() []CACertificate } // NoopEgressProxy is a no-op implementation of EgressProxy. type NoopEgressProxy struct{} -func (NoopEgressProxy) OnSlotCreate(_ *Slot, _ *iptables.IPTables) error { return nil } -func (NoopEgressProxy) OnSlotDelete(_ *Slot, _ *iptables.IPTables) error { return nil } +var _ EgressProxy = (*NoopEgressProxy)(nil) + +func NewNoopEgressProxy() NoopEgressProxy { + return NoopEgressProxy{} +} + +func (NoopEgressProxy) OnSlotCreate(_ *Slot, _ *iptables.IPTables) error { + return nil +} + +func (NoopEgressProxy) OnSlotDelete(_ *Slot, _ *iptables.IPTables) error { + return nil +} + +func (NoopEgressProxy) CACertificates() []CACertificate { + return nil +} diff --git a/packages/orchestrator/pkg/sandbox/sandbox.go b/packages/orchestrator/pkg/sandbox/sandbox.go index a03d94efb3..89683dd4d9 100644 --- a/packages/orchestrator/pkg/sandbox/sandbox.go +++ b/packages/orchestrator/pkg/sandbox/sandbox.go @@ -229,6 +229,8 @@ type Sandbox struct { // It was used to store the config to allow API restarts APIStoredConfig *orchestrator.SandboxConfig + CACertificates []network.CACertificate + exit *utils.ErrorOnce stop utils.Lazy[error] @@ -273,6 +275,7 @@ type Factory struct { featureFlags *featureflags.Client hostStatsDelivery hoststats.Delivery cgroupManager cgroup.Manager + egressProxy network.EgressProxy } func NewFactory( @@ -282,6 +285,7 @@ func NewFactory( featureFlags *featureflags.Client, hostStatsDelivery hoststats.Delivery, cgroupManager cgroup.Manager, + egressProxy network.EgressProxy, sandboxes *Map, ) *Factory { return &Factory{ @@ -292,6 +296,7 @@ func NewFactory( featureFlags: featureFlags, hostStatsDelivery: hostStatsDelivery, cgroupManager: cgroupManager, + egressProxy: egressProxy, } } @@ -467,6 +472,8 @@ func (f *Factory) CreateSandbox( APIStoredConfig: apiConfigToStore, + CACertificates: f.egressProxy.CACertificates(), + exit: exit, } @@ -805,6 +812,7 @@ func (f *Factory) ResumeSandbox( cleanup: cleanup, APIStoredConfig: apiConfigToStore, + CACertificates: f.egressProxy.CACertificates(), exit: exit, } diff --git a/packages/orchestrator/pkg/tcpfirewall/proxy.go b/packages/orchestrator/pkg/tcpfirewall/proxy.go index c640a0666b..6c5967df8a 100644 --- a/packages/orchestrator/pkg/tcpfirewall/proxy.go +++ b/packages/orchestrator/pkg/tcpfirewall/proxy.go @@ -175,6 +175,10 @@ func (p *Proxy) Close(_ context.Context) error { return nil } +func (p *Proxy) CACertificates() []network.CACertificate { + return nil +} + // handlerFunc is the signature for connection handlers. type handlerFunc func(ctx context.Context, conn net.Conn, dstIP net.IP, dstPort int, sbx *sandbox.Sandbox, logger logger.Logger, metrics *Metrics, protocol Protocol) From ce411e80dfcf048578ef2efcfa91bfebca80d234 Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 14:14:52 +0200 Subject: [PATCH 6/9] Fmt, linter --- packages/envd/internal/api/init_test.go | 1 + packages/orchestrator/pkg/sandbox/envd_test.go | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/envd/internal/api/init_test.go b/packages/envd/internal/api/init_test.go index cc8b109e32..6346b641b3 100644 --- a/packages/envd/internal/api/init_test.go +++ b/packages/envd/internal/api/init_test.go @@ -624,6 +624,7 @@ func TestInstallCACerts(t *testing.T) { t.Helper() api := newTestAPI(nil, &mockMMDSClient{}) api.certDir = t.TempDir() + return api, api.certDir } diff --git a/packages/orchestrator/pkg/sandbox/envd_test.go b/packages/orchestrator/pkg/sandbox/envd_test.go index 49e188df7c..845197bd55 100644 --- a/packages/orchestrator/pkg/sandbox/envd_test.go +++ b/packages/orchestrator/pkg/sandbox/envd_test.go @@ -71,9 +71,9 @@ func TestConvertCACertificates(t *testing.T) { // TestEnvdInitSendsCACertificates verifies the full injection chain: // EgressProxy.CACertificates() → Sandbox.CACertificates → POST /init body. -func TestEnvdInitSendsCACertificates(t *testing.T) { - // Not parallel: overrides the package-level sandboxHttpClient. - +// +// Not parallel: overrides the package-level sandboxHttpClient. +func TestEnvdInitSendsCACertificates(t *testing.T) { //nolint:paralleltest proxy := &mockEgressProxy{ certs: []network.CACertificate{ {Name: "proxy-ca", Cert: "-----BEGIN CERTIFICATE-----\nPROXY\n-----END CERTIFICATE-----\n"}, @@ -86,11 +86,11 @@ func TestEnvdInitSendsCACertificates(t *testing.T) { var captured envd.PostInitJSONBody server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - require.Equal(t, "/init", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/init", r.URL.Path) err := json.NewDecoder(r.Body).Decode(&captured) - require.NoError(t, err) + assert.NoError(t, err) w.WriteHeader(http.StatusNoContent) })) @@ -104,8 +104,9 @@ func TestEnvdInitSendsCACertificates(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - _, _, err := sbx.doRequestWithInfiniteRetries(ctx, http.MethodPost, server.URL+"/init") + resp, _, err := sbx.doRequestWithInfiniteRetries(ctx, http.MethodPost, server.URL+"/init") require.NoError(t, err) + defer resp.Body.Close() require.Len(t, captured.CaCertificates, 2) assert.Equal(t, proxy.certs[0].Name, captured.CaCertificates[0].Name) From b6c5614fa0cc93cbe9b1359c4a318fdd6fdc763f Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 14:42:36 +0200 Subject: [PATCH 7/9] Optimize for case where both certs and volumes are used CA certs needs to be apply in sync to fix case where init will finish before CA store update and some commands can fails as cert used by proxy is still not trusted. Running install CA function in sync mode makes init slower. This change introduces optimization for case where both volumes (also slowing init request) and CA are upsed, function will use shared wait groop and wait only once. --- packages/envd/internal/api/init.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/envd/internal/api/init.go b/packages/envd/internal/api/init.go index 8a800b03c7..1a134ee43f 100644 --- a/packages/envd/internal/api/init.go +++ b/packages/envd/internal/api/init.go @@ -217,12 +217,15 @@ func (a *API) SetData(ctx context.Context, logger zerolog.Logger, data PostInitJ a.defaults.Workdir = data.DefaultWorkdir } + var wg sync.WaitGroup + if data.CaCertificates != nil && len(*data.CaCertificates) > 0 { - a.installCACerts(ctx, *data.CaCertificates) + wg.Go(func() { + a.installCACerts(ctx, *data.CaCertificates) + }) } if data.VolumeMounts != nil { - var wg sync.WaitGroup for _, volume := range *data.VolumeMounts { logger.Debug().Msgf("Mounting %s at %q", volume.NfsTarget, volume.Path) @@ -230,10 +233,10 @@ func (a *API) SetData(ctx context.Context, logger zerolog.Logger, data PostInitJ a.setupNfs(context.WithoutCancel(ctx), volume.NfsTarget, volume.Path) }) } - - wg.Wait() } + wg.Wait() + return nil } From e49d7baf72e6854e3c997cc08aecd77f8928b0cc Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 14:56:40 +0200 Subject: [PATCH 8/9] Fix installCACerts early return skipping update-ca-certificates Previously a WriteFile failure for any cert caused an immediate return, leaving already-written certs on disk but never registered in the system trust store via update-ca-certificates. This meant a single bad cert could silently break TLS for all other injected certs. Changed the early return to continue so every cert is attempted and update-ca-certificates always runs at the end regardless of individual write failures. Errors are still logged per cert. Added a test that forces a write failure by pre-creating the target path as a directory, then asserts the next cert in the list is still written. --- packages/envd/internal/api/init.go | 2 +- packages/envd/internal/api/init_test.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/envd/internal/api/init.go b/packages/envd/internal/api/init.go index 1a134ee43f..fac904ef7d 100644 --- a/packages/envd/internal/api/init.go +++ b/packages/envd/internal/api/init.go @@ -282,7 +282,7 @@ func (a *API) installCACerts(ctx context.Context, certs []CACertificate) { if err := os.WriteFile(certPath, []byte(c.Cert), 0o644); err != nil { a.logger.Error().Err(err).Str("name", c.Name).Msg("failed to write CA certificate") - return + continue } } diff --git a/packages/envd/internal/api/init_test.go b/packages/envd/internal/api/init_test.go index 6346b641b3..461c13a55b 100644 --- a/packages/envd/internal/api/init_test.go +++ b/packages/envd/internal/api/init_test.go @@ -695,6 +695,24 @@ func TestInstallCACerts(t *testing.T) { require.NoError(t, err, "expected parseable X.509 certificate") }) + t.Run("skips failing cert and writes remaining certs", func(t *testing.T) { + t.Parallel() + api, certDir := newAPIWithTempCertDir(t) + certPEM := generateTestCACert(t) + + // Pre-create a directory at the path where "bad-ca.crt" would be written. + // WriteFile will fail because the path is a directory, not a regular file. + require.NoError(t, os.Mkdir(filepath.Join(certDir, "bad-ca.crt"), 0o755)) + + api.installCACerts(ctx, []CACertificate{ + {Name: "bad-ca", Cert: certPEM}, // will fail — path is a directory + {Name: "good-ca", Cert: certPEM}, // must still be written + }) + + _, err := os.ReadFile(filepath.Join(certDir, "good-ca.crt")) + require.NoError(t, err, "good-ca.crt should be written even though bad-ca failed") + }) + t.Run("re-init with one cert updates its content and leaves other cert untouched", func(t *testing.T) { t.Parallel() api, certDir := newAPIWithTempCertDir(t) From 377807f511320c8b6303b07ddbae945823a1138d Mon Sep 17 00:00:00 2001 From: Jiri Sveceny Date: Mon, 30 Mar 2026 15:06:41 +0200 Subject: [PATCH 9/9] Validate CA cert name before using it in a file path CodeQL flagged c.Name as user-controlled input flowing into a path expression (go/path-injection). Although the cert name is set by the orchestrator's egress proxy rather than a direct user, it is deserialized from the HTTP request body so CodeQL's taint analysis correctly traces it as external input. Fix: validate the name against a strict allowlist regexp (^[a-zA-Z0-9_-]+$) before constructing the path, and reject any name that contains dots, slashes, spaces, or other special characters. This makes the intent explicit and removes the taint from CodeQL's perspective without relying on filepath.Base as a sanitiser. The existing path-traversal test is updated to assert the cert is now rejected outright rather than silently sanitised. A second test covers other invalid name forms (dots, slashes, empty string). --- packages/envd/internal/api/init.go | 20 +++++++++++++++++-- packages/envd/internal/api/init_test.go | 26 ++++++++++++++++++------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/envd/internal/api/init.go b/packages/envd/internal/api/init.go index fac904ef7d..021c189e14 100644 --- a/packages/envd/internal/api/init.go +++ b/packages/envd/internal/api/init.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "sync" "time" @@ -262,6 +263,16 @@ func (a *API) setupNfs(ctx context.Context, nfsTarget, path string) { } } +// certNameRe matches safe filenames for CA certificates: lowercase letters, +// digits, hyphens, and underscores only. No path separators or dots allowed. +var certNameRe = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + +// validCertName reports whether name is safe to use as a filename in the +// CA certificate directory. +func validCertName(name string) bool { + return name != "" && certNameRe.MatchString(name) +} + // installCACerts writes PEM-encoded CA certificates to the system trust store // and runs update-ca-certificates to register them with the OS. // @@ -277,8 +288,13 @@ func (a *API) installCACerts(ctx context.Context, certs []CACertificate) { } for _, c := range certs { - // Use filepath.Base to strip any directory components from the name. - certPath := certDir + "/" + filepath.Base(c.Name) + ".crt" + if !validCertName(c.Name) { + a.logger.Error().Str("name", c.Name).Msg("skipping CA certificate with invalid name") + + continue + } + + certPath := filepath.Join(certDir, c.Name+".crt") if err := os.WriteFile(certPath, []byte(c.Cert), 0o644); err != nil { a.logger.Error().Err(err).Str("name", c.Name).Msg("failed to write CA certificate") diff --git a/packages/envd/internal/api/init_test.go b/packages/envd/internal/api/init_test.go index 461c13a55b..8ffb06842a 100644 --- a/packages/envd/internal/api/init_test.go +++ b/packages/envd/internal/api/init_test.go @@ -660,21 +660,33 @@ func TestInstallCACerts(t *testing.T) { assert.Equal(t, cert2, string(content)) }) - t.Run("strips directory traversal from name", func(t *testing.T) { + t.Run("rejects name with path traversal", func(t *testing.T) { t.Parallel() api, certDir := newAPIWithTempCertDir(t) certPEM := generateTestCACert(t) api.installCACerts(ctx, []CACertificate{{Name: "../../../etc/evil", Cert: certPEM}}) - // filepath.Base("../../../etc/evil") == "evil", so the file lands inside certDir - content, err := os.ReadFile(filepath.Join(certDir, "evil.crt")) - require.NoError(t, err) - assert.Equal(t, certPEM, string(content)) + // The name is invalid so no file should be written anywhere. + _, err := os.ReadFile(filepath.Join(certDir, "evil.crt")) + assert.True(t, os.IsNotExist(err), "no file should be written for a traversal name") - // Nothing should have escaped the temp dir _, err = os.ReadFile("/etc/evil.crt") - assert.True(t, os.IsNotExist(err)) + assert.True(t, os.IsNotExist(err), "cert must not escape the cert directory") + }) + + t.Run("rejects name with dots or slashes", func(t *testing.T) { + t.Parallel() + api, certDir := newAPIWithTempCertDir(t) + certPEM := generateTestCACert(t) + + for _, bad := range []string{"ca.bad", "ca/bad", "ca bad", ""} { + api.installCACerts(ctx, []CACertificate{{Name: bad, Cert: certPEM}}) + } + + entries, err := os.ReadDir(certDir) + require.NoError(t, err) + assert.Empty(t, entries, "no files should be written for invalid names") }) t.Run("cert content is valid PEM", func(t *testing.T) {