Skip to content

Actor identity

An actor moves between worker pods many times, since every suspend and resume can land it on a different pod (and often on a different node), and the pod’s own credentials change with each move. But the actor’s identity to external services, the thing that says “this request comes from actor checkout in atespace team-a”, must not change.

Substrate uses the actor itself as that stable identity, which is (atespace, name) plus its UID to tell a deleted and recreated actor from the original, and there’s no separate session resource for this. ateapi mints credentials that assert it, and they read the same whichever worker the actor is on.

Three identities

IdentityHeld byAssertsIssued by
Pod identityWorker pods, atelet, router, egress gateway, ateapi”I am this Kubernetes ServiceAccount” (spiffe://cluster.local/ns/<ns>/sa/<sa>)Kubernetes pod certificates
worker-for-actoratelet and atunnel while they host an actor”I am the Substrate component acting for this actor”ateapi WorkerService
ActorNever the actor itself; minted for the egress gateway to present on the actor’s behalf”This is the actor”ateapi Control

Keeping them apart lets a relying party tell who is calling. A worker-for-actor credential says a trusted Substrate component is acting for the actor, which is right for infrastructure such as the egress tunnel or snapshot storage, while an actor credential says the actor itself is calling, which is what an upstream API should see. Subjects and claim names differ (worker-for-actor/... vs. actor/...) so the two can be granted different access.

Actor credentials

Actor credentials come from two RPCs on ateapi’s Control service, and only the egress gateway may call them. An editor of the atespace can’t, because holding one would let a user impersonate the actor.

RPCOutput
MintActorJwtA JWT with sub=actor/<atespace>/<name>, the requested aud (at least one required), a lifetime of 5 to 60 minutes, nbf back-dated 5 minutes for clock skew, and an actor.ate.dev claim holding atespace, actorName, and actorUID
MintActorCertificateAn X.509 certificate signed from a CSR, valid one hour, with the SPIFFE URI spiffe://substrate-actor.local/actor/<atespace>/<name> and an X.509 extension carrying the atespace, name, and UID. The caller must authenticate with a client certificate, and must name the actor’s current UID

The actor must exist but need not be running, since a credential may be needed while it boots or resumes.

So how does an actor use its JWT if it never holds it? When an http or https rule in the actor’s EgressPolicy asks for actor JWT injection, the egress gateway mints a token for the actor making the request and writes it into the outbound header before the request leaves the cluster. The actor sends a placeholder, so it can’t read, snapshot, or leak the token.

To verify a token, a relying party starts from the issuer, which is ateapi’s --actor-jwt-issuer (by default https://idp.<namespace>.svc). The ate-idp-server Deployment serves the OpenID Connect discovery document and JWK set at that issuer, so any relying party that trusts the issuer can verify actor JWTs with standard OIDC discovery.

worker-for-actor credentials

These are minted through ateapi’s WorkerService, which serves only atelet, and only for an actor assigned to a worker on that atelet’s own node. The table below lists the two RPCs and what each credential is used for.

RPCOutputUsed for
MintWorkerActorCertificateOne-hour certificate, SPIFFE URI spiffe://substrate-actor.local/worker-for-actor/<atespace>/<name>, with the actor’s UID in the X.509 extensionatunnel presents it when it opens the actor’s egress tunnel to the egress gateway
MintWorkerActorJwtOne-hour JWT, sub=worker-for-actor/<atespace>/<name>, claims under worker-for-actor.ate.devatelet hands it to node plugins, such as the object-store plugin, which exchange it for storage credentials scoped to that actor

ateapi reads the caller’s node from atelet’s pod certificate and mints only for an actor on that node, so one node can’t obtain worker-for-actor credentials for actors it doesn’t host. The certificate additionally requires the actor to be assigned to a worker there, so a suspended actor gets none. The JWT is also issued while a suspended actor’s snapshot is still on the node, which is when its background upload runs.

flowchart LR
  ACTOR[Actor in sandbox] -- TCP redirected --> AT[atunnel<br/>worker supervisor]
  AT -- mTLS CONNECT<br/>worker-for-actor cert --> EG[Egress gateway]
  EG -- MintActorJwt --> API[ateapi]
  EG -- request with<br/>actor JWT injected --> UP[Upstream API]
  UP -. OIDC discovery .-> IDP[ate-idp-server]
  ATELET[atelet] -- MintWorkerActorCertificate --> API
  ATELET -. cert .-> AT

  click API "/components/ateapi/" "ateapi"
  click EG "/components/atenet/" "atenet"
  click ATELET "/components/atelet/" "atelet"

The egress gateway in the diagram above is part of atenet, where AgentGateway is the preferred dataplane and Envoy is the install default. We cover how each one handles egress on the atenet page.

Actor vs. worker

ActorWorker pod
Identifier(atespace, name), plus UIDPod name and UID
Lives inateapi’s PostgreSQL databaseKubernetes
LifetimeUntil DeleteActorUntil the pod is replaced
Credential identityactor/<atespace>/<name>ns/<ns>/sa/<sa>

An actor can also learn its own name, atespace, and UID from a system_info volume with an actor_metadata data source, although that’s information for the workload and doesn’t act as a credential.

  • Actor covers the lifecycle behind the identity.
  • Atespace covers the first half of every actor identity.
  • ateapi serves the Control and WorkerService RPCs.
  • Security & identity covers trust boundaries across the system.