Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/envd/internal/api/api.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions packages/envd/internal/api/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"io"
"net/http"
"net/netip"
"os"
"os/exec"
"path/filepath"
"sync"
"time"

Expand Down Expand Up @@ -215,6 +217,10 @@
a.defaults.Workdir = data.DefaultWorkdir
}

if data.CaCertificates != nil && len(*data.CaCertificates) > 0 {
a.installCACerts(ctx, *data.CaCertificates)
}

if data.VolumeMounts != nil {
var wg sync.WaitGroup
for _, volume := range *data.VolumeMounts {
Expand Down Expand Up @@ -253,6 +259,35 @@
}
}

// installCACerts writes PEM-encoded CA certificates to the system trust store
// 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

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
}
}

out, err := exec.CommandContext(ctx, "update-ca-certificates").CombinedOutput()
logFn := a.getLogger(err)
logFn.Str("output", string(out)).Msg("update-ca-certificates")
}

func (a *API) SetupHyperloop(address string) {
a.hyperloopLock.Lock()
defer a.hyperloopLock.Unlock()
Expand Down
141 changes: 141 additions & 0 deletions packages/envd/internal/api/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{
Expand Down Expand Up @@ -585,3 +615,114 @@ 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")
})

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")
})
}
7 changes: 7 additions & 0 deletions packages/envd/internal/api/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -48,6 +54,7 @@ func New(l *zerolog.Logger, defaults *execcontext.Defaults, mmdsChan chan *host.
mmdsClient: &DefaultMMDSClient{},
lastSetTime: utils.NewAtomicMax(),
accessToken: &SecureToken{},
certDir: systemCertDir,
}
}

Expand Down
18 changes: 0 additions & 18 deletions packages/envd/package.json

This file was deleted.

2 changes: 1 addition & 1 deletion packages/envd/pkg/version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package pkg

const Version = "0.5.8"
const Version = "0.5.9"
19 changes: 19 additions & 0 deletions packages/envd/spec/envd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/orchestrator/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/orchestrator/cmd/create-build/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/orchestrator/cmd/resume-build/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/orchestrator/cmd/smoketest/smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down
2 changes: 1 addition & 1 deletion packages/orchestrator/pkg/factories/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading