Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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.

60 changes: 57 additions & 3 deletions packages/envd/internal/api/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import (
"io"
"net/http"
"net/netip"
"os"
"os/exec"
"path/filepath"
"regexp"
"sync"
"time"

Expand Down Expand Up @@ -215,19 +218,26 @@ 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 {
wg.Go(func() {
a.installCACerts(ctx, *data.CaCertificates)
})
Comment thread
sitole marked this conversation as resolved.
}
Comment thread
sitole marked this conversation as resolved.

if data.VolumeMounts != nil {
var wg sync.WaitGroup
for _, volume := range *data.VolumeMounts {
logger.Debug().Msgf("Mounting %s at %q", volume.NfsTarget, volume.Path)

wg.Go(func() {
a.setupNfs(context.WithoutCancel(ctx), volume.NfsTarget, volume.Path)
})
}

wg.Wait()
}

wg.Wait()

return nil
}

Expand All @@ -253,6 +263,50 @@ 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.
//
// 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 {
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 {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
a.logger.Error().Err(err).Str("name", c.Name).Msg("failed to write CA certificate")

continue
}
}
Comment thread
sitole marked this conversation as resolved.

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
171 changes: 171 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,144 @@ 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("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}})

// 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")

_, err = os.ReadFile("/etc/evil.crt")
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) {
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("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)

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