Skip to content

atunnel: broker actor certificates through atelet - #708

Open
Eitan Yarmush (EItanya) wants to merge 7 commits into
agent-substrate:mainfrom
kagent-dev:issue-706-atunnel-identity
Open

atunnel: broker actor certificates through atelet#708
Eitan Yarmush (EItanya) wants to merge 7 commits into
agent-substrate:mainfrom
kagent-dev:issue-706-atunnel-identity

Conversation

@EItanya

@EItanya Eitan Yarmush (EItanya) commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Closes #706

Summary

  • broker short-lived actor certificates from atelet over a same-node mTLS Unix socket
  • keep the actor private key in atunnel and renew the certificate before expiry
  • authenticate egress CONNECT using the actor certificate instead of bearer tokens

Testing

  • make verify
  • go test -race ./internal/atunnel

@LiorLieberman

Copy link
Copy Markdown
Collaborator

Thanks Eitan Yarmush (@EItanya) !

Quick question before I review more deeply today.

With the goal of "having a JWT available to atunnel to append on a CONNECT request to the egress" -- What parts of the this PR addresses?

Here are the parts we needed (before this pr -- havent reviewed it yet);

  1. Improve the JWT issuance flow as part of MintJWT (properly authorize atelets, having the JWT formats solid, etc)
  2. Implement an in-memory cache with go background routine that refreshes certs for every active actor.
  3. Implement the UDS socket between atunnel and atelet to pass the certs+jwts.

From a very brief glance - I think your PR is attempting to do all three. Is that correct?

Also, For (1), we likely need to sort out two things to make the JWTs useful:

  • Get the issuer a publicly accessible DNS name that is also oidc-compliant (cc: Taahir Ahmed (@ahmedtd))
  • Agree on the format for the Subject field on JWTs

See for ref -

// TODO: This is currently API but it has to be a globally unique, oidc-compliant and accsible DNS name
Issuer: "https://api.ate-system.svc",
// TODO: this format is very likely going to change.
Subject: fmt.Sprintf("atespaces:%s:actors:%s", req.GetAtespace(), req.GetActorName()),

@EItanya

Copy link
Copy Markdown
Collaborator Author

Yes, with two clarifications: this PR renews JWTs, not actor certificates, and each worker has only one active actor.

  1. JWT issuance/authorization: implemented. Atunnel authenticates to atelet over mTLS using the worker Pod identity. Atelet derives the worker UID from that certificate and resolves its current actor assignment; the request does not supply actor identity. Ateapi then independently revalidates the atelet’s node, worker assignment, actor UID, and permitted audience before minting. The JWT includes the actor UID/resource version and worker Pod UID.

  2. In-memory renewal: implemented for the worker’s single active actor. Atunnel mints before activation, stores the JWT in memory, renews with roughly 10% lifetime remaining, retries until expiry, and fails closed for new tunnels after expiry. Deactivation clears the JWT and stops renewal.

  3. UDS broker: implemented. The UDS carries only JWT requests/responses—not certificates. The Pod certificate is used to mutually authenticate the atunnel↔atelet connection, including verifying both are on the same node.

I agree the issuer and subject contracts remain unresolved. This PR intentionally retains the existing issuer/subject behavior and does not implement PEP verification yet. Before verification lands, we need a stable OIDC issuer with discovery/JWKS reachable by the PEP.

@EItanya Eitan Yarmush (EItanya) changed the title atunnel: broker actor JWTs through atelet atunnel: broker actor certificates through atelet Aug 4, 2026
Comment on lines +139 to +140
ActorResourceVersion: actor.GetMetadata().GetVersion(),
WorkerPodUid: req.GetWorkerPodUid(),

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.

why do we need these?

Comment thread cmd/ateapi/main.go
clientJWTIssuer = pflag.String("client-jwt-issuer", "", "The expected issuer URL for client JWTs.")
clientJWTAudience = pflag.String("client-jwt-audience", "", "The expected audience for client JWTs.")
actorIDJWTPoolFile = pflag.String("actor-id-jwt-pool", "", "The file that contains the serialized JWT authority pool for signing actor JWTs")
egressGatewayAddress = pflag.String("egress-gateway-address", "", "Address of the egress PEP. Empty disables tunneled 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.

why do need egressgateway on ateapi?

Comment on lines +33 to +36
// control resolves the authenticated worker Pod to its current assignment.
control ateapipb.ControlClient
// identity revalidates that assignment and signs the actor certificate.
identity ateapipb.ActorIdentityClient

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.

identity as "identityService"? feels weird to call a field identity that does not represent an identity

Comment on lines +40 to +44
// TODO: Before release, request an atunnel-specific MintCert purpose and
// require the egress PEP to reject generic actor certificates.
// The request deliberately carries no actor identity. The authenticated Pod
// UID is the only input used to select a worker and its current assignment.
workerUID, err := authenticatedWorkerUID(ctx)

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.

not sure I am following the TODO comment. I thought we were saying that we need it to be an actor cert thats specifically for atunnel use.

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 it would be fine to merge without that, and then add it in a follow up? Either way works.

return identity.PodUID, nil
}

func restrictClientToNode(node *substratex509.PodIdentity) func(tls.ConnectionState) 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.

can we get a more descriptive name and/or a comment that tells what you are doing in this func?

containers: req.GetSpec().GetContainers(),
assetPaths: req.GetRuntimeAssetPaths(),

actorVersion: req.GetActorVersion(),

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.

whats the rationale for removing the actorVersion

// and in every ateom pod (which mounts them as overlay lowerdirs).
ImageCacheDir = filepath.Join(BasePath, "image-cache")
ImageCacheDir = filepath.Join(BasePath, "image-cache")
CredentialBrokerSocket = filepath.Join(BasePath, "credential-broker.sock")

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 add a comment on top of CredentialBrokerSocket?

"google.golang.org/grpc/credentials"
)

// BrokerCertificateSource owns atunnel's actor private key and obtains the

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 could be wrong but dont we want atelet to be the one that generate the private keys?

// Activate allows egress for one actor. There can be only one active actor per
// worker. bearerToken may be empty until actor JWT issuance is available.
func (e *Egress) Activate(dialer EgressDialer, atespace, actorName string, actorVersion int64, bearerToken string) error {
// Activate allows egress with a previously obtained actor certificate and

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.

with a previously obtained actor certificate

thinking out loud - do we care about actorVersion(s)?


message MintActorCertificateRequest {
// DER-encoded PKCS #10 certificate signing request. Atunnel retains the
// corresponding private key.

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.

My initial thinking was that atelet is the thing we trust, and hence it generates the private keys and hands them to atunnel. Like kubelet. But your approach may be bettter, though keys would have to be regenerated across suspend/resume?

/cc Taahir Ahmed (@ahmedtd) for thoughts.


// Exact worker Pod requesting the certificate. Ateapi verifies that this
// worker is still assigned to the actor before signing.
string worker_pod_uid = 5;

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 you add an additional note here to the effect that it's safe to trust the worker_pod_uid specified in the request body because atelet is the client, and atelet is trusted to tell us which worker pod is requesting the certificate (as long as the worker pod is associated with that particular atelet).

},
}

if err := substratex509.AddActorIdentityToCertificate(&substratex509.ActorIdentity{

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 need an additional indication here that this is an ateom or atunnel, not the actor using its own identity.

Maybe fields in the ActorIdentity extension for:

  • SystemComponentName
  • SystemComponentPodName
  • SystemComponentPodUID

Comment on lines +40 to +44
// TODO: Before release, request an atunnel-specific MintCert purpose and
// require the egress PEP to reject generic actor certificates.
// The request deliberately carries no actor identity. The authenticated Pod
// UID is the only input used to select a worker and its current assignment.
workerUID, err := authenticatedWorkerUID(ctx)

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 it would be fine to merge without that, and then add it in a follow up? Either way works.

return nil, err
}
var assigned *ateapipb.Worker
for pageToken := ""; ; {

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.

Looping over all workers isn't going to work even in the medium term. Do we have any way to fetch the correct worker in a single request?

Comment thread cmd/atelet/main.go
Comment on lines +77 to +79
ateapiAddress = pflag.String("ateapi-address", "dns:///api.ate-system.svc:443", "ateapi gRPC target used by the credential broker.")
ateapiCAFile = pflag.String("ateapi-ca-file", "/run/servicedns.podcert.ate.dev/trust-bundle.pem", "CA bundle used to verify ateapi.")
ateapiServerName = pflag.String("ateapi-server-name", "api.ate-system.svc", "DNS name expected on the ateapi certificate.")

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.

Normally ate-api-server dials out to atelet. In this case, we need to dial back.

Comment thread cmd/atelet/main.go
serverboot.Fatal(ctx, "Failed to load atelet Pod identity", fmt.Errorf("credential bundle has no Pod identity"))
}
brokerTLS := tlsCfg.Clone()
brokerTLS.VerifyConnection = restrictClientToNode(ateletIdentity)

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 over a UDS we should be able to get this level of control just from filesystem permissions? Ie, we create a UDS socket file for each individual ateom, and make sure only that ateom can access it.

TLS over a UDS is a bit strange.

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.

Broker actor JWTs from atelet to atunnel over an authenticated Unix socket

3 participants