-
Notifications
You must be signed in to change notification settings - Fork 282
Propagate CA bundle to sandbox trust store on init #2325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sitole
wants to merge
14
commits into
main
Choose a base branch
from
feat/envd-ca-certificates
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
ae800c7
Removed unused package.json
sitole 6f7ca0e
orchestrator: propagate egress proxy CA certificates to sandbox init
sitole 3ebbe78
envd: install CA certificate into system trust bundle on init
sitole 8befade
chore: auto-commit generated changes
github-actions[bot] 3940e44
envd: fix data race and goroutine ordering in CACertInstaller
sitole 7c2d649
envd: bind-mount CA bundle to tmpfs to eliminate NBD latency on inject
sitole 4511756
envd: fix EXDEV rename and 0600 permissions in removeCertFromBundle
sitole 1dd8a54
envd: fix BindMountCABundle self-overwrite on process restart
sitole 51f74f0
Merge branch 'main' into feat/envd-ca-certificates
sitole 6f27f7c
Fmt after merge
sitole 20cba47
Merge branch 'main' into feat/envd-ca-certificates
sitole c0890e3
Tmpfs mount logic moved to provision and systemd service startup
sitole 30618f5
envd: persist injected CA cert for update-ca-certificates compat
sitole c80e77c
tests/integration: add CA cert injection and rotation tests
sitole File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| package host | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "sync" | ||
| "syscall" | ||
| "time" | ||
|
|
||
| "github.com/rs/zerolog" | ||
| ) | ||
|
|
||
| const ( | ||
| CaBundlePath = "/etc/ssl/certs/ca-certificates.crt" | ||
| CaStatePath = E2BRunDir + "/ca-cert.pem" | ||
|
|
||
| // caBundleTmpfsPath is the tmpfs-backed copy of the CA bundle. | ||
| // CaBundlePath is bind-mounted over this so all writes bypass NBD. | ||
| caBundleTmpfsPath = E2BRunDir + "/ca-certificates.crt" | ||
| ) | ||
|
|
||
| // BindMountCABundle copies the system CA bundle to tmpfs and bind-mounts it | ||
| // back over the original path so all subsequent reads and writes bypass the | ||
| // NBD-backed filesystem entirely. This reduces CA cert injection from ~2 ms | ||
| // (warm NBD) / ~460 ms (cold GCS) to ~0.01 ms. | ||
| // | ||
| // Must be called once at startup, before any /init handler runs. No-op if the | ||
| // bind mount is already in place (safe to call after a process restart). | ||
| func BindMountCABundle() error { | ||
sitole marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Copy current bundle to tmpfs destination. | ||
| src, err := os.Open(CaBundlePath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer src.Close() | ||
|
|
||
| if err := os.MkdirAll(filepath.Dir(caBundleTmpfsPath), 0o755); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| dst, err := os.OpenFile(caBundleTmpfsPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer dst.Close() | ||
|
|
||
| if _, err := io.Copy(dst, src); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Flush and close before mounting. | ||
| dst.Close() | ||
| src.Close() | ||
|
|
||
| // Bind-mount the tmpfs file over the original bundle path. | ||
| // MS_BIND makes the target appear as the source; the underlying NBD file | ||
| // is shadowed for all processes in this mount namespace. | ||
| if err := syscall.Mount(caBundleTmpfsPath, CaBundlePath, "", syscall.MS_BIND, ""); err != nil { | ||
| // EBUSY means the bind mount is already in place (process restart). | ||
| if err == syscall.EBUSY { | ||
| return nil | ||
| } | ||
|
|
||
| return err | ||
| } | ||
|
|
||
| return nil | ||
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // CACertInstaller manages installation of a CA certificate into the VM's | ||
| // system trust bundle. | ||
| type CACertInstaller struct { | ||
| mu sync.Mutex | ||
| logger *zerolog.Logger | ||
|
|
||
| // lastCACert caches the most recently installed PEM so that resume (same | ||
| // cert, same process) is a zero-I/O hot-path hit. Empty on process start; | ||
| // the state file at CaStatePath is the durable record across restarts. | ||
| lastCACert string | ||
| } | ||
|
|
||
| func NewCACertInstaller(logger *zerolog.Logger) *CACertInstaller { | ||
| return &CACertInstaller{logger: logger} | ||
| } | ||
|
|
||
| // Install injects certPEM into the system CA bundle. | ||
| func (c *CACertInstaller) Install(ctx context.Context, certPEM string) { | ||
| c.install(ctx, certPEM, CaBundlePath, CaStatePath) | ||
| } | ||
|
|
||
| // install is the testable core; tests supply their own paths. | ||
| // | ||
| // The cert changes on every sandbox create but stays the same across | ||
| // pause/resume cycles. The critical path only appends to the bundle (~0.04 ms | ||
| // after BindMountCABundle moves the file to tmpfs); removing the previous cert | ||
| // happens in a background goroutine. | ||
| // | ||
| // The state file survives process restarts (OOM, crashes). The background | ||
| // goroutine reads it to find the previously installed cert — lastCACert is "" | ||
| // after a restart and cannot be used for that purpose. | ||
| // | ||
| // All goroutine work runs under mu to keep the bundle and state file | ||
| // consistent with concurrent foreground appends. | ||
| func (c *CACertInstaller) install(_ context.Context, certPEM, bundlePath, statePath string) { | ||
| if certPEM == "" { | ||
| return | ||
| } | ||
|
|
||
| start := time.Now() | ||
|
|
||
| // Normalise to a single trailing newline so comparisons and removals are | ||
| // consistent regardless of how the caller formatted the PEM. | ||
| normalized := strings.TrimRight(certPEM, "\n") + "\n" | ||
|
|
||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
|
|
||
| if c.lastCACert == normalized { | ||
sitole marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| c.logger.Debug(). | ||
| Dur("duration", time.Since(start)). | ||
| Msg("CA cert unchanged, skipping install") | ||
|
|
||
sitole marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Snapshot the previous cert before overwriting; used as fallback when no | ||
| // state file exists yet. | ||
| prevPEM := c.lastCACert | ||
|
|
||
| f, err := os.OpenFile(bundlePath, os.O_APPEND|os.O_WRONLY, 0o644) | ||
| if err != nil { | ||
| c.logger.Error().Err(err).Msg("Failed to open CA bundle") | ||
|
|
||
| return | ||
| } | ||
|
|
||
| _, err = f.WriteString(normalized) | ||
| f.Close() | ||
|
|
||
| if err != nil { | ||
| c.logger.Error().Err(err).Msg("Failed to write CA cert to bundle") | ||
|
|
||
| return | ||
| } | ||
|
|
||
| c.lastCACert = normalized | ||
|
|
||
| c.logger.Info(). | ||
| Dur("append_duration", time.Since(start)). | ||
| Msg("CA cert appended to bundle") | ||
|
|
||
| go func() { | ||
| cleanStart := time.Now() | ||
|
|
||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
|
|
||
| // A newer install has taken over; let that goroutine handle cleanup. | ||
| if c.lastCACert != normalized { | ||
| return | ||
| } | ||
|
|
||
| // State file takes priority over the in-memory prevPEM: it holds the | ||
| // cert from the previous process lifetime after a restart. | ||
| stateRaw, _ := os.ReadFile(statePath) | ||
| effectivePrev := string(stateRaw) | ||
| if effectivePrev == "" { | ||
| effectivePrev = prevPEM | ||
| } | ||
|
|
||
| if err := os.WriteFile(statePath, []byte(normalized), 0o644); err != nil { | ||
| c.logger.Error().Err(err).Msg("Failed to write CA cert state file") | ||
|
|
||
| return | ||
sitole marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // No prior cert, or same cert received again after a restart. | ||
| if effectivePrev == "" || effectivePrev == normalized { | ||
| return | ||
| } | ||
sitole marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if err := removeCertFromBundle(bundlePath, effectivePrev); err != nil { | ||
| c.logger.Error().Err(err).Msg("Failed to remove old CA cert from bundle") | ||
|
|
||
| return | ||
| } | ||
|
|
||
| c.logger.Info(). | ||
| Dur("cleanup_duration", time.Since(cleanStart)). | ||
| Msg("Old CA cert removed from bundle") | ||
| }() | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
sitole marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // removeCertFromBundle rewrites bundlePath removing all occurrences of certPEM. | ||
| // The write is atomic (write to temp file, then rename) so the bundle is never | ||
| // empty from the perspective of concurrent readers. Must be called under mu. | ||
| func removeCertFromBundle(bundlePath, certPEM string) error { | ||
| content, err := os.ReadFile(bundlePath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| cleaned := strings.ReplaceAll(string(content), certPEM, "") | ||
sitole marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| tmp, err := os.CreateTemp(filepath.Dir(bundlePath), "ca-bundle-*") | ||
sitole marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| return fmt.Errorf("create temp file: %w", err) | ||
| } | ||
|
|
||
| tmpPath := tmp.Name() | ||
|
|
||
| if _, err := tmp.WriteString(cleaned); err != nil { | ||
| tmp.Close() | ||
| os.Remove(tmpPath) | ||
|
|
||
| return fmt.Errorf("write temp file: %w", err) | ||
| } | ||
|
|
||
| if err := tmp.Close(); err != nil { | ||
| os.Remove(tmpPath) | ||
|
|
||
| return fmt.Errorf("close temp file: %w", err) | ||
| } | ||
|
|
||
| if err := os.Rename(tmpPath, bundlePath); err != nil { | ||
sitole marked this conversation as resolved.
Show resolved
Hide resolved
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| os.Remove(tmpPath) | ||
|
|
||
| return fmt.Errorf("rename temp file: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.