Skip to content

Add kubectl ate proxy actor command. - #3191

Merged
Benjamin Elder (BenTheElder) merged 2 commits into
agent-substrate:mainfrom
juli4n:atenet-client-proxy
Oct 28, 2026
Merged

Benjamin Elder (BenTheElder) merged 2 commits into
agent-substrate:mainfrom
juli4n:atenet-client-proxy

Conversation

@juli4n

@juli4n Julian Gutierrez Oschmann (juli4n) commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Allow a user to land traffic on an actor from a dev machine.

The kubectl-ate CLI now authenticates to atenet by minting a certificate (via PodCertificateRequest) for spiffe://cluster.local/ns/ate-system/sa/atenet-client.

Ideally, atenet should support JWT based auth and there would be no need for a certificate minting. But that support is not in place yet.

The kubectl ate proxy actor command also improves local dev ergonomics as it attachjes the actor header so clients don't need to be "substrate-aware". For example, a web browser can just point to the local proxy.

Fixed #3194 and #3187

@juli4n
Julian Gutierrez Oschmann (juli4n) force-pushed the atenet-client-proxy branch 3 times, most recently from 8f8975d to 928511f Compare October 26, 2026 15:58
@juli4n Julian Gutierrez Oschmann (juli4n) changed the title POC: Add kubectl ate proxy actor command. Add kubectl ate proxy actor command. Oct 26, 2026
Comment thread cmd/ate-setup/internal/config/config.go Outdated
// IngressAllowedSPIFFEIDs are the SPIFFE IDs the ingress router lets
// reach actors over its TLS listeners (ATE_INGRESS_ALLOWED_SPIFFE_IDS).
// See RequireIngressAllowedSPIFFEIDs.
// Unset defaults to spiffe://cluster.local/ns/<namespace>/sa/atenet-client.

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.

Personally, I feel that our demo setups should be adding this, rather than adding it to every install by default.

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.

Is this a value that the user could also want to be empty and patched later?

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 agree with taahir. Default should be empty. demo should patch 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.

I changed it so that the demo instructions explicitly add the atenet-client. I still think this is not well thought-out and that it's not consistent with how the control plane bootstraps the ownership, but let's discuss that as a follow up.

Comment thread demos/counter/README.md Resolved
Comment thread internal/podidentity/podidentity.go Resolved
Comment thread cmd/atenet/internal/router/README.md Resolved
Comment on lines -284 to -286
func (c *Config) RequireIngressAllowedSPIFFEIDs() error {
if c.Router == RouterAgentgateway || len(c.IngressAllowedSPIFFEIDs) > 0 {
return 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.

Taahir Ahmed (@ahmedtd) thinking about it more. I think we should allow setting this empty. it is a pretty common architecture that actors can pull for work vs get work. Resume is also called from different controllers and not necessarily through 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.

Empty now is allowed and means no egress.

@LiorLieberman Lior Lieberman (LiorLieberman) 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.

Some high level notes:

  • can we mint the proxy cert outside ate-system? whoever gets PCR create there can also mint atelet for any node and get actor certs
  • orthogonal to this PR: This pr is relying on the fact that non-Kubelet clients are allowed to create arbitary PodCertificateRequests, even if the pod or service account they reference does not exist (#2389). Bowei Du (@bowei) had an idea of two podcertcontrollers - a demo one that issues certs for made-up pods and a prod one that refuses them.

@HavenXia Haven Xia (HavenXia) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should the e2e router client use the new atenet-client identity instead of ate-e2e-router-client/router-client?

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.

LGTM generally,
Since we now dont reject empty list for spiffee ids, - can you add a big note somewhere in the installation md files that "In order to allow ingress traffic to actors you should add the identities in this flag. If you have one frontend / gateway - it could be this identity, but if you have different callers to atenet-router - it should list them" (not this words verbatim but something similar)

in a follow up (really not now) we may want to look at whether we want to even deploy atenet-router if the list is empty.

Theoretically - we it does nothing so why deploying it BUT on the flip side, if/once we support better ingress auth we should not really require this "simple" flag and deprecate this authz mode.

Comment thread cmd/ate-setup/internal/steps/overlay.go Resolved
Comment thread cmd/atenet/internal/router/config.go Resolved
Comment thread cmd/atenet/internal/router/README.md Resolved
Comment thread cmd/kubectl-ate/README.md
Comment on lines +228 to +229
the client certificate for `spiffe://cluster.local/ns/ate-system/sa/atenet-client`, so the install must list that ID in
`--ingress-allowed-spiffe-ids`.

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 have a kubectl command to validate --ingress-allowed-spiffe-ids was set?

Comment thread demos/counter/README.md Resolved
Comment thread demos/jupyter/jupyter.yaml.tmpl Resolved
Comment thread demos/multi-template/README.md Resolved
@LiorLieberman

Copy link
Copy Markdown
Collaborator

Should the e2e router client use the new atenet-client identity instead of ate-e2e-router-client/router-client?

follow up please !! :)

@juli4n
Julian Gutierrez Oschmann (juli4n) force-pushed the atenet-client-proxy branch 2 times, most recently from e37bcff to 7eb301f Compare October 27, 2026 23:24
atenet-router requires mTLS with an allowed SPIFFE client certificate, so plain
kubectl port-forward can no longer reach actors from a workstation.

- Add kubectl ate proxy actor <actor> [[local-port:]remote-port ...] (default
  8000:80) to tunnel TCP connections to an actor over atenet-router's :8444
  CONNECT listener. It presents a certificate for
  spiffe://cluster.local/ns/<namespace>/sa/atenet-client, which the install
  must list in ingressAllowedSPIFFEIDs.
- Extract podidentity.RequestCertificate and ateclient.ConnectActorWithHeaders
  so e2e tests and ateclient share certificate minting and CONNECT tunneling.
- Let ate-setup and atenet-router accept an empty ingressAllowedSPIFFEIDs. The
  Envoy router then serves no listener, and agentgateway denies every client.
  Document which identities to list.

AI use: AI-assisted, I reviewed and tested it.

Signed-off-by: Julian Gutierrez Oschmann <[email protected]>
- Replace kubectl port-forward svc/atenet-router 8000:80 with kubectl ate proxy
  actor across README.md, demo walkthroughs, and verification scripts.
- Remove the unused jupyter-proxy NGINX deployment from the Jupyter demo.
- Update demos/sandbox/client to dial ate-api-server via ateclient.NewClient.
- Update demos/egress/test-egress.sh for the worker-for-actor SPIFFE URI.

AI use: AI-assisted, I reviewed and tested it.

Signed-off-by: Julian Gutierrez Oschmann <[email protected]>

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 think these can all be small follow-ups.

Comment thread internal/ateclient/router.go Outdated
Comment on lines +202 to +214
func (c *routerClientCerts) get(ctx context.Context) (*tls.Certificate, error) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
if c.cert != nil && now.Before(c.refreshAt) {
return c.cert, nil
}
timeout := routerCertTimeout
if c.cert != nil && now.Before(c.cert.Leaf.NotAfter) {
timeout = routerCertRefreshTimeout
}
namespace := systemNamespace()
cert, err := podidentity.RequestCertificate(ctx, c.clientset, routerCertRequester, namespace, installdefaults.IngressClientServiceAccount, timeout)

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.

c.mu is held across RequestCertificate, and this runs inside the TLS handshake. Once refreshAt passes, every new connection blocks for up to 15s behind the refresh even though the cached cert is still valid. After the cert expires with the signer unavailable, each queued handshake then makes its own 2 minute attempt in turn, since refreshAt isn't moved on that path.

Can we hand out the cached cert while one caller refreshes, or refresh in the background?

Comment on lines +66 to +67
// certificate and presents a podidentity certificate for the ingress client
// ServiceAccount, which the router allows by default.

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.

stale, the allowlist defaults to empty now, so the router doesn't allow this client unless the install lists it

// client-go tears down a whole port-forward when any one of its
// streams errors, so each tunnel gets its own. That also picks
// a ready router pod again after one goes away.
localPort, stop, err := portforward.ServicePortForward(ctx, d.config, d.clientset, systemNamespace(), installdefaults.RouterServiceName, routerConnectTLSPort)

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.

non-blocking: every accepted connection does a Service GET and a Pod LIST (internal/portforward/portforward.go:66, :74) plus a port-forward upgrade before any bytes move, on a rest.Config left at client-go's default 5 QPS / burst 10. That's two apiserver round trips of latency per connection, and a browser opening a burst of connections (the Jupyter flow) gets throttled to ~2.5 new tunnels/s after the first few.

https://github.com/kubernetes/client-go/blob/28076445520055420e3be4255b4cd27fd19df1f9/rest/config.go#L48-L49

Could we resolve the router pod once and only re-resolve when a port-forward fails?

Comment thread demos/sandbox/README.md
```

### 3. Port-Forward Services
### 3. Proxy to the Actor

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.

nit: demos/claude-code-multiplex/README.md:31 links #3-port-forward-services here for the api port-forward, that anchor and the api port-forward are both gone now

@BenTheElder
Benjamin Elder (BenTheElder) added this pull request to the merge queue Oct 28, 2026
Merged via the queue into agent-substrate:main with commit c093a40 Oct 28, 2026
5 checks passed
Ziyi Tan (Ziy1-Tan) added a commit to Ziy1-Tan/substrate that referenced this pull request Nov 4, 2026
The client called ResumeActor before starting its REPL. A request through
the router resumes a suspended actor, so the call is redundant. It was
also the first thing to fail before agent-substrate#3191 gave the client credentials.

The connection stays where it was: before the REPL, on the signal
context. NewClient uses that context only while connecting, for the kube
API calls, the token mint and the port-forward readiness wait; the tunnel
and gRPC connection outlive it and close with the client.

Signed-off-by: Ziy1-Tan <[email protected]>
Ziyi Tan (Ziy1-Tan) added a commit to Ziy1-Tan/substrate that referenced this pull request Nov 4, 2026
The client called ResumeActor before starting its REPL. A request through
the router resumes a suspended actor, so the call is redundant. It was
also the first thing to fail before agent-substrate#3191 gave the client credentials.

The connection stays where it was: before the REPL, on the signal
context. NewClient uses that context only while connecting, for the kube
API calls, the token mint and the port-forward readiness wait; the tunnel
and gRPC connection outlive it and close with the client.

Signed-off-by: Ziy1-Tan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli area/network area/security Security related issue/pr kind/feature An enhancement / feature request or implementation milestone-ack prio/P0 Highest priority / required for next milestone

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Quickstart: deploy ate-system hard-requires an ingress SPIFFE allowlist the Quickstart never sets

8 participants