|
| 1 | +package envd |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/base64" |
| 5 | + "fmt" |
| 6 | + "strings" |
| 7 | + "testing" |
| 8 | + |
| 9 | + "github.com/stretchr/testify/assert" |
| 10 | + "github.com/stretchr/testify/require" |
| 11 | + |
| 12 | + "github.com/e2b-dev/infra/tests/integration/internal/api" |
| 13 | + "github.com/e2b-dev/infra/tests/integration/internal/setup" |
| 14 | + "github.com/e2b-dev/infra/tests/integration/internal/utils" |
| 15 | +) |
| 16 | + |
| 17 | +// bakedCertBundlePath is where the build packs the system trust store. On a |
| 18 | +// cold boot (filesystem-only reboot resume) envd.service seeds the /etc/ssl/certs |
| 19 | +// tmpfs from this tar and skips update-ca-certificates, so whatever is in here |
| 20 | +// is the trust store the sandbox comes up with. |
| 21 | +const bakedCertBundlePath = "/usr/local/share/e2b/ssl-certs.tar" |
| 22 | + |
| 23 | +// injectCACommand writes a PEM into destPath as a shell command, WITHOUT running |
| 24 | +// update-ca-certificates — the realistic case (the /usr/local/share/ca-certificates |
| 25 | +// dir exists precisely so a dropped cert is picked up later). base64 avoids |
| 26 | +// shell-escaping the multi-line PEM. |
| 27 | +func injectCACommand(certPEM, destPath string) string { |
| 28 | + b64 := base64.StdEncoding.EncodeToString([]byte(certPEM)) |
| 29 | + |
| 30 | + return fmt.Sprintf("echo '%s' | base64 -d > %s", b64, destPath) |
| 31 | +} |
| 32 | + |
| 33 | +// readBakedCertBundle extracts ca-certificates.crt from the build-time tar — i.e. |
| 34 | +// exactly what a cold boot would seed /etc/ssl/certs from. |
| 35 | +func readBakedCertBundle(t *testing.T, sbx *api.Sandbox, client *setup.EnvdClient) string { |
| 36 | + t.Helper() |
| 37 | + |
| 38 | + out, err := utils.ExecCommandAsRootWithOutput(t, t.Context(), sbx, client, |
| 39 | + "tar", "-xOf", bakedCertBundlePath, "./ca-certificates.crt") |
| 40 | + require.NoError(t, err, "extract ca-certificates.crt from baked tar") |
| 41 | + |
| 42 | + return out |
| 43 | +} |
| 44 | + |
| 45 | +// TestCACertFromBuildSurvivesInBakedBundle verifies that a CA a template adds |
| 46 | +// during the build ends up in the cert tar packed at build time — the trust |
| 47 | +// store a cold-boot (filesystem-only reboot) resume comes up with. |
| 48 | +// |
| 49 | +// Because the boot fast path seeds /etc/ssl/certs from the tar and skips |
| 50 | +// update-ca-certificates, the tar must already contain every CA the template |
| 51 | +// produced. The finalize phase runs update-ca-certificates and packs the tar as |
| 52 | +// the build's last guest step (after all build steps and start/ready), so: |
| 53 | +// - a CA dropped under /usr/local/share/ca-certificates without the user |
| 54 | +// running update-ca-certificates must still be merged (RUN-step case); |
| 55 | +// - a CA installed by the start command — after configure.sh — must still be |
| 56 | +// captured (start-command case). |
| 57 | +// |
| 58 | +// This validates the tar *artifact* only. The boot-time seeding that consumes |
| 59 | +// it runs solely on a cold boot, which no runtime path triggers today — every |
| 60 | +// resume is a memory resume that restores the tmpfs from RAM and skips |
| 61 | +// ExecStartPre. Once filesystem-only reboot resume lands, the seeding becomes a |
| 62 | +// live path and should get its own end-to-end test (build a CA template, pause |
| 63 | +// memory:false, resume, assert the trust store). Keeping this guard now means |
| 64 | +// the artifact is already proven correct when that path turns on. |
| 65 | +func TestCACertFromBuildSurvivesInBakedBundle(t *testing.T) { |
| 66 | + t.Parallel() |
| 67 | + |
| 68 | + tests := []struct { |
| 69 | + name string |
| 70 | + template string |
| 71 | + cn string |
| 72 | + buildData func(certPEM string) api.TemplateBuildStartV2 |
| 73 | + }{ |
| 74 | + { |
| 75 | + name: "added in RUN step without update-ca-certificates", |
| 76 | + template: "test-fs-only-ca-buildstep", |
| 77 | + cn: "e2b-test-ca-buildstep", |
| 78 | + buildData: func(certPEM string) api.TemplateBuildStartV2 { |
| 79 | + return api.TemplateBuildStartV2{ |
| 80 | + FromImage: new("ubuntu:22.04"), |
| 81 | + Steps: new([]api.TemplateStep{ |
| 82 | + { |
| 83 | + Type: "RUN", |
| 84 | + Force: new(true), |
| 85 | + // Run as root (RUN's optional 2nd arg): the CA dir is |
| 86 | + // root-owned and not world-writable until finalize, so |
| 87 | + // don't depend on the default build-step user. |
| 88 | + Args: new([]string{injectCACommand(certPEM, "/usr/local/share/ca-certificates/e2b-buildstep-ca.crt"), "root"}), |
| 89 | + }, |
| 90 | + }), |
| 91 | + } |
| 92 | + }, |
| 93 | + }, |
| 94 | + { |
| 95 | + name: "installed by start command after configure", |
| 96 | + template: "test-fs-only-ca-startcmd", |
| 97 | + cn: "e2b-test-ca-startcmd", |
| 98 | + buildData: func(certPEM string) api.TemplateBuildStartV2 { |
| 99 | + return api.TemplateBuildStartV2{ |
| 100 | + FromImage: new("ubuntu:22.04"), |
| 101 | + // Install the CA directly from the start command, which runs |
| 102 | + // after configure.sh (where /usr/local becomes world-writable), |
| 103 | + // then keep running. Exercises certs added after the |
| 104 | + // configuration script. Writing straight to the persisted CA dir |
| 105 | + // avoids staging through /tmp, which is tmpfs and would not |
| 106 | + // survive into the fresh finalize sandbox. |
| 107 | + StartCmd: new(injectCACommand(certPEM, "/usr/local/share/ca-certificates/e2b-startcmd-ca.crt") + " && sleep infinity"), |
| 108 | + ReadyCmd: new("for _ in $(seq 1 50); do test -f /usr/local/share/ca-certificates/e2b-startcmd-ca.crt && exit 0; sleep 0.2; done; exit 1"), |
| 109 | + } |
| 110 | + }, |
| 111 | + }, |
| 112 | + } |
| 113 | + |
| 114 | + for _, tc := range tests { |
| 115 | + t.Run(tc.name, func(t *testing.T) { |
| 116 | + t.Parallel() |
| 117 | + |
| 118 | + c := setup.GetAPIClient() |
| 119 | + client := setup.GetEnvdClient(t, t.Context()) |
| 120 | + |
| 121 | + certPEM := generateSelfSignedCA(t, tc.cn) |
| 122 | + |
| 123 | + tmpl := utils.BuildTemplate(t, utils.TemplateBuildOptions{ |
| 124 | + Name: tc.template, |
| 125 | + BuildData: tc.buildData(certPEM), |
| 126 | + LogHandler: utils.DefaultBuildLogHandler(t), |
| 127 | + ReqEditors: []api.RequestEditorFn{setup.WithAPIKey()}, |
| 128 | + }) |
| 129 | + |
| 130 | + sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithTemplateID(tmpl.TemplateID)) |
| 131 | + |
| 132 | + bundle := readBakedCertBundle(t, sbx, client) |
| 133 | + assert.Contains(t, bundle, strings.TrimRight(certPEM, "\n"), |
| 134 | + "CA added during the build must be in the tar that seeds the cold-boot trust store") |
| 135 | + }) |
| 136 | + } |
| 137 | +} |
0 commit comments