Skip to content

fix(execd): reload rotated mitmproxy CA - #1378

Open
Gujiassh wants to merge 14 commits into
opensandbox-group:mainfrom
Gujiassh:fix/execd-reload-rotated-mitm-ca-1370
Open

fix(execd): reload rotated mitmproxy CA#1378
Gujiassh wants to merge 14 commits into
opensandbox-group:mainfrom
Gujiassh:fix/execd-reload-rotated-mitm-ca-1370

Conversation

@Gujiassh

Copy link
Copy Markdown
Contributor

Summary

  • add an execd-owned watcher for the exported mitmproxy CA
  • refresh system, NSS, JDK, and stable bundle trust after an egress-only restart rotates the certificate
  • retry failed or raced refreshes without accepting an uninstalled fingerprint

Closes #1370. This completes scenario B after #1371 fixed scenario C's co-restart race.

Behavior

The egress container atomically replaces /opt/opensandbox/mitmproxy-ca-cert.pem after regenerating its CA. Bootstrap previously installed that certificate only once.

When transparent MITM is enabled, execd now fingerprints the parsed X.509 DER and ignores missing, empty, malformed, or non-CA files. A changed fingerprint invokes an internal bootstrap refresh mode, reusing the existing trust-store logic instead of duplicating it in Go.

Refresh mode atomically replaces merged-ca-certificates.pem, reports applicable installation failures, and never starts another execd. The watcher accepts the fingerprint only after refresh succeeds and the source remains unchanged. It has a two-minute command timeout and exits with execd.

Only the public certificate remains on the shared volume; the private key stays in egress. Future execd child processes use the refreshed trust. Long-running applications that cache TLS contexts must recreate them or restart.

Verification

  • go test -race ./pkg/...
  • go vet ./...
  • Linux build and Windows amd64 cross-build
  • bootstrap POSIX/Bash syntax and refresh regression
  • Docker bootstrap distribution test
  • docs build
  • Alpine runtime oracle with two atomically published CA generations; the logged DER fingerprint and merged bundle matched generation 2

Focused tests cover unchanged certificates, late arrival, invalid/non-CA input, failed refresh retry, a second rotation during refresh, and context cancellation.

@github-actions github-actions Bot added component/execd documentation Improvements or additions to documentation size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 23, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1aac0bc6a1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/execd/pkg/mitmca/watcher.go Outdated
refresh: func(refreshCtx context.Context) error {
commandCtx, cancel := context.WithTimeout(refreshCtx, refreshTimeout)
defer cancel()
output, refreshErr := exec.CommandContext(commandCtx, "/bin/sh", bootstrapPath, refreshOnlyArg).CombinedOutput()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist CA env vars from refresh-only mode

When this refresh path handles the first usable CA after the initial bootstrap timed out, it runs bootstrap.sh --refresh-mitm-ca-trust in a separate shell, so the export REQUESTS_CA_BUNDLE / SSL_CERT_FILE / NODE_EXTRA_CA_CERTS done by refresh_mitm_ca is discarded when that child exits. Execd keeps its original environment and future Python/Node commands launched through execd still won't see the CA bundle even though the watcher reports a refresh; persist those values through EXECD_ENVS or have execd update its own environment after a successful refresh.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

…tated-mitm-ca-1370

# Conflicts:
#	docs/components/egress.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc78604a75

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/execd/pkg/mitmca/watcher.go Outdated
panic("mitmca watcher requires a positive poll interval")
}

trusted, hasTrusted, err := readCertificateFingerprint(cfg.caPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the initial certificate instead of trusting it

When the CA rotates after bootstrap finishes installing the old certificate but before this initial snapshot, trusted is initialized to the new fingerprint even though none of the trust stores contain it, so the watcher will never invoke refresh until another rotation. The same problem occurs when bootstrap's best-effort installation encounters a transient failure; initialize without accepting the on-disk fingerprint, or otherwise pass the successfully installed fingerprint from bootstrap.

Useful? React with 👍 / 👎.

Comment on lines +50 to +53
OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT=true \
EXECD="$TEST_DIR/execd" \
PATH="$MOCK_BIN" \
sh "$TEST_BOOTSTRAP" --refresh-mitm-ca-trust

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear JAVA_HOME before running the isolated refresh test

When a developer or CI runner has JAVA_HOME set, restricting PATH here does not isolate the JDK scan: trust_mitm_ca_jdk still visits that real JDK, deletes any existing opensandbox-mitmproxy alias, and imports the test bundle into its live cacerts. Unset JAVA_HOME (and prevent the fallback scan) so this regression test cannot modify the host toolchain.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3f8a31118

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/execd/pkg/mitmca/watcher.go Outdated
refresh: func(refreshCtx context.Context) error {
commandCtx, cancel := context.WithTimeout(refreshCtx, refreshTimeout)
defer cancel()
output, refreshErr := exec.CommandContext(commandCtx, "/bin/sh", bootstrapPath, refreshOnlyArg).CombinedOutput()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Kill the refresh process group on timeout

When the two-minute timeout or daemon context cancellation occurs while bootstrap.sh is waiting on a child such as update-ca-certificates, certutil, keytool, or sudo, exec.CommandContext kills only the /bin/sh process. The child can continue modifying trust stores and, because it retains the combined-output descriptors, can keep CombinedOutput and the watcher blocked beyond the advertised timeout; on daemon shutdown it may also survive as an orphan. Start the refresh in its own process group and terminate that group when the context expires.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04c0934766

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/execd/pkg/mitmca/watcher.go Outdated
Comment on lines +101 to +103
{name: "NODE_EXTRA_CA_CERTS", value: defaultCAPath},
{name: "REQUESTS_CA_BUNDLE", value: defaultMergedCAPath},
{name: "SSL_CERT_FILE", value: defaultMergedCAPath},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve bootstrap trust-variable overrides

When transparent MITM is enabled and EXECD_BOOTSTRAP_PRE_SCRIPT intentionally exports a custom CA bundle, the script is sourced after the initial trust setup so its values are inherited by execd. However, the watcher refreshes the currently installed certificate on its first polling tick and these unconditional assignments overwrite that customization even when no rotation occurred; subsequent commands and PTY sessions launched through execd then lose the configured corporate or composite CA bundle. Preserve explicit post-bootstrap overrides, or only replace values that still contain the bootstrap-managed paths.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 284fad7d34

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/execd/pkg/mitmca/watcher.go Outdated
Comment on lines +81 to +84
if refreshErr != nil {
return fmt.Errorf("bootstrap refresh failed: %w output=%q", refreshErr, compactOutput(output))
}
return setProcessTrustEnvironment(os.Setenv)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply process trust env despite partial store failures

When the CA first appears after bootstrap timed out, any NSS or JDK import failure makes refresh_mitm_ca 1 exit nonzero even if the merged bundle was created successfully. This early return therefore skips setProcessTrustEnvironment, so future Node and certifi-based processes still lack the CA variables, while the watcher repeats the expensive full refresh every polling cycle. Update the process environment after the merged bundle succeeds while continuing to retry the failed trust stores separately.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c38873b502

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/execd/pkg/mitmca/watcher.go Outdated
Comment on lines +105 to +106
if getenv(variable.name) != "" {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update managed fallback CA variables after recovery

When the initial bootstrap finds the MITM certificate but cannot create the merged bundle, bootstrap.sh exports REQUESTS_CA_BUNDLE and SSL_CERT_FILE as the MITM-only certificate path. If a later watcher refresh successfully creates the merged bundle, this nonempty check treats those bootstrap-managed fallback values as custom overrides and leaves them unchanged, so future Python/OpenSSL processes still lack the normal public root set and can fail on HTTPS connections that bypass interception. Preserve genuine custom values, but replace the known managed fallback path with defaultMergedCAPath after recovery.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 310347db31

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# See the License for the specific language governing permissions and
# limitations under the License.

set -euo pipefail

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the refresh regression in CI

This new regression script is not referenced by components/execd/Makefile or .github/workflows/execd-test.yml, whose Linux jobs currently run only the existing smoke, SIGTERM, and bwrap scripts. Consequently, changes that break refresh-only bootstrap behavior can still pass every execd CI job; add this script to the Linux workflow or a test target invoked there.

AGENTS.md reference: AGENTS.md:L49-L51

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 931bd4147f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +102 to +103
{name: "REQUESTS_CA_BUNDLE", value: defaultMergedCAPath, managedFallback: defaultCAPath},
{name: "SSL_CERT_FILE", value: defaultMergedCAPath, managedFallback: defaultCAPath},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace managed fallbacks in cached session environments

When bootstrap initially falls back to the MITM-only path and a persistent bash session is created before a later successful recovery, these assignments update only execd's process environment. newBashSession in components/execd/pkg/runtime/bash_session.go caches the old values, and buildWrappedScript re-exports that snapshot over the updated inherited environment on every subsequent run_in_session, leaving REQUESTS_CA_BUNDLE and SSL_CERT_FILE without public roots indefinitely. Fresh evidence beyond the earlier process-environment issue is this separate session-level cache; replace known managed fallbacks there as well so processes launched after recovery use the merged bundle promised by the updated documentation.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86579d92b5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

updatedEnv := parseExportDump(envLines)
s.mu.Lock()
if len(updatedEnv) > 0 {
applyManagedEnvironmentUpdates(updatedEnv, s.managedEnvironment)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicitly cleared session variables

After a managed CA update has been recorded, any later session command that runs unset REQUESTS_CA_BUNDLE or exports it as an empty value has that explicit override silently replaced here, because a missing/empty entry is treated as needing the managed value whenever the environment dump is persisted. This breaks normal bash-session environment persistence and contradicts the documented promise to preserve explicit environment overrides; distinguish an explicitly cleared variable from an absent bootstrap-managed value instead of unconditionally restoring it.

AGENTS.md reference: AGENTS.md:L42-L44

Useful? React with 👍 / 👎.

Comment thread components/execd/pkg/runtime/ctrl.go Outdated
Comment on lines +79 to +81
c.bashSessionClientMap.Range(func(_, sessionValue any) bool {
if session, ok := sessionValue.(*bashSession); ok {
session.updateManagedEnvironment(name, value, managedFallback)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Synchronize session creation with managed updates

When a bash session is created concurrently with fallback recovery, newBashSession can snapshot the old process environment before os.Setenv, while its later Store can occur after this Range has already passed; that session is then absent from the callback and retains the MITM-only fallback indefinitely. Keep the latest managed updates on the controller and apply them while publishing new sessions, or otherwise synchronize creation with this traversal, so every session created around a refresh receives the merged bundle as documented.

AGENTS.md reference: AGENTS.md:L42-L44

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 27, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d454993f5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread components/execd/pkg/runtime/ctrl.go Outdated
s.managedEnvironment = make(map[string]managedEnvironmentUpdate)
}
s.managedEnvironment[name] = update
applyManagedEnvironmentUpdate(s.env, name, update, true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve clears made before the first managed update

When a session explicitly unsets one of these variables before the watcher records its first successful update—for example, during the initial two-second polling window or while waiting for a late CA—replaceEmpty=true treats the missing value as bootstrap-managed and silently restores it. The later persistence logic only preserves clears made after managedEnvironment has been registered, so this remains a distinct path beyond the earlier clear-handling comment; track whether the session originally inherited the managed fallback instead of replacing every absent value, as the updated egress documentation promises to preserve explicit overrides.

AGENTS.md reference: AGENTS.md:L42-L44

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d57268210b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +114 to +119
if err := setenv(variable.name, variable.value); err != nil {
return fmt.Errorf("set child-process trust environment %s: %w", variable.name, err)
}
if onUpdate != nil {
onUpdate(variable.name, variable.value, variable.managedFallback)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate recovered trust variables to the Jupyter server

When bootstrap times out before the CA appears, or initially falls back to the MITM-only bundle, this updates only execd's own environment. runJupyter delegates kernel creation to the separately running Jupyter server, which retains its original missing or fallback values, so even newly created Python or Node kernels after recovery do not inherit the corrected trust environment. Distinct from the earlier process-environment issue, the fresh evidence is the external Jupyter parent process in pkg/runtime/jupyter.go; recovery must update or restart that runtime as well.

AGENTS.md reference: AGENTS.md:L42-L44

Useful? React with 👍 / 👎.

Comment on lines +144 to +147
for name, previousValue := range previous {
currentValue, exists := current[name]
if !exists || (previousValue != "" && currentValue == "") {
cleared[name] = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid treating filtered environment values as explicit clears

When a bash session inherits an exported value larger than maxPersistedEnvValueSize, parseExportDump intentionally omits it, but this loop interprets that omission as an explicit unset. The next wrapped command therefore emits unset NAME, causing an unchanged inherited variable—such as a large credential or certificate payload—to disappear after the first session command; restrict clear tracking to variables that were actually eligible for persistence or to the managed CA variables.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/execd documentation Improvements or additions to documentation size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

egress sidecar restart rotates mitmproxy CA and breaks HTTPS from the agent container

1 participant