Skip to content

egress: MITM tunneled TLS with per-SNI certificate minting - #871

Merged
Bowei Du (bowei) merged 1 commit into
agent-substrate:mainfrom
haiyanmeng:sds-1
Aug 18, 2026
Merged

egress: MITM tunneled TLS with per-SNI certificate minting#871
Bowei Du (bowei) merged 1 commit into
agent-substrate:mainfrom
haiyanmeng:sds-1

Conversation

@haiyanmeng

@haiyanmeng haiyanmeng commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

New changes:

  • atenet sdsmint — an SDS server that mints a short-lived TLS leaf per SNI on demand. Runs as a native sidecar beside Envoy, sharing the atenet image but staying a separate container so the MITM signing key never lands on the data plane.
  • mitm_listener — the CONNECT route now hands the upgraded stream here instead of to the forward proxy. Two chains,
    picked by tls_inspector: tls terminates using Envoy 1.37's on_demand_secret selector + sni cert mapper (hence the
    1.34 → 1.37 bump), raw_buffer proxies plaintext http:// and enforces the allowlist as an :authority match, since
    there's no mint on that path to refuse.
  • CA plumbing — internal/localca gains GenerateCA(GenerateOptions); kubectl-ate admin make-ca-pool gains --key-type,
    --common-name; install-ate.sh creates the egress-mitm-ca-pool secret before the Deployment.
  • e2e (internal/e2e/suites/sdsmint) covers the tunnelled handshake, the leaf Envoy serves, and actor identity across the MITM, against the new egressprobe fixture. These e2e tests are skipped for now. We will enable them after figuring out how to test them in presubmit github workflows.

Not included:

  • how to make actors trust egress-mitm-ca-pool

Followups:

  • move functionalities common to CA pools cmd/atenet/internal/sdsmint/certauth/certauth.go from to internal/localca
  • figure out how to run sdsmint e2e tests in github workflow
  • look into whether we need to use go code to generate the egress envoy config

This is to address #823

It's a good idea to open an issue first for discussion.

  • Tests pass
  • Appropriate changes to documentation are included in the PR

Comment thread manifests/ate-install/atenet-egress.yaml Outdated

@bowei Bowei Du (bowei) left a comment

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.

  • Remove the intermediate CA code
  • Some of the options seem like we don't need to expose. If so, then the logic inside should be simplified.
  • Move the CA generating logic into its own package (suggest sdsmint/certauth) that exports a very narrow interface out to the server. Seem like the only things that need to be exported are a Get() and Forget()
  • I don't know if we need an optional minter.Forgetter() -- this comment applies to other places in the code that seem to have more options than we need.
  • Go through the comments and scrub for the agent-based self-conversation. The comments are pretty verbose in some places and look more like agent thinking tokens than code comments.

I mostly looked at the server code. Once you get the simplifications in, I will take a more detailed look at the rest of the change.

Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/server.go Outdated
@bowei Bowei Du (bowei) self-assigned this Aug 12, 2026
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/certauth/certauth.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/ca.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
@haiyanmeng

Copy link
Copy Markdown
Collaborator Author
  • Remove the intermediate CA code

Done

  • Some of the options seem like we don't need to expose. If so, then the logic inside should be simplified.

Done

  • Move the CA generating logic into its own package (suggest sdsmint/certauth) that exports a very narrow interface out to the server. Seem like the only things that need to be exported are a Get() and Forget()

Done

  • I don't know if we need an optional minter.Forgetter() -- this comment applies to other places in the code that seem to have more options than we need.

minter.Forgetter() iis called through the optional interface in cmd/atenet/internal/sdsmint/deltastream.go:391:

  if f, ok := d.srv.minter.(Forgetter); ok {
      for _, name := range expired {
          f.Forget(name)
      }
  }
  • Go through the comments and scrub for the agent-based self-conversation. The comments are pretty verbose in some places and look more like agent thinking tokens than code comments.

Done

Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/deltastream.go Outdated
Comment thread cmd/atenet/internal/sdsmint/deltastream.go Outdated
Comment thread cmd/atenet/internal/sdsmint/idle_test.go Outdated
Comment thread cmd/atenet/internal/sdsmint/metrics.go Outdated
Comment thread cmd/kubectl-ate/internal/cmd/admin_make_ca_pool.go
Comment thread cmd/atenet/internal/sdsmint/certauth/certauth.go Outdated
Comment thread cmd/atenet/internal/sdsmint/certauth/certauth.go Outdated
Comment thread cmd/atenet/internal/sdsmint/certauth/certauth.go Outdated
Comment thread cmd/atenet/internal/sdsmint/certauth/leafkey.go Outdated
Comment thread cmd/atenet/internal/sdsmint/certauth/certauth.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cache.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/listen.go Outdated
Comment thread cmd/atenet/internal/sdsmint/minter.go Outdated
Comment thread cmd/atenet/internal/sdsmint/certauth/certauth.go Outdated
Comment thread cmd/atenet/internal/sdsmint/certauth/certauth.go Outdated
Comment thread cmd/atenet/internal/sdsmint/certauth/certauth.go Outdated

cmd.Flags().StringVar(&cfg.UDSPath, "uds-path", "", "unix socket to listen on; required, and the only transport offered, because leaf private keys transit this channel")
cmd.Flags().StringVar(&cfg.CAPoolPath, "ca-pool-path", "", "path to a localca pool JSON holding the MITM CA, the format substrate mounts its other CAs in")
cmd.Flags().StringVar(&cfg.CAID, "ca-id", "", "which CA in the pool to sign with; empty takes the first")

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.

Let's leave the behavior to just take the first --- localca.Pool should be tracking which localca.CA is active for signing. I will send a PR to do this (and add rotation commands for the CA secrets).

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.

Taahir Ahmed (@ahmedtd) , are you suggesting removing this flag and take the first CA from the pool by default?

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.

Does your PR need to block this PR?

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.

Can we default to first for now or it doesn't work without this flag.

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.

It works as is. Here is how the sdsmint container in the egress gateway Pod sets it

        args:
        - "sdsmint"
        - "--uds-path=/var/run/sdsmint/sdsmint.sock"
        - "--ca-pool-path=/run/ca-state/mitm-pool.json"
        - "--ca-id=mitm"

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.

Yeah, we can just default to the first CA for now.

Comment thread cmd/atenet/internal/sdsmint/cmd.go Outdated
Comment thread cmd/atenet/internal/sdsmint/deltastream.go
@haiyanmeng

Copy link
Copy Markdown
Collaborator Author

I rebased after #959 is merged. We should expect TestActorEgressHTTPS to fail.

@haiyanmeng

haiyanmeng commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

https://github.com/agent-substrate/substrate/actions/runs/31837847684/job/94888039437?pr=871

The test TestActorEgressHTTPS indeed failed with {"error":"request failed: Get \"[https://example.com/\](https://example.com//)": tls: failed to verify certificate: x509: certificate signed by unknown authority"}:

=== RUN   TestActorEgressHTTPS
    networking_test.go:117: creating actor networking-e2e/egress-https-1786739670953880925
    networking_test.go:117: resumed actor networking-e2e/egress-https-1786739670953880925
    networking_test.go:128: Actor HTTPS egress fetch returned HTTP 502, want 200; body: {"error":"request failed: Get \"[https://example.com/\](https://example.com//)": tls: failed to verify certificate: x509: certificate signed by unknown authority"}
--- FAIL: TestActorEgressHTTPS (1.31s)

Bowei Du (@bowei) , let us merge #926 first. Then I will rebase and update this PR.

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.

Keith Mattix II (@keithmattix) has done a nice thing on #715 where he builds the listener in go (see xds.go in his PR) based on whether someone enabled connect (using a flag).

we should do something similar for MITM listener and other config changes.

@ahmedtd

Copy link
Copy Markdown
Collaborator

We should also be careful about exhausting randomness sources.

Go's random sources cannot be exhausted --- they are a CSPRNG keyed with initial randomness read from /dev/urandom. /dev/urandom itself is a CSPRNG seeded with a few bytes of random derived from startup or the RDRAND instruction.

Linux does a bunch of accounting of entropy sources, and tries to reseed /dev/random and /dev/urandom once many values are read from them. This is widely considered to be superstitious hokum.


cmd.Flags().StringVar(&cfg.UDSPath, "uds-path", "", "unix socket to listen on; required, and the only transport offered, because leaf private keys transit this channel")
cmd.Flags().StringVar(&cfg.CAPoolPath, "ca-pool-path", "", "path to a localca pool JSON holding the MITM CA, the format substrate mounts its other CAs in")
cmd.Flags().StringVar(&cfg.CAID, "ca-id", "", "which CA in the pool to sign with; empty takes the first")

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.

Yeah, we can just default to the first CA for now.

Comment thread cmd/atenet/internal/sdsmint/deltastream.go
Comment thread cmd/atenet/internal/sdsmint/server.go Outdated
Comment thread cmd/atenet/internal/sdsmint/server.go Outdated
@ahmedtd

Copy link
Copy Markdown
Collaborator

LGTM with some nits

@haiyanmeng

haiyanmeng commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Keith Mattix II (Keith Mattix II (@keithmattix)) has done a nice thing on #715 where he builds the listener in go (see xds.go in his PR) based on whether someone enabled connect (using a flag).

we should do something similar for MITM listener and other config changes.

Thanks for sharing. Filed an issue #1003.

return nil, errors.New("empty host")
}

ca := s.active

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.

Should we defensively validate the CA is not expired, to avoid signing certs with an invalid CA?

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.

Let us do it in a follwoup PR since we plan to move selectCA to internal/localca in a follow up PR (see line 77)

if err != nil {
return nil, err
}
if err := active.Validate(); err != nil {

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.

Should we also validate that the CA is not expired/past NotBefore here (or in ca.Validate())?

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.

Let us do it in a follwoup PR since we plan to move selectCA to internal/localca in a follow up PR (see line 77)

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.

A few more comments:

  1. I think grpc and websocket break with this change. The issue is that we are trying to now parse in the inner connection, and we dont have any support for anything thats not h1 AFAICT. This breaks egress traffic that was not http in many ways

  2. related to 1 - we need to add e2e tests for non http traffic..

Comment on lines +176 to +179
# Two chains, selected by what the tunnel actually carries. tls_inspector
# tags a ClientHello "tls" and anything else "raw_buffer", so a cleartext
# HTTP tunnel gets an HTTP chain instead of being fed to a TLS transport
# socket, which would fail the handshake and close the tunnel.

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.

What about non http traffic? Actors sends non http all the time, I think with this config it would not work for them. common example includes git clone ssh:// but any other tcp traffic as well.

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.

I created #1017 to add e2e test coverage for non-HTTP traffic.

To unblock this PR, I will do a manual verification of running git clone git@github.com:agent-substrate/substrate.git from an actor.

@haiyanmeng

Copy link
Copy Markdown
Collaborator Author
  1. I think grpc and websocket break with this change. The issue is that we are trying to now parse in the inner connection, and we dont have any support for anything thats not h1 AFAICT. This breaks egress traffic that was not http in many ways

I filed #1018 to add support for non-HTTP traffic when sdsmint is enabled. We can address it as a followup.

@haiyanmeng

Copy link
Copy Markdown
Collaborator Author
  1. I think grpc and websocket break with this change. The issue is that we are trying to now parse in the inner connection, and we dont have any support for anything thats not h1 AFAICT. This breaks egress traffic that was not http in many ways

What does h1 mean? Lior Lieberman (@LiorLieberman)

@bowei

Copy link
Copy Markdown
Collaborator

h1 is HTTP/1.1. Mostly people call HTTP/2 as h2.

@bowei

Copy link
Copy Markdown
Collaborator

The experimental flag guards enablement, so we should not see any issues with this change.

@bowei

Copy link
Copy Markdown
Collaborator

For some reason, the Google CLA is not working
I bumped the PR to see if this clears the issue.

Envoy now terminates the TLS session inside an actor's CONNECT tunnel
with a leaf minted on demand for the SNI the client asked for, instead
of forwarding the tunneled bytes opaquely. That puts the plaintext
request on a filter chain where policy can be applied later. A new
atenet sdsmint subcommand serves those leaves over DELTA_GRPC SDS on a
unix socket.

Opt-in: --experimental-use-sdsmint selects atenet-egress-with-sdsmint.yaml
and creates the egress-mitm-ca-pool secret. The default egress manifest
and install path are unchanged.

Also: localca gains CA.Validate, GenerateCA options and a crypto.Signer
SigningKey; kubectl-ate admin make-ca-pool gains --key-type and
--common-name; atunnel returns ErrGatewayHandshake and
ConnectRejectedError instead of formatted strings. The sdsmint e2e suite
is skipped for now.
@haiyanmeng

Copy link
Copy Markdown
Collaborator Author

Bowei Du (@bowei) , I force-pushed the branch. The Google CLA is green now.

@bowei
Bowei Du (bowei) merged commit d4c68d7 into agent-substrate:main Aug 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants