Skip to content

egress: add egress gateway support - atenet-egress - #693

Merged
Bowei Du (bowei) merged 6 commits into
agent-substrate:mainfrom
LiorLieberman:pr2-atenet-egress
Aug 11, 2026
Merged

egress: add egress gateway support - atenet-egress#693
Bowei Du (bowei) merged 6 commits into
agent-substrate:mainfrom
LiorLieberman:pr2-atenet-egress

Conversation

@LiorLieberman

@LiorLieberman Lior Lieberman (LiorLieberman) commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Added pluggable egress PEP support. (Feedback on atenet-egress name is welcomed - will open a separate PR to rename atenet-router to atenet-ingress )

#559 shipped the actor egress data path without any egress gateway. This adds an Envoy deployment that terminates actor CONNECTs. It requires downstream mTLS, so only a worker's atunnel can reach it.

The gateway consists of an Envoy and an atenet router --standalone ext_proc sidecar.

The same ext_proc binary now can serve both directions. It has a mode=ingress|egress|all.

  • --egress-gateway-address flag, This turns on pluggable actor egress cluster-wide. ateapi stamps this address onto every atelet Run/Restore, ateom hands it to atunnel, and actor TCP egress is transparently redirected into atunnel, which wraps it in mTLS + HTTP CONNECT to the egress gateway.

We should remove this when egress API pr is merged

One more FYI

The egress listener's is pinned to the actor-identity CA root alone, so the trust anchor is a property of the filter chain rather than the request, any client certificate not signed by the actor-identity CA fails the TLS handshake, and only traffic with actor-identities can open a tunnel through the egress gateway.

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

@LiorLieberman

Copy link
Copy Markdown
Collaborator Author

FYI Dmitry Berkovich (@dberkov)

# Envoy calls it over localhost to authenticate actor identity against the
# ate API on every CONNECT. This mirrors the ingress gateway topology
# (Envoy + ext_proc in one pod); a shared/standalone ext_proc is a future step.
- name: ext-proc

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 us make ext-proc a native sidecar (initContainers + restartPolicy: Always).
Regular containers get SIGTERM together, ext_proc has no drain and will exit first,
and with failure_mode_allow: false every CONNECT arriving during Envoy's drain
window fails closed with a 503.

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.

good point. created an issue for that - #760

@shrutiyam-glitch shrutiyam-glitch Aug 6, 2026

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.

I've actually opened a PR to handle the graceful termination of the atenet-router (#774, which should help mitigate the Envoy drain window issue mentioned here.

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.

You can also just have ext-proc register the SIGTERM but not quit (alternatively)

# Co-located ext_proc server (the atenet router, ext_proc-only). The egress
# Envoy calls it over localhost to authenticate actor identity against the
# ate API on every CONNECT. This mirrors the ingress gateway topology
# (Envoy + ext_proc in one pod); a shared/standalone ext_proc is a future step.

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 shared/standalone ext_proc is a future step: can we first move the exe_proc logic into a separate binary?

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.

ext_proc is already separate

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.

I don't think so. See Line 268 in this file:

image: ko://github.com/agent-substrate/substrate/cmd/atenet

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.

with what we have now - atenet == extproc server
atenet-egress deployment == envoy + atenet (handling egress releated extproc).

atenet-ingress deployment == envoy + atenet(handling ingress releated extproc).

@bowei

Copy link
Copy Markdown
Collaborator

implementation itself looks ok, but would like to fix a few organization things:

  • Make it so that egress, ingress for ext_proc can be enabled separately (add flags and gated deployment). We currently deploy both in the same deployment, but at scale, you would probably want to have separate ingress and egress deployments as they will be allocated resources very differently.
  • Put ingress,egress logic in .../atenet/router/{ingress,egress} packages.
  • router/ext_proc just has mux logic

probably something like:

switch {
case EgressEnabled && IsEgressRequest(...):
case IngressEnabled:
default:
   // send 4xx nothing is enabled.
}

@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.

You need to scrub through all of the comments -- there seems to some internal notes to yourself and your friend Claude.

Comment thread cmd/atelet/main.go Outdated
Comment thread cmd/atelet/main.go Outdated
Comment thread cmd/atelet/main.go Outdated
Comment thread cmd/atenet/internal/router/extproc.go Outdated
Comment thread cmd/atenet/internal/router/extproc_egress.go Outdated
@bowei

Copy link
Copy Markdown
Collaborator

Needs rebase

@EItanya Eitan Yarmush (EItanya) 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.

Offline we discussed cleaning up the locations in which the egressgateway address needs to be set, right now it's set on quite a few pods, but we can definitely simplify.

// assigned, and atunnel documents it as a lower bound on trustworthy actor
// metadata. If our authoritative view is older than what the worker asserts,
// we cannot yet vouch for the identity, so reject rather than allow blindly.
if assertedVersion != "" {

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.

Will not be necessary once my PR goes in, this version is removed

@EItanya

Copy link
Copy Markdown
Collaborator

Here is the list of specific PEP todos as a result of #708

  - Require mTLS client certificates signed by the actor-identity CA.
  - Verify validity period, ClientAuth EKU, and IsCA == false.
  - Require exactly one valid ActorIdentity extension.
  - Require non-empty atespace, actor name, and actor UID.
  - Require Purpose == "atunnel"; reject generic, missing, or unknown purposes.
  - Authorize using the verified actor UID—not CONNECT headers or actor-provided metadata.

Comment on lines +79 to +85
# SEE(lior): this pair is how the actor's certificate reaches the
# ext_proc handler. ext_proc can request Envoy attributes, but none
# of them carry a custom X.509 extension, so the only way to check
# ActorIdentity in Go is to have Envoy hand over the raw chain.
# SANITIZE_SET drops whatever x-forwarded-client-cert the client
# sent and writes Envoy's own view of the verified peer, and
# chain: true puts the full URL-encoded PEM chain in it.

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.

verify this is what we want

botengyao Taahir Ahmed (@ahmedtd)

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.

It would be ideal to have a cleaner solution ... and ideally not in the config itself, but we should not block on getting it working.

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.

verified with Boteng. This is ~fine. Added a TODO. Dont think an issue is needed since "cleaner" is undefined yet.

@bowei

Copy link
Copy Markdown
Collaborator

Probably closes #338

@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.

Mostly looks ok.

Comment on lines +79 to +85
# SEE(lior): this pair is how the actor's certificate reaches the
# ext_proc handler. ext_proc can request Envoy attributes, but none
# of them carry a custom X.509 extension, so the only way to check
# ActorIdentity in Go is to have Envoy hand over the raw chain.
# SANITIZE_SET drops whatever x-forwarded-client-cert the client
# sent and writes Envoy's own view of the verified peer, and
# chain: true puts the full URL-encoded PEM chain in it.

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.

It would be ideal to have a cleaner solution ... and ideally not in the config itself, but we should not block on getting it working.

Comment thread hack/verify-egress-demo.sh Outdated
# hack/install-ate-kind.sh --deploy-demo-egress
#
# The egress demo Actor accepts {"url":"..."} and performs an HTTP GET. With
# egress turned on (ate-api-server --egress-gateway-address, which ateapi stamps

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.

probably detail is no needed and could change in the future, can be confusing for humans and agents.

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 believe you meant for the thing in the "()". Removed.

Comment thread demos/egress/test-egress.sh
Comment thread cmd/atenet/internal/router/egress/egress.go Outdated
Comment thread cmd/atenet/internal/router/egress/egress.go Outdated
Comment thread cmd/atenet/internal/router/egress/egress.go Outdated

// Confirm the certified actor still exists. The name is only a lookup key
// here; the UID below is what actually authorizes.
actor, err := h.apiClient.GetActor(ctx, &ateapipb.GetActorRequest{

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.

we should be careful here as this results in load to the ATEAPI server

file a TODO as a sub issue of the actor state caching issue

#592

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.

added a ref to this issue here

Comment thread cmd/atenet/internal/router/egress/egress.go Outdated
// is ever loosened, and costs one signature check per CONNECT rather than per
// request. The IsCA, ClientAuth-EKU, and purpose checks below have no Envoy-side
// equivalent at all.
func (h *Handler) verifyActorCertificate(chain []*x509.Certificate) (*substratex509.ActorIdentity, error) {

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.

put this and the functions it calls in its own package

egress/cert

cert.VerifyActor(...)

If a func doesn't really need to be a method, you should make it a free func for easier testing.

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.

this PR has grew very big (partly because I addressed a similar comment from you to split ingress + egress under router). Lets take this as a follow up

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.

Ok

The gateway terminates actor's CONNECT request.
It requires downstream mTLS, so only a worker's atunnel can reach it. The gateway consists of an Envoy and an `atenet router --standalone` ext_proc sidecar.
demos/egress is a small Actor that fetches a URL it is given and echoes the
upstream status and body back, which makes the egress path observable from
outside the sandbox. hack/install-demo-egress.sh registers it as a
--deploy-demo-egress fixture and hack/verify-egress-demo.sh drives it and
checks the atenet-egress logs for the corresponding authorized CONNECT.

TestActorEgress in the networking suite covers the same path automatically:
it creates an Actor from the demo template, POSTs a fetch request through
atenet-router, and asserts 200. The suite's actor helper is parameterised by
template so the ingress test keeps using the counter fixture.
The gateway access-log assertion read the log once, immediately after curl
returned, and failed if it saw nothing new. That is racy twice over:

  * Envoy emits the CONNECT access-log entry asynchronously, so for an
    external destination it can land seconds after the actor's response.
  * The Actor's HTTP client keeps the tunnel alive. A repeat fetch to a host
    it already reached rides the open tunnel and produces no new entry at
    all, so a run against a warm actor failed even though egress was working.

Poll for the new entry, and fall back to any tunnel already open for this
actor's SAN before declaring failure. Also read -c envoy explicitly (the pod
also runs the ext-proc sidecar) and mark the script executable, as every other
directly-invoked script in hack/ is.
// egress CONNECTs, and so the one that selects the egress handler. It must
// stay in sync with the filter chain name in
// manifests/ate-install/atenet-egress.yaml.
EgressFilterChainName = "egress"

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.

Take this as a follow-up: it should be named something more unique to give a clue that there is something special about the name. Ideally, git grep <name> should just yield this and the configuration location.

@bowei

Copy link
Copy Markdown
Collaborator

There are a number of follow ups. Let's make sure we get them in as well.

@bowei
Bowei Du (bowei) merged commit 24cc538 into agent-substrate:main Aug 11, 2026
11 checks passed
@BenTheElder

Copy link
Copy Markdown
Collaborator

Bowei Du (@bowei) if we don't have clear logical commits, let's squash merge?

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.

6 participants