From 2b49e523423c9810992d7d564c7217f322d893ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=84=B6?= Date: Wed, 22 Jul 2026 15:42:46 +0800 Subject: [PATCH] fix(egress): purge stale mitmproxy CA on startup mitmproxy's CA lives in the egress container's ephemeral confdir (/var/lib/mitmproxy/.mitmproxy/), so every egress container restart regenerates a fresh CA. The previous generation's public cert may still be on the pod-scoped shared volume; without a purge, an agent container starting or restarting alongside egress passes its bootstrap readiness check on the stale file and installs a CA that no longer matches what mitmproxy will sign with. Erase /opt/opensandbox/mitmproxy-ca-cert.pem as early as possible in egress main() (immediately after logger initialization, before any telemetry / policy / iptables / mitm setup) so the agent's wait-loop blocks until this generation's SyncRootCA has completed. Scope: - Closes the race for pod-container co-restart (both containers restarting together with an emptyDir that survived). - Does NOT address the egress-alone container restart case where the agent's bootstrap has long finished and never re-reads the file; that still requires an agent-side reload path. Refs opensandbox-group/OpenSandbox#1370 --- components/egress/main.go | 5 +++ .../egress/pkg/mitmproxy/cacert_export.go | 34 ++++++++++++++++ .../pkg/mitmproxy/cacert_export_test.go | 39 +++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/components/egress/main.go b/components/egress/main.go index 347e87869..d74c98c23 100644 --- a/components/egress/main.go +++ b/components/egress/main.go @@ -50,6 +50,11 @@ func main() { ctx = withLogger(ctx) defer log.Logger.Sync() + // Erase any stale mitmproxy CA left on the shared volume by a previous + // egress generation so the agent's bootstrap wait-loop blocks for this + // generation's export. See PurgeStaleExportedCA / upstream issue #1370. + mitmproxy.PurgeStaleExportedCA() + otelShutdown, err := telemetry.Init(ctx) if err != nil { log.Warnf("OpenTelemetry metrics disabled (continuing without OTLP): %v", err) diff --git a/components/egress/pkg/mitmproxy/cacert_export.go b/components/egress/pkg/mitmproxy/cacert_export.go index 258767aa8..3fc235dd4 100644 --- a/components/egress/pkg/mitmproxy/cacert_export.go +++ b/components/egress/pkg/mitmproxy/cacert_export.go @@ -62,6 +62,40 @@ func waitMitmCACertPath(confDirEnv, home string) (string, error) { return "", fmt.Errorf("mitmproxy CA not found after %v (tried: %v)", waitCACert, cands) } +// PurgeStaleExportedCA removes any mitmproxy-ca-cert.pem left on the shared +// volume by a previous generation of this egress container. mitmproxy's CA +// lives in the egress container's ephemeral confdir, so an egress container +// restart rotates the CA while the previous public cert is still on the +// (pod-scoped) shared volume; without this purge, an agent container starting +// alongside us would pass its bootstrap readiness check on the stale file +// and install a CA that no longer matches what mitmproxy will sign with. +// See upstream issue #1370. Must be called as early as possible in main(). +func PurgeStaleExportedCA() { + if !constants.IsTruthy(os.Getenv(constants.EnvMitmproxyTransparent)) { + return + } + purgeStaleExportedCAFrom(constants.OpenSandboxRootDir) +} + +// purgeStaleExportedCAFrom is the testable core of PurgeStaleExportedCA. +func purgeStaleExportedCAFrom(rootDir string) { + path := filepath.Join(rootDir, mitmCACertName) + err := os.Remove(path) + switch { + case err == nil: + // warn: on first pod startup this file should not exist; its presence + // means the egress container (or process) has restarted, which is + // useful signal when diagnosing HTTPS-from-agent problems. + log.Warnf("[mitmproxy] removed stale exported CA at %s (previous egress generation left it behind; see upstream issue #1370)", path) + case os.IsNotExist(err): + // Common on first pod startup. + default: + // Non-fatal: SyncRootCA will overwrite the file. But on the race + // path the agent may still grab the old contents before we do. + log.Warnf("[mitmproxy] failed to remove stale exported CA at %s: %v", path, err) + } +} + func SyncRootCA(confDirEnv, home string) error { src, err := waitMitmCACertPath(confDirEnv, home) if err != nil { diff --git a/components/egress/pkg/mitmproxy/cacert_export_test.go b/components/egress/pkg/mitmproxy/cacert_export_test.go index 13731c759..157d07936 100644 --- a/components/egress/pkg/mitmproxy/cacert_export_test.go +++ b/components/egress/pkg/mitmproxy/cacert_export_test.go @@ -15,6 +15,7 @@ package mitmproxy import ( + "os" "path/filepath" "testing" @@ -33,3 +34,41 @@ func TestCandidateCACertPaths(t *testing.T) { filepath.Join(h, ".mitmproxy", mitmCACertName), }, got) } + +func TestPurgeStaleExportedCAFrom_RemovesExistingFile(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, mitmCACertName) + require.NoError(t, os.WriteFile(path, []byte("stale-ca"), 0o644)) + + purgeStaleExportedCAFrom(root) + + _, err := os.Stat(path) + require.True(t, os.IsNotExist(err), "expected file to be removed, got err=%v", err) +} + +func TestPurgeStaleExportedCAFrom_NoFileIsNoOp(t *testing.T) { + root := t.TempDir() + // No file present; must not panic and must not create anything. + purgeStaleExportedCAFrom(root) + + entries, err := os.ReadDir(root) + require.NoError(t, err) + require.Empty(t, entries) +} + +func TestPurgeStaleExportedCAFrom_LeavesUnrelatedFiles(t *testing.T) { + root := t.TempDir() + stale := filepath.Join(root, mitmCACertName) + require.NoError(t, os.WriteFile(stale, []byte("stale-ca"), 0o644)) + + // A sibling file that must survive (e.g. the agent's merged bundle). + other := filepath.Join(root, "merged-ca-certificates.pem") + require.NoError(t, os.WriteFile(other, []byte("merged"), 0o644)) + + purgeStaleExportedCAFrom(root) + + _, err := os.Stat(stale) + require.True(t, os.IsNotExist(err)) + _, err = os.Stat(other) + require.NoError(t, err) +}