Skip to content

Commit 4cb560e

Browse files
committed
Envd support for custom CA
1 parent 9fdee90 commit 4cb560e

File tree

6 files changed

+181
-1
lines changed

6 files changed

+181
-1
lines changed

packages/envd/internal/api/api.gen.go

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/envd/internal/api/init.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"io"
99
"net/http"
1010
"net/netip"
11+
"os"
1112
"os/exec"
13+
"path/filepath"
1214
"sync"
1315
"time"
1416

@@ -215,6 +217,10 @@ func (a *API) SetData(ctx context.Context, logger zerolog.Logger, data PostInitJ
215217
a.defaults.Workdir = data.DefaultWorkdir
216218
}
217219

220+
if data.CaCertificates != nil && len(*data.CaCertificates) > 0 {
221+
go a.installCACerts(context.WithoutCancel(ctx), *data.CaCertificates)
222+
}
223+
218224
if data.VolumeMounts != nil {
219225
var wg sync.WaitGroup
220226
for _, volume := range *data.VolumeMounts {
@@ -253,6 +259,33 @@ func (a *API) setupNfs(ctx context.Context, nfsTarget, path string) {
253259
}
254260
}
255261

262+
// installCACerts writes PEM-encoded CA certificates to the system trust store
263+
// and runs update-ca-certificates once to make them trusted by all TLS clients.
264+
func (a *API) installCACerts(ctx context.Context, certs []CACertificate) {
265+
certDir := a.certDir
266+
267+
if err := os.MkdirAll(certDir, 0o755); err != nil {
268+
a.logger.Error().Err(err).Msg("failed to create ca-certificates directory")
269+
270+
return
271+
}
272+
273+
for _, c := range certs {
274+
// Use filepath.Base to strip any directory components from the name.
275+
certPath := certDir + "/" + filepath.Base(c.Name) + ".crt"
276+
if err := os.WriteFile(certPath, []byte(c.Cert), 0o644); err != nil {
277+
a.logger.Error().Err(err).Str("name", c.Name).Msg("failed to write CA certificate")
278+
279+
return
280+
}
281+
}
282+
283+
data, err := exec.CommandContext(ctx, "update-ca-certificates").CombinedOutput()
284+
285+
logFn := a.getLogger(err)
286+
logFn.Str("output", string(data)).Msg("update-ca-certificates")
287+
}
288+
256289
func (a *API) SetupHyperloop(address string) {
257290
a.hyperloopLock.Lock()
258291
defer a.hyperloopLock.Unlock()

packages/envd/internal/api/init_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ package api
22

33
import (
44
"context"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/rand"
8+
"crypto/x509"
9+
"crypto/x509/pkix"
10+
"encoding/pem"
11+
"math/big"
512
"os"
613
"path/filepath"
714
"strings"
@@ -18,6 +25,29 @@ import (
1825
utilsShared "github.com/e2b-dev/infra/packages/shared/pkg/utils"
1926
)
2027

28+
// generateTestCACert creates a minimal self-signed CA certificate and returns
29+
// it as a PEM-encoded string. The cert is not written to disk.
30+
func generateTestCACert(t *testing.T) string {
31+
t.Helper()
32+
33+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
34+
require.NoError(t, err)
35+
36+
template := &x509.Certificate{
37+
SerialNumber: big.NewInt(1),
38+
Subject: pkix.Name{CommonName: "Test CA"},
39+
NotBefore: time.Now().Add(-time.Minute),
40+
NotAfter: time.Now().Add(time.Hour),
41+
IsCA: true,
42+
BasicConstraintsValid: true,
43+
}
44+
45+
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
46+
require.NoError(t, err)
47+
48+
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
49+
}
50+
2151
func TestSimpleCases(t *testing.T) {
2252
t.Parallel()
2353
testCases := map[string]func(string) string{
@@ -585,3 +615,82 @@ func TestSetData(t *testing.T) {
585615
assert.Equal(t, "value", val)
586616
})
587617
}
618+
619+
func TestInstallCACerts(t *testing.T) {
620+
t.Parallel()
621+
ctx := context.Background()
622+
623+
newAPIWithTempCertDir := func(t *testing.T) (*API, string) {
624+
t.Helper()
625+
api := newTestAPI(nil, &mockMMDSClient{})
626+
api.certDir = t.TempDir()
627+
return api, api.certDir
628+
}
629+
630+
t.Run("writes single cert file with .crt extension", func(t *testing.T) {
631+
t.Parallel()
632+
api, certDir := newAPIWithTempCertDir(t)
633+
certPEM := generateTestCACert(t)
634+
635+
api.installCACerts(ctx, []CACertificate{{Name: "my-proxy-ca", Cert: certPEM}})
636+
637+
content, err := os.ReadFile(filepath.Join(certDir, "my-proxy-ca.crt"))
638+
require.NoError(t, err)
639+
assert.Equal(t, certPEM, string(content))
640+
})
641+
642+
t.Run("writes multiple cert files", func(t *testing.T) {
643+
t.Parallel()
644+
api, certDir := newAPIWithTempCertDir(t)
645+
cert1 := generateTestCACert(t)
646+
cert2 := generateTestCACert(t)
647+
648+
api.installCACerts(ctx, []CACertificate{
649+
{Name: "ca-one", Cert: cert1},
650+
{Name: "ca-two", Cert: cert2},
651+
})
652+
653+
content, err := os.ReadFile(filepath.Join(certDir, "ca-one.crt"))
654+
require.NoError(t, err)
655+
assert.Equal(t, cert1, string(content))
656+
657+
content, err = os.ReadFile(filepath.Join(certDir, "ca-two.crt"))
658+
require.NoError(t, err)
659+
assert.Equal(t, cert2, string(content))
660+
})
661+
662+
t.Run("strips directory traversal from name", func(t *testing.T) {
663+
t.Parallel()
664+
api, certDir := newAPIWithTempCertDir(t)
665+
certPEM := generateTestCACert(t)
666+
667+
api.installCACerts(ctx, []CACertificate{{Name: "../../../etc/evil", Cert: certPEM}})
668+
669+
// filepath.Base("../../../etc/evil") == "evil", so the file lands inside certDir
670+
content, err := os.ReadFile(filepath.Join(certDir, "evil.crt"))
671+
require.NoError(t, err)
672+
assert.Equal(t, certPEM, string(content))
673+
674+
// Nothing should have escaped the temp dir
675+
_, err = os.ReadFile("/etc/evil.crt")
676+
assert.True(t, os.IsNotExist(err))
677+
})
678+
679+
t.Run("cert content is valid PEM", func(t *testing.T) {
680+
t.Parallel()
681+
api, certDir := newAPIWithTempCertDir(t)
682+
certPEM := generateTestCACert(t)
683+
684+
api.installCACerts(ctx, []CACertificate{{Name: "valid-ca", Cert: certPEM}})
685+
686+
raw, err := os.ReadFile(filepath.Join(certDir, "valid-ca.crt"))
687+
require.NoError(t, err)
688+
689+
block, _ := pem.Decode(raw)
690+
require.NotNil(t, block, "expected valid PEM block in written file")
691+
assert.Equal(t, "CERTIFICATE", block.Type)
692+
693+
_, err = x509.ParseCertificate(block.Bytes)
694+
require.NoError(t, err, "expected parseable X.509 certificate")
695+
})
696+
}

packages/envd/internal/api/store.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ type API struct {
3737

3838
lastSetTime *utils.AtomicMax
3939
initLock sync.Mutex
40+
41+
// certDir is the directory where CA certificates are written before
42+
// update-ca-certificates is run. Overridable in tests.
43+
certDir string
4044
}
4145

46+
const systemCertDir = "/usr/local/share/ca-certificates"
47+
4248
func New(l *zerolog.Logger, defaults *execcontext.Defaults, mmdsChan chan *host.MMDSOpts, isNotFC bool) *API {
4349
return &API{
4450
logger: l,
@@ -48,6 +54,7 @@ func New(l *zerolog.Logger, defaults *execcontext.Defaults, mmdsChan chan *host.
4854
mmdsClient: &DefaultMMDSClient{},
4955
lastSetTime: utils.NewAtomicMax(),
5056
accessToken: &SecureToken{},
57+
certDir: systemCertDir,
5158
}
5259
}
5360

packages/envd/pkg/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package pkg
22

3-
const Version = "0.5.8"
3+
const Version = "0.5.9"

packages/envd/spec/envd.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ paths:
6464
defaultWorkdir:
6565
type: string
6666
description: The default working directory to use for operations
67+
caCertificates:
68+
type: array
69+
description: CA certificates to install into the system trust store
70+
items:
71+
$ref: "#/components/schemas/CACertificate"
6772
responses:
6873
"204":
6974
description: Env vars set, the time and metadata is synced with the host
@@ -378,6 +383,20 @@ components:
378383
username:
379384
type: string
380385
description: User for setting ownership and resolving relative paths
386+
CACertificate:
387+
type: object
388+
description: A CA certificate to install into the system trust store
389+
additionalProperties: false
390+
required:
391+
- name
392+
- cert
393+
properties:
394+
name:
395+
type: string
396+
description: Filename (without extension) for the certificate
397+
cert:
398+
type: string
399+
description: PEM-encoded CA certificate
381400
VolumeMount:
382401
type: object
383402
description: Volume mount configuration

0 commit comments

Comments
 (0)