Skip to content

ateapi: auto-inject the egress trust volume into every actor - #1252

Open
Max Thompson (thompsonmax) wants to merge 1 commit into
agent-substrate:mainfrom
thompsonmax:egress-trust-injection
Open

ateapi: auto-inject the egress trust volume into every actor#1252
Max Thompson (thompsonmax) wants to merge 1 commit into
agent-substrate:mainfrom
thompsonmax:egress-trust-injection

Conversation

@thompsonmax

@thompsonmax Max Thompson (thompsonmax) commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Part of #932 (PR 3 of 3). PR 1 (#941) is merged; PR 2 (#1231) is orthogonal and can land in either order. This closes #871's open item: on a deployment that intercepts actor egress TLS, every actor gets the CA anchors with nothing declared in its template.

What this does

A new ateapi flag, --inject-egress-trust-bundle (default off), adds one systemInfo volume, trust.ate.dev, projecting the egress-mitm.ate.dev bundle to /run/substrate/certs/egress-mitm.ate.dev.pem in every container. Injection lives in an ActorWorkflow.workloadSpec wrapper on the seam every atelet RPC builds its spec through (the proto-template path, after #1232), so Run, Restore, both Checkpoint types, and Terminate agree on the volume set. The wire still carries only {name, path}; atelet resolves the bundle node-side as for a declared projection, so a missing or unusable bundle fails actor start with no new code. No atelet, proto, CRD, or RBAC changes.

Injection rules

  • Reserved name: template volume names must be DNS labels, which cannot contain ., so trust.ate.dev cannot collide. The injector also fails the spec build if a spec somehow carries the name; the proto-template path does not validate volume names yet, so this check is load-bearing there.
  • Opt-out: a container keeps its own mounts and gets no injection when it mounts a volume at /run/substrate/certs or below it (the injected parent bind would shadow a nested mount on the micro-VM runtime), or an image volume above it (micro-VM binds image volumes after systemInfo, shadowing the injected mount). Every opt-out is logged with the actor, container, and colliding path. If all containers opt out, no volume is injected.

Deployment wiring

ateapi has no runtime signal for "MITM is deployed" (--egress-gateway-address is set on passthrough installs too, and ateapi deliberately has no ClusterTrustBundle RBAC), so the gate is an install-time flag. hack/install-ate.sh splices it into the ate-api-server args whenever an invocation that deploys ate-api-server runs under --experimental-use-sdsmint, the only install shape that publishes the bundle. After every ate-api-server rollout it verifies live cluster state: a deployed sdsmint gateway with the flag missing fails the install, so a later plain redeploy cannot silently strip injection while MITM keeps intercepting. CI's MITM step redeploys ateapi with the flag and recreates the counter demos through a new demo-only --deploy-demo-counter-microvm handler; the full run-microvm-demo-kind.sh path would redeploy the control plane without the sdsmint switch and revert both the gateway and the flag.

The snapshot constraint

Flipping the flag changes the spec of every later activation, and a FULL snapshot only restores against the spec it was captured with: gVisor refuses the mismatch outright, and a micro-VM guest resumes its old mount table without the file until its next cold boot. The shipped rule (docs, flag help, and CI's own runbook): enable injection at install time, before templates and workloads exist; a later flip means regenerating goldens and discarding paused or suspended actors. We could put the injection state into snapshots, which would remove the constraint entirely, but left that out of this PR.

Tests

  • Unit tables for every injection rule: per-container injection, exact and nested opt-out, image-parent opt-out, parent and sibling-prefix non-opt-out, all-opt-out volume suppression, reserved-name rejection, and flag gating.
  • A functional lifecycle test asserts the injected volume in each recorded wire spec: Run (cold boot), Checkpoint local and external (checked by type), Restore, and Terminate via a crashed-actor delete.
  • TestActorEgressTrustAutoInjection (egressmitm suite, both sandbox classes): a probe that declares nothing about trust reads the injected file, which must match the published ClusterTrustBundle as certificate sets (sanitization shuffles), then completes TLS through the MITM gateway using only those anchors.

Open choices

Not in this PR

Live refresh of running actors' projections (#1231, which an injected volume rides for free once merged), the configurable backend registry (#932), and trust-pickup guidance (#1005).

@haiyanmeng

haiyanmeng commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

A new ateapi flag, --inject-egress-trust-bundle (default off), adds one systemInfo volume, trust.ate.dev, projecting the egress-mitm.ate.dev bundle to /run/substrate/certs/egress-mitm.ate.dev.pem in every container.

Max Thompson (@thompsonmax) , do you know the time overhead of auto-injecting the egress trust volume? IIUC, this will be on the critical path of every actor resume operation.

AFAIK, we currently plan to auto-inject MITM trust bundle and actor identity. Each of these auto-injection will have a overhead. If the number of auto-injected info keep going up, the time overhead may be too high for our sub-100ms actor-resume goal.

Opt-out: a container keeps its own mounts and gets no injection when it mounts a volume at /run/substrate/certs or below it (the injected parent bind would shadow a nested mount on the micro-VM runtime), or an image volume above it (micro-VM binds image volumes after systemInfo, shadowing the injected mount). Every opt-out is logged with the actor, container, and colliding path. If all containers opt out, no volume is injected.

Should we disallow users to mount things at /run/substrate and fail to start an actor if it happens?

Strictly speaking, the egress MITM trust volume is only needed for actors generating HTTPS traffic. I wonder whether we should allow users to indicate whether an actor generates HTTPS traffic. This can be a followup PR. Bowei Du (@bowei) , WDYT?

@haiyanmeng

Copy link
Copy Markdown
Collaborator

Env vars (SSL_CERT_FILE and friends) stay out of scope; #1005 proposal has no signoff, and whatever lands there can point at this path.

I filed #1424 to this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Max Thompson (@thompsonmax) , what is the impact of this change on e2e-test presubmit? How much longer does it make?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Quick timing check:

Both runs are from today. Main is commit 9b333c6, run 33915867922 at 20:22 UTC. The PR is 6ae844e, run 33923715673 at 22:01 UTC.

Step main PR
Deploy MITM egress (sdsmint) 24s 53s
Recreate counter demos under injection none 56s
E2E egress MITM trust, gVisor 18s 45s
E2E egress MITM trust, micro-VM 21s 41s
Total 63s 195s

So, this adds about 2 minutes at the step level.

Comment thread cmd/ateapi/main.go Outdated
Comment thread cmd/ateapi/main.go Outdated
@thompsonmax

Copy link
Copy Markdown
Collaborator Author

A new ateapi flag, --inject-egress-trust-bundle (default off), adds one systemInfo volume, trust.ate.dev, projecting the egress-mitm.ate.dev bundle to /run/substrate/certs/egress-mitm.ate.dev.pem in every container.

Max Thompson (Max Thompson (@thompsonmax)) , do you know the time overhead of auto-injecting the egress trust volume? IIUC, this will be on the critical path of every actor resume operation.

AFAIK, we currently plan to auto-inject MITM trust bundle and actor identity. Each of these auto-injection will have a overhead. If the number of auto-injected info keep going up, the time overhead may be too high for our sub-100ms actor-resume goal.

Good question, I did some microbenchmarking on a kind cluster and came up with these numbers on how this affects actor resume latency. Checked the operation latency for both gVisor and microVM.

Local restores, p50 in ms, 20 samples per cell:

gVisor off gVisor on micro-VM off micro-VM on
atelet oci_unpack 1.0 4.5 0.9 4.4
atelet download 3.3 3.4 43.3 43.5
atelet ateom_restore 364.5 359.6 177.2 181.2
atelet total 372.1 368.3 228.0 230.7
ResumeActor round trip 420.1 421.4 256.6 267.0
ResumeActor round trip, mean 427.8 429.6 271.4 271.8

Golden restores, p50 in ms, 5 samples per cell:

gVisor off gVisor on micro-VM off micro-VM on
atelet oci_unpack 1.0 4.7 1.1 5.5
atelet download 8.9 9.2 217.2 216.1
ResumeActor round trip 414.8 382.6 399.5 405.6

So it seems to extend the E2E latency by about 3-5 ms for either runtime. Doesn't seem like an immediate concern, but perhaps something to keep an eye on as/if we increase auto-injected volumes.

Here's the branch with the microbenchmarking code: https://github.com/thompsonmax/substrate/tree/egress-trust-injection-bench

@thompsonmax
Max Thompson (thompsonmax) force-pushed the egress-trust-injection branch 2 times, most recently from 2fe9f00 to 6eca3eb Compare September 4, 2026 21:49
Deployments that MITM actor egress TLS need every actor to trust the
minting CA, not just templates that project the bundle themselves. Under
the new --inject-egress-trust-bundle flag, ateapi appends a well-known
systemInfo volume projecting egress-mitm.ate.dev to
/run/substrate/certs/egress-mitm.ate.dev.pem on every workload spec it
sends to atelet, which resolves it node-side and fails the actor start
closed while the bundle is absent, exactly as for a declared projection.

The volume is wire-level policy, invisible to templates. Its name,
trust.ate.dev, cannot collide with template volumes (template volume
names must be DNS labels, which cannot contain '.'). A template that
mounts its own volume at /run/substrate/certs, or anywhere below it,
opts that container out — kubelet's serviceaccount-token override rule.
Nested mounts opt out too because the injected parent bind would shadow
them on the micro-VM runtime, which mounts binds grouped by volume kind
rather than parent-first. If every container opts out, no volume is
injected at all.

The flag must be set before templates and workloads exist: a FULL
snapshot only restores against the spec it was captured with, so gVisor
refuses to restore a snapshot spanning a flip, and a micro-VM guest
resumes its snapshotted mount table without the file. CI therefore
recreates the counter demos after enabling injection so their goldens
regenerate under the new policy.

hack/install-ate.sh enables the flag whenever the sdsmint egress variant
is installed (the only deployment shape that publishes the bundle). The
egressmitm e2e proves the policy end to end — a probe template that
declares nothing about trust reads the published anchors at the
well-known path and completes TLS through the MITM gateway with only
those anchors — and a functional test pins the injected volume in the
wire spec of every atelet RPC across an actor's lifecycle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants